diff options
309 files changed, 10164 insertions, 2214 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 37ceb091f035..526e63cf7e29 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -1384,8 +1384,8 @@ public class JobInfo implements Parcelable { * want to call one of these methods. * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, - * an app must hold the {@link android.Manifest.permission#INTERNET} and - * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to + * an app must hold the + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to * schedule a job that requires a network. * * <p class="note"> @@ -1445,8 +1445,8 @@ public class JobInfo implements Parcelable { * constraint. * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, - * an app must hold the {@link android.Manifest.permission#INTERNET} and - * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to + * an app must hold the + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to * schedule a job that requires a network. * * @param networkRequest The detailed description of the kind of network diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index a0634f0e74eb..dc608e7fddfd 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -810,7 +810,7 @@ class JobConcurrencyManager { mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable, mRecycledAssignmentInfo, mRecycledPrivilegedState); - noteConcurrency(); + noteConcurrency(true); } @VisibleForTesting @@ -1437,11 +1437,13 @@ class JobConcurrencyManager { } } - private void noteConcurrency() { + private void noteConcurrency(boolean logForHistogram) { mService.mJobPackageTracker.noteConcurrency(mRunningJobs.size(), // TODO: log per type instead of only TOP mWorkCountTracker.getRunningJobCount(WORK_TYPE_TOP)); - sConcurrencyHistogramLogger.logSample(mActiveServices.size()); + if (logForHistogram) { + sConcurrencyHistogramLogger.logSample(mActiveServices.size()); + } } @GuardedBy("mLock") @@ -1582,7 +1584,9 @@ class JobConcurrencyManager { final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue(); if (pendingJobQueue.size() == 0) { worker.clearPreferredUid(); - noteConcurrency(); + // Don't log the drop in concurrency to the histogram, otherwise, we'll end up + // overcounting lower concurrency values as jobs end execution. + noteConcurrency(false); return; } if (mActiveServices.size() >= mSteadyStateConcurrencyLimit) { @@ -1612,7 +1616,9 @@ class JobConcurrencyManager { // scheduled), but we should // be able to stop the other jobs soon so don't start running anything new until we // get back below the limit. - noteConcurrency(); + // Don't log the drop in concurrency to the histogram, otherwise, we'll end up + // overcounting lower concurrency values as jobs end execution. + noteConcurrency(false); return; } } @@ -1761,7 +1767,9 @@ class JobConcurrencyManager { } } - noteConcurrency(); + // Don't log the drop in concurrency to the histogram, otherwise, we'll end up + // overcounting lower concurrency values as jobs end execution. + noteConcurrency(false); } /** diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index e4e3de270aee..3f552b6906d0 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -195,7 +195,7 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; /** - * Require the app to have the INTERNET and ACCESS_NETWORK_STATE permissions when scheduling + * Require the app to have the ACCESS_NETWORK_STATE permissions when scheduling * a job with a connectivity constraint. */ @ChangeId @@ -1503,6 +1503,16 @@ public class JobSchedulerService extends com.android.server.SystemService } toCancel.enqueueWorkLocked(work); + if (toCancel.getJob().isUserInitiated()) { + // The app is in a state to successfully schedule a UI job. Presumably, the + // user has asked for this additional bit of work, so remove any demotion + // flags. Only do this for UI jobs since they have strict scheduling + // requirements; it's harder to assume other jobs were scheduled due to + // user interaction/request. + toCancel.removeInternalFlags( + JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + } mJobs.touchJob(toCancel); sEnqueuedJwiHighWaterMarkLogger.logSampleWithUid(uId, toCancel.getWorkCount()); @@ -4001,12 +4011,6 @@ public class JobSchedulerService extends com.android.server.SystemService if (job.getRequiredNetwork() != null && CompatChanges.isChangeEnabled( REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { - // All networking, including with the local network and even local to the device, - // requires the INTERNET permission. - if (!hasPermission(uid, pid, Manifest.permission.INTERNET)) { - throw new SecurityException(Manifest.permission.INTERNET - + " required for jobs with a connectivity constraint"); - } if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) { throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE + " required for jobs with a connectivity constraint"); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 2944095ec8db..bf2e4560a4ef 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -551,9 +551,6 @@ public final class JobServiceContext implements ServiceConnection { if (CompatChanges.isChangeEnabled( JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { final String pkgName = job.getServiceComponent().getPackageName(); - if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.INTERNET)) { - return false; - } if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) { return false; } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 6445c3bb6f8d..b5d763c17e59 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -1177,6 +1177,10 @@ public final class JobStatus { mInternalFlags |= flags; } + public void removeInternalFlags(int flags) { + mInternalFlags = mInternalFlags & ~flags; + } + int getPreferredConstraintFlags() { return mPreferredConstraints; } diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index c0c59a24dd8d..98fe61616c78 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -18,6 +18,7 @@ package android.app; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_FOREGROUND; +import static android.app.AppOpsManager.MODE_IGNORED; import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; @@ -43,6 +44,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.compat.CompatChanges; +import android.app.role.RoleManager; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.Disabled; @@ -58,6 +60,7 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.permission.PermissionCheckerManager; import android.provider.DeviceConfig; import android.text.TextUtils; @@ -74,6 +77,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Optional; /** @@ -333,7 +337,8 @@ public abstract class ForegroundServiceTypePolicy { new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL) }, true), new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { - new RegularPermission(Manifest.permission.MANAGE_OWN_CALLS) + new RegularPermission(Manifest.permission.MANAGE_OWN_CALLS), + new RolePermission(RoleManager.ROLE_DIALER) }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_PHONE_CALL /* permissionEnforcementFlag */, true /* permissionEnforcementFlagDefaultValue */ @@ -1078,9 +1083,9 @@ public abstract class ForegroundServiceTypePolicy { int checkPermission(@NonNull Context context, @NonNull String name, int callerUid, int callerPid, String packageName, boolean allowWhileInUse) { // Simple case, check if it's already granted. - @PackageManager.PermissionResult int result; - if ((result = PermissionChecker.checkPermissionForPreflight(context, name, - callerPid, callerUid, packageName)) == PERMISSION_GRANTED) { + @PermissionCheckerManager.PermissionResult int result; + if ((result = PermissionChecker.checkPermissionForPreflight(context, name, callerPid, + callerUid, packageName)) == PermissionCheckerManager.PERMISSION_GRANTED) { return PERMISSION_GRANTED; } if (allowWhileInUse && result == PermissionCheckerManager.PERMISSION_SOFT_DENIED) { @@ -1093,6 +1098,13 @@ public abstract class ForegroundServiceTypePolicy { if (currentMode == MODE_FOREGROUND) { // It's in foreground only mode and we're allowing while-in-use. return PERMISSION_GRANTED; + } else if (currentMode == MODE_IGNORED) { + // If it's soft denied with the mode "ignore", semantically it's a silent + // failure and no exception should be thrown, we might not want to allow + // the FGS. However, since the user has agreed with this permission + // (otherwise it's going to be a hard denial), and we're allowing + // while-in-use here, it's safe to allow the FGS run here. + return PERMISSION_GRANTED; } } } @@ -1123,6 +1135,29 @@ public abstract class ForegroundServiceTypePolicy { } /** + * This represents a particular role an app needs to hold for a specific service type. + */ + static class RolePermission extends ForegroundServiceTypePermission { + final String mRole; + + RolePermission(@NonNull String role) { + super(role); + mRole = role; + } + + @Override + @PackageManager.PermissionResult + public int checkPermission(@NonNull Context context, int callerUid, int callerPid, + String packageName, boolean allowWhileInUse) { + final RoleManager rm = context.getSystemService(RoleManager.class); + final List<String> holders = rm.getRoleHoldersAsUser(mRole, + UserHandle.getUserHandleForUid(callerUid)); + return holders != null && holders.contains(packageName) + ? PERMISSION_GRANTED : PERMISSION_DENIED; + } + } + + /** * This represents a special Android permission to be required for accessing usb devices. */ static class UsbDevicePermission extends ForegroundServiceTypePermission { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index e15e08fc0ef0..0ec3847d29f4 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -147,13 +147,13 @@ interface IActivityManager { int checkPermission(in String permission, int pid, int uid); /** Logs start of an API call to associate with an FGS, used for FGS Type Metrics */ - void logFgsApiBegin(int apiType, int appUid, int appPid); + oneway void logFgsApiBegin(int apiType, int appUid, int appPid); /** Logs stop of an API call to associate with an FGS, used for FGS Type Metrics */ - void logFgsApiEnd(int apiType, int appUid, int appPid); + oneway void logFgsApiEnd(int apiType, int appUid, int appPid); /** Logs API state change to associate with an FGS, used for FGS Type Metrics */ - void logFgsApiStateChanged(int apiType, int state, int appUid, int appPid); + oneway void logFgsApiStateChanged(int apiType, int state, int appUid, int appPid); // =============== End of transactions used on native side as well ============================ // Special low-level communication with activity manager. diff --git a/core/java/android/app/admin/DevicePolicyResources.java b/core/java/android/app/admin/DevicePolicyResources.java index 0512c7511ec6..456c6af134cf 100644 --- a/core/java/android/app/admin/DevicePolicyResources.java +++ b/core/java/android/app/admin/DevicePolicyResources.java @@ -1864,9 +1864,54 @@ public final class DevicePolicyResources { public static final String WORK_PROFILE_TELEPHONY_PAUSED_TURN_ON_BUTTON = PREFIX + "TURN_ON_WORK_PROFILE_BUTTON_TEXT"; + /** + * Information section shown on a dialog when the user is unable to place a call in + * the personal profile due to admin restrictions, and must choose whether to place + * the call from the work profile or cancel. + */ + public static final String MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION = + PREFIX + "MINIRESOLVER_WORK_TELEPHONY_INFORMATION"; + + /** + * Information section shown on a dialog when the user is unable to send a text in + * the personal profile due to admin restrictions, and must choose whether to place + * the call from the work profile or cancel. + */ + public static final String MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION = + PREFIX + "MINIRESOLVER_WORK_TELEPHONY_INFORMATION"; + + + /** + * Button for a dialog shown when the user is unable to place a call in the personal + * profile due to admin restrictions, and must choose whether to place the call from + * the work profile or cancel. + */ + public static final String MINIRESOLVER_CALL_FROM_WORK = + PREFIX + "MINIRESOLVER_CALL_FROM_WORK"; + + /** + * Button for a dialog shown when the user has no apps capable of handling an intent + * in the personal profile, and must choose whether to open the intent in a + * cross-profile app in the work profile, or cancel. + */ + public static final String MINIRESOLVER_SWITCH_TO_WORK = + PREFIX + "MINIRESOLVER_SWITCH_TO_WORK"; + + /** + * Title for a dialog shown when the user has no apps capable of handling an intent + * in the personal profile, and must choose whether to open the intent in a + * cross-profile app in the work profile, or open in the same profile browser. Accepts + * the app name as a param. + */ public static final String MINIRESOLVER_OPEN_IN_WORK = PREFIX + "MINIRESOLVER_OPEN_IN_WORK"; + /** + * Title for a dialog shown when the user has no apps capable of handling an intent + * in the personal profile, and must choose whether to open the intent in a + * cross-profile app in the personal profile, or open in the same profile browser. + * Accepts the app name as a param. + */ public static final String MINIRESOLVER_OPEN_IN_PERSONAL = PREFIX + "MINIRESOLVER_OPEN_IN_PERSONAL"; diff --git a/core/java/android/app/admin/DeviceStateCache.java b/core/java/android/app/admin/DeviceStateCache.java index d1d130d88a39..f37f5411be2c 100644 --- a/core/java/android/app/admin/DeviceStateCache.java +++ b/core/java/android/app/admin/DeviceStateCache.java @@ -50,6 +50,14 @@ public abstract class DeviceStateCache { public abstract boolean isUserOrganizationManaged(@UserIdInt int userHandle); /** + * Returns whether a user has affiliated IDs. + */ + + public boolean hasAffiliationWithDevice(int userId) { + return false; + } + + /** * Empty implementation. */ private static class EmptyDeviceStateCache extends DeviceStateCache { diff --git a/core/java/android/app/admin/PolicyKey.java b/core/java/android/app/admin/PolicyKey.java index 3544c19fe869..9b12e5969c2e 100644 --- a/core/java/android/app/admin/PolicyKey.java +++ b/core/java/android/app/admin/PolicyKey.java @@ -25,6 +25,7 @@ import android.os.Parcelable; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import android.util.Log; import org.xmlpull.v1.XmlPullParserException; @@ -41,6 +42,9 @@ import java.util.Objects; @SuppressLint({"ParcelNotFinal", "ParcelCreator"}) @SystemApi public abstract class PolicyKey implements Parcelable { + + static final String TAG = "PolicyKey"; + /** * @hide */ @@ -76,9 +80,14 @@ public abstract class PolicyKey implements Parcelable { /** * @hide */ + @Nullable public static PolicyKey readGenericPolicyKeyFromXml(TypedXmlPullParser parser) { String identifier = parser.getAttributeValue( /* namespace= */ null, ATTR_POLICY_IDENTIFIER); + if (identifier == null) { + Log.wtf(TAG, "Error parsing generic policy key, identifier is null."); + return null; + } return new NoArgsPolicyKey(identifier); } diff --git a/core/java/android/app/assist/OWNERS b/core/java/android/app/assist/OWNERS index e857c72bb28e..80ecaa41dbf3 100644 --- a/core/java/android/app/assist/OWNERS +++ b/core/java/android/app/assist/OWNERS @@ -3,3 +3,5 @@ joannechung@google.com markpun@google.com lpeter@google.com tymtsai@google.com +hackz@google.com +volnov@google.com
\ No newline at end of file diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index fa99b59888f5..379a0111b58c 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -145,7 +145,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall private boolean mExported; private boolean mNoPerms; private boolean mSingleUser; - private SparseBooleanArray mUsersRedirectedToOwner = new SparseBooleanArray(); + private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray(); private ThreadLocal<AttributionSource> mCallingAttributionSource; @@ -874,34 +874,42 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall return true; } - if (isAuthorityRedirectedForCloneProfile(mAuthority)) { - if (mUsersRedirectedToOwner.indexOfKey(callingUserId) >= 0) { - return mUsersRedirectedToOwner.get(callingUserId); + // Provider user-id will be determined from User Space of the calling app. + return isContentRedirectionAllowedForUser(callingUserId); + } + + /** + * Verify that content redirection is allowed or not. + * We check: + * 1. Type of Authority + * 2. UserProperties allow content sharing + * + * @param incomingUserId - Provider's user-id to be passed should be based upon: + * 1. If client is a cloned app running in user 10, it should be that (10) + * 2. If client is accessing content by hinting user space of content, + * like sysUi (residing in user 0) accessing 'content://11@media/external' + * then it should be 11. + */ + private boolean isContentRedirectionAllowedForUser(int incomingUserId) { + if (MediaStore.AUTHORITY.equals(mAuthority)) { + if (mUsersRedirectedToOwnerForMedia.indexOfKey(incomingUserId) >= 0) { + return mUsersRedirectedToOwnerForMedia.valueAt(incomingUserId); } // Haven't seen this user yet, look it up - try { - UserHandle callingUser = UserHandle.getUserHandleForUid(uid); - Context callingUserContext = mContext.createPackageContextAsUser("system", - 0, callingUser); - UserManager um = callingUserContext.getSystemService(UserManager.class); - - if (um != null && um.isCloneProfile()) { - UserHandle parent = um.getProfileParent(callingUser); - - if (parent != null && parent.equals(myUserHandle())) { - mUsersRedirectedToOwner.put(callingUserId, true); - return true; - } + UserManager um = mContext.getSystemService(UserManager.class); + if (um != null && um.getUserProperties(UserHandle.of(incomingUserId)) + .isMediaSharedWithParent()) { + UserHandle parent = um.getProfileParent(UserHandle.of(incomingUserId)); + if (parent != null && parent.equals(myUserHandle())) { + mUsersRedirectedToOwnerForMedia.put(incomingUserId, true); + return true; } - } catch (PackageManager.NameNotFoundException e) { - // ignore } - mUsersRedirectedToOwner.put(callingUserId, false); + mUsersRedirectedToOwnerForMedia.put(incomingUserId, false); return false; } - return false; } @@ -2734,7 +2742,11 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall String auth = uri.getAuthority(); if (!mSingleUser) { int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT); - if (userId != UserHandle.USER_CURRENT && userId != mContext.getUserId()) { + if (userId != UserHandle.USER_CURRENT + && userId != mContext.getUserId() + // Since userId specified in content uri, the provider userId would be + // determined from it. + && !isContentRedirectionAllowedForUser(userId)) { throw new SecurityException("trying to query a ContentProvider in user " + mContext.getUserId() + " with a uri belonging to user " + userId); } diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 7e0954a55560..be8b2a20cfb1 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -168,7 +168,8 @@ public class ServiceInfo extends ComponentInfo * <p>Starting foreground service with this type from apps targeting API level * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission * {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and - * {@link android.Manifest.permission#MANAGE_OWN_CALLS}. + * {@link android.Manifest.permission#MANAGE_OWN_CALLS} or holding the default + * {@link android.app.role.RoleManager#ROLE_DIALER dialer role}. */ @RequiresPermission( allOf = { diff --git a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java index 257ad7162e9e..5b24fb6860a2 100644 --- a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java +++ b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java @@ -218,7 +218,8 @@ public interface BiometricFingerprintConstants { FINGERPRINT_ACQUIRED_UNKNOWN, FINGERPRINT_ACQUIRED_IMMOBILE, FINGERPRINT_ACQUIRED_TOO_BRIGHT, - FINGERPRINT_ACQUIRED_POWER_PRESSED}) + FINGERPRINT_ACQUIRED_POWER_PRESSED, + FINGERPRINT_ACQUIRED_RE_ENROLL}) @Retention(RetentionPolicy.SOURCE) @interface FingerprintAcquired {} @@ -310,6 +311,12 @@ public interface BiometricFingerprintConstants { int FINGERPRINT_ACQUIRED_POWER_PRESSED = 11; /** + * This message is sent to encourage the user to re-enroll their fingerprints. + * @hide + */ + int FINGERPRINT_ACQUIRED_RE_ENROLL = 12; + + /** * @hide */ int FINGERPRINT_ACQUIRED_VENDOR_BASE = 1000; diff --git a/core/java/android/hardware/biometrics/ComponentInfoInternal.java b/core/java/android/hardware/biometrics/ComponentInfoInternal.java index 2e708de21762..3b61a56bd9f1 100644 --- a/core/java/android/hardware/biometrics/ComponentInfoInternal.java +++ b/core/java/android/hardware/biometrics/ComponentInfoInternal.java @@ -19,8 +19,6 @@ package android.hardware.biometrics; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; -import android.util.IndentingPrintWriter; /** * The internal class for storing the component info for a subsystem of the biometric sensor, @@ -92,19 +90,12 @@ public class ComponentInfoInternal implements Parcelable { dest.writeString(softwareVersion); } - /** - * Print the component info into the given stream. - * - * @param pw The stream to dump the info into. - * @hide - */ - public void dump(@NonNull IndentingPrintWriter pw) { - pw.println(TextUtils.formatSimple("componentId: %s", componentId)); - pw.increaseIndent(); - pw.println(TextUtils.formatSimple("hardwareVersion: %s", hardwareVersion)); - pw.println(TextUtils.formatSimple("firmwareVersion: %s", firmwareVersion)); - pw.println(TextUtils.formatSimple("serialNumber: %s", serialNumber)); - pw.println(TextUtils.formatSimple("softwareVersion: %s", softwareVersion)); - pw.decreaseIndent(); + @Override + public String toString() { + return "ComponentId: " + componentId + + ", HardwareVersion: " + hardwareVersion + + ", FirmwareVersion: " + firmwareVersion + + ", SerialNumber " + serialNumber + + ", SoftwareVersion: " + softwareVersion; } } diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl index 1286046e6a01..18c8d1bd3a1e 100644 --- a/core/java/android/hardware/biometrics/IBiometricService.aidl +++ b/core/java/android/hardware/biometrics/IBiometricService.aidl @@ -58,10 +58,10 @@ interface IBiometricService { boolean hasEnrolledBiometrics(int userId, String opPackageName); // Registers an authenticator (e.g. face, fingerprint, iris). - // Sensor Id in sensor props must be unique, whereas modality doesn't need to be. + // Id must be unique, whereas strength and modality don't need to be. // TODO(b/123321528): Turn strength and modality into enums. @EnforcePermission("USE_BIOMETRIC_INTERNAL") - void registerAuthenticator(int modality, in SensorPropertiesInternal props, + void registerAuthenticator(int id, int modality, int strength, IBiometricAuthenticator authenticator); // Register callback for when keyguard biometric eligibility changes. diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 14f050df2b49..708ebdf3627b 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -18,6 +18,7 @@ package android.service.voice; import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; import static android.Manifest.permission.RECORD_AUDIO; +import static android.service.voice.SoundTriggerFailure.ERROR_CODE_UNKNOWN; import static android.service.voice.VoiceInteractionService.MULTIPLE_ACTIVE_HOTWORD_DETECTORS; import android.annotation.ElapsedRealtimeLong; @@ -270,6 +271,15 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { static final long THROW_ON_INITIALIZE_IF_NO_DSP = 269165460L; /** + * Gates returning {@link Callback#onFailure} and {@link Callback#onUnknownFailure} + * when asynchronous exceptions are propagated to the client. If the change is not enabled, + * the existing behavior of delivering {@link #STATE_ERROR} is retained. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + static final long SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS = 280471513L; + + /** * Controls the sensitivity threshold adjustment factor for a given model. * Negative value corresponds to less sensitive model (high threshold) and * a positive value corresponds to a more sensitive model (low threshold). @@ -313,7 +323,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { private final Executor mExternalExecutor; private final Handler mHandler; private final IBinder mBinder = new Binder(); - private final int mTargetSdkVersion; private final boolean mSupportSandboxedDetectionService; @GuardedBy("mLock") @@ -856,7 +865,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { new Handler(Looper.myLooper())); mInternalCallback = new SoundTriggerListener(mHandler); mModelManagementService = modelManagementService; - mTargetSdkVersion = targetSdkVersion; mSupportSandboxedDetectionService = supportSandboxedDetectionService; } @@ -1382,6 +1390,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { * * @hide */ + // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector void onSoundModelsChanged() { synchronized (mLock) { if (mAvailability == STATE_INVALID @@ -1401,20 +1410,38 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { return; } - // Stop the recognition before proceeding. - // This is done because we want to stop the recognition on an older model if it changed - // or was deleted. - // The availability change callback should ensure that the client starts recognition - // again if needed. + // Stop the recognition before proceeding if we are in the enrolled state. + // The framework makes the guarantee that an actively used model is present in the + // system server's enrollment database. For this reason we much stop an actively running + // model when the underlying sound model in enrollment database no longer match. if (mAvailability == STATE_KEYPHRASE_ENROLLED) { + // A SoundTriggerFailure will be sent to the client if the model state was + // changed. This is an overloading of the onFailure usage because we are sending a + // callback even in the successful stop case. If stopRecognition is successful, + // suggested next action RESTART_RECOGNITION will be sent. + // TODO(b/281608561): This code path will be removed with other enrollment flows in + // this class. try { - stopRecognitionLocked(); - } catch (SecurityException e) { - Slog.w(TAG, "Failed to Stop the recognition", e); - if (mTargetSdkVersion <= Build.VERSION_CODES.R) { - throw e; + int result = stopRecognitionLocked(); + if (result == STATUS_OK) { + sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN, + "stopped recognition because of enrollment update", + FailureSuggestedAction.RESTART_RECOGNITION)); + } + // only log to logcat here because many failures can be false positives such as + // calling stopRecognition where there is no started session. + Log.w(TAG, "Failed to stop recognition after enrollment update: code=" + + result); + } catch (Exception e) { + Slog.w(TAG, "Failed to stop recognition after enrollment update", e); + if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { + sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN, + "Failed to stop recognition after enrollment update: " + + Log.getStackTraceString(e), + FailureSuggestedAction.RECREATE_DETECTOR)); + } else { + updateAndNotifyStateChangedLocked(STATE_ERROR); } - updateAndNotifyStateChangedLocked(STATE_ERROR); return; } } @@ -1538,6 +1565,12 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { @GuardedBy("mLock") private void updateAndNotifyStateChangedLocked(int availability) { + updateAvailabilityLocked(availability); + notifyStateChangedLocked(); + } + + @GuardedBy("mLock") + private void updateAvailabilityLocked(int availability) { if (DBG) { Slog.d(TAG, "Hotword availability changed from " + mAvailability + " -> " + availability); @@ -1545,7 +1578,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { if (!mIsAvailabilityOverriddenByTestApi) { mAvailability = availability; } - notifyStateChangedLocked(); } @GuardedBy("mLock") @@ -1555,6 +1587,18 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { message.sendToTarget(); } + @GuardedBy("mLock") + private void sendUnknownFailure(String failureMessage) { + // update but do not call onAvailabilityChanged callback for STATE_ERROR + updateAvailabilityLocked(STATE_ERROR); + Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget(); + } + + private void sendSoundTriggerFailure(@NonNull SoundTriggerFailure soundTriggerFailure) { + Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE, soundTriggerFailure) + .sendToTarget(); + } + /** @hide */ static final class SoundTriggerListener extends IHotwordRecognitionStatusCallback.Stub { private final Handler mHandler; @@ -1577,6 +1621,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { .build()) .sendToTarget(); } + @Override public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) { Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event); @@ -1726,6 +1771,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { } } + // TODO(b/267681692): remove the AsyncTask usage class RefreshAvailabilityTask extends AsyncTask<Void, Void, Void> { @Override @@ -1744,13 +1790,17 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { } updateAndNotifyStateChangedLocked(availability); } - } catch (SecurityException e) { + } catch (Exception e) { + // Any exception here not caught will crash the process because AsyncTask does not + // bubble up the exceptions to the client app, so we must propagate it to the app. Slog.w(TAG, "Failed to refresh availability", e); - if (mTargetSdkVersion <= Build.VERSION_CODES.R) { - throw e; - } synchronized (mLock) { - updateAndNotifyStateChangedLocked(STATE_ERROR); + if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { + sendUnknownFailure( + "Failed to refresh availability: " + Log.getStackTraceString(e)); + } else { + updateAndNotifyStateChangedLocked(STATE_ERROR); + } } } diff --git a/core/java/android/service/voice/SoundTriggerFailure.java b/core/java/android/service/voice/SoundTriggerFailure.java index 2ce5e5da4724..b4b0ffaf7c2a 100644 --- a/core/java/android/service/voice/SoundTriggerFailure.java +++ b/core/java/android/service/voice/SoundTriggerFailure.java @@ -74,14 +74,22 @@ public final class SoundTriggerFailure implements Parcelable { public @interface SoundTriggerErrorCode {} private final int mErrorCode; + private final int mSuggestedAction; private final String mErrorMessage; /** * @hide */ @TestApi - public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, - @NonNull String errorMessage) { + public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage) { + this(errorCode, errorMessage, getSuggestedActionBasedOnErrorCode(errorCode)); + } + + /** + * @hide + */ + public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage, + @FailureSuggestedAction.FailureSuggestedActionDef int suggestedAction) { if (TextUtils.isEmpty(errorMessage)) { throw new IllegalArgumentException("errorMessage is empty or null."); } @@ -95,7 +103,13 @@ public final class SoundTriggerFailure implements Parcelable { default: throw new IllegalArgumentException("Invalid ErrorCode: " + errorCode); } + if (suggestedAction != getSuggestedActionBasedOnErrorCode(errorCode) + && errorCode != ERROR_CODE_UNKNOWN) { + throw new IllegalArgumentException("Invalid suggested next action: " + + "errorCode=" + errorCode + ", suggestedAction=" + suggestedAction); + } mErrorMessage = errorMessage; + mSuggestedAction = suggestedAction; } /** @@ -119,7 +133,11 @@ public final class SoundTriggerFailure implements Parcelable { */ @FailureSuggestedAction.FailureSuggestedActionDef public int getSuggestedAction() { - switch (mErrorCode) { + return mSuggestedAction; + } + + private static int getSuggestedActionBasedOnErrorCode(@SoundTriggerErrorCode int errorCode) { + switch (errorCode) { case ERROR_CODE_UNKNOWN: case ERROR_CODE_MODULE_DIED: case ERROR_CODE_UNEXPECTED_PREEMPTION: @@ -144,8 +162,11 @@ public final class SoundTriggerFailure implements Parcelable { @Override public String toString() { - return "SoundTriggerFailure { errorCode = " + mErrorCode + ", errorMessage = " - + mErrorMessage + " }"; + return "SoundTriggerFailure {" + + " errorCode = " + mErrorCode + + ", errorMessage = " + mErrorMessage + + ", suggestedNextAction = " + mSuggestedAction + + " }"; } public static final @NonNull Parcelable.Creator<SoundTriggerFailure> CREATOR = diff --git a/core/java/android/speech/OWNERS b/core/java/android/speech/OWNERS index 462d8bed743c..162e02904075 100644 --- a/core/java/android/speech/OWNERS +++ b/core/java/android/speech/OWNERS @@ -2,3 +2,4 @@ volnov@google.com eugeniom@google.com schfan@google.com andreaambu@google.com +hackz@google.com
\ No newline at end of file diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index f662c7372667..4cbb0409dafc 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6969,10 +6969,10 @@ public final class ViewRootImpl implements ViewParent, if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_TAB) { - if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) { + if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_CTRL_ON)) { groupNavigationDirection = View.FOCUS_FORWARD; } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(), - KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) { + KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) { groupNavigationDirection = View.FOCUS_BACKWARD; } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 5b6df1cb754e..185046222101 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1185,6 +1185,37 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"; /** + * Application level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the app should be opted-out from the + * compatibility override that changes the min aspect ratio. + * + * <p>When this compat override is enabled the min aspect ratio given in the app's manifest can + * be overridden by the device manufacturer using their discretion to improve display + * compatibility unless the app's manifest value is higher. This treatment will also apply if + * no min aspect ratio value is provided in the manifest. These treatments can apply only in + * specific cases (e.g. device is in portrait) or each time the app is displayed on screen. + * + * <p>Setting this property to {@code false} informs the system that the app must be + * opted-out from the compatibility treatment even if the device manufacturer has opted the app + * into the treatment. + * + * <p>Not setting this property at all, or setting this property to {@code true} has no effect. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE" + * android:value="true|false"/> + * </application> + * </pre> + * @hide + */ + // TODO(b/279428317): Make this public API. + String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = + "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; diff --git a/core/java/android/view/contentcapture/OWNERS b/core/java/android/view/contentcapture/OWNERS index 1a5cb1e4ca4a..d1eda9648520 100644 --- a/core/java/android/view/contentcapture/OWNERS +++ b/core/java/android/view/contentcapture/OWNERS @@ -5,3 +5,5 @@ joannechung@google.com markpun@google.com lpeter@google.com tymtsai@google.com +hackz@google.com +volnov@google.com
\ No newline at end of file diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index c28950662fb3..34e6e49d390f 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -783,7 +783,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private class SetEmptyView extends Action { + private static class SetEmptyView extends Action { int emptyViewId; SetEmptyView(@IdRes int viewId, @IdRes int emptyViewId) { @@ -820,7 +820,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private class SetPendingIntentTemplate extends Action { + private static class SetPendingIntentTemplate extends Action { public SetPendingIntentTemplate(@IdRes int id, PendingIntent pendingIntentTemplate) { this.viewId = id; this.pendingIntentTemplate = pendingIntentTemplate; @@ -891,7 +891,7 @@ public class RemoteViews implements Parcelable, Filter { PendingIntent pendingIntentTemplate; } - private class SetRemoteViewsAdapterList extends Action { + private static class SetRemoteViewsAdapterList extends Action { public SetRemoteViewsAdapterList(@IdRes int id, ArrayList<RemoteViews> list, int viewTypeCount) { this.viewId = id; @@ -1354,7 +1354,7 @@ public class RemoteViews implements Parcelable, Filter { } @Nullable - private MethodHandle getMethod(View view, String methodName, Class<?> paramType, + private static MethodHandle getMethod(View view, String methodName, Class<?> paramType, boolean async) { MethodArgs result; Class<? extends View> klass = view.getClass(); @@ -1433,7 +1433,7 @@ public class RemoteViews implements Parcelable, Filter { * to {@link ImageView#getDrawable()}. * <p> */ - private class SetDrawableTint extends Action { + private static class SetDrawableTint extends Action { SetDrawableTint(@IdRes int id, boolean targetBackground, @ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) { this.viewId = id; @@ -1697,7 +1697,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Base class for the reflection actions. */ - private abstract class BaseReflectionAction extends Action { + private abstract static class BaseReflectionAction extends Action { static final int BOOLEAN = 1; static final int BYTE = 2; static final int SHORT = 3; @@ -1860,7 +1860,7 @@ public class RemoteViews implements Parcelable, Filter { } /** Class for the reflection actions. */ - private final class ReflectionAction extends BaseReflectionAction { + private static final class ReflectionAction extends BaseReflectionAction { @UnsupportedAppUsage Object value; @@ -2006,7 +2006,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private final class ResourceReflectionAction extends BaseReflectionAction { + private static final class ResourceReflectionAction extends BaseReflectionAction { static final int DIMEN_RESOURCE = 1; static final int COLOR_RESOURCE = 2; @@ -2093,7 +2093,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private final class AttributeReflectionAction extends BaseReflectionAction { + private static final class AttributeReflectionAction extends BaseReflectionAction { static final int DIMEN_RESOURCE = 1; static final int COLOR_RESOURCE = 2; @@ -2187,7 +2187,7 @@ public class RemoteViews implements Parcelable, Filter { return ATTRIBUTE_REFLECTION_ACTION_TAG; } } - private final class ComplexUnitDimensionReflectionAction extends BaseReflectionAction { + private static final class ComplexUnitDimensionReflectionAction extends BaseReflectionAction { private final float mValue; @ComplexDimensionUnit @@ -2243,7 +2243,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private final class NightModeReflectionAction extends BaseReflectionAction { + private static final class NightModeReflectionAction extends BaseReflectionAction { private final Object mLightValue; private final Object mDarkValue; @@ -2603,7 +2603,7 @@ public class RemoteViews implements Parcelable, Filter { /** * ViewGroup methods related to removing child views. */ - private class ViewGroupActionRemove extends Action { + private static class ViewGroupActionRemove extends Action { /** * Id that indicates that all child views of the affected ViewGroup should be removed. * @@ -2725,7 +2725,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Action to remove a view from its parent. */ - private class RemoveFromParentAction extends Action { + private static class RemoveFromParentAction extends Action { RemoveFromParentAction(@IdRes int viewId) { this.viewId = viewId; @@ -2795,7 +2795,7 @@ public class RemoteViews implements Parcelable, Filter { * Helper action to set compound drawables on a TextView. Supports relative * (s/t/e/b) or cardinal (l/t/r/b) arrangement. */ - private class TextViewDrawableAction extends Action { + private static class TextViewDrawableAction extends Action { public TextViewDrawableAction(@IdRes int viewId, boolean isRelative, @DrawableRes int d1, @DrawableRes int d2, @DrawableRes int d3, @DrawableRes int d4) { this.viewId = viewId; @@ -2942,7 +2942,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to set text size on a TextView in any supported units. */ - private class TextViewSizeAction extends Action { + private static class TextViewSizeAction extends Action { TextViewSizeAction(@IdRes int viewId, @ComplexDimensionUnit int units, float size) { this.viewId = viewId; this.units = units; @@ -2980,7 +2980,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to set padding on a View. */ - private class ViewPaddingAction extends Action { + private static class ViewPaddingAction extends Action { public ViewPaddingAction(@IdRes int viewId, @Px int left, @Px int top, @Px int right, @Px int bottom) { this.viewId = viewId; @@ -3210,7 +3210,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to add a view tag with RemoteInputs. */ - private class SetRemoteInputsAction extends Action { + private static class SetRemoteInputsAction extends Action { public SetRemoteInputsAction(@IdRes int viewId, RemoteInput[] remoteInputs) { this.viewId = viewId; @@ -3246,7 +3246,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to override all textViewColors */ - private class OverrideTextColorsAction extends Action { + private static class OverrideTextColorsAction extends Action { private final int textColor; @@ -3289,7 +3289,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private class SetIntTagAction extends Action { + private static class SetIntTagAction extends Action { @IdRes private final int mViewId; @IdRes private final int mKey; private final int mTag; diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 88447daf7338..ff3c015cf66f 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -52,6 +52,8 @@ interface IAppOpsService { int checkAudioOperation(int code, int usage, int uid, String packageName); boolean shouldCollectNotes(int opCode); void setCameraAudioRestriction(int mode); + void startWatchingModeWithFlags(int op, String packageName, int flags, + IAppOpsCallback callback); // End of methods also called by native code. // Any new method exposed to native must be added after the last one, do not reorder @@ -110,8 +112,6 @@ interface IAppOpsService { void startWatchingStarted(in int[] ops, IAppOpsStartedCallback callback); void stopWatchingStarted(IAppOpsStartedCallback callback); - void startWatchingModeWithFlags(int op, String packageName, int flags, IAppOpsCallback callback); - void startWatchingNoted(in int[] ops, IAppOpsNotedCallback callback); void stopWatchingNoted(IAppOpsNotedCallback callback); diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 904fb665335b..db65cb37234b 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -19,7 +19,11 @@ package com.android.internal.app; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL; import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_CALL_FROM_WORK; import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_OPEN_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_SWITCH_TO_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION; import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -32,6 +36,7 @@ import android.app.Activity; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.admin.DevicePolicyManager; +import android.app.admin.ManagedSubscriptionsPolicy; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.ContentResolver; @@ -41,6 +46,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; import android.metrics.LogMaker; import android.os.Build; import android.os.Bundle; @@ -48,6 +54,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.telecom.TelecomManager; import android.util.Slog; import android.view.View; import android.widget.Button; @@ -203,35 +210,116 @@ public class IntentForwarderActivity extends Activity { findViewById(R.id.title_container).setElevation(0); - ImageView icon = findViewById(R.id.icon); PackageManager packageManagerForTargetUser = createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0) .getPackageManager(); - icon.setImageDrawable(target.loadIcon(packageManagerForTargetUser)); + + ImageView icon = findViewById(R.id.icon); + icon.setImageDrawable( + getAppIcon(target, launchIntent, targetUserId, packageManagerForTargetUser)); View buttonContainer = findViewById(R.id.button_bar_container); buttonContainer.setPadding(0, 0, 0, buttonContainer.getPaddingBottom()); ((TextView) findViewById(R.id.open_cross_profile)).setText( - getOpenInWorkMessage(target.loadLabel(packageManagerForTargetUser))); + getOpenInWorkMessage(launchIntent, target.loadLabel(packageManagerForTargetUser))); // The mini-resolver's negative button is reused in this flow to cancel the intent ((Button) findViewById(R.id.use_same_profile_browser)).setText(R.string.cancel); findViewById(R.id.use_same_profile_browser).setOnClickListener(v -> finish()); + ((Button) findViewById(R.id.button_open)).setText(getOpenInWorkButtonString(launchIntent)); findViewById(R.id.button_open).setOnClickListener(v -> { startActivityAsCaller(launchIntent, targetUserId); finish(); }); + + + View telephonyInfo = findViewById(R.id.miniresolver_info_section); + DevicePolicyManager devicePolicyManager = + getSystemService(DevicePolicyManager.class); + // Additional information section is work telephony specific. Therefore, it is only shown + // for telephony related intents, when all sim subscriptions are in the work profile. + if ((isDialerIntent(launchIntent) || isTextMessageIntent(launchIntent)) + && devicePolicyManager.getManagedSubscriptionsPolicy().getPolicyType() + == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) { + telephonyInfo.setVisibility(View.VISIBLE); + ((TextView) findViewById(R.id.miniresolver_info_section_text)) + .setText(getWorkTelephonyInfoSectionMessage(launchIntent)); + } else { + telephonyInfo.setVisibility(View.GONE); + } + } + + private Drawable getAppIcon( + ResolveInfo target, + Intent launchIntent, + int targetUserId, + PackageManager packageManagerForTargetUser) { + if (isDialerIntent(launchIntent)) { + // The icon for the call intent will be a generic phone icon as the target will be + // the telecom call handler. From the user's perspective, they are being directed + // to the dialer app, so use the icon from that app instead. + TelecomManager telecomManager = + getApplicationContext().getSystemService(TelecomManager.class); + String defaultDialerPackageName = + telecomManager.getDefaultDialerPackage(UserHandle.of(targetUserId)); + try { + return packageManagerForTargetUser + .getApplicationInfo(defaultDialerPackageName, /* flags= */ 0) + .loadIcon(packageManagerForTargetUser); + } catch (PackageManager.NameNotFoundException e) { + // Allow to fall-through to the icon from the target if we can't find the default + // dialer icon. + Slog.w(TAG, "Cannot load icon for default dialer package"); + } + } + return target.loadIcon(packageManagerForTargetUser); } - private String getOpenInWorkMessage(CharSequence targetLabel) { + private int getOpenInWorkButtonString(Intent launchIntent) { + if (isDialerIntent(launchIntent)) { + return R.string.miniresolver_call; + } + if (isTextMessageIntent(launchIntent)) { + return R.string.miniresolver_switch; + } + return R.string.whichViewApplicationLabel; + } + + private String getOpenInWorkMessage(Intent launchIntent, CharSequence targetLabel) { + if (isDialerIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_CALL_FROM_WORK, + () -> getString(R.string.miniresolver_call_in_work)); + } + if (isTextMessageIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_SWITCH_TO_WORK, + () -> getString(R.string.miniresolver_switch_to_work)); + } return getSystemService(DevicePolicyManager.class).getResources().getString( MINIRESOLVER_OPEN_WORK, () -> getString(R.string.miniresolver_open_work, targetLabel), targetLabel); } + private String getWorkTelephonyInfoSectionMessage(Intent launchIntent) { + if (isDialerIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION, + () -> getString(R.string.miniresolver_call_information)); + } + if (isTextMessageIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION, + () -> getString(R.string.miniresolver_sms_information)); + } + return ""; + } + + + private String getForwardToPersonalMessage() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_PERSONAL, diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 80f540cca79b..506f19f7719a 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -275,7 +275,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener } } - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public Handler getHandler() { return mHandler; } diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 2b9db70c57bd..e530aec2119a 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -772,11 +772,12 @@ public class InteractionJankMonitor { return true; } + @UiThread private void putTracker(@CujType int cuj, @NonNull FrameTracker tracker) { synchronized (mLock) { mRunningTrackers.put(cuj, tracker); if (mDebugOverlay != null) { - mDebugOverlay.onTrackerAdded(cuj, tracker.getViewRoot()); + mDebugOverlay.onTrackerAdded(cuj, tracker); } if (DEBUG) { Log.d(TAG, "Added tracker for " + getNameOfCuj(cuj) @@ -791,6 +792,7 @@ public class InteractionJankMonitor { } } + @UiThread private void removeTracker(@CujType int cuj, int reason) { synchronized (mLock) { mRunningTrackers.remove(cuj); @@ -818,7 +820,7 @@ public class InteractionJankMonitor { SETTINGS_DEBUG_OVERLAY_ENABLED_KEY, DEFAULT_DEBUG_OVERLAY_ENABLED); if (debugOverlayEnabled && mDebugOverlay == null) { - mDebugOverlay = new InteractionMonitorDebugOverlay(mDebugBgColor, mDebugYOffset); + mDebugOverlay = new InteractionMonitorDebugOverlay(mLock, mDebugBgColor, mDebugYOffset); } else if (!debugOverlayEnabled && mDebugOverlay != null) { mDebugOverlay.dispose(); mDebugOverlay = null; diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java index 99b9f2f35fd4..ef7944c21ad2 100644 --- a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java +++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java @@ -19,25 +19,30 @@ package com.android.internal.jank; import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL; import android.annotation.ColorInt; +import android.annotation.UiThread; import android.app.ActivityThread; import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.Rect; +import android.os.Handler; import android.os.Trace; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.WindowCallbacks; +import com.android.internal.annotations.GuardedBy; import com.android.internal.jank.FrameTracker.Reasons; import com.android.internal.jank.InteractionJankMonitor.CujType; /** * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window * associated with one of the CUJs being tracked. There's no guarantee which window it will - * draw to. NOTE: sometimes the CUJ names will remain displayed on the screen longer than they - * are actually running. + * draw to. Traces that use the debug overlay should not be used for performance analysis. + * <p> + * To enable the overlay, run the following: <code>adb shell device_config put + * interaction_jank_monitor debug_overlay_enabled true</code> * <p> * CUJ names will be drawn as follows: * <ul> @@ -45,12 +50,16 @@ import com.android.internal.jank.InteractionJankMonitor.CujType; * <li> Grey text indicates the CUJ ended normally and is no longer running * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally * </ul> + * @hide */ class InteractionMonitorDebugOverlay implements WindowCallbacks { private static final int REASON_STILL_RUNNING = -1000; + private final Object mLock; // Sparse array where the key in the CUJ and the value is the session status, or null if // it's currently running + @GuardedBy("mLock") private final SparseIntArray mRunningCujs = new SparseIntArray(); + private Handler mHandler = null; private FrameTracker.ViewRootWrapper mViewRoot = null; private final Paint mDebugPaint; private final Paint.FontMetrics mDebugFontMetrics; @@ -59,8 +68,10 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { private final int mBgColor; private final double mYOffset; private final String mPackageName; + private static final String TRACK_NAME = "InteractionJankMonitor"; - InteractionMonitorDebugOverlay(@ColorInt int bgColor, double yOffset) { + InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset) { + mLock = lock; mBgColor = bgColor; mYOffset = yOffset; mDebugPaint = new Paint(); @@ -70,18 +81,30 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { mPackageName = context.getPackageName(); } + @UiThread void dispose() { - if (mViewRoot != null) { - mViewRoot.removeWindowCallbacks(this); + if (mViewRoot != null && mHandler != null) { + mHandler.runWithScissors(() -> mViewRoot.removeWindowCallbacks(this), + InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); forceRedraw(); } + mHandler = null; mViewRoot = null; + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0); } - private boolean attachViewRootIfNeeded(FrameTracker.ViewRootWrapper viewRoot) { + @UiThread + private boolean attachViewRootIfNeeded(FrameTracker tracker) { + FrameTracker.ViewRootWrapper viewRoot = tracker.getViewRoot(); if (mViewRoot == null && viewRoot != null) { + // Add a trace marker so we can identify traces that were captured while the debug + // overlay was enabled. Traces that use the debug overlay should NOT be used for + // performance analysis. + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0); + mHandler = tracker.getHandler(); mViewRoot = viewRoot; - viewRoot.addWindowCallbacks(this); + mHandler.runWithScissors(() -> viewRoot.addWindowCallbacks(this), + InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); forceRedraw(); return true; } @@ -115,52 +138,61 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { } } + @UiThread private void forceRedraw() { - if (mViewRoot != null) { - mViewRoot.requestInvalidateRootRenderNode(); - mViewRoot.getView().invalidate(); + if (mViewRoot != null && mHandler != null) { + mHandler.runWithScissors(() -> { + mViewRoot.requestInvalidateRootRenderNode(); + mViewRoot.getView().invalidate(); + }, InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); } } + @UiThread void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason, SparseArray<FrameTracker> runningTrackers) { - mRunningCujs.put(removedCuj, reason); - // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended - if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { - mRunningCujs.clear(); - dispose(); - } else { - boolean needsNewViewRoot = true; - if (mViewRoot != null) { - // Check to see if this viewroot is still associated with one of the running - // trackers - for (int i = 0; i < runningTrackers.size(); i++) { - if (mViewRoot.equals( - runningTrackers.valueAt(i).getViewRoot())) { - needsNewViewRoot = false; - break; + synchronized (mLock) { + mRunningCujs.put(removedCuj, reason); + // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended + if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { + mRunningCujs.clear(); + dispose(); + } else { + boolean needsNewViewRoot = true; + if (mViewRoot != null) { + // Check to see if this viewroot is still associated with one of the running + // trackers + for (int i = 0; i < runningTrackers.size(); i++) { + if (mViewRoot.equals( + runningTrackers.valueAt(i).getViewRoot())) { + needsNewViewRoot = false; + break; + } } } - } - if (needsNewViewRoot) { - dispose(); - for (int i = 0; i < runningTrackers.size(); i++) { - if (attachViewRootIfNeeded(runningTrackers.valueAt(i).getViewRoot())) { - break; + if (needsNewViewRoot) { + dispose(); + for (int i = 0; i < runningTrackers.size(); i++) { + if (attachViewRootIfNeeded(runningTrackers.valueAt(i))) { + break; + } } + } else { + forceRedraw(); } - } else { - forceRedraw(); } } } - void onTrackerAdded(@CujType int addedCuj, FrameTracker.ViewRootWrapper viewRoot) { - // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ - // is still running - mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); - attachViewRootIfNeeded(viewRoot); - forceRedraw(); + @UiThread + void onTrackerAdded(@CujType int addedCuj, FrameTracker tracker) { + synchronized (mLock) { + // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ + // is still running + mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); + attachViewRootIfNeeded(tracker); + forceRedraw(); + } } @Override @@ -188,7 +220,6 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { @Override public void onPostDraw(RecordingCanvas canvas) { - Trace.beginSection("InteractionJankMonitor#drawDebug"); final int padding = dipToPx(5); final int h = canvas.getHeight(); final int w = canvas.getWidth(); @@ -235,6 +266,5 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { canvas.translate(0, cujNameTextHeight); canvas.drawText(cujName, 0, 0, mDebugPaint); } - Trace.endSection(); } } diff --git a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java index 6fa6fa5d37f3..3ba4ea55b5d3 100644 --- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java +++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java @@ -118,6 +118,9 @@ public class AnrLatencyTracker implements AutoCloseable { private boolean mIsSkipped = false; private boolean mCopyingFirstPidSucceeded = false; + private long mPreDumpIfLockTooSlowStartUptime; + private long mPreDumpIfLockTooSlowDuration = 0; + private final int mAnrRecordPlacedOnQueueCookie = sNextAnrRecordPlacedOnQueueCookieGenerator.incrementAndGet(); @@ -401,6 +404,17 @@ public class AnrLatencyTracker implements AutoCloseable { Trace.traceCounter(TRACE_TAG_ACTIVITY_MANAGER, "anrRecordsQueueSize", queueSize); } + /** Records the start of AnrController#preDumpIfLockTooSlow. */ + public void preDumpIfLockTooSlowStarted() { + mPreDumpIfLockTooSlowStartUptime = getUptimeMillis(); + } + + /** Records the end of AnrController#preDumpIfLockTooSlow. */ + public void preDumpIfLockTooSlowEnded() { + mPreDumpIfLockTooSlowDuration += + getUptimeMillis() - mPreDumpIfLockTooSlowStartUptime; + } + /** Records a skipped ANR in ProcessErrorStateRecord#appNotResponding. */ public void anrSkippedProcessErrorStateRecordAppNotResponding() { anrSkipped("appNotResponding"); @@ -415,7 +429,7 @@ public class AnrLatencyTracker implements AutoCloseable { * Returns latency data as a comma separated value string for inclusion in ANR report. */ public String dumpAsCommaSeparatedArrayWithHeader() { - return "DurationsV3: " + mAnrTriggerUptime + return "DurationsV4: " + mAnrTriggerUptime /* triggering_to_app_not_responding_duration = */ + "," + (mAppNotRespondingStartUptime - mAnrTriggerUptime) /* app_not_responding_duration = */ @@ -464,6 +478,8 @@ public class AnrLatencyTracker implements AutoCloseable { + "," + mEarlyDumpStatus /* copying_first_pid_succeeded = */ + "," + (mCopyingFirstPidSucceeded ? 1 : 0) + /* preDumpIfLockTooSlow_duration = */ + + "," + mPreDumpIfLockTooSlowDuration + "\n\n"; } diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml index 1ad3acd7a3ea..db0ea547fbd5 100644 --- a/core/res/res/layout/miniresolver.xml +++ b/core/res/res/layout/miniresolver.xml @@ -63,6 +63,37 @@ android:textColor="?android:textColorPrimary" /> </RelativeLayout> + <!-- Additional information section, currently only shown when redirecting to Telephony apps --> + <LinearLayout + android:id="@+id/miniresolver_info_section" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="24dp" + android:paddingBottom="48dp" + android:visibility="gone" + android:background="?attr/colorBackground" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/miniresolver_info_section_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="8dp" + android:src="@drawable/ic_info_outline_24" + android:tint="?android:textColorSecondary" + /> + + <TextView + android:id="@+id/miniresolver_info_section_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:paddingEnd="8dp" + android:textSize="14sp" + android:textAllCaps="false" + android:textColor="?android:textColorSecondary" + /> + </LinearLayout> <LinearLayout android:id="@+id/button_bar_container" @@ -83,7 +114,7 @@ android:orientation="horizontal" android:layoutDirection="locale" android:measureWithLargestChild="true" - android:paddingHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:paddingBottom="2dp" android:elevation="@dimen/resolver_elevation"> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 1bbe8eeaf37f..ba92360e10ca 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1618,7 +1618,8 @@ {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground service with this type will require permission {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and - {@link android.Manifest.permission#MANAGE_OWN_CALLS}. + {@link android.Manifest.permission#MANAGE_OWN_CALLS} or holding the default + {@link android.app.role.RoleManager#ROLE_DIALER dialer role}. --> <flag name="phoneCall" value="0x04" /> <!-- GPS, map, navigation location update. diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 23e3139c3d3a..8899785a85a0 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5177,10 +5177,11 @@ <item>0,0,1.0,0,1</item> <item>1,1,1.0,0,1</item> <item>1,1,1.0,.4,1</item> + <item>1,1,1.0,.15,15</item> </string-array> <!-- The integer index of the selected option in config_udfps_touch_detection_options --> - <integer name="config_selected_udfps_touch_detection">2</integer> + <integer name="config_selected_udfps_touch_detection">3</integer> <!-- An array of arrays of side fingerprint sensor properties relative to each display. Note: this value is temporary and is expected to be queried directly diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index faaa2e8cc0f5..91fbf6bb9f06 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -861,10 +861,10 @@ <!-- "Switch" is a verb; it means to change user profile by tapping another user profile name. --> <string name="managed_profile_label">Switch to work profile</string> - <!-- "Switch" is a verb; it means to change user profile by tapping another user profile name. --> + <!-- "Switch" is a verb; it means to change user profile by switching to an app in the personal profile. --> <string name="user_owner_app_label">Switch to personal <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string> - <!-- "Switch" is a verb; it means to change user profile by tapping another user profile name. --> + <!-- "Switch" is a verb; it means to change user profile by switching to an app in the work profile. --> <string name="managed_profile_app_label">Switch to work <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string> <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. --> @@ -5392,11 +5392,11 @@ <string name="app_streaming_blocked_message" product="tablet">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your tablet instead.</string> <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message" product="default">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your phone instead.</string> - <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> + <!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv">This app is requesting additional security. Try on your Android TV device instead.</string> - <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> + <!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet">This app is requesting additional security. Try on your tablet instead.</string> - <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> + <!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default">This app is requesting additional security. Try on your phone instead.</string> <!-- Message shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_settings_dialog" product="tv">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your Android TV device instead.</string> @@ -5907,10 +5907,24 @@ <string name="miniresolver_open_in_personal">Open in personal <xliff:g id="app" example="YouTube">%s</xliff:g>?</string> <!-- Dialog title. User must choose between opening content in a cross-profile app or same-profile browser. [CHAR LIMIT=NONE] --> <string name="miniresolver_open_in_work">Open in work <xliff:g id="app" example="YouTube">%s</xliff:g>?</string> + <!-- Dialog title. User must place the phone call in the other profile, or cancel. [CHAR LIMIT=NONE] --> + <string name="miniresolver_call_in_work">Call from work app?</string> + <!-- Dialog title. User much choose between opening content in a cross-profile app or cancelling. [CHAR LIMIT=NONE] --> + <string name="miniresolver_switch_to_work">Switch to work app?</string> + <!-- Dialog text. Shown when the user is unable to make a phone call from a personal app due to restrictions set + by their organization, and so must switch to a work app or cancel. [CHAR LIMIT=NONE] --> + <string name="miniresolver_call_information">Your organization only allows you to make calls from work apps</string> + <!-- Dialog text. Shown when the user is unable to send a text message from a personal app due to restrictions set + by their organization, and so must switch to a work app or cancel. [CHAR LIMIT=NONE] --> + <string name="miniresolver_sms_information">Your organization only allows you to send messages from work apps</string> <!-- Button option. Open the link in the personal browser. [CHAR LIMIT=NONE] --> <string name="miniresolver_use_personal_browser">Use personal browser</string> <!-- Button option. Open the link in the work browser. [CHAR LIMIT=NONE] --> <string name="miniresolver_use_work_browser">Use work browser</string> + <!-- Button option. Action to place a phone call. [CHAR LIMIT=NONE] --> + <string name="miniresolver_call">Call</string> + <!-- Button option. Action to open an app in the work profile. [CHAR LIMIT=NONE] --> + <string name="miniresolver_switch">Switch</string> <!-- Icc depersonalization related strings --> <!-- Label text for PIN entry widget on SIM Network Depersonalization panel [CHAR LIMIT=none] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 73e3b417f67a..1e9e8ddbf63f 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -675,8 +675,6 @@ <java-symbol type="string" name="contentServiceSyncNotificationTitle" /> <java-symbol type="string" name="contentServiceTooManyDeletesNotificationDesc" /> <java-symbol type="string" name="csd_dose_reached_warning" /> - <java-symbol type="string" name="csd_dose_repeat_warning" /> - <java-symbol type="string" name="csd_entering_RS2_warning" /> <java-symbol type="string" name="csd_momentary_exposure_warning" /> <java-symbol type="string" name="date_and_time" /> <java-symbol type="string" name="date_picker_decrement_day_button" /> @@ -1579,6 +1577,14 @@ <java-symbol type="string" name="miniresolver_open_work" /> <java-symbol type="string" name="miniresolver_use_personal_browser" /> <java-symbol type="string" name="miniresolver_use_work_browser" /> + <java-symbol type="string" name="miniresolver_call_in_work" /> + <java-symbol type="string" name="miniresolver_switch_to_work" /> + <java-symbol type="string" name="miniresolver_call" /> + <java-symbol type="string" name="miniresolver_switch" /> + <java-symbol type="string" name="miniresolver_call_information" /> + <java-symbol type="string" name="miniresolver_sms_information" /> + <java-symbol type="id" name="miniresolver_info_section" /> + <java-symbol type="id" name="miniresolver_info_section_text" /> <java-symbol type="id" name="button_open" /> <java-symbol type="id" name="use_same_profile_browser" /> diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java index 55680abb159d..9595332afc6c 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java @@ -229,20 +229,36 @@ public class InsetsSourceTest { @Test public void testGetIndex() { - for (int index = 0; index < 2048; index++) { - for (int type = FIRST; type <= LAST; type = type << 1) { - final int id = InsetsSource.createId(null, index, type); - assertEquals(index, InsetsSource.getIndex(id)); + // Here doesn't iterate all the owners, or the test cannot be done before timeout. + for (int owner = 0; owner < 100; owner++) { + for (int index = 0; index < 2048; index++) { + for (int type = FIRST; type <= LAST; type = type << 1) { + final int id = InsetsSource.createId(owner, index, type); + final int indexFromId = InsetsSource.getIndex(id); + assertEquals("index and indexFromId must be the same. id=" + id + + ", owner=" + owner + + ", index=" + index + + ", type=" + type + + ", indexFromId=" + indexFromId + ".", index, indexFromId); + } } } } @Test public void testGetType() { - for (int index = 0; index < 2048; index++) { - for (int type = FIRST; type <= LAST; type = type << 1) { - final int id = InsetsSource.createId(null, index, type); - assertEquals(type, InsetsSource.getType(id)); + // Here doesn't iterate all the owners, or the test cannot be done before timeout. + for (int owner = 0; owner < 100; owner++) { + for (int index = 0; index < 2048; index++) { + for (int type = FIRST; type <= LAST; type = type << 1) { + final int id = InsetsSource.createId(owner, index, type); + final int typeFromId = InsetsSource.getType(id); + assertEquals("type and typeFromId must be the same. id=" + id + + ", owner=" + owner + + ", index=" + index + + ", type=" + type + + ", typeFromId=" + typeFromId + ".", type, typeFromId); + } } } } diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 7c2759af156f..92c0dabfd13e 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -3865,6 +3865,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/WindowState.java" }, + "1511273241": { + "message": "Refreshing activity for camera compatibility treatment, activityRecord=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "1518495446": { "message": "removeWindowToken: Attempted to remove non-existing token: %s", "level": "WARN", @@ -4297,12 +4303,6 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, - "1967643923": { - "message": "Refershing activity for camera compatibility treatment, activityRecord=%s", - "level": "VERBOSE", - "group": "WM_DEBUG_STATES", - "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" - }, "1967975839": { "message": "Changing app %s visible=%b performLayout=%b", "level": "VERBOSE", diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java index 658d92cc7489..ff423c2c6e39 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -342,11 +342,22 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, mRearDisplayPresentationController); DeviceStateRequest concurrentDisplayStateRequest = DeviceStateRequest.newBuilder( mConcurrentDisplayState).build(); - mDeviceStateManager.requestState( - concurrentDisplayStateRequest, - mExecutor, - deviceStateCallback - ); + + try { + mDeviceStateManager.requestState( + concurrentDisplayStateRequest, + mExecutor, + deviceStateCallback + ); + } catch (SecurityException e) { + // If a SecurityException occurs when invoking DeviceStateManager#requestState + // (e.g. if the caller is not in the foreground, or if it does not have the required + // permissions), we should first clean up our local state before re-throwing the + // SecurityException to the caller. Otherwise, subsequent attempts to + // startRearDisplayPresentationSession will always fail. + mRearDisplayPresentationController = null; + throw e; + } } } diff --git a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml index 49491a7b572c..69b339ad77fb 100644 --- a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml @@ -20,47 +20,48 @@ android:focusable="false" android:focusableInTouchMode="false" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:theme="@style/ReachabilityEduHandLayout"> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" android:layout_gravity="center_horizontal|top" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_up_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" android:layout_gravity="center_vertical|right" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_right_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_left_hand" android:layout_gravity="center_vertical|left" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_left_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" android:layout_gravity="center_horizontal|bottom" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_down_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> </com.android.wm.shell.compatui.ReachabilityEduLayout> diff --git a/libs/WindowManager/Shell/res/values-night/styles.xml b/libs/WindowManager/Shell/res/values-night/styles.xml index 758c99db7bb4..4871f7ada627 100644 --- a/libs/WindowManager/Shell/res/values-night/styles.xml +++ b/libs/WindowManager/Shell/res/values-night/styles.xml @@ -17,12 +17,10 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android"> - <style name="ReachabilityEduHandLayout"> + <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat"> <item name="android:focusable">false</item> <item name="android:focusableInTouchMode">false</item> <item name="android:background">@android:color/transparent</item> - <item name="android:contentDescription">@string/restart_button_description</item> - <item name="android:visibility">invisible</item> <item name="android:lineSpacingExtra">-1sp</item> <item name="android:textSize">12sp</item> <item name="android:textAlignment">center</item> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 2b3888854d5a..8635c56b7bc6 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -151,12 +151,10 @@ </item> </style> - <style name="ReachabilityEduHandLayout"> + <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat.Light"> <item name="android:focusable">false</item> <item name="android:focusableInTouchMode">false</item> <item name="android:background">@android:color/transparent</item> - <item name="android:contentDescription">@string/restart_button_description</item> - <item name="android:visibility">invisible</item> <item name="android:lineSpacingExtra">-1sp</item> <item name="android:textSize">12sp</item> <item name="android:textAlignment">center</item> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index e7ec7aa164e9..8e9fc1131e4a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1784,13 +1784,17 @@ public class BubbleStackView extends FrameLayout // We're expanded while the last bubble is being removed. Let the scrim animate away // and then remove our views (removing the icon view triggers the removal of the // bubble window so do that at the end of the animation so we see the scrim animate). + BadgedImageView iconView = bubble.getIconView(); showScrim(false, () -> { mRemovingLastBubbleWhileExpanded = false; bubble.cleanupExpandedView(); - mBubbleContainer.removeView(bubble.getIconView()); + if (iconView != null) { + mBubbleContainer.removeView(iconView); + } bubble.cleanupViews(); // cleans up the icon view updateExpandedView(); // resets state for no expanded bubble }); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } // Remove it from the views @@ -1816,6 +1820,7 @@ public class BubbleStackView extends FrameLayout // If a bubble is suppressed, it is not attached to the container. Clean it up. if (bubble.isSuppressed()) { bubble.cleanupViews(); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); } else { Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 28368ef37061..74ef57e4baae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -77,6 +77,8 @@ import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; @@ -562,6 +564,28 @@ public abstract class WMShellBaseModule { } // + // Keyguard transitions (optional feature) + // + + @WMSingleton + @Provides + static KeyguardTransitionHandler provideKeyguardTransitionHandler( + ShellInit shellInit, + Transitions transitions, + @ShellMainThread Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor) { + return new KeyguardTransitionHandler( + shellInit, transitions, mainHandler, mainExecutor); + } + + @WMSingleton + @Provides + static KeyguardTransitions provideKeyguardTransitions( + KeyguardTransitionHandler handler) { + return handler.asKeyguardTransitions(); + } + + // // Display areas // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index f3130d358ec1..be0288e238ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -60,6 +60,7 @@ import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; import com.android.wm.shell.freeform.FreeformTaskTransitionObserver; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -532,9 +533,10 @@ public abstract class WMShellModule { Optional<SplitScreenController> splitScreenOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, Optional<RecentsTransitionHandler> recentsTransitionHandler, + KeyguardTransitionHandler keyguardTransitionHandler, Transitions transitions) { return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional, - pipTouchHandlerOptional, recentsTransitionHandler); + pipTouchHandlerOptional, recentsTransitionHandler, keyguardTransitionHandler); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java new file mode 100644 index 000000000000..4d8075a9b56c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.keyguard; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; +import static android.view.WindowManager.TRANSIT_NONE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; +import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; + +import static com.android.wm.shell.util.TransitionUtil.isOpeningType; +import static com.android.wm.shell.util.TransitionUtil.isClosingType; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.RemoteException; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.util.ArrayMap; +import android.util.Log; +import android.view.SurfaceControl; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback; + +import java.util.Map; + +/** + * The handler for Keyguard enter/exit and occlude/unocclude animations. + * + * <p>This takes the highest priority. + */ +public class KeyguardTransitionHandler implements Transitions.TransitionHandler { + private static final String TAG = "KeyguardTransition"; + + private final Transitions mTransitions; + private final Handler mMainHandler; + private final ShellExecutor mMainExecutor; + + private final Map<IBinder, IRemoteTransition> mStartedTransitions = new ArrayMap<>(); + + /** + * Local IRemoteTransition implementations registered by the keyguard service. + * @see KeyguardTransitions + */ + private IRemoteTransition mExitTransition = null; + private IRemoteTransition mOccludeTransition = null; + private IRemoteTransition mOccludeByDreamTransition = null; + private IRemoteTransition mUnoccludeTransition = null; + + public KeyguardTransitionHandler( + @NonNull ShellInit shellInit, + @NonNull Transitions transitions, + @NonNull Handler mainHandler, + @NonNull ShellExecutor mainExecutor) { + mTransitions = transitions; + mMainHandler = mainHandler; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mTransitions.addHandler(this); + } + + /** + * Interface for SystemUI implementations to set custom Keyguard exit/occlude handlers. + */ + @ExternalThread + public KeyguardTransitions asKeyguardTransitions() { + return new KeyguardTransitionsImpl(); + } + + public static boolean handles(TransitionInfo info) { + return (info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0 + || (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0 + || info.getType() == TRANSIT_KEYGUARD_OCCLUDE + || info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull TransitionFinishCallback finishCallback) { + if (!handles(info)) { + return false; + } + + boolean hasOpeningOcclude = false; + boolean hasOpeningDream = false; + boolean hasClosingApp = false; + + // Check for occluding/dream/closing apps + for (int i = info.getChanges().size() - 1; i >= 0; i--) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (isOpeningType(change.getMode())) { + if (change.hasFlags(FLAG_OCCLUDES_KEYGUARD)) { + hasOpeningOcclude = true; + } + if (change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_DREAM) { + hasOpeningDream = true; + } + } else if (isClosingType(change.getMode())) { + hasClosingApp = true; + } + } + + // Choose a transition applicable for the changes and keyguard state. + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) { + return startAnimation(mExitTransition, + "going-away", + transition, info, startTransaction, finishTransaction, finishCallback); + } + if (hasOpeningOcclude || info.getType() == TRANSIT_KEYGUARD_OCCLUDE) { + if (hasOpeningDream) { + return startAnimation(mOccludeByDreamTransition, + "occlude-by-dream", + transition, info, startTransaction, finishTransaction, finishCallback); + } else { + return startAnimation(mOccludeTransition, + "occlude", + transition, info, startTransaction, finishTransaction, finishCallback); + } + } else if (hasClosingApp || info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE) { + return startAnimation(mUnoccludeTransition, + "unocclude", + transition, info, startTransaction, finishTransaction, finishCallback); + } else { + Log.wtf(TAG, "Failed to play: " + info); + return false; + } + } + + private boolean startAnimation(IRemoteTransition remoteHandler, String description, + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "start keyguard %s transition, info = %s", description, info); + + try { + remoteHandler.startAnimation(transition, info, startTransaction, + new IRemoteTransitionFinishedCallback.Stub() { + @Override + public void onTransitionFinished( + WindowContainerTransaction wct, SurfaceControl.Transaction sct) { + mMainExecutor.execute(() -> { + finishCallback.onTransitionFinished(wct, null); + }); + } + }); + mStartedTransitions.put(transition, remoteHandler); + } catch (RemoteException e) { + Log.wtf(TAG, "RemoteException thrown from local IRemoteTransition", e); + return false; + } + startTransaction.clear(); + return true; + } + + @Override + public void mergeAnimation(@NonNull IBinder nextTransition, @NonNull TransitionInfo nextInfo, + @NonNull SurfaceControl.Transaction nextT, @NonNull IBinder currentTransition, + @NonNull TransitionFinishCallback nextFinishCallback) { + final IRemoteTransition playing = mStartedTransitions.get(currentTransition); + + if (playing == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "unknown keyguard transition %s", currentTransition); + return; + } + + if (nextInfo.getType() == TRANSIT_SLEEP) { + // An empty SLEEP transition comes in as a signal to abort transitions whenever a sleep + // token is held. In cases where keyguard is showing, we are running the animation for + // the device sleeping/waking, so it's best to ignore this and keep playing anyway. + return; + } else { + finishAnimationImmediately(currentTransition); + } + } + + @Override + public void onTransitionConsumed(IBinder transition, boolean aborted, + SurfaceControl.Transaction finishTransaction) { + finishAnimationImmediately(transition); + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + private void finishAnimationImmediately(IBinder transition) { + final IRemoteTransition playing = mStartedTransitions.get(transition); + + if (playing != null) { + final IBinder fakeTransition = new Binder(); + final TransitionInfo fakeInfo = new TransitionInfo(TRANSIT_SLEEP, 0x0); + final SurfaceControl.Transaction fakeT = new SurfaceControl.Transaction(); + final FakeFinishCallback fakeFinishCb = new FakeFinishCallback(); + try { + playing.mergeAnimation(fakeTransition, fakeInfo, fakeT, transition, fakeFinishCb); + } catch (RemoteException e) { + // There is no good reason for this to happen because the player is a local object + // implementing an AIDL interface. + Log.wtf(TAG, "RemoteException thrown from KeyguardService transition", e); + } + } + } + + private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub { + @Override + public void onTransitionFinished( + WindowContainerTransaction wct, SurfaceControl.Transaction t) { + return; + } + } + + @ExternalThread + private final class KeyguardTransitionsImpl implements KeyguardTransitions { + @Override + public void register( + IRemoteTransition exitTransition, + IRemoteTransition occludeTransition, + IRemoteTransition occludeByDreamTransition, + IRemoteTransition unoccludeTransition) { + mMainExecutor.execute(() -> { + mExitTransition = exitTransition; + mOccludeTransition = occludeTransition; + mOccludeByDreamTransition = occludeByDreamTransition; + mUnoccludeTransition = unoccludeTransition; + }); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java new file mode 100644 index 000000000000..b4b327f0eff2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.keyguard; + +import android.annotation.NonNull; +import android.window.IRemoteTransition; + +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface exposed to SystemUI Keyguard to register handlers for running + * animations on keyguard visibility changes. + * + * TODO(b/274954192): Merge the occludeTransition and occludeByDream handlers and just let the + * keyguard handler make the decision on which version it wants to play. + */ +@ExternalThread +public interface KeyguardTransitions { + /** + * Registers a set of remote transitions for Keyguard. + */ + default void register( + @NonNull IRemoteTransition unlockTransition, + @NonNull IRemoteTransition occludeTransition, + @NonNull IRemoteTransition occludeByDreamTransition, + @NonNull IRemoteTransition unoccludeTransition) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 6fa1861a23e2..42633b722649 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -40,6 +40,7 @@ import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -63,6 +64,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, private PipTransitionController mPipHandler; private RecentsTransitionHandler mRecentsHandler; private StageCoordinator mSplitHandler; + private final KeyguardTransitionHandler mKeyguardHandler; private static class MixedTransition { static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; @@ -76,6 +78,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, /** Recents transition while split-screen foreground. */ static final int TYPE_RECENTS_DURING_SPLIT = 4; + /** Keyguard exit/occlude/unocclude transition. */ + static final int TYPE_KEYGUARD = 5; + /** The default animation for this mixed transition. */ static final int ANIM_TYPE_DEFAULT = 0; @@ -126,8 +131,10 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, - Optional<RecentsTransitionHandler> recentsHandlerOptional) { + Optional<RecentsTransitionHandler> recentsHandlerOptional, + KeyguardTransitionHandler keyguardHandler) { mPlayer = player; + mKeyguardHandler = keyguardHandler; if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent() && splitScreenControllerOptional.isPresent()) { // Add after dependencies because it is higher priority @@ -263,12 +270,26 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + MixedTransition mixed = null; for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { if (mActiveTransitions.get(i).mTransition != transition) continue; mixed = mActiveTransitions.get(i); break; } + + // Offer Keyguard the opportunity to take over lock transitions - ideally we could know by + // the time of handleRequest, but we need more information than is available at that time. + if (KeyguardTransitionHandler.handles(info)) { + if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Converting mixed transition into a keyguard transition"); + onTransitionConsumed(transition, false, null); + } + mixed = new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition); + mActiveTransitions.add(mixed); + } + if (mixed == null) return false; if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { @@ -282,6 +303,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction, finishCallback); + } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) { + return mKeyguardHandler.startAnimation( + transition, info, startTransaction, finishTransaction, finishCallback); } else { mActiveTransitions.remove(mixed); throw new IllegalStateException("Starting mixed animation without a known mixed type? " @@ -574,6 +598,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, } mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) { + mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); } else { throw new IllegalStateException("Playing a mixed transition with unknown type? " + mixed.mType); @@ -597,6 +623,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); + } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) { + mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 21dca95d056a..6a2468a7eaa2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -435,6 +435,23 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { backgroundColorForTransition = uiContext.getColor(R.color.overview_background); } + if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN + && TransitionUtil.isOpeningType(info.getType())) { + // Need to flip the z-order of opening/closing because the WALLPAPER_OPEN + // always animates the closing task over the opening one while + // traditionally, an OPEN transition animates the opening over the closing. + + // See Transitions#setupAnimHierarchy for details about these variables. + final int numChanges = info.getChanges().size(); + final int zSplitLine = numChanges + 1; + if (TransitionUtil.isOpeningType(mode)) { + final int layer = zSplitLine - i; + startTransaction.setLayer(change.getLeash(), layer); + } else if (TransitionUtil.isClosingType(mode)) { + final int layer = zSplitLine + numChanges - i; + startTransaction.setLayer(change.getLeash(), layer); + } + } } final float cornerRadius; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index ef2a511177b2..a242c72db8b3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -94,8 +94,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (!Transitions.SHELL_TRANSITIONS_ROTATION && TransitionUtil.hasDisplayChange(info) - && !TransitionUtil.alwaysReportToKeyguard(info)) { + if (!Transitions.SHELL_TRANSITIONS_ROTATION && TransitionUtil.hasDisplayChange(info)) { // Note that if the remote doesn't have permission ACCESS_SURFACE_FLINGER, some // operations of the start transaction may be ignored. mRequestedRemotes.remove(transition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index ab27c55f20dc..f33b0778a1b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -71,6 +71,7 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -686,7 +687,11 @@ public class Transitions implements RemoteCallable<Transitions>, active.mToken, info, active.mStartT, active.mFinishT); } - if (info.getRootCount() == 0 && !TransitionUtil.alwaysReportToKeyguard(info)) { + /* + * Some transitions we always need to report to keyguard even if they are empty. + * TODO (b/274954192): Remove this once keyguard dispatching fully moves to Shell. + */ + if (info.getRootCount() == 0 && !KeyguardTransitionHandler.handles(info)) { // No root-leashes implies that the transition is empty/no-op, so just do // housekeeping and return. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots in %s so" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java index 402b0ce7c87c..ef0c7a8a755b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java @@ -85,23 +85,6 @@ public class TransitionUtil { return false; } - /** - * Some transitions we always need to report to keyguard even if they are empty. - * TODO (b/274954192): Remove this once keyguard dispatching moves to Shell. - */ - public static boolean alwaysReportToKeyguard(TransitionInfo info) { - // occlusion status of activities can change while screen is off so there will be no - // visibility change but we still need keyguardservice to be notified. - if (info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE) return true; - - // It's possible for some activities to stop with bad timing (esp. since we can't yet - // queue activity transitions initiated by apps) that results in an empty transition for - // keyguard going-away. In general, we should should always report Keyguard-going-away. - if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) return true; - - return false; - } - /** Returns `true` if `change` is a wallpaper. */ public static boolean isWallpaper(TransitionInfo.Change change) { return (change.getTaskInfo() == null) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 95ed42a222b8..ce11b2604559 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -296,15 +296,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /** * Fade in the resize veil */ - void showResizeVeil() { - mResizeVeil.showVeil(mTaskSurface); + void showResizeVeil(Rect taskBounds) { + mResizeVeil.showVeil(mTaskSurface, taskBounds); } /** * Set new bounds for the resize veil */ void updateResizeVeil(Rect newBounds) { - mResizeVeil.relayout(newBounds); + mResizeVeil.updateResizeVeil(newBounds); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java index 4899453ac991..b785ef005062 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java @@ -104,7 +104,7 @@ public class ResizeVeil { /** * Animate veil's alpha to 1, fading it in. */ - public void showVeil(SurfaceControl parentSurface) { + public void showVeil(SurfaceControl parentSurface, Rect taskBounds) { // Parent surface can change, ensure it is up to date. SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); if (!parentSurface.equals(mParentSurface)) { @@ -115,8 +115,6 @@ public class ResizeVeil { int backgroundColorId = getBackgroundColorId(); mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId)); - t.show(mVeilSurface) - .apply(); final ValueAnimator animator = new ValueAnimator(); animator.setFloatValues(0f, 1f); animator.setDuration(RESIZE_ALPHA_DURATION); @@ -124,19 +122,32 @@ public class ResizeVeil { t.setAlpha(mVeilSurface, animator.getAnimatedFraction()); t.apply(); }); - animator.start(); + + relayout(taskBounds, t); + t.show(mVeilSurface) + .addTransactionCommittedListener(mContext.getMainExecutor(), () -> animator.start()) + .setAlpha(mVeilSurface, 0); + mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); } /** * Update veil bounds to match bounds changes. * @param newBounds bounds to update veil to. */ - public void relayout(Rect newBounds) { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + private void relayout(Rect newBounds, SurfaceControl.Transaction t) { mViewHost.relayout(newBounds.width(), newBounds.height()); t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height()); t.setPosition(mParentSurface, newBounds.left, newBounds.top); t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); + } + + /** + * Calls relayout to update task and veil bounds. + * @param newBounds bounds to update veil to. + */ + public void updateResizeVeil(Rect newBounds) { + SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + relayout(newBounds, t); mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 3a3ac4ca7d0c..56475a800ef9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -77,7 +77,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback { mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); mRepositionStartPoint.set(x, y); if (isResizing()) { - mDesktopWindowDecoration.showResizeVeil(); + mDesktopWindowDecoration.showResizeVeil(mTaskBoundsAtDragStart); if (!mDesktopWindowDecoration.mTaskInfo.isFocused) { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 445a73a2ad38..337e40df44bf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -123,7 +123,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil() + verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningEnd( STARTING_BOUNDS.left.toFloat(), @@ -180,7 +180,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil() + verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.right.toFloat() + 10, @@ -224,7 +224,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil() + verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.left.toFloat(), diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index 9a06be006dca..701a87f0cce4 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -51,6 +51,9 @@ using namespace android; sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const { const skcms_ICCProfile* encodedProfile = mCodec->getICCProfile(); if (encodedProfile) { + if (encodedProfile->has_CICP) { + return mCodec->computeOutputColorSpace(kN32_SkColorType); + } // If the profile maps directly to an SkColorSpace, that SkColorSpace // will be returned. Otherwise, nullptr will be returned. In either // case, using this SkColorSpace results in doing no color correction. diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index c58ba6868eb5..8d5967bbd461 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -27,6 +27,7 @@ #include "include/gpu/GpuTypes.h" // from Skia #include "utils/GLUtils.h" #include <effects/GainmapRenderer.h> +#include "renderthread/CanvasContext.h" namespace android { namespace uirenderer { @@ -131,6 +132,8 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { mat4.getColMajor(&info.transform[0]); info.color_space_ptr = canvas->imageInfo().colorSpace(); info.currentHdrSdrRatio = getTargetHdrSdrRatio(info.color_space_ptr); + info.fboColorType = canvas->imageInfo().colorType(); + info.shouldDither = renderthread::CanvasContext::shouldDither(); // ensure that the framebuffer that the webview will render into is bound before we clear // the stencil and/or draw the functor. diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index e299d12b1d67..b62711f50c94 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -15,22 +15,25 @@ */ #include "VkFunctorDrawable.h" -#include <private/hwui/DrawVkInfo.h> #include <GrBackendDrawableInfo.h> #include <SkAndroidFrameworkUtils.h> #include <SkImage.h> #include <SkM44.h> #include <gui/TraceUtils.h> +#include <private/hwui/DrawVkInfo.h> #include <utils/Color.h> #include <utils/Trace.h> #include <vk/GrVkTypes.h> + #include <thread> + +#include "effects/GainmapRenderer.h" +#include "renderthread/CanvasContext.h" #include "renderthread/RenderThread.h" #include "renderthread/VulkanManager.h" #include "thread/ThreadBase.h" #include "utils/TimeUtils.h" -#include "effects/GainmapRenderer.h" namespace android { namespace uirenderer { @@ -75,6 +78,7 @@ void VkFunctorDrawHandler::draw(const GrBackendDrawableInfo& info) { .clip_bottom = mClip.fBottom, .is_layer = !vulkan_info.fFromSwapchainOrAndroidWindow, .currentHdrSdrRatio = getTargetHdrSdrRatio(mImageInfo.colorSpace()), + .shouldDither = renderthread::CanvasContext::shouldDither(), }; mat4.getColMajor(¶ms.transform[0]); params.secondary_command_buffer = vulkan_info.fSecondaryCommandBuffer; diff --git a/libs/hwui/private/hwui/DrawGlInfo.h b/libs/hwui/private/hwui/DrawGlInfo.h index 7888c8719e88..eb1f9304a5c8 100644 --- a/libs/hwui/private/hwui/DrawGlInfo.h +++ b/libs/hwui/private/hwui/DrawGlInfo.h @@ -18,6 +18,7 @@ #define ANDROID_HWUI_DRAW_GL_INFO_H #include <SkColorSpace.h> +#include <SkColorType.h> namespace android { namespace uirenderer { @@ -91,6 +92,12 @@ struct DrawGlInfo { // be baked into the color_space_ptr, so this is just to indicate the amount of extended // range is available if desired float currentHdrSdrRatio; + + // Whether or not dithering is globally enabled + bool shouldDither; + + // The color type of the destination framebuffer + SkColorType fboColorType; }; // struct DrawGlInfo } // namespace uirenderer diff --git a/libs/hwui/private/hwui/DrawVkInfo.h b/libs/hwui/private/hwui/DrawVkInfo.h index 8f7063d72314..122080658927 100644 --- a/libs/hwui/private/hwui/DrawVkInfo.h +++ b/libs/hwui/private/hwui/DrawVkInfo.h @@ -76,6 +76,9 @@ struct VkFunctorDrawParams { // be baked into the color_space_ptr, so this is just to indicate the amount of extended // range is available if desired float currentHdrSdrRatio; + + // Whether or not dithering is globally enabled + bool shouldDither; }; } // namespace uirenderer diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 2bd400dba346..16b35ffcabac 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -1078,6 +1078,12 @@ void CanvasContext::startHintSession() { mHintSessionWrapper.init(); } +bool CanvasContext::shouldDither() { + CanvasContext* self = getActiveContext(); + if (!self) return false; + return self->mColorMode != ColorMode::Default; +} + } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index 08e24245d16c..5219b5757008 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -234,6 +234,8 @@ public: void startHintSession(); + static bool shouldDither(); + private: CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline, diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4759689335e9..b1d2e33df3f7 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -3730,12 +3730,7 @@ public class AudioManager { @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @RequiresPermission(Manifest.permission.BLUETOOTH_STACK) public void setA2dpSuspended(boolean enable) { - final IAudioService service = getService(); - try { - service.setA2dpSuspended(enable); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + AudioSystem.setParameters("A2dpSuspended=" + enable); } /** @@ -3748,12 +3743,7 @@ public class AudioManager { @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @RequiresPermission(Manifest.permission.BLUETOOTH_STACK) public void setLeAudioSuspended(boolean enable) { - final IAudioService service = getService(); - try { - service.setLeAudioSuspended(enable); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + AudioSystem.setParameters("LeAudioSuspended=" + enable); } /** diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 7ce189ba85d5..fe5afc5a717e 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -231,12 +231,6 @@ interface IAudioService { void setBluetoothScoOn(boolean on); - @EnforcePermission("BLUETOOTH_STACK") - void setA2dpSuspended(boolean on); - - @EnforcePermission("BLUETOOTH_STACK") - void setLeAudioSuspended(boolean enable); - boolean isBluetoothScoOn(); void setBluetoothA2dpOn(boolean on); diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java index b888739016c7..946185a3c420 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.os.Bundle; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.webkit.WebView; @@ -183,7 +184,14 @@ public class SlicePurchaseActivity extends Activity { setContentView(mWebView); // Load the URL - mWebView.loadUrl(mUrl.toString()); + String userData = mIntent.getStringExtra(SlicePurchaseController.EXTRA_USER_DATA); + if (TextUtils.isEmpty(userData)) { + logd("Starting WebView with url: " + mUrl.toString()); + mWebView.loadUrl(mUrl.toString()); + } else { + logd("Starting WebView with url: " + mUrl.toString() + ", userData=" + userData); + mWebView.postUrl(mUrl.toString(), userData.getBytes()); + } } private static void logd(@NonNull String s) { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java index 097e47f58db1..483fb8c451ae 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java @@ -30,6 +30,7 @@ import android.content.Intent; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.res.AssetFileDescriptor; +import android.Manifest; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -178,6 +179,9 @@ public class InstallStaging extends AlertActivity { params.setInstallerPackageName(intent.getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME)); params.setInstallReason(PackageManager.INSTALL_REASON_USER); + // Disable full screen intent usage by for sideloads. + params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT, + PackageInstaller.SessionParams.PERMISSION_STATE_DENIED); if (pfd != null) { try { diff --git a/packages/SettingsLib/ActivityEmbedding/OWNERS b/packages/SettingsLib/ActivityEmbedding/OWNERS index 702240273e43..6f1ab9bc7d2d 100644 --- a/packages/SettingsLib/ActivityEmbedding/OWNERS +++ b/packages/SettingsLib/ActivityEmbedding/OWNERS @@ -1,5 +1,4 @@ # Default reviewers for this and subdirectories. -arcwang@google.com -chiujason@google.com +sunnyshao@google.com # Emergency approvers in case the above are not available diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml b/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml index ad888e538ffe..1b80cc6f6fde 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml @@ -20,5 +20,6 @@ <style name="MainSwitchText.Settingslib" parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title.Inverse"> <item name="android:textSize">20sp</item> <item name="android:fontFamily">@string/settingslib_config_headlineFontFamily</item> + <item name="android:textColor">@android:color/black</item> </style> </resources> diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS index 81340f561bc1..b2b7b61f002c 100644 --- a/packages/SettingsLib/OWNERS +++ b/packages/SettingsLib/OWNERS @@ -1,4 +1,7 @@ # People who can approve changes for submission +cantol@google.com +chiujason@google.com +cipson@google.com dsandler@android.com edgarwang@google.com evanlaird@google.com diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt index c6e13a14063f..aa5ce301e9f5 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt @@ -20,12 +20,21 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.TabRow import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import com.android.settingslib.spa.framework.theme.SettingsDimension import kotlin.math.absoluteValue import kotlinx.coroutines.launch @@ -41,7 +50,7 @@ fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit Column { val coroutineScope = rememberCoroutineScope() - val pagerState = rememberPagerState() + val pagerState by rememberPageStateFixed() TabRow( selectedTabIndex = pagerState.currentPage, @@ -69,3 +78,28 @@ fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit } } } + +/** + * Gets the state of [PagerState]. + * + * This is a work around. + * + * TODO: Remove this and replace with rememberPageState() after the Compose Foundation 1.5.0-alpha04 + * updated in the platform. + */ +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun rememberPageStateFixed(): State<PagerState> { + var currentPage by rememberSaveable { mutableStateOf(0) } + val pagerStateHolder = remember { mutableStateOf(PagerState(currentPage)) } + LaunchedEffect(LocalConfiguration.current.orientation) { + // Reset pager state to fix an issue after configuration change. + // When we declare android:configChanges="orientation" in the manifest, the pager state is + // in a weird state after configuration change. + pagerStateHolder.value = PagerState(currentPage) + } + SideEffect { + currentPage = pagerStateHolder.value.currentPage + } + return pagerStateHolder +} diff --git a/packages/SettingsLib/res/drawable/ic_person_add.xml b/packages/SettingsLib/res/drawable/ic_person_add.xml new file mode 100644 index 000000000000..d138c69acb6a --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_person_add.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M18,14V11H15V9H18V6H20V9H23V11H20V14ZM9,12Q7.35,12 6.175,10.825Q5,9.65 5,8Q5,6.35 6.175,5.175Q7.35,4 9,4Q10.65,4 11.825,5.175Q13,6.35 13,8Q13,9.65 11.825,10.825Q10.65,12 9,12ZM1,20V17.2Q1,16.35 1.438,15.637Q1.875,14.925 2.6,14.55Q4.15,13.775 5.75,13.387Q7.35,13 9,13Q10.65,13 12.25,13.387Q13.85,13.775 15.4,14.55Q16.125,14.925 16.562,15.637Q17,16.35 17,17.2V20ZM3,18H15V17.2Q15,16.925 14.863,16.7Q14.725,16.475 14.5,16.35Q13.15,15.675 11.775,15.337Q10.4,15 9,15Q7.6,15 6.225,15.337Q4.85,15.675 3.5,16.35Q3.275,16.475 3.138,16.7Q3,16.925 3,17.2ZM9,10Q9.825,10 10.413,9.412Q11,8.825 11,8Q11,7.175 10.413,6.588Q9.825,6 9,6Q8.175,6 7.588,6.588Q7,7.175 7,8Q7,8.825 7.588,9.412Q8.175,10 9,10ZM9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8ZM9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Z"/> +</vector> diff --git a/packages/SettingsLib/res/layout/dialog_with_icon.xml b/packages/SettingsLib/res/layout/dialog_with_icon.xml index 9081ca5cc1bb..54f8096b87bf 100644 --- a/packages/SettingsLib/res/layout/dialog_with_icon.xml +++ b/packages/SettingsLib/res/layout/dialog_with_icon.xml @@ -13,87 +13,88 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center" - android:padding="@dimen/grant_admin_dialog_padding" - android:paddingBottom="0dp"> - <ImageView - android:id="@+id/dialog_with_icon_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:contentDescription=""/> - <TextView - android:id="@+id/dialog_with_icon_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - style="@style/DialogWithIconTitle" - android:text="@string/user_grant_admin_title" - android:textDirection="locale"/> - <TextView - android:id="@+id/dialog_with_icon_message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="10dp" - android:gravity="center" - style="@style/TextAppearanceSmall" - android:text="" - android:textDirection="locale"/> + +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/new_user_dialog_id"> + <LinearLayout - android:id="@+id/custom_layout" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="center" - android:paddingBottom="0dp"> - </LinearLayout> - <LinearLayout - android:id="@+id/button_panel" - android:orientation="horizontal" - android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" - android:paddingBottom="0dp"> - <Button - android:id="@+id/button_cancel" - style="@style/DialogButtonNegative" + android:padding="@dimen/dialog_content_padding"> + <ImageView + android:id="@+id/dialog_with_icon_icon" android:layout_width="wrap_content" - android:buttonCornerRadius="28dp" android:layout_height="wrap_content" - android:visibility="gone"/> - <Space - android:layout_width="0dp" - android:layout_height="1dp" - android:layout_weight="1" > - </Space> - <Button - android:id="@+id/button_back" + android:importantForAccessibility="no"/> + <TextView + android:id="@+id/dialog_with_icon_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/DialogButtonNegative" - android:buttonCornerRadius="40dp" - android:clickable="true" - android:focusable="true" - android:text="Back" - android:visibility="gone" - /> - <Space - android:layout_width="0dp" - android:layout_height="1dp" - android:layout_weight="0.05" - > - </Space> - <Button - android:id="@+id/button_ok" + android:gravity="center" + style="@style/DialogWithIconTitle"/> + <TextView + android:id="@+id/dialog_with_icon_message" android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/DialogButtonPositive" - android:clickable="true" - android:focusable="true" - android:visibility="gone" - /> + android:padding="10dp" + android:gravity="center" + style="@style/TextAppearanceSmall"/> + + <LinearLayout + android:id="@+id/custom_layout" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"> + </LinearLayout> + + <LinearLayout + android:id="@+id/button_panel" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"> + + <Button + android:id="@+id/button_cancel" + style="@style/DialogButtonNegative" + android:layout_width="wrap_content" + android:buttonCornerRadius="28dp" + android:layout_height="wrap_content" + android:visibility="gone"/> + + <Space + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_weight="1"> + </Space> + + <Button + android:id="@+id/button_back" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/DialogButtonNegative" + android:buttonCornerRadius="40dp" + android:visibility="gone"/> + + <Space + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_weight="0.05"> + </Space> + + <Button + android:id="@+id/button_ok" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/DialogButtonPositive" + android:visibility="gone" + /> + </LinearLayout> </LinearLayout> -</LinearLayout> +</ScrollView> diff --git a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml index 33c2e4855b64..4ffaf1b0c3e4 100644 --- a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml +++ b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml @@ -28,7 +28,8 @@ android:orientation="vertical"> <TextView android:id="@+id/user_info_title" - android:layout_width="wrap_content" + android:gravity="center" + android:layout_width="match_parent" android:layout_height="wrap_content" style="@style/EditUserDialogTitle" android:text="@string/user_info_settings_title" diff --git a/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml b/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml index d6acac2cf495..26d82456973c 100644 --- a/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml +++ b/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml @@ -14,10 +14,14 @@ ~ limitations under the License. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/grant_admin_view" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="@dimen/grant_admin_dialog_padding"> + android:paddingLeft="@dimen/dialog_content_padding" + android:paddingBottom="@dimen/dialog_content_padding" + android:paddingRight="@dimen/dialog_content_padding"> + <RadioGroup android:id="@+id/choose_admin" android:layout_width="match_parent" @@ -25,13 +29,19 @@ android:orientation="vertical"> <RadioButton android:id="@+id/grant_admin_yes" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="@dimen/radio_button_padding" + android:paddingBottom="@dimen/radio_button_padding" + android:paddingStart="@dimen/radio_button_padding_start" android:text="@string/grant_admin"/> <RadioButton android:id="@+id/grant_admin_no" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="@dimen/radio_button_padding" + android:paddingBottom="@dimen/radio_button_padding" + android:paddingStart="@dimen/radio_button_padding_start" android:text="@string/not_grant_admin"/> </RadioGroup> </LinearLayout> diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index 2372c802168c..91549d73dfdb 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -112,8 +112,9 @@ <dimen name="broadcast_dialog_btn_minHeight">44dp</dimen> <dimen name="broadcast_dialog_margin">16dp</dimen> - <!-- Size of grant admin privileges dialog padding --> - <dimen name="grant_admin_dialog_padding">16dp</dimen> + <dimen name="dialog_content_padding">16dp</dimen> + <dimen name="radio_button_padding">12dp</dimen> + <dimen name="radio_button_padding_start">8dp</dimen> <dimen name="dialog_button_horizontal_padding">16dp</dimen> <dimen name="dialog_button_vertical_padding">8dp</dimen> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 7b4c86207a2a..67e3e036cc3e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -318,7 +318,10 @@ public class CachedBluetoothDeviceManager { return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, state); } - if (profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { + if (profileId == BluetoothProfile.HEADSET + || profileId == BluetoothProfile.A2DP + || profileId == BluetoothProfile.LE_AUDIO + || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, state); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index 356bb82a92e0..8269b56c425b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -28,6 +28,7 @@ import androidx.annotation.ChecksSdkIntAtLeast; import com.android.internal.annotations.VisibleForTesting; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -83,14 +84,14 @@ public class CsipDeviceManager { boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) { final int groupId = newDevice.getGroupId(); if (isValidGroupId(groupId)) { - final CachedBluetoothDevice CsipDevice = getCachedDevice(groupId); - log("setMemberDeviceIfNeeded, main: " + CsipDevice + ", member: " + newDevice); + final CachedBluetoothDevice mainDevice = getCachedDevice(groupId); + log("setMemberDeviceIfNeeded, main: " + mainDevice + ", member: " + newDevice); // Just add one of the coordinated set from a pair in the list that is shown in the UI. // Once there is other devices with the same groupId, to add new device as member // devices. - if (CsipDevice != null) { - CsipDevice.addMemberDevice(newDevice); - newDevice.setName(CsipDevice.getName()); + if (mainDevice != null) { + mainDevice.addMemberDevice(newDevice); + newDevice.setName(mainDevice.getName()); return true; } } @@ -152,14 +153,7 @@ public class CsipDeviceManager { log("onGroupIdChanged: groupId is invalid"); return; } - log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString()); - List<CachedBluetoothDevice> memberDevicesList = getMemberDevicesList(groupId); - CachedBluetoothDevice newMainDevice = - getPreferredMainDeviceWithoutConectionState(groupId, memberDevicesList); - - log("onGroupIdChanged: The mainDevice= " + newMainDevice - + " and the memberDevicesList of groupId= " + groupId + " =" + memberDevicesList); - addMemberDevicesIntoMainDevice(memberDevicesList, newMainDevice); + updateRelationshipOfGroupDevices(groupId); } // @return {@code true}, the event is processed inside the method. It is for updating @@ -168,61 +162,30 @@ public class CsipDeviceManager { boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state) { log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state); - switch (state) { - case BluetoothProfile.STATE_CONNECTED: - onGroupIdChanged(cachedDevice.getGroupId()); - CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice); - if (mainDevice != null) { - if (mainDevice.isConnected()) { - // When main device exists and in connected state, receiving member device - // connection. To refresh main device UI - mainDevice.refresh(); - return true; - } else { - // When both LE Audio devices are disconnected, receiving member device - // connection. To switch content and dispatch to notify UI change - mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice); - mainDevice.switchMemberDeviceContent(cachedDevice); - mainDevice.refresh(); - // It is necessary to do remove and add for updating the mapping on - // preference and device - mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); - return true; - } - } - break; - case BluetoothProfile.STATE_DISCONNECTED: - mainDevice = findMainDevice(cachedDevice); - if (mainDevice != null) { - // When main device exists, receiving sub device disconnection - // To update main device UI - mainDevice.refresh(); - return true; - } - final Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice(); - if (memberSet.isEmpty()) { - break; - } - for (CachedBluetoothDevice device : memberSet) { - if (device.isConnected()) { - log("set device: " + device + " as the main device"); - // Main device is disconnected and sub device is connected - // To copy data from sub device to main device - mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); - cachedDevice.switchMemberDeviceContent(device); - cachedDevice.refresh(); - // It is necessary to do remove and add for updating the mapping on - // preference and device - mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice); - return true; - } - } - break; - default: - // Do not handle this state. + if (state != BluetoothProfile.STATE_CONNECTED + && state != BluetoothProfile.STATE_DISCONNECTED) { + return false; } - return false; + return updateRelationshipOfGroupDevices(cachedDevice.getGroupId()); + } + + @VisibleForTesting + boolean updateRelationshipOfGroupDevices(int groupId) { + if (!isValidGroupId(groupId)) { + log("The device is not group."); + return false; + } + log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString()); + + // Get the preferred main device by getPreferredMainDeviceWithoutConectionState + List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId); + CachedBluetoothDevice preferredMainDevice = + getPreferredMainDevice(groupId, groupDevicesList); + log("The preferredMainDevice= " + preferredMainDevice + + " and the groupDevicesList of groupId= " + groupId + + " =" + groupDevicesList); + return addMemberDevicesIntoMainDevice(groupId, preferredMainDevice); } CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { @@ -262,114 +225,153 @@ public class CsipDeviceManager { return false; } - private List<CachedBluetoothDevice> getMemberDevicesList(int groupId) { - return mCachedDevices.stream() - .filter(cacheDevice -> cacheDevice.getGroupId() == groupId) - .collect(Collectors.toList()); + @VisibleForTesting + List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) { + List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>(); + if (!isValidGroupId(groupId)) { + return groupDevicesList; + } + for (CachedBluetoothDevice item : mCachedDevices) { + if (groupId != item.getGroupId()) { + continue; + } + groupDevicesList.add(item); + groupDevicesList.addAll(item.getMemberDevice()); + } + return groupDevicesList; } - private CachedBluetoothDevice getPreferredMainDeviceWithoutConectionState(int groupId, - List<CachedBluetoothDevice> memberDevicesList) { - // First, priority connected lead device from LE profile - // Second, the DUAL mode device which has A2DP/HFP and LE audio - // Last, any one of LE device in the list. - if (memberDevicesList == null || memberDevicesList.isEmpty()) { + @VisibleForTesting + CachedBluetoothDevice getPreferredMainDevice(int groupId, + List<CachedBluetoothDevice> groupDevicesList) { + // How to select the preferred main device? + // 1. The DUAL mode connected device which has A2DP/HFP and LE audio. + // 2. One of connected LE device in the list. Default is the lead device from LE profile. + // 3. If there is no connected device, then reset the relationship. Set the DUAL mode + // deviced as the main device. Otherwise, set any one of the device. + if (groupDevicesList == null || groupDevicesList.isEmpty()) { return null; } + CachedBluetoothDevice dualModeDevice = groupDevicesList.stream() + .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .anyMatch(profile -> profile instanceof LeAudioProfile)) + .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .anyMatch(profile -> profile instanceof A2dpProfile + || profile instanceof HeadsetProfile)) + .findFirst().orElse(null); + if (dualModeDevice != null && dualModeDevice.isConnected()) { + log("getPreferredMainDevice: The connected DUAL mode device"); + return dualModeDevice; + } + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); - final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) + final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT()) ? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null; - if (mainBluetoothDevice != null) { + if (leAudioLeadDevice != null) { log("getPreferredMainDevice: The LeadDevice from LE profile is " - + mainBluetoothDevice.getAnonymizedAddress()); + + leAudioLeadDevice.getAnonymizedAddress()); } - - // 1st - CachedBluetoothDevice newMainDevice = - mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null; - if (newMainDevice != null) { - if (newMainDevice.isConnected()) { - log("getPreferredMainDevice: The connected LeadDevice from LE profile"); - return newMainDevice; - } else { - log("getPreferredMainDevice: The LeadDevice is not connect."); - } - } else { + CachedBluetoothDevice leAudioLeadCachedDevice = + leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null; + if (leAudioLeadCachedDevice == null) { log("getPreferredMainDevice: The LeadDevice is not in the all of devices list"); + } else if (leAudioLeadCachedDevice.isConnected()) { + log("getPreferredMainDevice: The connected LeadDevice from LE profile"); + return leAudioLeadCachedDevice; } - - // 2nd - newMainDevice = memberDevicesList.stream() - .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() - .anyMatch(profile -> profile instanceof A2dpProfile - || profile instanceof HeadsetProfile)) + CachedBluetoothDevice oneOfConnectedDevices = groupDevicesList.stream() + .filter(cachedDevice -> cachedDevice.isConnected()) .findFirst().orElse(null); - if (newMainDevice != null) { - log("getPreferredMainDevice: The DUAL mode device"); - return newMainDevice; + if (oneOfConnectedDevices != null) { + log("getPreferredMainDevice: One of the connected devices."); + return oneOfConnectedDevices; } + if (dualModeDevice != null) { + log("getPreferredMainDevice: The DUAL mode device."); + return dualModeDevice; + } // last - if (!memberDevicesList.isEmpty()) { - newMainDevice = memberDevicesList.get(0); + if (!groupDevicesList.isEmpty()) { + log("getPreferredMainDevice: One of the group devices."); + return groupDevicesList.get(0); } - return newMainDevice; + return null; } - private void addMemberDevicesIntoMainDevice(List<CachedBluetoothDevice> memberDevicesList, - CachedBluetoothDevice newMainDevice) { - if (newMainDevice == null) { + @VisibleForTesting + boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) { + boolean hasChanged = false; + if (preferredMainDevice == null) { log("addMemberDevicesIntoMainDevice: No main device. Do nothing."); - return; + return hasChanged; } - if (memberDevicesList.isEmpty()) { - log("addMemberDevicesIntoMainDevice: No member device in list. Do nothing."); - return; - } - CachedBluetoothDevice mainDeviceOfNewMainDevice = findMainDevice(newMainDevice); - boolean isMemberInOtherMainDevice = mainDeviceOfNewMainDevice != null; - if (!memberDevicesList.contains(newMainDevice) && isMemberInOtherMainDevice) { - log("addMemberDevicesIntoMainDevice: The 'new main device' is not in list, and it is " - + "the member at other device. Do switch main and member."); + + // If the current main device is not preferred main device, then set it as new main device. + // Otherwise, do nothing. + BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice(); + CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice); + boolean hasPreferredMainDeviceAlreadyBeenMainDevice = + mainDeviceOfPreferredMainDevice == null; + + if (!hasPreferredMainDeviceAlreadyBeenMainDevice) { + // preferredMainDevice has not been the main device. + // switch relationship between the mainDeviceOfPreferredMainDevice and + // PreferredMainDevice + + log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. " + + "Do switch relationship between the mainDeviceOfPreferredMainDevice and " + + "PreferredMainDevice"); // To switch content and dispatch to notify UI change - mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfNewMainDevice); - mainDeviceOfNewMainDevice.switchMemberDeviceContent(newMainDevice); - mainDeviceOfNewMainDevice.refresh(); + mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice); + mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice); + mainDeviceOfPreferredMainDevice.refresh(); // It is necessary to do remove and add for updating the mapping on // preference and device - mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfNewMainDevice); - } else { - log("addMemberDevicesIntoMainDevice: Set new main device"); - for (CachedBluetoothDevice memberDeviceItem : memberDevicesList) { - if (memberDeviceItem.equals(newMainDevice)) { + mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice); + hasChanged = true; + } + + // If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are + // the same groupId, then combine them and also keep the preferred main device as main + // device. + List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream() + .filter(device -> device.getGroupId() == groupId) + .collect(Collectors.toList()); + boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1; + // Update the new main of CachedBluetoothDevice, since it may be changed in above step. + final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); + preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice); + if (haveMultiMainDevicesInAllOfDevicesList) { + // put another devices into main device. + for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) { + if (deviceItem.getDevice() == null || deviceItem.getDevice().equals( + bluetoothDeviceOfPreferredMainDevice)) { continue; } - Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice(); - if (!memberSet.isEmpty()) { - for (CachedBluetoothDevice memberSetItem : memberSet) { - if (!memberSetItem.equals(newMainDevice)) { - newMainDevice.addMemberDevice(memberSetItem); - } + + Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice(); + for (CachedBluetoothDevice memberSetItem : memberSet) { + if (!memberSetItem.equals(preferredMainDevice)) { + preferredMainDevice.addMemberDevice(memberSetItem); } - memberSet.clear(); } - - newMainDevice.addMemberDevice(memberDeviceItem); - mCachedDevices.remove(memberDeviceItem); - mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem); - } - - if (!mCachedDevices.contains(newMainDevice)) { - mCachedDevices.add(newMainDevice); - mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice); + memberSet.clear(); + preferredMainDevice.addMemberDevice(deviceItem); + mCachedDevices.remove(deviceItem); + mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem); + hasChanged = true; } } - log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " - + mCachedDevices); + if (hasChanged) { + log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " + + mCachedDevices); + } + return hasChanged; } private void log(String msg) { diff --git a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt index d55a027c2374..b386e5e12504 100644 --- a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt +++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt @@ -36,6 +36,9 @@ data class UdfpsOverlayParams( /** Same as [sensorBounds], but in native resolution. */ val nativeSensorBounds = Rect(sensorBounds).apply { scale(1f / scaleFactor) } + /** Same as [overlayBounds], but in native resolution. */ + val nativeOverlayBounds = Rect(overlayBounds).apply { scale(1f / scaleFactor) } + /** See [android.view.DisplayInfo.logicalWidth] */ val logicalDisplayWidth = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java new file mode 100644 index 000000000000..e61c8f5ab152 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.users; + +import android.annotation.IntDef; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.UserIcons; +import com.android.settingslib.R; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.drawable.CircleFramedDrawable; +import com.android.settingslib.utils.CustomDialogHelper; +import com.android.settingslib.utils.ThreadUtils; + +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This class encapsulates a Dialog for editing the user nickname and photo. + */ +public class CreateUserDialogController { + + private static final String KEY_AWAITING_RESULT = "awaiting_result"; + private static final String KEY_CURRENT_STATE = "current_state"; + private static final String KEY_SAVED_PHOTO = "pending_photo"; + private static final String KEY_SAVED_NAME = "saved_name"; + private static final String KEY_IS_ADMIN = "admin_status"; + private static final String KEY_ADD_USER_LONG_MESSAGE_DISPLAYED = + "key_add_user_long_message_displayed"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({EXIT_DIALOG, INITIAL_DIALOG, GRANT_ADMIN_DIALOG, + EDIT_NAME_DIALOG, CREATE_USER_AND_CLOSE}) + public @interface AddUserState {} + + private static final int EXIT_DIALOG = -1; + private static final int INITIAL_DIALOG = 0; + private static final int GRANT_ADMIN_DIALOG = 1; + private static final int EDIT_NAME_DIALOG = 2; + private static final int CREATE_USER_AND_CLOSE = 3; + + private @AddUserState int mCurrentState; + + private CustomDialogHelper mCustomDialogHelper; + + private EditUserPhotoController mEditUserPhotoController; + private Bitmap mSavedPhoto; + private String mSavedName; + private Drawable mSavedDrawable; + private Boolean mIsAdmin; + private Dialog mUserCreationDialog; + private View mGrantAdminView; + private View mEditUserInfoView; + private EditText mUserNameView; + private Activity mActivity; + private ActivityStarter mActivityStarter; + private boolean mWaitingForActivityResult; + private NewUserData mSuccessCallback; + + private final String mFileAuthority; + + public CreateUserDialogController(String fileAuthority) { + mFileAuthority = fileAuthority; + } + + /** + * Resets saved values. + */ + public void clear() { + mUserCreationDialog = null; + mCustomDialogHelper = null; + mEditUserPhotoController = null; + mSavedPhoto = null; + mSavedName = null; + mSavedDrawable = null; + mIsAdmin = null; + mActivity = null; + mActivityStarter = null; + mGrantAdminView = null; + mEditUserInfoView = null; + mUserNameView = null; + mSuccessCallback = null; + mCurrentState = INITIAL_DIALOG; + } + + /** + * Notifies that the containing activity or fragment was reinitialized. + */ + public void onRestoreInstanceState(Bundle savedInstanceState) { + String pendingPhoto = savedInstanceState.getString(KEY_SAVED_PHOTO); + if (pendingPhoto != null) { + ThreadUtils.postOnBackgroundThread(() -> { + mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap( + new File(pendingPhoto)); + }); + } + mCurrentState = savedInstanceState.getInt(KEY_CURRENT_STATE); + if (savedInstanceState.containsKey(KEY_IS_ADMIN)) { + mIsAdmin = savedInstanceState.getBoolean(KEY_IS_ADMIN); + } + mSavedName = savedInstanceState.getString(KEY_SAVED_NAME); + mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false); + } + + /** + * Notifies that the containing activity or fragment is saving its state for later use. + */ + public void onSaveInstanceState(Bundle savedInstanceState) { + if (mUserCreationDialog != null && mEditUserPhotoController != null) { + // Bitmap cannot be stored into bundle because it may exceed parcel limit + // Store it in a temporary file instead + ThreadUtils.postOnBackgroundThread(() -> { + File file = mEditUserPhotoController.saveNewUserPhotoBitmap(); + if (file != null) { + savedInstanceState.putString(KEY_SAVED_PHOTO, file.getPath()); + } + }); + } + if (mIsAdmin != null) { + savedInstanceState.putBoolean(KEY_IS_ADMIN, Boolean.TRUE.equals(mIsAdmin)); + } + savedInstanceState.putString(KEY_SAVED_NAME, mUserNameView.getText().toString().trim()); + savedInstanceState.putInt(KEY_CURRENT_STATE, mCurrentState); + savedInstanceState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); + } + + /** + * Notifies that an activity has started. + */ + public void startingActivityForResult() { + mWaitingForActivityResult = true; + } + + /** + * Notifies that the result from activity has been received. + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mWaitingForActivityResult = false; + if (mEditUserPhotoController != null) { + mEditUserPhotoController.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * Creates an add user dialog with option to set the user's name and photo and choose their + * admin status. + */ + public Dialog createDialog(Activity activity, + ActivityStarter activityStarter, boolean isMultipleAdminEnabled, + NewUserData successCallback, Runnable cancelCallback) { + mActivity = activity; + mCustomDialogHelper = new CustomDialogHelper(activity); + mSuccessCallback = successCallback; + mActivityStarter = activityStarter; + addCustomViews(isMultipleAdminEnabled); + mUserCreationDialog = mCustomDialogHelper.getDialog(); + updateLayout(); + mUserCreationDialog.setOnDismissListener(view -> { + cancelCallback.run(); + clear(); + }); + mUserCreationDialog.setCanceledOnTouchOutside(true); + return mUserCreationDialog; + } + + private void addCustomViews(boolean isMultipleAdminEnabled) { + addGrantAdminView(); + addUserInfoEditView(); + mCustomDialogHelper.setPositiveButton(R.string.next, view -> { + mCurrentState++; + if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + mCurrentState++; + } + updateLayout(); + }); + mCustomDialogHelper.setNegativeButton(R.string.back, view -> { + mCurrentState--; + if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + mCurrentState--; + } + updateLayout(); + }); + return; + } + + private void updateLayout() { + switch (mCurrentState) { + case INITIAL_DIALOG: + mEditUserInfoView.setVisibility(View.GONE); + mGrantAdminView.setVisibility(View.GONE); + final SharedPreferences preferences = mActivity.getPreferences( + Context.MODE_PRIVATE); + final boolean longMessageDisplayed = preferences.getBoolean( + KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, false); + final int messageResId = longMessageDisplayed + ? R.string.user_add_user_message_short + : R.string.user_add_user_message_long; + if (!longMessageDisplayed) { + preferences.edit().putBoolean( + KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, + true).apply(); + } + Drawable icon = mActivity.getDrawable(R.drawable.ic_person_add); + mCustomDialogHelper.setVisibility(mCustomDialogHelper.ICON, true) + .setVisibility(mCustomDialogHelper.TITLE, true) + .setVisibility(mCustomDialogHelper.MESSAGE, true) + .setIcon(icon) + .setButtonEnabled(true) + .setTitle(R.string.user_add_user_title) + .setMessage(messageResId) + .setNegativeButtonText(R.string.cancel) + .setPositiveButtonText(R.string.next); + break; + case GRANT_ADMIN_DIALOG: + mEditUserInfoView.setVisibility(View.GONE); + mGrantAdminView.setVisibility(View.VISIBLE); + mCustomDialogHelper + .setVisibility(mCustomDialogHelper.ICON, true) + .setVisibility(mCustomDialogHelper.TITLE, true) + .setVisibility(mCustomDialogHelper.MESSAGE, true) + .setIcon(mActivity.getDrawable(R.drawable.ic_admin_panel_settings)) + .setTitle(R.string.user_grant_admin_title) + .setMessage(R.string.user_grant_admin_message) + .setNegativeButtonText(R.string.back) + .setPositiveButtonText(R.string.next); + if (mIsAdmin == null) { + mCustomDialogHelper.setButtonEnabled(false); + } + break; + case EDIT_NAME_DIALOG: + mCustomDialogHelper + .setVisibility(mCustomDialogHelper.ICON, false) + .setVisibility(mCustomDialogHelper.TITLE, false) + .setVisibility(mCustomDialogHelper.MESSAGE, false) + .setNegativeButtonText(R.string.back) + .setPositiveButtonText(R.string.done); + mEditUserInfoView.setVisibility(View.VISIBLE); + mGrantAdminView.setVisibility(View.GONE); + break; + case CREATE_USER_AND_CLOSE: + Drawable newUserIcon = mEditUserPhotoController != null + ? mEditUserPhotoController.getNewUserPhotoDrawable() + : null; + + String newName = mUserNameView.getText().toString().trim(); + String defaultName = mActivity.getString(R.string.user_new_user_name); + String userName = !newName.isEmpty() ? newName : defaultName; + + if (mSuccessCallback != null) { + mSuccessCallback.onSuccess(userName, newUserIcon, + Boolean.TRUE.equals(mIsAdmin)); + } + mCustomDialogHelper.getDialog().dismiss(); + clear(); + break; + case EXIT_DIALOG: + mCustomDialogHelper.getDialog().dismiss(); + break; + default: + if (mCurrentState < EXIT_DIALOG) { + mCurrentState = EXIT_DIALOG; + updateLayout(); + } else { + mCurrentState = CREATE_USER_AND_CLOSE; + updateLayout(); + } + break; + } + } + + private Drawable getUserIcon(Drawable defaultUserIcon) { + if (mSavedPhoto != null) { + mSavedDrawable = CircleFramedDrawable.getInstance(mActivity, mSavedPhoto); + return mSavedDrawable; + } + return defaultUserIcon; + } + + private void addUserInfoEditView() { + mEditUserInfoView = View.inflate(mActivity, R.layout.edit_user_info_dialog_content, null); + mCustomDialogHelper.addCustomView(mEditUserInfoView); + setUserName(); + ImageView userPhotoView = mEditUserInfoView.findViewById(R.id.user_photo); + + // if oldUserIcon param is null then we use a default gray user icon + Drawable defaultUserIcon = UserIcons.getDefaultUserIcon( + mActivity.getResources(), UserHandle.USER_NULL, false); + // in case a new photo was selected and the activity got recreated we have to load the image + Drawable userIcon = getUserIcon(defaultUserIcon); + userPhotoView.setImageDrawable(userIcon); + + if (isChangePhotoRestrictedByBase(mActivity)) { + // some users can't change their photos so we need to remove the suggestive icon + mEditUserInfoView.findViewById(R.id.add_a_photo_icon).setVisibility(View.GONE); + } else { + RestrictedLockUtils.EnforcedAdmin adminRestriction = + getChangePhotoAdminRestriction(mActivity); + if (adminRestriction != null) { + userPhotoView.setOnClickListener(view -> + RestrictedLockUtils.sendShowAdminSupportDetailsIntent( + mActivity, adminRestriction)); + } else { + mEditUserPhotoController = createEditUserPhotoController(userPhotoView); + } + } + } + + private void setUserName() { + mUserNameView = mEditUserInfoView.findViewById(R.id.user_name); + if (mSavedName == null) { + mUserNameView.setText(R.string.user_new_user_name); + } else { + mUserNameView.setText(mSavedName); + } + } + + private void addGrantAdminView() { + mGrantAdminView = View.inflate(mActivity, R.layout.grant_admin_dialog_content, null); + mCustomDialogHelper.addCustomView(mGrantAdminView); + RadioGroup radioGroup = mGrantAdminView.findViewById(R.id.choose_admin); + radioGroup.setOnCheckedChangeListener((group, checkedId) -> { + mCustomDialogHelper.setButtonEnabled(true); + mIsAdmin = checkedId == R.id.grant_admin_yes; + } + ); + if (Boolean.TRUE.equals(mIsAdmin)) { + RadioButton button = radioGroup.findViewById(R.id.grant_admin_yes); + button.setChecked(true); + } else if (Boolean.FALSE.equals(mIsAdmin)) { + RadioButton button = radioGroup.findViewById(R.id.grant_admin_no); + button.setChecked(true); + } + } + + @VisibleForTesting + boolean isChangePhotoRestrictedByBase(Context context) { + return RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); + } + + @VisibleForTesting + RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) { + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); + } + + @VisibleForTesting + EditUserPhotoController createEditUserPhotoController(ImageView userPhotoView) { + return new EditUserPhotoController(mActivity, mActivityStarter, userPhotoView, + mSavedPhoto, mSavedDrawable, mFileAuthority); + } + + public boolean isActive() { + return mCustomDialogHelper != null && mCustomDialogHelper.getDialog() != null; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java new file mode 100644 index 000000000000..3d18b59258b3 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.users; + +import android.graphics.drawable.Drawable; + +/** + * Defines a callback when a new user data is filled out. + */ +public interface NewUserData { + + /** + * Consumes data relevant to new user that needs to be created. + * @param userName New user name. + * @param userImage New user icon. + * @param isNewUserAdmin A boolean that indicated whether new user has admin status. + */ + void onSuccess(String userName, Drawable userImage, Boolean isNewUserAdmin); + +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java new file mode 100644 index 000000000000..9b41bc4f8b00 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothCsipSetCoordinator; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.os.Parcel; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class CsipDeviceManagerTest { + private final static String DEVICE_NAME_1 = "TestName_1"; + private final static String DEVICE_NAME_2 = "TestName_2"; + private final static String DEVICE_NAME_3 = "TestName_3"; + private final static String DEVICE_ALIAS_1 = "TestAlias_1"; + private final static String DEVICE_ALIAS_2 = "TestAlias_2"; + private final static String DEVICE_ALIAS_3 = "TestAlias_3"; + private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; + private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; + private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; + private final static int GROUP1 = 1; + private final BluetoothClass DEVICE_CLASS_1 = + createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); + private final BluetoothClass DEVICE_CLASS_2 = + createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE); + + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + @Mock + private LocalBluetoothProfileManager mLocalProfileManager; + @Mock + private BluetoothEventManager mBluetoothEventManager; + @Mock + private BluetoothDevice mDevice1; + @Mock + private BluetoothDevice mDevice2; + @Mock + private BluetoothDevice mDevice3; + @Mock + private HeadsetProfile mHfpProfile; + @Mock + private A2dpProfile mA2dpProfile; + @Mock + private LeAudioProfile mLeAudioProfile; + + private CachedBluetoothDevice mCachedDevice1; + private CachedBluetoothDevice mCachedDevice2; + private CachedBluetoothDevice mCachedDevice3; + private CachedBluetoothDeviceManager mCachedDeviceManager; + private CsipDeviceManager mCsipDeviceManager; + private Context mContext; + private List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); + + + private BluetoothClass createBtClass(int deviceClass) { + Parcel p = Parcel.obtain(); + p.writeInt(deviceClass); + p.setDataPosition(0); // reset position of parcel before passing to constructor + + BluetoothClass bluetoothClass = BluetoothClass.CREATOR.createFromParcel(p); + p.recycle(); + return bluetoothClass; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + when(mDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1); + when(mDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2); + when(mDevice3.getAddress()).thenReturn(DEVICE_ADDRESS_3); + when(mDevice1.getName()).thenReturn(DEVICE_NAME_1); + when(mDevice2.getName()).thenReturn(DEVICE_NAME_2); + when(mDevice3.getName()).thenReturn(DEVICE_NAME_3); + when(mDevice1.getAlias()).thenReturn(DEVICE_ALIAS_1); + when(mDevice2.getAlias()).thenReturn(DEVICE_ALIAS_2); + when(mDevice3.getAlias()).thenReturn(DEVICE_ALIAS_3); + when(mDevice1.getBluetoothClass()).thenReturn(DEVICE_CLASS_1); + when(mDevice2.getBluetoothClass()).thenReturn(DEVICE_CLASS_2); + when(mDevice3.getBluetoothClass()).thenReturn(DEVICE_CLASS_2); + when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalProfileManager); + when(mLocalProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + when(mLocalProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); + when(mLocalProfileManager.getHeadsetProfile()).thenReturn(mHfpProfile); + + when(mLeAudioProfile.getConnectedGroupLeadDevice(anyInt())).thenReturn(null); + mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager); + when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); + + mCachedDevices = mCachedDeviceManager.mCachedDevices; + mCsipDeviceManager = mCachedDeviceManager.mCsipDeviceManager; + + // Setup the default for testing + mCachedDevice1 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1)); + mCachedDevice2 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2)); + mCachedDevice3 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3)); + + mCachedDevice1.setGroupId(GROUP1); + mCachedDevice2.setGroupId(GROUP1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + mCachedDevices.add(mCachedDevice1); + mCachedDevices.add(mCachedDevice3); + + List<LocalBluetoothProfile> profiles = new ArrayList<LocalBluetoothProfile>(); + profiles.add(mHfpProfile); + profiles.add(mA2dpProfile); + profiles.add(mLeAudioProfile); + when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice1.isConnected()).thenReturn(true); + + profiles.clear(); + profiles.add(mLeAudioProfile); + when(mCachedDevice2.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice2.isConnected()).thenReturn(true); + + profiles.clear(); + profiles.add(mHfpProfile); + profiles.add(mA2dpProfile); + when(mCachedDevice3.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice3.isConnected()).thenReturn(true); + + } + + @Test + public void onProfileConnectionStateChangedIfProcessed_profileIsConnecting_returnOff() { + assertThat(mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_CONNECTING)).isFalse(); + } + + @Test + public void onProfileConnectionStateChangedIfProcessed_profileIsDisconnecting_returnOff() { + assertThat(mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_DISCONNECTING)).isFalse(); + } + + @Test + public void updateRelationshipOfGroupDevices_invalidGroupId_returnOff() { + assertThat(mCsipDeviceManager.updateRelationshipOfGroupDevices( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID)).isFalse(); + } + + @Test + public void getGroupDevicesFromAllOfDevicesList_invalidGroupId_returnEmpty() { + assertThat(mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID).isEmpty()).isTrue(); + } + + @Test + public void getGroupDevicesFromAllOfDevicesList_validGroupId_returnGroupDevices() { + List<CachedBluetoothDevice> expectedList = new ArrayList<>(); + expectedList.add(mCachedDevice1); + expectedList.add(mCachedDevice2); + + assertThat(mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1)) + .isEqualTo(expectedList); + } + + @Test + public void getPreferredMainDevice_dualModeDevice_returnDualModeDevice() { + CachedBluetoothDevice expectedDevice = mCachedDevice1; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDualModeDevice_returnLeadDevice() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mLeAudioProfile.getConnectedGroupLeadDevice(anyInt())).thenReturn(mDevice2); + CachedBluetoothDevice expectedDevice = mCachedDevice2; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDualModeDeviceNoLeadDevice_returnConnectedOne() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mCachedDevice2.isConnected()).thenReturn(true); + CachedBluetoothDevice expectedDevice = mCachedDevice2; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDevice_returnDualModeDevice() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mCachedDevice2.isConnected()).thenReturn(false); + CachedBluetoothDevice expectedDevice = mCachedDevice1; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDeviceNoDualMode_returnFirstOneDevice() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mCachedDevice2.isConnected()).thenReturn(false); + List<LocalBluetoothProfile> profiles = new ArrayList<LocalBluetoothProfile>(); + profiles.add(mLeAudioProfile); + when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + CachedBluetoothDevice expectedDevice = mCachedDevice1; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void addMemberDevicesIntoMainDevice_noPreferredDevice_returnFalseAndNoChangeList() { + CachedBluetoothDevice preferredDevice = null; + List<CachedBluetoothDevice> expectedList = new ArrayList<>(); + for (CachedBluetoothDevice item : mCachedDevices) { + expectedList.add(item); + } + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isFalse(); + for (CachedBluetoothDevice expectedItem : expectedList) { + assertThat(mCachedDevices.contains(expectedItem)).isTrue(); + } + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndNoOtherInList_noChangeList() + { + // Condition: The preferredDevice is main and no other main device in top list + // Expected Result: return false and the list is no changed + CachedBluetoothDevice preferredDevice = mCachedDevice1; + List<CachedBluetoothDevice> expectedList = new ArrayList<>(); + for (CachedBluetoothDevice item : mCachedDevices) { + expectedList.add(item); + } + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isFalse(); + for (CachedBluetoothDevice expectedItem : expectedList) { + assertThat(mCachedDevices.contains(expectedItem)).isTrue(); + } + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() { + // Condition: The preferredDevice is main and there is another main device in top list + // Expected Result: return true and there is the preferredDevice in top list + CachedBluetoothDevice preferredDevice = mCachedDevice1; + mCachedDevice1.getMemberDevice().clear(); + mCachedDevices.clear(); + mCachedDevices.add(preferredDevice); + mCachedDevices.add(mCachedDevice2); + mCachedDevices.add(mCachedDevice3); + + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isTrue(); + assertThat(mCachedDevices.contains(preferredDevice)).isTrue(); + assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse(); + assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); + assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMember_returnTrue() { + // Condition: The preferredDevice is member + // Expected Result: return true and there is the preferredDevice in top list + CachedBluetoothDevice preferredDevice = mCachedDevice2; + BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isTrue(); + // expected main is mCachedDevice1 which is the main of preferredDevice, since system + // switch the relationship between preferredDevice and the main of preferredDevice + assertThat(mCachedDevices.contains(mCachedDevice1)).isTrue(); + assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse(); + assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice2); + assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() { + // Condition: The preferredDevice is member and there are two main device in top list + // Expected Result: return true and there is the preferredDevice in top list + CachedBluetoothDevice preferredDevice = mCachedDevice2; + BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); + mCachedDevice3.setGroupId(GROUP1); + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isTrue(); + // expected main is mCachedDevice1 which is the main of preferredDevice, since system + // switch the relationship between preferredDevice and the main of preferredDevice + assertThat(mCachedDevices.contains(mCachedDevice1)).isTrue(); + assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse(); + assertThat(mCachedDevices.contains(mCachedDevice3)).isFalse(); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice2); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3); + assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java new file mode 100644 index 000000000000..e989ed27508b --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.users; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.RadioButton; + +import androidx.fragment.app.FragmentActivity; + +import com.android.settingslib.R; +import com.android.settingslib.RestrictedLockUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ActivityController; + +@RunWith(RobolectricTestRunner.class) +public class CreateUserDialogControllerTest { + + @Mock + private ActivityStarter mActivityStarter; + + private boolean mPhotoRestrictedByBase; + private Activity mActivity; + private TestCreateUserDialogController mUnderTest; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mActivity = spy(ActivityController.of(new FragmentActivity()).get()); + mActivity.setTheme(R.style.Theme_AppCompat_DayNight); + mUnderTest = new TestCreateUserDialogController(); + mPhotoRestrictedByBase = false; + } + + @Test + public void positiveButton_grantAdminStage_noValue_OkButtonShouldBeDisabled() { + Runnable cancelCallback = mock(Runnable.class); + + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(false); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + dialog.dismiss(); + } + + @Test + public void positiveButton_MultipleAdminDisabled_shouldSkipGrantAdminStage() { + Runnable cancelCallback = mock(Runnable.class); + + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, false, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button back = dialog.findViewById(R.id.button_cancel); + back.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + dialog.dismiss(); + } + + @Test + public void editUserInfoController_shouldOnlyBeVisibleOnLastStage() { + Runnable cancelCallback = mock(Runnable.class); + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE); + next.performClick(); + assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()) + .isEqualTo(View.VISIBLE); + dialog.dismiss(); + } + + @Test + public void positiveButton_MultipleAdminEnabled_shouldShowGrantAdminStage() { + Runnable cancelCallback = mock(Runnable.class); + + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()) + .isEqualTo(View.VISIBLE); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + next.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + dialog.dismiss(); + } + + @Test + public void cancelCallback_isCalled_whenCancelled() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + dialog.show(); + dialog.cancel(); + verifyNoInteractions(successCallback); + verify(cancelCallback, times(1)) + .run(); + } + + @Test + public void cancelCallback_isCalled_whenNegativeButtonClickedOnFirstStage() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + dialog.show(); + Button back = dialog.findViewById(R.id.button_cancel); + back.performClick(); + verifyNoInteractions(successCallback); + verify(cancelCallback, times(1)) + .run(); + } + + @Test + public void cancelCallback_isNotCalled_whenNegativeButtonClickedOnSecondStage() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + dialog.show(); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + Button back = dialog.findViewById(R.id.button_cancel); + back.performClick(); + verifyNoInteractions(successCallback); + verifyNoInteractions(cancelCallback); + dialog.dismiss(); + } + + @Test + public void successCallback_isCalled_setNameAndAdminStatus() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + // No photo chosen + when(mUnderTest.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null); + dialog.show(); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + next.performClick(); + String expectedNewName = "Test"; + EditText editText = dialog.findViewById(R.id.user_name); + editText.setText(expectedNewName); + next.performClick(); + verify(successCallback, times(1)) + .onSuccess(expectedNewName, null, true); + } + + @Test + public void successCallback_isCalled_setName_MultipleAdminDisabled() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, false, successCallback, + cancelCallback); + // No photo chosen + when(mUnderTest.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null); + dialog.show(); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + String expectedNewName = "Test"; + EditText editText = dialog.findViewById(R.id.user_name); + editText.setText(expectedNewName); + next.performClick(); + verify(successCallback, times(1)) + .onSuccess(expectedNewName, null, false); + } + + private class TestCreateUserDialogController extends CreateUserDialogController { + private EditUserPhotoController mPhotoController; + + TestCreateUserDialogController() { + super("file_authority"); + } + + private EditUserPhotoController getPhotoController() { + return mPhotoController; + } + + @Override + EditUserPhotoController createEditUserPhotoController(ImageView userPhotoView) { + mPhotoController = mock(EditUserPhotoController.class, Answers.RETURNS_DEEP_STUBS); + return mPhotoController; + } + @Override + RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) { + return null; + } + + @Override + boolean isChangePhotoRestrictedByBase(Context context) { + return mPhotoRestrictedByBase; + } + } +} diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/shared/page/SceneModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/shared/page/SceneModule.kt new file mode 100644 index 000000000000..18c9513acf2a --- /dev/null +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/shared/page/SceneModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.shared.page + +import com.android.systemui.scene.shared.model.Scene +import dagger.Module +import dagger.multibindings.Multibinds + +@Module +interface SceneModule { + @Multibinds fun scenes(): Set<Scene> +} diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt new file mode 100644 index 000000000000..530706e47dcc --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.composable + +import com.android.systemui.bouncer.ui.composable.BouncerScene +import com.android.systemui.keyguard.ui.composable.LockScreenScene +import com.android.systemui.qs.ui.composable.QuickSettingsScene +import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.shade.ui.composable.ShadeScene +import dagger.Module +import dagger.Provides + +@Module +object SceneModule { + @Provides + fun scenes( + bouncer: BouncerScene, + gone: GoneScene, + lockScreen: LockScreenScene, + qs: QuickSettingsScene, + shade: ShadeScene, + ): Set<Scene> { + return setOf( + bouncer, + gone, + lockScreen, + qs, + shade, + ) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt new file mode 100644 index 000000000000..853300cf9508 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */ +@SysUISingleton +class BouncerScene +@Inject +constructor( + private val viewModel: BouncerViewModel, +) : ComposableScene { + override val key = SceneKey.Bouncer + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + MutableStateFlow<Map<UserAction, SceneModel>>( + mapOf( + UserAction.Back to SceneModel(SceneKey.LockScreen), + ) + ) + .asStateFlow() + + @Composable + override fun Content( + modifier: Modifier, + ) { + // TODO(b/280877228): implement the real UI. + + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Bouncer", style = MaterialTheme.typography.headlineLarge) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button(onClick = { viewModel.onAuthenticateButtonClicked() }) { + Text("Authenticate") + } + } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockScreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockScreenScene.kt new file mode 100644 index 000000000000..ad33eb518f91 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockScreenScene.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.ui.viewmodel.LockScreenSceneViewModel +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** The lock screen scene shows when the device is locked. */ +@SysUISingleton +class LockScreenScene +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val viewModel: LockScreenSceneViewModel, +) : ComposableScene { + override val key = SceneKey.LockScreen + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + viewModel.upDestinationSceneKey + .map { pageKey -> destinationScenes(up = pageKey) } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value) + ) + + @Composable + override fun Content( + modifier: Modifier, + ) { + LockScreenScene( + viewModel = viewModel, + modifier = modifier, + ) + } + + private fun destinationScenes( + up: SceneKey, + ): Map<UserAction, SceneModel> { + return mapOf( + UserAction.Swipe(Direction.UP) to SceneModel(up), + UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade) + ) + } +} + +@Composable +private fun LockScreenScene( + viewModel: LockScreenSceneViewModel, + modifier: Modifier = Modifier, +) { + // TODO(b/280879610): implement the real UI. + + val lockButtonIcon: Icon by viewModel.lockButtonIcon.collectAsState() + + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Lock screen", style = MaterialTheme.typography.headlineMedium) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button(onClick = { viewModel.onLockButtonClicked() }) { Icon(lockButtonIcon) } + + Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt new file mode 100644 index 000000000000..130395a38512 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** The Quick Settings (AKA "QS") scene shows the quick setting tiles. */ +@SysUISingleton +class QuickSettingsScene +@Inject +constructor( + private val viewModel: QuickSettingsSceneViewModel, +) : ComposableScene { + override val key = SceneKey.QuickSettings + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + MutableStateFlow<Map<UserAction, SceneModel>>( + mapOf( + UserAction.Swipe(Direction.UP) to SceneModel(SceneKey.Shade), + ) + ) + .asStateFlow() + + @Composable + override fun Content( + modifier: Modifier, + ) { + QuickSettingsScene( + viewModel = viewModel, + modifier = modifier, + ) + } +} + +@Composable +private fun QuickSettingsScene( + viewModel: QuickSettingsSceneViewModel, + modifier: Modifier = Modifier, +) { + // TODO(b/280887232): implement the real UI. + + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Quick settings", style = MaterialTheme.typography.headlineMedium) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt new file mode 100644 index 000000000000..a21366695f66 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.composable + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.android.systemui.scene.shared.model.Scene + +/** Compose-capable extension of [Scene]. */ +interface ComposableScene : Scene { + @Composable fun Content(modifier: Modifier) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt new file mode 100644 index 000000000000..007055221691 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any + * content from the scene framework. + */ +@SysUISingleton +class GoneScene @Inject constructor() : ComposableScene { + override val key = SceneKey.Gone + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + MutableStateFlow<Map<UserAction, SceneModel>>( + mapOf( + UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade), + ) + ) + .asStateFlow() + + @Composable + override fun Content( + modifier: Modifier, + ) { + /* + * TODO(b/279501596): once we start testing with the real Content Dynamics Framework, + * replace this with an error to make sure it doesn't get rendered. + */ + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Gone", style = MaterialTheme.typography.headlineMedium) + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt new file mode 100644 index 000000000000..f8a73d5294bc --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalAnimationApi::class) + +package com.android.systemui.scene.ui.composable + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import java.util.Locale + +/** + * Renders a container of a collection of "scenes" that the user can switch between using certain + * user actions (for instance, swiping up and down) or that can be switched automatically based on + * application business logic in response to certain events (for example, the device unlocking). + * + * It's possible for the application to host several such scene containers, the configuration system + * allows configuring each container with its own set of scenes. Scenes can be present in multiple + * containers. + * + * @param viewModel The UI state holder for this container. + * @param sceneByKey Mapping of [ComposableScene] by [SceneKey], ordered by z-order such that the + * last scene is rendered on top of all other scenes. It's critical that this map contains exactly + * and only the scenes on this container. In other words: (a) there should be no scene in this map + * that is not in the configuration for this container and (b) all scenes in the configuration + * must have entries in this map. + * @param modifier A modifier. + */ +@Composable +fun SceneContainer( + viewModel: SceneContainerViewModel, + sceneByKey: Map<SceneKey, ComposableScene>, + modifier: Modifier = Modifier, +) { + val currentScene: SceneModel by viewModel.currentScene.collectAsState() + + AnimatedContent( + targetState = currentScene.key, + label = "scene container", + modifier = modifier, + ) { currentSceneKey -> + sceneByKey.forEach { (key, composableScene) -> + if (key == currentSceneKey) { + Scene( + scene = composableScene, + onSceneChanged = viewModel::setCurrentScene, + modifier = Modifier.fillMaxSize(), + ) + } + } + } +} + +/** Renders the given [ComposableScene]. */ +@Composable +private fun Scene( + scene: ComposableScene, + onSceneChanged: (SceneModel) -> Unit, + modifier: Modifier = Modifier, +) { + // TODO(b/280880714): replace with the real UI and make sure to call onTransitionProgress. + Box(modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center), + ) { + scene.Content( + modifier = Modifier, + ) + + val destinationScenes: Map<UserAction, SceneModel> by + scene.destinationScenes().collectAsState() + val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)] + val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)] + val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)] + val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)] + val backDestinationScene = destinationScenes[UserAction.Back] + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + DirectionalButton(Direction.LEFT, swipeLeftDestinationScene, onSceneChanged) + DirectionalButton(Direction.UP, swipeUpDestinationScene, onSceneChanged) + DirectionalButton(Direction.RIGHT, swipeRightDestinationScene, onSceneChanged) + DirectionalButton(Direction.DOWN, swipeDownDestinationScene, onSceneChanged) + } + + if (backDestinationScene != null) { + BackHandler { onSceneChanged.invoke(backDestinationScene) } + } + } + } +} + +@Composable +private fun DirectionalButton( + direction: Direction, + destinationScene: SceneModel?, + onSceneChanged: (SceneModel) -> Unit, + modifier: Modifier = Modifier, +) { + Button( + onClick = { destinationScene?.let { onSceneChanged.invoke(it) } }, + enabled = destinationScene != null, + modifier = modifier, + ) { + Text(direction.name.lowercase(Locale.getDefault())) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt new file mode 100644 index 000000000000..5a092041df93 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */ +@SysUISingleton +class ShadeScene +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val viewModel: ShadeSceneViewModel, +) : ComposableScene { + override val key = SceneKey.Shade + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + viewModel.upDestinationSceneKey + .map { sceneKey -> destinationScenes(up = sceneKey) } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value), + ) + + @Composable + override fun Content( + modifier: Modifier, + ) { + ShadeScene( + viewModel = viewModel, + modifier = modifier, + ) + } + + private fun destinationScenes( + up: SceneKey, + ): Map<UserAction, SceneModel> { + return mapOf( + UserAction.Swipe(Direction.UP) to SceneModel(up), + UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.QuickSettings), + ) + } +} + +@Composable +private fun ShadeScene( + viewModel: ShadeSceneViewModel, + modifier: Modifier = Modifier, +) { + // TODO(b/280887022): implement the real UI. + + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Shade", style = MaterialTheme.typography.headlineMedium) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { viewModel.onContentClicked() }, + ) { + Text("Open some content") + } + } + } + } +} diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt index af1a11f6597a..2007e7606ab8 100644 --- a/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt +++ b/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt @@ -271,11 +271,13 @@ constructor( } private fun echoToSystrace(message: LogMessage, strMessage: String) { - Trace.instantForTrack( - Trace.TRACE_TAG_APP, - "UI Events", - "$name - ${message.level.shortString} ${message.tag}: $strMessage" - ) + if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) { + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + "UI Events", + "$name - ${message.level.shortString} ${message.tag}: $strMessage" + ) + } } private fun echoToLogcat(message: LogMessage, strMessage: String) { diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt new file mode 100644 index 000000000000..50b3f78a49bc --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins.log + +/** + * Base interface for a logger that logs changes in table format. + * + * This is a plugin interface for classes outside of SystemUI core. + */ +interface TableLogBufferBase { + /** + * Logs a String? change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: String?) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs a String? change. */ + fun logChange(prefix: String, columnName: String, value: String?, isInitial: Boolean) + + /** + * Logs a Boolean change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: Boolean) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs a Boolean change. */ + fun logChange(prefix: String, columnName: String, value: Boolean, isInitial: Boolean) + + /** + * Logs an Int? change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: Int?) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs an Int? change. */ + fun logChange(prefix: String, columnName: String, value: Int?, isInitial: Boolean) +} diff --git a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml index c12bfcca4eff..0e6b2812a8a9 100644 --- a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml +++ b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml @@ -32,8 +32,8 @@ <com.airbnb.lottie.LottieAnimationView android:id="@+id/rear_display_folded_animation" android:importantForAccessibility="no" - android:layout_width="@dimen/rear_display_animation_width" - android:layout_height="@dimen/rear_display_animation_height" + android:layout_width="@dimen/rear_display_animation_width_opened" + android:layout_height="@dimen/rear_display_animation_height_opened" android:layout_gravity="center" android:contentDescription="@string/rear_display_accessibility_unfolded_animation" android:scaleType="fitXY" @@ -49,8 +49,8 @@ android:text="@string/rear_display_unfolded_bottom_sheet_title" android:textAppearance="@style/TextAppearance.Dialog.Title" android:lineSpacingExtra="2sp" - android:paddingTop="@dimen/rear_display_title_top_padding" - android:paddingBottom="@dimen/rear_display_title_bottom_padding" + android:paddingTop="@dimen/rear_display_title_top_padding_opened" + android:paddingBottom="@dimen/rear_display_title_bottom_padding_opened" android:gravity="center_horizontal|center_vertical" /> diff --git a/packages/SystemUI/res/layout/ongoing_call_chip.xml b/packages/SystemUI/res/layout/ongoing_call_chip.xml index 238fc8438331..6a0217ec5fe8 100644 --- a/packages/SystemUI/res/layout/ongoing_call_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_call_chip.xml @@ -23,7 +23,7 @@ android:layout_gravity="center_vertical|start" android:layout_marginStart="5dp" > - <com.android.systemui.animation.view.LaunchableLinearLayout + <com.android.systemui.statusbar.phone.ongoingcall.OngoingCallBackgroundContainer android:id="@+id/ongoing_call_chip_background" android:layout_width="wrap_content" android:layout_height="@dimen/ongoing_appops_chip_height" @@ -55,5 +55,5 @@ android:textColor="?android:attr/colorPrimary" /> - </com.android.systemui.animation.view.LaunchableLinearLayout> + </com.android.systemui.statusbar.phone.ongoingcall.OngoingCallBackgroundContainer> </FrameLayout> diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 908aac4a7b7f..f277e8a6f02f 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -67,6 +67,12 @@ <dimen name="controls_header_horizontal_padding">12dp</dimen> <dimen name="controls_content_margin_horizontal">16dp</dimen> + <!-- Rear Display Education dimens --> + <dimen name="rear_display_animation_width">246dp</dimen> + <dimen name="rear_display_animation_height">180dp</dimen> + <dimen name="rear_display_title_top_padding">4dp</dimen> + <dimen name="rear_display_title_bottom_padding">0dp</dimen> + <!-- Bouncer user switcher margins --> <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index bf0b8a660432..2024daefbdff 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -617,8 +617,8 @@ <dimen name="qs_header_height">120dp</dimen> <dimen name="qs_header_row_min_height">48dp</dimen> - <dimen name="qs_header_non_clickable_element_height">24dp</dimen> - <dimen name="new_qs_header_non_clickable_element_height">24dp</dimen> + <dimen name="qs_header_non_clickable_element_height">24sp</dimen> + <dimen name="new_qs_header_non_clickable_element_height">24sp</dimen> <dimen name="qs_footer_padding">20dp</dimen> <dimen name="qs_security_footer_height">88dp</dimen> @@ -1042,7 +1042,7 @@ <dimen name="display_cutout_margin_consumption">0px</dimen> <!-- Height of the Ongoing App Ops chip --> - <dimen name="ongoing_appops_chip_height">24dp</dimen> + <dimen name="ongoing_appops_chip_height">24sp</dimen> <!-- Side padding between background of Ongoing App Ops chip and content --> <dimen name="ongoing_appops_chip_side_padding">8dp</dimen> <!-- Margin between icons of Ongoing App Ops chip --> @@ -1772,8 +1772,12 @@ <!-- Rear Display Education dimens --> <dimen name="rear_display_animation_width">273dp</dimen> <dimen name="rear_display_animation_height">200dp</dimen> + <dimen name="rear_display_animation_width_opened">273dp</dimen> + <dimen name="rear_display_animation_height_opened">200dp</dimen> <dimen name="rear_display_title_top_padding">24dp</dimen> <dimen name="rear_display_title_bottom_padding">16dp</dimen> + <dimen name="rear_display_title_top_padding_opened">24dp</dimen> + <dimen name="rear_display_title_bottom_padding_opened">16dp</dimen> <!-- Bouncer user switcher margins --> <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cbc73faa46af..5795e4facad4 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3101,17 +3101,20 @@ <!-- Switch to work profile dialer app for placing a call dialog. --> <!-- Text for Switch to work profile dialog's Title. Switch to work profile dialog guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=60] --> - <string name="call_from_work_profile_title">Can\'t call from this profile</string> + <string name="call_from_work_profile_title">Can\'t call from a personal app</string> <!-- Text for switch to work profile for call dialog to guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=NONE] --> - <string name="call_from_work_profile_text">Your work policy allows you to make phone calls only from the work profile</string> + <string name="call_from_work_profile_text">Your organization only allows you to make calls from work apps</string> <!-- Label for the button to switch to work profile for placing call. Switch to work profile dialog guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> <string name="call_from_work_profile_action">Switch to work profile</string> + <!-- Label for the button to switch to work profile for placing call. Switch to work profile dialog guide users to make call from work + profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> + <string name="install_dialer_on_work_profile_action">Install a work phone app</string> <!-- Label for the close button on switch to work profile dialog. Switch to work profile dialog guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> - <string name="call_from_work_profile_close">Close</string> + <string name="call_from_work_profile_close">Cancel</string> <!-- Label for a menu item in a menu that is shown when the user wishes to customize the lock screen. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java index 6bf1ce57b01e..9bead9410a51 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java @@ -234,10 +234,27 @@ public abstract class Condition { } protected final String getTag() { + if (isOverridingCondition()) { + return mTag + "[OVRD]"; + } + return mTag; } /** + * Returns the state of the condition. + * - "Invalid", condition hasn't been set / not monitored + * - "True", condition has been met + * - "False", condition has not been met + */ + protected final String getState() { + if (!isConditionSet()) { + return "Invalid"; + } + return isConditionMet() ? "True" : "False"; + } + + /** * Creates a new condition which will only be true when both this condition and all the provided * conditions are true. */ diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java index 43df08df9f45..1f61c64dd057 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java @@ -22,6 +22,7 @@ import android.util.Log; import androidx.annotation.NonNull; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.log.TableLogBufferBase; import java.util.ArrayList; import java.util.Collections; @@ -41,6 +42,7 @@ public class Monitor { private final String mTag = getClass().getSimpleName(); private final Executor mExecutor; private final Set<Condition> mPreconditions; + private final TableLogBufferBase mLogBuffer; private final HashMap<Condition, ArraySet<Subscription.Token>> mConditions = new HashMap<>(); private final HashMap<Subscription.Token, SubscriptionState> mSubscriptions = new HashMap<>(); @@ -160,11 +162,23 @@ public class Monitor { * Main constructor, allowing specifying preconditions. */ public Monitor(Executor executor, Set<Condition> preconditions) { + this(executor, preconditions, null); + } + + /** + * Main constructor, allowing specifying preconditions and a log buffer for logging. + */ + public Monitor(Executor executor, Set<Condition> preconditions, TableLogBufferBase logBuffer) { mExecutor = executor; mPreconditions = preconditions; + mLogBuffer = logBuffer; } private void updateConditionMetState(Condition condition) { + if (mLogBuffer != null) { + mLogBuffer.logChange(/* prefix= */ "", condition.getTag(), condition.getState()); + } + final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition); // It's possible the condition was removed between the time the callback occurred and diff --git a/packages/SystemUI/shared/src/com/android/systemui/utils/TraceUtils.kt b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt index 64234c205617..64234c205617 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/utils/TraceUtils.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index ec8fa921c6fa..9f21a31bd2c0 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -19,27 +19,34 @@ import android.app.Presentation; import android.content.Context; import android.graphics.Color; import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.DisplayManager; import android.media.MediaRouter; import android.media.MediaRouter.RouteInfo; import android.os.Bundle; import android.os.Trace; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.view.Display; +import android.view.DisplayAddress; import android.view.DisplayInfo; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.systemui.R; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.settings.DisplayTracker; +import com.android.systemui.statusbar.policy.KeyguardStateController; import java.util.concurrent.Executor; @@ -47,6 +54,7 @@ import javax.inject.Inject; import dagger.Lazy; +@SysUISingleton public class KeyguardDisplayManager { protected static final String TAG = "KeyguardDisplayManager"; private static final boolean DEBUG = KeyguardConstants.DEBUG; @@ -61,6 +69,9 @@ public class KeyguardDisplayManager { private boolean mShowing; private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); + private final DeviceStateHelper mDeviceStateHelper; + private final KeyguardStateController mKeyguardStateController; + private final SparseArray<Presentation> mPresentations = new SparseArray<>(); private final DisplayTracker.Callback mDisplayCallback = @@ -92,7 +103,9 @@ public class KeyguardDisplayManager { KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory, DisplayTracker displayTracker, @Main Executor mainExecutor, - @UiBackground Executor uiBgExecutor) { + @UiBackground Executor uiBgExecutor, + DeviceStateHelper deviceStateHelper, + KeyguardStateController keyguardStateController) { mContext = context; mNavigationBarControllerLazy = navigationBarControllerLazy; mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory; @@ -100,6 +113,8 @@ public class KeyguardDisplayManager { mDisplayService = mContext.getSystemService(DisplayManager.class); mDisplayTracker = displayTracker; mDisplayTracker.addDisplayChangeCallback(mDisplayCallback, mainExecutor); + mDeviceStateHelper = deviceStateHelper; + mKeyguardStateController = keyguardStateController; } private boolean isKeyguardShowable(Display display) { @@ -122,6 +137,18 @@ public class KeyguardDisplayManager { } return false; } + if (mKeyguardStateController.isOccluded() + && mDeviceStateHelper.isConcurrentDisplayActive(display)) { + if (DEBUG) { + // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the + // Keyguard state becomes "occluded". In this case, we should not show the + // KeyguardPresentation, since the activity is presenting content onto the + // non-default display. + Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent" + + " display is active"); + } + return false; + } return true; } @@ -260,6 +287,53 @@ public class KeyguardDisplayManager { } + /** + * Helper used to receive device state info from {@link DeviceStateManager}. + */ + static class DeviceStateHelper implements DeviceStateManager.DeviceStateCallback { + + @Nullable + private final DisplayAddress.Physical mRearDisplayPhysicalAddress; + + // TODO(b/271317597): These device states should be defined in DeviceStateManager + private final int mConcurrentState; + private boolean mIsInConcurrentDisplayState; + + @Inject + DeviceStateHelper(Context context, + DeviceStateManager deviceStateManager, + @Main Executor mainExecutor) { + + final String rearDisplayPhysicalAddress = context.getResources().getString( + com.android.internal.R.string.config_rearDisplayPhysicalAddress); + if (TextUtils.isEmpty(rearDisplayPhysicalAddress)) { + mRearDisplayPhysicalAddress = null; + } else { + mRearDisplayPhysicalAddress = DisplayAddress + .fromPhysicalDisplayId(Long.parseLong(rearDisplayPhysicalAddress)); + } + + mConcurrentState = context.getResources().getInteger( + com.android.internal.R.integer.config_deviceStateConcurrentRearDisplay); + deviceStateManager.registerCallback(mainExecutor, this); + } + + @Override + public void onStateChanged(int state) { + // When concurrent state ends, the display also turns off. This is enforced in various + // ExtensionRearDisplayPresentationTest CTS tests. So, we don't need to invoke + // hide() since that will happen through the onDisplayRemoved callback. + mIsInConcurrentDisplayState = state == mConcurrentState; + } + + boolean isConcurrentDisplayActive(Display display) { + return mIsInConcurrentDisplayState + && mRearDisplayPhysicalAddress != null + && mRearDisplayPhysicalAddress.equals(display.getAddress()); + } + } + + @VisibleForTesting static final class KeyguardPresentation extends Presentation { private static final int VIDEO_SAFE_REGION = 80; // Percentage of display width & height diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 1cbcb9d56566..5b1edc7516d6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -257,14 +257,14 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard * Authentication has happened and it's time to dismiss keyguard. This function * should clean up and inform KeyguardViewMediator. * - * @param strongAuth whether the user has authenticated with strong authentication like + * @param fromPrimaryAuth whether the user has authenticated with primary auth like * pattern, password or PIN but not by trust agents or fingerprint * @param targetUserId a user that needs to be the foreground user at the dismissal * completion. */ @Override - public void finish(boolean strongAuth, int targetUserId) { - if (!mKeyguardStateController.canDismissLockScreen() && !strongAuth) { + public void finish(boolean fromPrimaryAuth, int targetUserId) { + if (!mKeyguardStateController.canDismissLockScreen() && !fromPrimaryAuth) { Log.e(TAG, "Tried to dismiss keyguard when lockscreen is not dismissible and user " + "was not authenticated with a primary security method " @@ -283,9 +283,9 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } if (mViewMediatorCallback != null) { if (deferKeyguardDone) { - mViewMediatorCallback.keyguardDonePending(strongAuth, targetUserId); + mViewMediatorCallback.keyguardDonePending(fromPrimaryAuth, targetUserId); } else { - mViewMediatorCallback.keyguardDone(strongAuth, targetUserId); + mViewMediatorCallback.keyguardDone(fromPrimaryAuth, targetUserId); } } } @@ -603,8 +603,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard /** * Dismiss keyguard due to a user unlock event. */ - public void finish(boolean strongAuth, int currentUser) { - mKeyguardSecurityCallback.finish(strongAuth, currentUser); + public void finish(boolean primaryAuth, int currentUser) { + mKeyguardSecurityCallback.finish(primaryAuth, currentUser); } /** @@ -736,7 +736,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } boolean finish = false; - boolean strongAuth = false; + boolean primaryAuth = false; int eventSubtype = -1; BouncerUiEvent uiEvent = BouncerUiEvent.UNKNOWN; if (mUpdateMonitor.getUserHasTrust(targetUserId)) { @@ -761,7 +761,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard case Pattern: case Password: case PIN: - strongAuth = true; + primaryAuth = true; finish = true; eventSubtype = BOUNCER_DISMISS_PASSWORD; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_PASSWORD; @@ -805,7 +805,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mUiEventLogger.log(uiEvent, getSessionId()); } if (finish) { - mKeyguardSecurityCallback.finish(strongAuth, targetUserId); + mKeyguardSecurityCallback.finish(primaryAuth, targetUserId); } return finish; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 77e13ce28d96..95e97fffe444 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -893,7 +893,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } @VisibleForTesting - protected void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) { + public void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) { Assert.isMainThread(); Trace.beginSection("KeyGuardUpdateMonitor#onFingerPrintAuthenticated"); mUserFingerprintAuthenticated.put(userId, @@ -1169,7 +1169,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } @VisibleForTesting - protected void onFaceAuthenticated(int userId, boolean isStrongBiometric) { + public void onFaceAuthenticated(int userId, boolean isStrongBiometric) { Trace.beginSection("KeyGuardUpdateMonitor#onFaceAuthenticated"); Assert.isMainThread(); mUserFaceAuthenticated.put(userId, diff --git a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java index 9308773858e3..50f8f7e61230 100644 --- a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java @@ -29,11 +29,11 @@ public interface ViewMediatorCallback { /** * Report that the keyguard is done. * - * @param strongAuth whether the user has authenticated with strong authentication like + * @param primaryAuth whether the user has authenticated with primary authentication like * pattern, password or PIN but not by trust agents or fingerprint * @param targetUserId a user that needs to be the foreground user at the completion. */ - void keyguardDone(boolean strongAuth, int targetUserId); + void keyguardDone(boolean primaryAuth, int targetUserId); /** * Report that the keyguard is done drawing. @@ -49,11 +49,11 @@ public interface ViewMediatorCallback { /** * Report that the keyguard is dismissable, pending the next keyguardDone call. * - * @param strongAuth whether the user has authenticated with strong authentication like + * @param primaryAuth whether the user has authenticated with primary authentication like * pattern, password or PIN but not by trust agents or fingerprint * @param targetUserId a user that needs to be the foreground user at the completion. */ - void keyguardDonePending(boolean strongAuth, int targetUserId); + void keyguardDonePending(boolean primaryAuth, int targetUserId); /** * Report when keyguard is actually gone diff --git a/packages/SystemUI/src/com/android/systemui/LatencyTester.java b/packages/SystemUI/src/com/android/systemui/LatencyTester.java index 8f419560c78d..39d9714f2209 100644 --- a/packages/SystemUI/src/com/android/systemui/LatencyTester.java +++ b/packages/SystemUI/src/com/android/systemui/LatencyTester.java @@ -20,7 +20,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricSourceType; import android.os.Build; import android.provider.DeviceConfig; @@ -57,6 +56,7 @@ public class LatencyTester implements CoreStartable { private final BiometricUnlockController mBiometricUnlockController; private final BroadcastDispatcher mBroadcastDispatcher; private final DeviceConfigProxy mDeviceConfigProxy; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private boolean mEnabled; @@ -65,11 +65,13 @@ public class LatencyTester implements CoreStartable { BiometricUnlockController biometricUnlockController, BroadcastDispatcher broadcastDispatcher, DeviceConfigProxy deviceConfigProxy, - @Main DelayableExecutor mainExecutor + @Main DelayableExecutor mainExecutor, + KeyguardUpdateMonitor keyguardUpdateMonitor ) { mBiometricUnlockController = biometricUnlockController; mBroadcastDispatcher = broadcastDispatcher; mDeviceConfigProxy = deviceConfigProxy; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; updateEnabled(); mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_LATENCY_TRACKER, @@ -85,10 +87,13 @@ public class LatencyTester implements CoreStartable { if (!mEnabled) { return; } - mBiometricUnlockController.onBiometricAcquired(type, - BiometricConstants.BIOMETRIC_ACQUIRED_GOOD); - mBiometricUnlockController.onBiometricAuthenticated( - KeyguardUpdateMonitor.getCurrentUser(), type, true /* isStrongBiometric */); + if (type == BiometricSourceType.FACE) { + mKeyguardUpdateMonitor.onFaceAuthenticated(KeyguardUpdateMonitor.getCurrentUser(), + true); + } else if (type == BiometricSourceType.FINGERPRINT) { + mKeyguardUpdateMonitor.onFingerprintAuthenticated( + KeyguardUpdateMonitor.getCurrentUser(), true); + } } private void registerForBroadcasts(boolean register) { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index 632fcdc16259..2b468cf7c16b 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dagger.WMComponent; import com.android.systemui.util.InitializationChecker; import com.android.wm.shell.dagger.WMShellConcurrencyModule; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.sysui.ShellInterface; import com.android.wm.shell.transition.ShellTransitions; @@ -93,6 +94,7 @@ public abstract class SystemUIInitializer { .setBubbles(mWMComponent.getBubbles()) .setTaskViewFactory(mWMComponent.getTaskViewFactory()) .setTransitions(mWMComponent.getTransitions()) + .setKeyguardTransitions(mWMComponent.getKeyguardTransitions()) .setStartingSurface(mWMComponent.getStartingSurface()) .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper()) .setRecentTasks(mWMComponent.getRecentTasks()) @@ -113,6 +115,7 @@ public abstract class SystemUIInitializer { .setBubbles(Optional.ofNullable(null)) .setTaskViewFactory(Optional.ofNullable(null)) .setTransitions(new ShellTransitions() {}) + .setKeyguardTransitions(new KeyguardTransitions() {}) .setDisplayAreaHelper(Optional.ofNullable(null)) .setStartingSurface(Optional.ofNullable(null)) .setRecentTasks(Optional.ofNullable(null)) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index 3349fe5f1147..c3bb423e5e4e 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -87,19 +87,14 @@ public class MagnificationSettingsController implements ComponentCallbacks { } /** - * Shows magnification settings panel {@link WindowMagnificationSettings}. The panel ui would be - * various for different magnification mode. - * - * @param mode The magnification mode - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN + * Shows magnification settings panel {@link WindowMagnificationSettings}. */ - void showMagnificationSettings(int mode) { + void showMagnificationSettings() { if (!mWindowMagnificationSettings.isSettingPanelShowing()) { onConfigurationChanged(mContext.getResources().getConfiguration()); mContext.registerComponentCallbacks(this); } - mWindowMagnificationSettings.showSettingPanel(mode); + mWindowMagnificationSettings.showSettingPanel(); } void closeMagnificationSettings() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index 1c030da99e15..c820e4f105c2 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -16,7 +16,6 @@ package com.android.systemui.accessibility; -import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY; @@ -169,8 +168,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback mModeSwitchesController.setClickListenerDelegate( displayId -> mHandler.post(() -> { - showMagnificationSettingsPanel(displayId, - ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + showMagnificationSettingsPanel(displayId); })); } @@ -253,11 +251,11 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback } @MainThread - void showMagnificationSettingsPanel(int displayId, int mode) { + void showMagnificationSettingsPanel(int displayId) { final MagnificationSettingsController magnificationSettingsController = mMagnificationSettingsSupplier.get(displayId); if (magnificationSettingsController != null) { - magnificationSettingsController.showMagnificationSettings(mode); + magnificationSettingsController.showMagnificationSettings(); } } @@ -334,7 +332,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback @Override public void onClickSettingsButton(int displayId) { mHandler.post(() -> { - showMagnificationSettingsPanel(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + showMagnificationSettingsPanel(displayId); }); } }; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 155c26d9fe9d..44b49b8c2386 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; @@ -27,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; +import android.database.ContentObserver; import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Rect; @@ -101,8 +103,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private static final float A11Y_SCALE_MIN_VALUE = 1.0f; private WindowMagnificationSettingsCallback mCallback; - // the magnification mode that triggers showing the panel - private int mTriggeringMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE; + private ContentObserver mMagnificationCapabilityObserver; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -142,6 +143,16 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mGestureDetector = new MagnificationGestureDetector(context, context.getMainThreadHandler(), this); + + mMagnificationCapabilityObserver = new ContentObserver( + mContext.getMainThreadHandler()) { + @Override + public void onChange(boolean selfChange) { + mSettingView.post(() -> { + updateUIControlsIfNeeded(); + }); + } + }; } private class ZoomSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener { @@ -257,7 +268,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Override public boolean onFinish(float xOffset, float yOffset) { if (!mSingleTapDetected) { - showSettingPanel(mTriggeringMode); + showSettingPanel(); } mSingleTapDetected = false; return true; @@ -285,7 +296,8 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest return; } - // Reset button status. + // Unregister observer before removing view + mSecureSettings.unregisterContentObserver(mMagnificationCapabilityObserver); mWindowManager.removeView(mSettingView); mIsVisible = false; if (resetPosition) { @@ -297,16 +309,8 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false); } - /** - * Shows magnification settings panel. The panel ui would be various for - * different magnification mode. - * - * @param mode The magnification mode - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN - */ - public void showSettingPanel(int mode) { - showSettingPanel(mode, true); + public void showSettingPanel() { + showSettingPanel(true); } public boolean isSettingPanelShowing() { @@ -322,18 +326,15 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest } /** - * Shows magnification panel for set magnification. + * Shows the panel for magnification settings. * When the panel is going to be visible by calling this method, the layout position can be * reset depending on the flag. * - * @param mode The magnification mode - * @param resetPosition if the button position needs be reset + * @param resetPosition if the panel position needs to be reset */ - private void showSettingPanel(int mode, boolean resetPosition) { + private void showSettingPanel(boolean resetPosition) { if (!mIsVisible) { - updateUIControlsIfNeed(mode); - mTriggeringMode = mode; - + updateUIControlsIfNeeded(); if (resetPosition) { mDraggableWindowBounds.set(getDraggableWindowBounds()); mParams.x = mDraggableWindowBounds.right; @@ -342,6 +343,11 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mWindowManager.addView(mSettingView, mParams); + mSecureSettings.registerContentObserverForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, + mMagnificationCapabilityObserver, + UserHandle.USER_CURRENT); + // Exclude magnification switch button from system gesture area. setSystemGestureExclusion(); mIsVisible = true; @@ -359,35 +365,74 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); } - private void updateUIControlsIfNeed(int mode) { - if (mode == mTriggeringMode) { - return; - } + private int getMagnificationMode() { + return mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT); + } - int selectedButtonIndex = mLastSelectedButtonIndex; - switch (mode) { - case ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN: - // set the edit button visibility to View.INVISIBLE to keep the height, to prevent - // the size title from too close to the size buttons - mEditButton.setVisibility(View.INVISIBLE); - mAllowDiagonalScrollingView.setVisibility(View.GONE); - // force the fullscreen button showing - selectedButtonIndex = MagnificationSize.FULLSCREEN; - break; + private int getMagnificationCapability() { + return mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT); + } + private void updateUIControlsIfNeeded() { + int capability = getMagnificationCapability(); + + int selectedButtonIndex = mLastSelectedButtonIndex; + switch (capability) { case ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW: mEditButton.setVisibility(View.VISIBLE); mAllowDiagonalScrollingView.setVisibility(View.VISIBLE); + mFullScreenButton.setVisibility(View.GONE); if (selectedButtonIndex == MagnificationSize.FULLSCREEN) { selectedButtonIndex = MagnificationSize.NONE; } break; + case ACCESSIBILITY_MAGNIFICATION_MODE_ALL: + int mode = getMagnificationMode(); + mFullScreenButton.setVisibility(View.VISIBLE); + if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { + // set the edit button visibility to View.INVISIBLE to keep the height, to + // prevent the size title from too close to the size buttons + mEditButton.setVisibility(View.INVISIBLE); + mAllowDiagonalScrollingView.setVisibility(View.GONE); + // force the fullscreen button showing + selectedButtonIndex = MagnificationSize.FULLSCREEN; + } else { // mode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW + mEditButton.setVisibility(View.VISIBLE); + mAllowDiagonalScrollingView.setVisibility(View.VISIBLE); + } + break; + + case ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN: + // We will never fall into this case since we never show settings panel when + // capability equals to ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN. + // Currently, the case follows the UI controls when capability equals to + // ACCESSIBILITY_MAGNIFICATION_MODE_ALL and mode equals to + // ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, but we could also consider to + // remove the whole icon button selections int the future since they are no use + // for fullscreen only capability. + + mFullScreenButton.setVisibility(View.VISIBLE); + // set the edit button visibility to View.INVISIBLE to keep the height, to + // prevent the size title from too close to the size buttons + mEditButton.setVisibility(View.INVISIBLE); + mAllowDiagonalScrollingView.setVisibility(View.GONE); + // force the fullscreen button showing + selectedButtonIndex = MagnificationSize.FULLSCREEN; + break; + default: break; } updateSelectedButton(selectedButtonIndex); + mSettingView.requestLayout(); } private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { @@ -474,11 +519,11 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext); - boolean showSettingPanelAfterThemeChange = mIsVisible; + boolean showSettingPanelAfterConfigChange = mIsVisible; hideSettingPanel(/* resetPosition= */ false); inflateView(); - if (showSettingPanelAfterThemeChange) { - showSettingPanel(mTriggeringMode, /* resetPosition= */ false); + if (showSettingPanelAfterConfigChange) { + showSettingPanel(/* resetPosition= */ false); } return; } diff --git a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt new file mode 100644 index 000000000000..7c394a62a5f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.authentication + +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule +import dagger.Module + +@Module( + includes = + [ + AuthenticationRepositoryModule::class, + ], +) +object AuthenticationModule diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt new file mode 100644 index 000000000000..cd195f6f9f97 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.authentication.data.repository + +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import dagger.Binds +import dagger.Module +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Defines interface for classes that can access authentication-related application state. */ +interface AuthenticationRepository { + + /** + * Whether the device is unlocked. + * + * A device that is not yet unlocked requires unlocking by completing an authentication + * challenge according to the current authentication method. + * + * Note that this state has no real bearing on whether the lock screen is showing or dismissed. + */ + val isUnlocked: StateFlow<Boolean> + + /** + * The currently-configured authentication method. This determines how the authentication + * challenge is completed in order to unlock an otherwise locked device. + */ + val authenticationMethod: StateFlow<AuthenticationMethodModel> + + /** + * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically + * dismisses once the authentication challenge is completed. For example, completing a biometric + * authentication challenge via face unlock or fingerprint sensor can automatically bypass the + * lock screen. + */ + val isBypassEnabled: StateFlow<Boolean> + + /** See [isUnlocked]. */ + fun setUnlocked(isUnlocked: Boolean) + + /** See [authenticationMethod]. */ + fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) + + /** See [isBypassEnabled]. */ + fun setBypassEnabled(isBypassEnabled: Boolean) +} + +class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository { + // TODO(b/280883900): get data from real data sources in SysUI. + + private val _isUnlocked = MutableStateFlow(false) + override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow() + + private val _authenticationMethod = + MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234)) + override val authenticationMethod: StateFlow<AuthenticationMethodModel> = + _authenticationMethod.asStateFlow() + + private val _isBypassEnabled = MutableStateFlow(false) + override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow() + + override fun setUnlocked(isUnlocked: Boolean) { + _isUnlocked.value = isUnlocked + } + + override fun setBypassEnabled(isBypassEnabled: Boolean) { + _isBypassEnabled.value = isBypassEnabled + } + + override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) { + _authenticationMethod.value = authenticationMethod + } +} + +@Module +interface AuthenticationRepositoryModule { + @Binds fun repository(impl: AuthenticationRepositoryImpl): AuthenticationRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt new file mode 100644 index 000000000000..5aea930401d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.authentication.domain.interactor + +import com.android.systemui.authentication.data.repository.AuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Hosts application business logic related to authentication. */ +@SysUISingleton +class AuthenticationInteractor +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val repository: AuthenticationRepository, +) { + /** + * The currently-configured authentication method. This determines how the authentication + * challenge is completed in order to unlock an otherwise locked device. + */ + val authenticationMethod: StateFlow<AuthenticationMethodModel> = repository.authenticationMethod + + /** + * Whether the device is unlocked. + * + * A device that is not yet unlocked requires unlocking by completing an authentication + * challenge according to the current authentication method. + * + * Note that this state has no real bearing on whether the lock screen is showing or dismissed. + */ + val isUnlocked: StateFlow<Boolean> = + combine(authenticationMethod, repository.isUnlocked) { authMethod, isUnlocked -> + isUnlockedWithAuthMethod( + isUnlocked = isUnlocked, + authMethod = authMethod, + ) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = + isUnlockedWithAuthMethod( + isUnlocked = repository.isUnlocked.value, + authMethod = repository.authenticationMethod.value, + ) + ) + + /** + * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically + * dismisses once the authentication challenge is completed. For example, completing a biometric + * authentication challenge via face unlock or fingerprint sensor can automatically bypass the + * lock screen. + */ + val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled + + init { + // UNLOCKS WHEN AUTH METHOD REMOVED. + // + // Unlocks the device if the auth method becomes None. + applicationScope.launch { + repository.authenticationMethod.collect { + if (it is AuthenticationMethodModel.None) { + unlockDevice() + } + } + } + } + + /** + * Returns `true` if the device currently requires authentication before content can be viewed; + * `false` if content can be displayed without unlocking first. + */ + fun isAuthenticationRequired(): Boolean { + return !isUnlocked.value && authenticationMethod.value.isSecure + } + + /** + * Unlocks the device, assuming that the authentication challenge has been completed + * successfully. + */ + fun unlockDevice() { + repository.setUnlocked(true) + } + + /** + * Locks the device. From now on, the device will remain locked until [authenticate] is called + * with the correct input. + */ + fun lockDevice() { + repository.setUnlocked(false) + } + + /** + * Attempts to authenticate the user and unlock the device. + * + * @param input The input from the user to try to authenticate with. This can be a list of + * different things, based on the current authentication method. + * @return `true` if the authentication succeeded and the device is now unlocked; `false` + * otherwise. + */ + fun authenticate(input: List<Any>): Boolean { + val isSuccessful = + when (val authMethod = this.authenticationMethod.value) { + is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code + is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password + is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates + else -> true + } + + if (isSuccessful) { + repository.setUnlocked(true) + } + + return isSuccessful + } + + /** Triggers a biometric-powered unlock of the device. */ + fun biometricUnlock() { + // TODO(b/280883900): only allow this if the biometric is enabled and there's a match. + repository.setUnlocked(true) + } + + /** See [authenticationMethod]. */ + fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) { + repository.setAuthenticationMethod(authenticationMethod) + } + + /** See [isBypassEnabled]. */ + fun toggleBypassEnabled() { + repository.setBypassEnabled(!repository.isBypassEnabled.value) + } + + companion object { + private fun isUnlockedWithAuthMethod( + isUnlocked: Boolean, + authMethod: AuthenticationMethodModel, + ): Boolean { + return if (authMethod is AuthenticationMethodModel.None) { + true + } else { + isUnlocked + } + } + + /** + * Returns a PIN code from the given list. It's assumed the given list elements are all + * [Int]. + */ + private fun List<Any>.asCode(): Int? { + if (isEmpty()) { + return null + } + + var code = 0 + map { it as Int }.forEach { integer -> code = code * 10 + integer } + + return code + } + + /** + * Returns a password from the given list. It's assumed the given list elements are all + * [Char]. + */ + private fun List<Any>.asPassword(): String { + val anyList = this + return buildString { anyList.forEach { append(it as Char) } } + } + + /** + * Returns a list of [AuthenticationMethodModel.Pattern.PatternCoordinate] from the given + * list. It's assumed the given list elements are all + * [AuthenticationMethodModel.Pattern.PatternCoordinate]. + */ + private fun List<Any>.asPattern(): + List<AuthenticationMethodModel.Pattern.PatternCoordinate> { + val anyList = this + return buildList { + anyList.forEach { add(it as AuthenticationMethodModel.Pattern.PatternCoordinate) } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt new file mode 100644 index 000000000000..83250b638424 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.authentication.shared.model + +/** Enumerates all known authentication methods. */ +sealed class AuthenticationMethodModel( + /** + * Whether the authentication method is considered to be "secure". + * + * "Secure" authentication methods require authentication to unlock the device. Non-secure auth + * methods simply require user dismissal. + */ + open val isSecure: Boolean, +) { + /** There is no authentication method on the device. We shouldn't even show the lock screen. */ + object None : AuthenticationMethodModel(isSecure = false) + + /** The most basic authentication method. The lock screen can be swiped away when displayed. */ + object Swipe : AuthenticationMethodModel(isSecure = false) + + data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true) + + data class Password(val password: String) : AuthenticationMethodModel(isSecure = true) + + data class Pattern(val coordinates: List<PatternCoordinate>) : + AuthenticationMethodModel(isSecure = true) { + + data class PatternCoordinate( + val x: Int, + val y: Int, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java index 4b17be3c45d4..6db266f4f1cb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java @@ -29,7 +29,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.biometrics.BiometricFaceConstants; -import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricSourceType; import android.os.Handler; import android.os.UserHandle; @@ -43,6 +42,9 @@ import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.policy.KeyguardStateController; + +import java.util.Optional; + import javax.inject.Inject; /** @@ -66,6 +68,7 @@ public class BiometricNotificationService implements CoreStartable { private final Handler mHandler; private final NotificationManager mNotificationManager; private final BiometricNotificationBroadcastReceiver mBroadcastReceiver; + private final FingerprintReEnrollNotification mFingerprintReEnrollNotification; private NotificationChannel mNotificationChannel; private boolean mFaceNotificationQueued; private boolean mFingerprintNotificationQueued; @@ -102,8 +105,15 @@ public class BiometricNotificationService implements CoreStartable { Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_REQUIRED, UserHandle.USER_CURRENT); - } else if (msgId == BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL - && biometricSourceType == BiometricSourceType.FINGERPRINT) { + } + } + + @Override + public void onBiometricHelp(int msgId, String helpString, + BiometricSourceType biometricSourceType) { + if (biometricSourceType == BiometricSourceType.FINGERPRINT + && mFingerprintReEnrollNotification.isFingerprintReEnrollRequired( + msgId)) { mFingerprintReenrollRequired = true; } } @@ -115,13 +125,16 @@ public class BiometricNotificationService implements CoreStartable { KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardStateController keyguardStateController, Handler handler, NotificationManager notificationManager, - BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver) { + BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver, + Optional<FingerprintReEnrollNotification> fingerprintReEnrollNotification) { mContext = context; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mKeyguardStateController = keyguardStateController; mHandler = handler; mNotificationManager = notificationManager; mBroadcastReceiver = biometricNotificationBroadcastReceiver; + mFingerprintReEnrollNotification = fingerprintReEnrollNotification.orElse( + new FingerprintReEnrollNotificationImpl()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java new file mode 100644 index 000000000000..ca94e9993f76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +/** + * Checks if the fingerprint HAL has sent a re-enrollment request. + */ +public interface FingerprintReEnrollNotification { + /** Returns true if msgId corresponds to FINGERPRINT_ACQUIRED_RE_ENROLL. */ + boolean isFingerprintReEnrollRequired(int msgId); +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java new file mode 100644 index 000000000000..1f86bc6ae298 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +import android.hardware.biometrics.BiometricFingerprintConstants; + +/** + * Checks if the fingerprint HAL has sent a re-enrollment request. + */ +public class FingerprintReEnrollNotificationImpl implements FingerprintReEnrollNotification{ + @Override + public boolean isFingerprintReEnrollRequired(int msgId) { + return msgId == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_RE_ENROLL; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index bc44df488d90..2eb533029cf5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -466,7 +466,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOnFingerDown) { playStartHaptic(); } - mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); + mKeyguardViewManager.notifyKeyguardAuthenticated(false /* primaryAuth */); mAttemptedToDismissKeyguard = true; } @@ -636,7 +636,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mOverlay.getOverlayView().getViewRootImpl().getInputToken()); } - return processedTouch.getTouchData().isWithinSensor(mOverlayParams.getNativeSensorBounds()); + return processedTouch.getTouchData().isWithinBounds(mOverlayParams.getNativeSensorBounds()); } private boolean oldOnTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt index 79a0acb8bbc1..cf6044f146b0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt @@ -22,6 +22,11 @@ import com.android.systemui.dagger.SysUISingleton /** Returns whether the touch coordinates are within the sensor's bounding box. */ @SysUISingleton class BoundingBoxOverlapDetector : OverlapDetector { - override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean = - touchData.isWithinSensor(nativeSensorBounds) + override fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect, + ): Boolean = + touchData.isWithinBounds(nativeOverlayBounds) && + touchData.isWithinBounds(nativeSensorBounds) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt index baf8d743938d..f70e01dc9050 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt @@ -30,8 +30,8 @@ private enum class SensorPixelPosition { TARGET // Pixel within sensor center target } -private val isDebug = true -private val TAG = "EllipseOverlapDetector" +private const val isDebug = false +private const val TAG = "EllipseOverlapDetector" /** * Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap @@ -39,12 +39,21 @@ private val TAG = "EllipseOverlapDetector" */ @SysUISingleton class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : OverlapDetector { - override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { - // First, check if touch is within bounding box, - if (nativeSensorBounds.contains(touchData.x.toInt(), touchData.y.toInt())) { + override fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect, + ): Boolean { + // First, check if touch is within bounding box to exit early + if (touchData.isWithinBounds(nativeSensorBounds)) { return true } + // Check touch is within overlay bounds, not worth checking if outside + if (!touchData.isWithinBounds(nativeOverlayBounds)) { + return false + } + var isTargetTouched = false var sensorPixels = 0 var coveredPixels = 0 @@ -77,7 +86,7 @@ class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : val percentage: Float = coveredPixels.toFloat() / sensorPixels if (isDebug) { - Log.v( + Log.d( TAG, "covered: $coveredPixels, sensor: $sensorPixels, " + "percentage: $percentage, isCenterTouched: $isTargetTouched" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt index 6854b50370ba..1b4062a4e9bc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt @@ -51,16 +51,16 @@ data class NormalizedTouchData( ) { /** - * [nativeSensorBounds] contains the location and dimensions of the sensor area in native - * resolution and natural orientation. + * [nativeBounds] contains the location and dimensions of the area in native resolution and + * natural orientation. * - * Returns whether the coordinates of the given pointer are within the sensor's bounding box. + * Returns whether the coordinates of the given pointer are within the bounding box. */ - fun isWithinSensor(nativeSensorBounds: Rect): Boolean { - return nativeSensorBounds.left <= x && - nativeSensorBounds.right >= x && - nativeSensorBounds.top <= y && - nativeSensorBounds.bottom >= y + fun isWithinBounds(nativeBounds: Rect): Boolean { + return nativeBounds.left <= x && + nativeBounds.right >= x && + nativeBounds.top <= y && + nativeBounds.bottom >= y } @JvmOverloads diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt index 0fec8ffbaa0a..f16347159010 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt @@ -20,5 +20,9 @@ import android.graphics.Rect /** Determines whether the touch has a sufficient overlap with the sensor. */ interface OverlapDetector { - fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean + fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect + ): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt index 6c9390ddad7d..eeb0f4c7bb13 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -46,7 +46,13 @@ class SinglePointerTouchProcessor @Inject constructor(val overlapDetector: Overl val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) } val pointersOnSensor = touchData - .filter { overlapDetector.isGoodOverlap(it, overlayParams.nativeSensorBounds) } + .filter { + overlapDetector.isGoodOverlap( + it, + overlayParams.nativeSensorBounds, + overlayParams.nativeOverlayBounds + ) + } .map { it.pointerId } return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt new file mode 100644 index 000000000000..4c817b2e46a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.repo + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Provides access to bouncer-related application state. */ +@SysUISingleton +class BouncerRepository @Inject constructor() { + private val _message = MutableStateFlow<String?>(null) + /** The user-facing message to show in the bouncer. */ + val message: StateFlow<String?> = _message.asStateFlow() + + fun setMessage(message: String?) { + _message.value = message + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt new file mode 100644 index 000000000000..57ce58049f13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.domain.interactor + +import android.content.Context +import com.android.systemui.R +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Encapsulates business logic and application state accessing use-cases. */ +class BouncerInteractor +@AssistedInject +constructor( + @Application private val applicationScope: CoroutineScope, + @Application private val applicationContext: Context, + private val repository: BouncerRepository, + private val authenticationInteractor: AuthenticationInteractor, + private val sceneInteractor: SceneInteractor, + @Assisted private val containerName: String, +) { + + /** The user-facing message to show in the bouncer. */ + val message: StateFlow<String?> = repository.message + + /** + * The currently-configured authentication method. This determines how the authentication + * challenge is completed in order to unlock an otherwise locked device. + */ + val authenticationMethod: StateFlow<AuthenticationMethodModel> = + authenticationInteractor.authenticationMethod + + init { + applicationScope.launch { + combine( + sceneInteractor.currentScene(containerName), + authenticationInteractor.authenticationMethod, + ::Pair, + ) + .collect { (currentScene, authMethod) -> + if (currentScene.key == SceneKey.Bouncer) { + when (authMethod) { + is AuthenticationMethodModel.None -> + sceneInteractor.setCurrentScene( + containerName, + SceneModel(SceneKey.Gone), + ) + is AuthenticationMethodModel.Swipe -> + sceneInteractor.setCurrentScene( + containerName, + SceneModel(SceneKey.LockScreen), + ) + else -> Unit + } + } + } + } + } + + /** + * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown. + * + * @param containerName The name of the scene container to show the bouncer in. + * @param message An optional message to show to the user in the bouncer. + */ + fun showOrUnlockDevice( + containerName: String, + message: String? = null, + ) { + if (authenticationInteractor.isAuthenticationRequired()) { + repository.setMessage(message ?: promptMessage(authenticationMethod.value)) + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Bouncer), + ) + } else { + authenticationInteractor.unlockDevice() + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } + } + + /** + * Resets the user-facing message back to the default according to the current authentication + * method. + */ + fun resetMessage() { + repository.setMessage(promptMessage(authenticationMethod.value)) + } + + /** Removes the user-facing message. */ + fun clearMessage() { + repository.setMessage(null) + } + + /** + * Attempts to authenticate based on the given user input. + * + * If the input is correct, the device will be unlocked and the lock screen and bouncer will be + * dismissed and hidden. + */ + fun authenticate( + input: List<Any>, + ) { + val isAuthenticated = authenticationInteractor.authenticate(input) + if (isAuthenticated) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } else { + repository.setMessage(errorMessage(authenticationMethod.value)) + } + } + + private fun promptMessage(authMethod: AuthenticationMethodModel): String { + return when (authMethod) { + is AuthenticationMethodModel.PIN -> + applicationContext.getString(R.string.keyguard_enter_your_pin) + is AuthenticationMethodModel.Password -> + applicationContext.getString(R.string.keyguard_enter_your_password) + is AuthenticationMethodModel.Pattern -> + applicationContext.getString(R.string.keyguard_enter_your_pattern) + else -> "" + } + } + + private fun errorMessage(authMethod: AuthenticationMethodModel): String { + return when (authMethod) { + is AuthenticationMethodModel.PIN -> applicationContext.getString(R.string.kg_wrong_pin) + is AuthenticationMethodModel.Password -> + applicationContext.getString(R.string.kg_wrong_password) + is AuthenticationMethodModel.Pattern -> + applicationContext.getString(R.string.kg_wrong_pattern) + else -> "" + } + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): BouncerInteractor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt new file mode 100644 index 000000000000..8a183ae6ca7c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.ui.viewmodel + +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.dagger.qualifiers.Application +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Holds UI state and handles user input on bouncer UIs. */ +class BouncerViewModel +@AssistedInject +constructor( + @Application private val applicationScope: CoroutineScope, + interactorFactory: BouncerInteractor.Factory, + containerName: String, +) { + private val interactor: BouncerInteractor = interactorFactory.create(containerName) + + /** The user-facing message to show in the bouncer. */ + val message: StateFlow<String> = + interactor.message + .map { it ?: "" } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = interactor.message.value ?: "", + ) + + /** Notifies that the authenticate button was clicked. */ + fun onAuthenticateButtonClicked() { + // TODO(b/280877228): remove this and send the real input. + interactor.authenticate( + when (interactor.authenticationMethod.value) { + is AuthenticationMethodModel.PIN -> listOf(1, 2, 3, 4) + is AuthenticationMethodModel.Password -> "password".toList() + is AuthenticationMethodModel.Pattern -> + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0), + AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2), + AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0), + AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2), + ) + else -> emptyList() + } + ) + } + + /** Notifies that the emergency services button was clicked. */ + fun onEmergencyServicesButtonClicked() { + // TODO(b/280877228): implement this. + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 92607c6101af..d73c85b0803b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -328,6 +328,13 @@ class ControlsUiControllerImpl @Inject constructor ( @VisibleForTesting internal fun startRemovingApp(componentName: ComponentName, appName: CharSequence) { + activityStarter.dismissKeyguardThenExecute({ + showAppRemovalDialog(componentName, appName) + true + }, null, true) + } + + private fun showAppRemovalDialog(componentName: ComponentName, appName: CharSequence) { removeAppDialog?.cancel() removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) { shouldRemove -> if (!shouldRemove || !controlsController.get().removeFavorites(componentName)) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index dba353b0d70e..8d5a2dd8df25 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -32,6 +32,7 @@ import com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActiv import com.android.systemui.sensorprivacy.television.TvUnblockSensorActivity; import com.android.systemui.settings.brightness.BrightnessDialog; import com.android.systemui.statusbar.tv.notifications.TvNotificationPanelActivity; +import com.android.systemui.telephony.ui.activity.SwitchToManagedProfileForCallActivity; import com.android.systemui.tuner.TunerActivity; import com.android.systemui.usb.UsbAccessoryUriActivity; import com.android.systemui.usb.UsbConfirmActivity; @@ -178,4 +179,11 @@ public abstract class DefaultActivityBinder { @ClassKey(TvSensorPrivacyChangedActivity.class) public abstract Activity bindTvSensorPrivacyChangedActivity( TvSensorPrivacyChangedActivity activity); + + /** Inject into SwitchToManagedProfileForCallActivity. */ + @Binds + @IntoMap + @ClassKey(SwitchToManagedProfileForCallActivity.class) + public abstract Activity bindSwitchToManagedProfileForCallActivity( + SwitchToManagedProfileForCallActivity activity); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index f68bd49230d9..2262d8ab2000 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -40,6 +40,7 @@ import com.android.systemui.qs.tileimpl.QSFactoryImpl; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsImplementation; import com.android.systemui.rotationlock.RotationLockModule; +import com.android.systemui.scene.SceneContainerFrameworkModule; import com.android.systemui.screenshot.ReferenceScreenshotModule; import com.android.systemui.settings.dagger.MultiUserUtilsModule; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; @@ -103,6 +104,7 @@ import javax.inject.Named; QSModule.class, ReferenceScreenshotModule.class, RotationLockModule.class, + SceneContainerFrameworkModule.class, StatusBarEventsModule.class, StartCentralSurfacesModule.class, VolumeModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 1a0fcea6ca87..52355f34a71d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -42,6 +42,7 @@ import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.recents.RecentTasks; @@ -104,6 +105,9 @@ public interface SysUIComponent { Builder setTransitions(ShellTransitions t); @BindsInstance + Builder setKeyguardTransitions(KeyguardTransitions k); + + @BindsInstance Builder setStartingSurface(Optional<StartingSurface> s); @BindsInstance diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 17cf8084df1f..5bcf32a9ebdb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -32,8 +32,10 @@ import com.android.systemui.accessibility.AccessibilityModule; import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule; import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; +import com.android.systemui.authentication.AuthenticationModule; import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; +import com.android.systemui.biometrics.FingerprintReEnrollNotification; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.biometrics.dagger.UdfpsModule; @@ -53,6 +55,8 @@ import com.android.systemui.flags.FlagsModule; import com.android.systemui.keyboard.KeyboardModule; import com.android.systemui.keyguard.data.BouncerViewModule; import com.android.systemui.log.dagger.LogModule; +import com.android.systemui.log.dagger.MonitorLog; +import com.android.systemui.log.table.TableLogBuffer; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.model.SysUiState; import com.android.systemui.motiontool.MotionToolModule; @@ -152,6 +156,7 @@ import javax.inject.Named; AccessibilityRepositoryModule.class, AppOpsModule.class, AssistModule.class, + AuthenticationModule.class, BiometricsModule.class, BouncerViewModule.class, ClipboardOverlayModule.class, @@ -247,8 +252,8 @@ public abstract class SystemUIModule { @Provides @SystemUser static Monitor provideSystemUserMonitor(@Main Executor executor, - SystemProcessCondition systemProcessCondition) { - return new Monitor(executor, Collections.singleton(systemProcessCondition)); + SystemProcessCondition systemProcessCondition, @MonitorLog TableLogBuffer logBuffer) { + return new Monitor(executor, Collections.singleton(systemProcessCondition), logBuffer); } @BindsOptionalOf @@ -289,6 +294,9 @@ public abstract class SystemUIModule { @BindsOptionalOf abstract SystemStatusAnimationScheduler optionalSystemStatusAnimationScheduler(); + @BindsOptionalOf + abstract FingerprintReEnrollNotification optionalFingerprintReEnrollNotification(); + @SysUISingleton @Binds abstract SystemClock bindSystemClock(SystemClockImpl systemClock); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index 17d2332a4dac..b71871ebdb55 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -31,6 +31,7 @@ import com.android.wm.shell.dagger.WMShellModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.recents.RecentTasks; @@ -99,6 +100,9 @@ public interface WMComponent { ShellTransitions getTransitions(); @WMSingleton + KeyguardTransitions getKeyguardTransitions(); + + @WMSingleton Optional<StartingSurface> getStartingSurface(); @WMSingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 8b6bd2486117..6e77dcbd75cf 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -21,7 +21,6 @@ import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; @@ -37,7 +36,6 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.TransitionFlags; import static android.view.WindowManager.TransitionOldType; import static android.view.WindowManager.TransitionType; -import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -67,8 +65,6 @@ import android.view.WindowManager; import android.view.WindowManagerPolicyConstants; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; -import android.window.RemoteTransition; -import android.window.TransitionFilter; import android.window.TransitionInfo; import com.android.internal.policy.IKeyguardDismissCallback; @@ -178,7 +174,7 @@ public class KeyguardService extends Service { // Wrap Keyguard going away animation. // Note: Also used for wrapping occlude by Dream animation. It works (with some redundancy). - private static IRemoteTransition wrap(IRemoteAnimationRunner runner) { + public static IRemoteTransition wrap(IRemoteAnimationRunner runner) { return new IRemoteTransition.Stub() { final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks = new ArrayMap<>(); @@ -273,7 +269,8 @@ public class KeyguardService extends Service { if (mShellTransitions == null || !Transitions.ENABLE_SHELL_TRANSITIONS) { RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); final RemoteAnimationAdapter exitAnimationAdapter = - new RemoteAnimationAdapter(mExitAnimationRunner, 0, 0); + new RemoteAnimationAdapter( + mKeyguardViewMediator.getExitAnimationRunner(), 0, 0); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, exitAnimationAdapter); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER, @@ -297,92 +294,7 @@ public class KeyguardService extends Service { unoccludeAnimationAdapter); ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay( mDisplayTracker.getDefaultDisplayId(), definition); - return; - } - Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_GOING_AWAY"); - TransitionFilter f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; - mShellTransitions.registerRemote(f, new RemoteTransition( - wrap(mExitAnimationRunner), getIApplicationThread(), "ExitKeyguard")); - - Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_(UN)OCCLUDE"); - // Register for occluding - final RemoteTransition occludeTransition = new RemoteTransition( - mOccludeAnimation, getIApplicationThread(), "KeyguardOcclude"); - f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED; - f.mRequirements = new TransitionFilter.Requirement[]{ - new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; - // First require at-least one app showing that occludes. - f.mRequirements[0].mMustBeIndependent = false; - f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - // Then require that we aren't closing any occludes (because this would mean a - // regular task->task or activity->activity animation not involving keyguard). - f.mRequirements[1].mNot = true; - f.mRequirements[1].mMustBeIndependent = false; - f.mRequirements[1].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; - mShellTransitions.registerRemote(f, occludeTransition); - - // Now register for un-occlude. - final RemoteTransition unoccludeTransition = new RemoteTransition( - mUnoccludeAnimation, getIApplicationThread(), "KeyguardUnocclude"); - f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED; - f.mRequirements = new TransitionFilter.Requirement[]{ - new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; - // First require at-least one app going-away (doesn't need occlude flag - // as that is implicit by it having been visible and we don't want to exclude - // cases where we are un-occluding because the app removed its showWhenLocked - // capability at runtime). - f.mRequirements[1].mMustBeIndependent = false; - f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; - f.mRequirements[1].mMustBeTask = true; - // Then require that we aren't opening any occludes (otherwise we'd remain - // occluded). - f.mRequirements[0].mNot = true; - f.mRequirements[0].mMustBeIndependent = false; - f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - mShellTransitions.registerRemote(f, unoccludeTransition); - - // Register for specific transition type. - // Above filter cannot fulfill all conditions. - // E.g. close top activity while screen off but next activity is occluded, this should - // an occluded transition, but since the activity is invisible, the condition would - // match unoccluded transition. - // But on the contrary, if we add above condition in occluded transition, then when user - // trying to dismiss occluded activity when unlock keyguard, the condition would match - // occluded transition. - f = new TransitionFilter(); - f.mTypeSet = new int[]{TRANSIT_KEYGUARD_OCCLUDE}; - mShellTransitions.registerRemote(f, occludeTransition); - - f = new TransitionFilter(); - f.mTypeSet = new int[]{TRANSIT_KEYGUARD_UNOCCLUDE}; - mShellTransitions.registerRemote(f, unoccludeTransition); - - Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_OCCLUDE for DREAM"); - // Register for occluding by Dream - f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED; - f.mRequirements = new TransitionFilter.Requirement[]{ - new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; - // First require at-least one app of type DREAM showing that occludes. - f.mRequirements[0].mActivityType = WindowConfiguration.ACTIVITY_TYPE_DREAM; - f.mRequirements[0].mMustBeIndependent = false; - f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - // Then require that we aren't closing any occludes (because this would mean a - // regular task->task or activity->activity animation not involving keyguard). - f.mRequirements[1].mNot = true; - f.mRequirements[1].mMustBeIndependent = false; - f.mRequirements[1].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; - mShellTransitions.registerRemote(f, new RemoteTransition( - wrap(mKeyguardViewMediator.getOccludeByDreamAnimationRunner()), - getIApplicationThread(), "KeyguardOccludeByDream")); + } } @Override @@ -402,27 +314,6 @@ public class KeyguardService extends Service { } } - private final IRemoteAnimationRunner.Stub mExitAnimationRunner = - new IRemoteAnimationRunner.Stub() { - @Override // Binder interface - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) { - Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation"); - checkPermission(); - mKeyguardViewMediator.startKeyguardExitAnimation(transit, apps, wallpapers, - nonApps, finishedCallback); - Trace.endSection(); - } - - @Override // Binder interface - public void onAnimationCancelled() { - mKeyguardViewMediator.cancelKeyguardExitAnimation(); - } - }; - final IRemoteTransition mOccludeAnimation = new IRemoteTransition.Stub() { @Override public void startAnimation(IBinder transition, TransitionInfo info, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 46ff0d9ded55..2a94b082a016 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -79,7 +79,6 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; -import android.util.EventLog; import android.util.Log; import android.util.Slog; import android.util.SparseBooleanArray; @@ -94,6 +93,7 @@ import android.view.WindowManager; import android.view.WindowManagerPolicyConstants; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.window.IRemoteTransition; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -154,12 +154,14 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; +import com.android.wm.shell.keyguard.KeyguardTransitions; import dagger.Lazy; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.Executor; /** @@ -744,7 +746,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } @Override - public void keyguardDone(boolean strongAuth, int targetUserId) { + public void keyguardDone(boolean primaryAuth, int targetUserId) { if (targetUserId != KeyguardUpdateMonitor.getCurrentUser()) { return; } @@ -765,7 +767,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } @Override - public void keyguardDonePending(boolean strongAuth, int targetUserId) { + public void keyguardDonePending(boolean primaryAuth, int targetUserId) { Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending"); if (DEBUG) Log.d(TAG, "keyguardDonePending"); if (targetUserId != KeyguardUpdateMonitor.getCurrentUser()) { @@ -962,7 +964,26 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; - private IRemoteAnimationRunner mOccludeAnimationRunner = + private final IRemoteAnimationRunner.Stub mExitAnimationRunner = + new IRemoteAnimationRunner.Stub() { + @Override // Binder interface + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) { + Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation"); + startKeyguardExitAnimation(transit, apps, wallpapers, nonApps, finishedCallback); + Trace.endSection(); + } + + @Override // Binder interface + public void onAnimationCancelled() { + cancelKeyguardExitAnimation(); + } + }; + + private final IRemoteAnimationRunner mOccludeAnimationRunner = new OccludeActivityLaunchRemoteAnimationRunner(mOccludeAnimationController); private final IRemoteAnimationRunner mOccludeByDreamAnimationRunner = @@ -1004,7 +1025,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } final RemoteAnimationTarget primary = apps[0]; - final boolean isDream = (apps[0].taskInfo.topActivityType + final boolean isDream = (apps[0].taskInfo != null + && apps[0].taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM); if (!isDream) { Log.w(TAG, "The occluding app isn't Dream; " @@ -1104,7 +1126,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } final RemoteAnimationTarget primary = apps[0]; - final boolean isDream = (apps[0].taskInfo.topActivityType + final boolean isDream = (apps[0].taskInfo != null + && apps[0].taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM); final SyncRtSurfaceTransactionApplier applier = @@ -1187,6 +1210,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final InteractionJankMonitor mInteractionJankMonitor; private boolean mWallpaperSupportsAmbientMode; private ScreenOnCoordinator mScreenOnCoordinator; + private final KeyguardTransitions mKeyguardTransitions; private Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator; private Lazy<ScrimController> mScrimControllerLazy; @@ -1222,6 +1246,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, ScreenOffAnimationController screenOffAnimationController, Lazy<NotificationShadeDepthController> notificationShadeDepthController, ScreenOnCoordinator screenOnCoordinator, + KeyguardTransitions keyguardTransitions, InteractionJankMonitor interactionJankMonitor, DreamOverlayStateController dreamOverlayStateController, Lazy<ShadeController> shadeControllerLazy, @@ -1249,6 +1274,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, dumpManager.registerDumpable(getClass().getName(), this); mDeviceConfig = deviceConfig; mScreenOnCoordinator = screenOnCoordinator; + mKeyguardTransitions = keyguardTransitions; mNotificationShadeWindowControllerLazy = notificationShadeWindowControllerLazy; mShowHomeOverLockscreen = mDeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, @@ -1324,6 +1350,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(false /* showing */, true /* forceCallbacks */); } + mKeyguardTransitions.register( + KeyguardService.wrap(getExitAnimationRunner()), + KeyguardService.wrap(getOccludeAnimationRunner()), + KeyguardService.wrap(getOccludeByDreamAnimationRunner()), + KeyguardService.wrap(getUnoccludeAnimationRunner())); + final ContentResolver cr = mContext.getContentResolver(); mDeviceInteractive = mPM.isInteractive(); @@ -1859,6 +1891,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Trace.endSection(); } + public IRemoteAnimationRunner getExitAnimationRunner() { + return mExitAnimationRunner; + } + public IRemoteAnimationRunner getOccludeAnimationRunner() { return mOccludeAnimationRunner; } @@ -2219,16 +2255,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; - private void keyguardDone() { - Trace.beginSection("KeyguardViewMediator#keyguardDone"); - if (DEBUG) Log.d(TAG, "keyguardDone()"); - userActivity(); - EventLog.writeEvent(70000, 2); - Message msg = mHandler.obtainMessage(KEYGUARD_DONE); - mHandler.sendMessage(msg); - Trace.endSection(); - } - /** * This handler will be associated with the policy thread, which will also * be the UI thread of the keyguard. Since the apis of the policy, and therefore @@ -3124,7 +3150,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Trace.beginSection("KeyguardViewMediator#onWakeAndUnlocking"); mWakeAndUnlocking = true; - keyguardDone(); + mKeyguardViewControllerLazy.get().notifyKeyguardAuthenticated(/* primaryAuth */ false); + userActivity(); Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index deb8f5d96cbd..255556c80121 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -64,6 +64,7 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; +import com.android.wm.shell.keyguard.KeyguardTransitions; import dagger.Lazy; import dagger.Module; @@ -119,6 +120,7 @@ public class KeyguardModule { ScreenOffAnimationController screenOffAnimationController, Lazy<NotificationShadeDepthController> notificationShadeDepthController, ScreenOnCoordinator screenOnCoordinator, + KeyguardTransitions keyguardTransitions, InteractionJankMonitor interactionJankMonitor, DreamOverlayStateController dreamOverlayStateController, Lazy<ShadeController> shadeController, @@ -152,6 +154,7 @@ public class KeyguardModule { screenOffAnimationController, notificationShadeDepthController, screenOnCoordinator, + keyguardTransitions, interactionJankMonitor, dreamOverlayStateController, shadeController, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt new file mode 100644 index 000000000000..61701802e231 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.util.kotlin.pairwise +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Hosts business and application state accessing logic for the lock screen scene. */ +class LockScreenSceneInteractor +@AssistedInject +constructor( + @Application applicationScope: CoroutineScope, + private val authenticationInteractor: AuthenticationInteractor, + bouncerInteractorFactory: BouncerInteractor.Factory, + private val sceneInteractor: SceneInteractor, + @Assisted private val containerName: String, +) { + private val bouncerInteractor: BouncerInteractor = + bouncerInteractorFactory.create(containerName) + + /** Whether the device is currently locked. */ + val isDeviceLocked: StateFlow<Boolean> = + authenticationInteractor.isUnlocked + .map { !it } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = !authenticationInteractor.isUnlocked.value, + ) + + /** Whether it's currently possible to swipe up to dismiss the lock screen. */ + val isSwipeToDismissEnabled: StateFlow<Boolean> = + combine( + authenticationInteractor.isUnlocked, + authenticationInteractor.authenticationMethod, + ) { isUnlocked, authMethod -> + isSwipeToUnlockEnabled( + isUnlocked = isUnlocked, + authMethod = authMethod, + ) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = + isSwipeToUnlockEnabled( + isUnlocked = authenticationInteractor.isUnlocked.value, + authMethod = authenticationInteractor.authenticationMethod.value, + ), + ) + + init { + // LOCKING SHOWS LOCK SCREEN. + // + // Move to the lock screen scene if the device becomes locked while in any scene. + applicationScope.launch { + authenticationInteractor.isUnlocked + .map { !it } + .distinctUntilChanged() + .collect { isLocked -> + if (isLocked) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.LockScreen), + ) + } + } + } + + // BYPASS UNLOCK. + // + // Moves to the gone scene if bypass is enabled and the device becomes unlocked while in the + // lock screen scene. + applicationScope.launch { + combine( + authenticationInteractor.isBypassEnabled, + authenticationInteractor.isUnlocked, + sceneInteractor.currentScene(containerName), + ::Triple, + ) + .collect { (isBypassEnabled, isUnlocked, currentScene) -> + if (isBypassEnabled && isUnlocked && currentScene.key == SceneKey.LockScreen) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } + } + } + + // SWIPE TO DISMISS LOCK SCREEN. + // + // If switched from the lock screen to the gone scene and the auth method was a swipe, + // unlocks the device. + applicationScope.launch { + combine( + authenticationInteractor.authenticationMethod, + sceneInteractor.currentScene(containerName).pairwise(), + ::Pair, + ) + .collect { (authMethod, scenes) -> + val (previousScene, currentScene) = scenes + if ( + authMethod is AuthenticationMethodModel.Swipe && + previousScene.key == SceneKey.LockScreen && + currentScene.key == SceneKey.Gone + ) { + authenticationInteractor.unlockDevice() + } + } + } + + // DISMISS LOCK SCREEN IF AUTH METHOD IS REMOVED. + // + // If the auth method becomes None while on the lock screen scene, dismisses the lock + // screen. + applicationScope.launch { + combine( + authenticationInteractor.authenticationMethod, + sceneInteractor.currentScene(containerName), + ::Pair, + ) + .collect { (authMethod, scene) -> + if ( + scene.key == SceneKey.LockScreen && + authMethod == AuthenticationMethodModel.None + ) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } + } + } + } + + /** Attempts to dismiss the lock screen. This will cause the bouncer to show, if needed. */ + fun dismissLockScreen() { + bouncerInteractor.showOrUnlockDevice(containerName = containerName) + } + + private fun isSwipeToUnlockEnabled( + isUnlocked: Boolean, + authMethod: AuthenticationMethodModel, + ): Boolean { + return !isUnlocked && authMethod is AuthenticationMethodModel.Swipe + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): LockScreenSceneInteractor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 3aa57dde3178..0db4ab15ab80 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -25,6 +25,7 @@ import android.content.IntentFilter import android.graphics.Rect import android.hardware.display.DisplayManager import android.os.Bundle +import android.os.Handler import android.os.IBinder import android.view.LayoutInflater import android.view.SurfaceControlViewHost @@ -41,6 +42,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.shared.clocks.ClockRegistry +import com.android.systemui.shared.clocks.DefaultClockController import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController @@ -57,6 +59,7 @@ class KeyguardPreviewRenderer constructor( @Application private val context: Context, @Main private val mainDispatcher: CoroutineDispatcher, + @Main private val mainHandler: Handler, private val bottomAreaViewModel: KeyguardBottomAreaViewModel, displayManager: DisplayManager, private val windowManager: WindowManager, @@ -112,7 +115,7 @@ constructor( } fun render() { - runBlocking(mainDispatcher) { + mainHandler.post { val rootView = FrameLayout(context) setUpBottomArea(rootView) @@ -168,14 +171,12 @@ constructor( * @param hide TRUE hides smartspace, FALSE shows smartspace */ fun hideSmartspace(hide: Boolean) { - runBlocking(mainDispatcher) { - smartSpaceView?.visibility = if (hide) View.INVISIBLE else View.VISIBLE - } + mainHandler.post { smartSpaceView?.visibility = if (hide) View.INVISIBLE else View.VISIBLE } } /** Sets the clock's color to the overridden seed color. */ fun onColorOverridden(@ColorInt color: Int?) { - runBlocking(mainDispatcher) { + mainHandler.post { colorOverride = color clockController.clock?.run { events.onSeedColorChanged(color) } } @@ -313,6 +314,36 @@ constructor( ) disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + val layoutChangeListener = + object : View.OnLayoutChangeListener { + override fun onLayoutChange( + v: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + if (clockController.clock !is DefaultClockController) { + clockController.clock + ?.largeClock + ?.events + ?.onTargetRegionChanged( + KeyguardClockSwitch.getLargeClockRegion(parentView) + ) + } + } + } + + parentView.addOnLayoutChangeListener(layoutChangeListener) + + disposables.add( + DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) } + ) + onClockChanged(parentView) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt new file mode 100644 index 000000000000..08b961306817 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.R +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Models UI state and handles user input for the lock screen scene. */ +class LockScreenSceneViewModel +@AssistedInject +constructor( + @Application applicationScope: CoroutineScope, + interactorFactory: LockScreenSceneInteractor.Factory, + @Assisted containerName: String, +) { + private val interactor: LockScreenSceneInteractor = interactorFactory.create(containerName) + + /** The icon for the "lock" button on the lock screen. */ + val lockButtonIcon: StateFlow<Icon> = + interactor.isDeviceLocked + .map { isLocked -> lockIcon(isLocked = isLocked) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = lockIcon(isLocked = interactor.isDeviceLocked.value), + ) + + /** The key of the scene we should switch to when swiping up. */ + val upDestinationSceneKey: StateFlow<SceneKey> = + interactor.isSwipeToDismissEnabled + .map { isSwipeToUnlockEnabled -> upDestinationSceneKey(isSwipeToUnlockEnabled) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = upDestinationSceneKey(interactor.isSwipeToDismissEnabled.value), + ) + + /** Notifies that the lock button on the lock screen was clicked. */ + fun onLockButtonClicked() { + interactor.dismissLockScreen() + } + + /** Notifies that some content on the lock screen was clicked. */ + fun onContentClicked() { + interactor.dismissLockScreen() + } + + private fun upDestinationSceneKey( + isSwipeToUnlockEnabled: Boolean, + ): SceneKey { + return if (isSwipeToUnlockEnabled) SceneKey.Gone else SceneKey.Bouncer + } + + private fun lockIcon( + isLocked: Boolean, + ): Icon { + return Icon.Resource( + res = + if (isLocked) { + R.drawable.ic_device_lock_on + } else { + R.drawable.ic_device_lock_off + }, + contentDescription = + ContentDescription.Resource( + res = + if (isLocked) { + R.string.accessibility_lock_icon + } else { + R.string.accessibility_unlock_button + } + ) + ) + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): LockScreenSceneViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 9be18ace79fa..408628f3842b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -60,7 +60,7 @@ public class LogModule { if (Compile.IS_DEBUG && notifPipelineFlags.isDevLoggingEnabled()) { maxSize *= 10; } - return factory.create("NotifLog", maxSize, false /* systrace */); + return factory.create("NotifLog", maxSize, Compile.IS_DEBUG /* systrace */); } /** Provides a logging buffer for all logs related to notifications on the lockscreen. */ @@ -421,6 +421,14 @@ public class LogModule { return factory.create("BouncerLog", 250); } + /** Provides a table logging buffer for the Monitor. */ + @Provides + @SysUISingleton + @MonitorLog + public static TableLogBuffer provideMonitorTableLogBuffer(TableLogBufferFactory factory) { + return factory.create("MonitorLog", 250); + } + /** * Provides a {@link LogBuffer} for Udfps logs. */ diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MonitorLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/MonitorLog.kt new file mode 100644 index 000000000000..b6a4ec935d14 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MonitorLog.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger + +import javax.inject.Qualifier +import kotlin.annotation.Retention + +/** Logger for Monitor. */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class MonitorLog diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt index cabe319d317d..8d622ae1ca03 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt @@ -23,6 +23,7 @@ import com.android.systemui.common.buffer.RingBuffer import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogLevel import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.TableLogBufferBase import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.util.Locale @@ -84,7 +85,7 @@ class TableLogBuffer( @Background private val bgDispatcher: CoroutineDispatcher, private val coroutineScope: CoroutineScope, private val localLogcat: LogProxy = LogProxyDefault(), -) : Dumpable { +) : Dumpable, TableLogBufferBase { init { if (maxSize <= 0) { throw IllegalArgumentException("maxSize must be > 0") @@ -177,7 +178,7 @@ class TableLogBuffer( * * @param isInitial see [TableLogBuffer.logChange(String, Boolean, (TableRowLogger) -> Unit]. */ - fun logChange(prefix: String, columnName: String, value: String?, isInitial: Boolean = false) { + override fun logChange(prefix: String, columnName: String, value: String?, isInitial: Boolean) { logChange(systemClock.currentTimeMillis(), prefix, columnName, value, isInitial) } @@ -186,7 +187,7 @@ class TableLogBuffer( * * @param isInitial see [TableLogBuffer.logChange(String, Boolean, (TableRowLogger) -> Unit]. */ - fun logChange(prefix: String, columnName: String, value: Boolean, isInitial: Boolean = false) { + override fun logChange(prefix: String, columnName: String, value: Boolean, isInitial: Boolean) { logChange(systemClock.currentTimeMillis(), prefix, columnName, value, isInitial) } @@ -195,7 +196,7 @@ class TableLogBuffer( * * @param isInitial see [TableLogBuffer.logChange(String, Boolean, (TableRowLogger) -> Unit]. */ - fun logChange(prefix: String, columnName: String, value: Int?, isInitial: Boolean = false) { + override fun logChange(prefix: String, columnName: String, value: Int?, isInitial: Boolean) { logChange(systemClock.currentTimeMillis(), prefix, columnName, value, isInitial) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index d949cf56ff0e..162766289ba4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -207,10 +207,10 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { updateTitleIcon(R.drawable.media_output_icon_volume, mController.getColorItemContent()); } - initMutingExpectedDevice(); mCurrentActivePosition = position; updateFullItemClickListener(v -> onItemClick(v, device)); setSingleLineLayout(getItemTitle(device)); + initMutingExpectedDevice(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && mController.isSubStatusSupported() && mController.isAdvancedLayoutSupported() && device.hasSubtext()) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 151dbb2746aa..6ebda40a8001 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -47,7 +47,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.RecyclerView; -import com.android.settingslib.Utils; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; @@ -277,9 +276,10 @@ public abstract class MediaOutputBaseAdapter extends backgroundDrawable = mContext.getDrawable( showSeekBar || isFakeActive ? R.drawable.media_output_item_background_active : R.drawable.media_output_item_background).mutate(); - backgroundDrawable.setTint( + mItemLayout.setBackgroundTintList(ColorStateList.valueOf( showSeekBar || isFakeActive ? mController.getColorConnectedItemBackground() - : mController.getColorItemBackground()); + : mController.getColorItemBackground() + )); mIconAreaLayout.setBackgroundTintList( ColorStateList.valueOf(showProgressBar || isFakeActive ? mController.getColorConnectedItemBackground() @@ -299,7 +299,8 @@ public abstract class MediaOutputBaseAdapter extends backgroundDrawable = mContext.getDrawable( R.drawable.media_output_item_background) .mutate(); - backgroundDrawable.setTint(mController.getColorItemBackground()); + mItemLayout.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorItemBackground())); } mItemLayout.setBackground(backgroundDrawable); mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); @@ -455,11 +456,16 @@ public abstract class MediaOutputBaseAdapter extends void initMutingExpectedDevice() { disableSeekBar(); + updateTitleIcon(R.drawable.media_output_icon_volume, + mController.getColorItemContent()); final Drawable backgroundDrawable = mContext.getDrawable( R.drawable.media_output_item_background_active) .mutate(); - backgroundDrawable.setTint(mController.getColorConnectedItemBackground()); mItemLayout.setBackground(backgroundDrawable); + mItemLayout.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorConnectedItemBackground())); + mIconAreaLayout.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorConnectedItemBackground())); } private void animateCornerAndVolume(int fromProgress, int toProgress) { @@ -530,14 +536,6 @@ public abstract class MediaOutputBaseAdapter extends }); } - Drawable getSpeakerDrawable() { - final Drawable drawable = mContext.getDrawable(R.drawable.ic_speaker_group_black_24dp) - .mutate(); - drawable.setTint(Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_item_main_content)); - return drawable; - } - protected void disableSeekBar() { mSeekBar.setEnabled(false); mSeekBar.setOnTouchListener((v, event) -> true); @@ -574,7 +572,6 @@ public abstract class MediaOutputBaseAdapter extends return; } mTitleIcon.setImageIcon(icon); - icon.setTint(mController.getColorItemContent()); mTitleIcon.setImageTintList( ColorStateList.valueOf(mController.getColorItemContent())); }); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 46724adee848..d8899796b336 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -24,6 +24,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.LinearLayout; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; @@ -198,13 +199,23 @@ public class QuickQSPanel extends QSPanel { @Override public boolean updateResources() { - mCellHeightResId = R.dimen.qs_quick_tile_size; + mResourceCellHeightResId = R.dimen.qs_quick_tile_size; boolean b = super.updateResources(); mMaxAllowedRows = getResources().getInteger(R.integer.quick_qs_panel_max_rows); return b; } @Override + protected void estimateCellHeight() { + FontSizeUtils.updateFontSize(mTempTextView, R.dimen.qs_tile_text_size); + int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mTempTextView.measure(unspecifiedSpec, unspecifiedSpec); + int padding = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_padding); + // the QQS only have 1 label + mEstimatedCellHeight = mTempTextView.getMeasuredHeight() + padding * 2; + } + + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateResources(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 269a158c6b87..19bf0188c9d2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -9,10 +9,12 @@ import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.TextView; import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.QSPanelControllerBase.TileRecord; @@ -29,9 +31,10 @@ public class TileLayout extends ViewGroup implements QSTileLayout { protected int mColumns; protected int mCellWidth; - protected int mCellHeightResId = R.dimen.qs_tile_height; + protected int mResourceCellHeightResId = R.dimen.qs_tile_height; + protected int mResourceCellHeight; + protected int mEstimatedCellHeight; protected int mCellHeight; - protected int mMaxCellHeight; protected int mCellMarginHorizontal; protected int mCellMarginVertical; protected int mSidePadding; @@ -49,6 +52,8 @@ public class TileLayout extends ViewGroup implements QSTileLayout { private float mSquishinessFraction = 1f; protected int mLastTileBottom; + protected TextView mTempTextView; + public TileLayout(Context context) { this(context, null); } @@ -57,6 +62,7 @@ public class TileLayout extends ViewGroup implements QSTileLayout { super(context, attrs); mLessRows = ((Settings.System.getInt(context.getContentResolver(), "qs_less_rows", 0) != 0) || useQsMediaPlayer(context)); + mTempTextView = new TextView(context); updateResources(); } @@ -120,14 +126,19 @@ public class TileLayout extends ViewGroup implements QSTileLayout { } public boolean updateResources() { - final Resources res = mContext.getResources(); + Resources res = getResources(); mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); - mMaxCellHeight = mContext.getResources().getDimensionPixelSize(mCellHeightResId); + mResourceCellHeight = res.getDimensionPixelSize(mResourceCellHeightResId); mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal); mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0; mCellMarginVertical= res.getDimensionPixelSize(R.dimen.qs_tile_margin_vertical); mMaxAllowedRows = Math.max(1, getResources().getInteger(R.integer.quick_settings_max_rows)); - if (mLessRows) mMaxAllowedRows = Math.max(mMinRows, mMaxAllowedRows - 1); + if (mLessRows) { + mMaxAllowedRows = Math.max(mMinRows, mMaxAllowedRows - 1); + } + // update estimated cell height under current font scaling + mTempTextView.dispatchConfigurationChanged(mContext.getResources().getConfiguration()); + estimateCellHeight(); if (updateColumns()) { requestLayout(); return true; @@ -211,8 +222,23 @@ public class TileLayout extends ViewGroup implements QSTileLayout { return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); } + // Estimate the height for the tile with 2 labels (general case) under current font scaling. + protected void estimateCellHeight() { + FontSizeUtils.updateFontSize(mTempTextView, R.dimen.qs_tile_text_size); + int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mTempTextView.measure(unspecifiedSpec, unspecifiedSpec); + int padding = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_padding); + mEstimatedCellHeight = mTempTextView.getMeasuredHeight() * 2 + padding * 2; + } + protected int getCellHeight() { - return mMaxCellHeight; + // Compare estimated height with resource height and return the larger one. + // If estimated height > resource height, it means the resource height is not enough + // for the tile content under current font scaling. Therefore, we need to use the estimated + // height to have a full tile content view. + // If estimated height <= resource height, we can use the resource height for tile to keep + // the same UI as original behavior. + return Math.max(mResourceCellHeight, mEstimatedCellHeight); } private void layoutTileRecords(int numRecords, boolean forLayout) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt new file mode 100644 index 000000000000..6525a98b9102 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Models UI state and handles user input for the quick settings scene. */ +class QuickSettingsSceneViewModel +@AssistedInject +constructor( + lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory, + @Assisted containerName: String, +) { + private val lockScreenSceneInteractor: LockScreenSceneInteractor = + lockScreenSceneInteractorFactory.create(containerName) + + /** Notifies that some content in quick settings was clicked. */ + fun onContentClicked() { + lockScreenSceneInteractor.dismissLockScreen() + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): QuickSettingsSceneViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java index dc3c8203d1a2..6912114140b0 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java @@ -16,12 +16,16 @@ package com.android.systemui.reardisplay; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.content.Context; +import android.content.res.Configuration; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManagerGlobal; import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.CoreStartable; @@ -70,6 +74,7 @@ public class RearDisplayDialogController implements CoreStartable, CommandQueue. @VisibleForTesting SystemUIDialog mRearDisplayEducationDialog; + @Nullable LinearLayout mDialogViewContainer; @Inject public RearDisplayDialogController(Context context, CommandQueue commandQueue, @@ -90,26 +95,51 @@ public class RearDisplayDialogController implements CoreStartable, CommandQueue. createAndShowDialog(); } + @Override + public void onConfigurationChanged(Configuration newConfig) { + if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing() + && mDialogViewContainer != null) { + // Refresh the dialog view when configuration is changed. + Context dialogContext = mRearDisplayEducationDialog.getContext(); + View dialogView = createDialogView(dialogContext); + mDialogViewContainer.removeAllViews(); + mDialogViewContainer.addView(dialogView); + } + } + private void createAndShowDialog() { mServiceNotified = false; Context dialogContext = mRearDisplayEducationDialog.getContext(); + View dialogView = createDialogView(dialogContext); + + mDialogViewContainer = new LinearLayout(dialogContext); + mDialogViewContainer.setLayoutParams( + new LinearLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + mDialogViewContainer.setOrientation(LinearLayout.VERTICAL); + mDialogViewContainer.addView(dialogView); + + mRearDisplayEducationDialog.setView(mDialogViewContainer); + + configureDialogButtons(); + + mRearDisplayEducationDialog.show(); + } + + private View createDialogView(Context context) { View dialogView; if (mStartedFolded) { - dialogView = View.inflate(dialogContext, + dialogView = View.inflate(context, R.layout.activity_rear_display_education, null); } else { - dialogView = View.inflate(dialogContext, + dialogView = View.inflate(context, R.layout.activity_rear_display_education_opened, null); } LottieAnimationView animationView = dialogView.findViewById( R.id.rear_display_folded_animation); animationView.setRepeatCount(mAnimationRepeatCount); - mRearDisplayEducationDialog.setView(dialogView); - - configureDialogButtons(); - - mRearDisplayEducationDialog.show(); + return dialogView; } /** @@ -164,6 +194,7 @@ public class RearDisplayDialogController implements CoreStartable, CommandQueue. mServiceNotified = true; mDeviceStateManagerGlobal.unregisterDeviceStateCallback(mDeviceStateManagerCallback); mDeviceStateManagerGlobal.onStateRequestOverlayDismissed(shouldCancelRequest); + mDialogViewContainer = null; } /** diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt new file mode 100644 index 000000000000..0ed8b21c100e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene + +import com.android.systemui.scene.shared.page.SceneModule +import dagger.Module + +@Module( + includes = + [ + SceneModule::class, + ], +) +object SceneContainerFrameworkModule diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 8c01bae43c3d..5ad16f0a6078 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -293,10 +293,9 @@ public class ImageExporter { final ContentValues values = createMetadata(time, format, fileName); Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - if (UserHandle.myUserId() != owner.getIdentifier()) { - baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); - } - Uri uri = resolver.insert(baseUri, values); + Uri uriWithUserId = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); + + Uri uri = resolver.insert(uriWithUserId, values); if (uri == null) { throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java index 72613249552a..2f96f6c656c4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -247,7 +247,7 @@ public class AppClipsActivity extends ComponentActivity { } updateImageDimensions(); - mViewModel.saveScreenshotThenFinish(drawable, bounds); + mViewModel.saveScreenshotThenFinish(drawable, bounds, getUser()); } private void setResultThenFinish(Uri uri) { @@ -255,6 +255,11 @@ public class AppClipsActivity extends ComponentActivity { return; } + // Grant permission here instead of in the trampoline activity because this activity can run + // as work profile user so the URI can belong to the work profile user while the trampoline + // activity always runs as main user. + grantUriPermission(mCallingPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + Bundle data = new Bundle(); data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java index e1619dc9b6ee..afc8bff91766 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java @@ -19,7 +19,7 @@ package com.android.systemui.screenshot.appclips; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.view.Display; +import android.os.UserManager; import androidx.annotation.Nullable; @@ -27,6 +27,7 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.settings.DisplayTracker; import javax.inject.Inject; @@ -35,14 +36,20 @@ import javax.inject.Inject; class AppClipsCrossProcessHelper { private final ServiceConnector<IAppClipsScreenshotHelperService> mProxyConnector; + private final DisplayTracker mDisplayTracker; @Inject - AppClipsCrossProcessHelper(@Application Context context) { - mProxyConnector = new ServiceConnector.Impl<IAppClipsScreenshotHelperService>(context, + AppClipsCrossProcessHelper(@Application Context context, UserManager userManager, + DisplayTracker displayTracker) { + // Start a service as main user so that even if the app clips activity is running as work + // profile user the service is able to use correct instance of Bubbles to grab a screenshot + // excluding the bubble layer. + mProxyConnector = new ServiceConnector.Impl<>(context, new Intent(context, AppClipsScreenshotHelperService.class), Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_VISIBLE, context.getUserId(), + | Context.BIND_NOT_VISIBLE, userManager.getMainUser().getIdentifier(), IAppClipsScreenshotHelperService.Stub::asInterface); + mDisplayTracker = displayTracker; } /** @@ -56,7 +63,9 @@ class AppClipsCrossProcessHelper { try { AndroidFuture<ScreenshotHardwareBufferInternal> future = mProxyConnector.postForResult( - service -> service.takeScreenshot(Display.DEFAULT_DISPLAY)); + service -> + // Take a screenshot of the default display of the user. + service.takeScreenshot(mDisplayTracker.getDefaultDisplayId())); return future.get().createBitmapThenCloseBuffer(); } catch (Exception e) { return null; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index 0487cbc995dd..f00803c6d64b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -21,7 +21,6 @@ import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; -import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; @@ -34,6 +33,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; @@ -82,6 +82,8 @@ public class AppClipsTrampolineActivity extends Activity { private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName(); static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI"; + @VisibleForTesting + static final String EXTRA_USE_WP_USER = TAG + "USE_WP_USER"; static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE"; static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER"; static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME"; @@ -98,6 +100,7 @@ public class AppClipsTrampolineActivity extends Activity { private final ResultReceiver mResultReceiver; private Intent mKillAppClipsBroadcastIntent; + private UserHandle mNotesAppUser; @Inject public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags, @@ -165,15 +168,21 @@ public class AppClipsTrampolineActivity extends Activity { return; } + mNotesAppUser = getUser(); + if (getIntent().getBooleanExtra(EXTRA_USE_WP_USER, /* defaultValue= */ false)) { + // Get the work profile user internally instead of passing around via intent extras as + // this activity is exported apps could potentially mess around with intent extras. + mNotesAppUser = getWorkProfileUser().orElse(mNotesAppUser); + } + String callingPackageName = getCallingPackage(); Intent intent = new Intent().setComponent(componentName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver) .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName); - try { - // Start the App Clips activity. - startActivity(intent); + // Start the App Clips activity for the user corresponding to the notes app user. + startActivityAsUser(intent, mNotesAppUser); // Set up the broadcast intent that will inform the above App Clips activity to finish // when this trampoline activity is finished. @@ -198,6 +207,13 @@ public class AppClipsTrampolineActivity extends Activity { } } + private Optional<UserHandle> getWorkProfileUser() { + return mUserTracker.getUserProfiles().stream() + .filter(profile -> mUserManager.isManagedProfile(profile.id)) + .findFirst() + .map(UserInfo::getUserHandle); + } + private void maybeStartActivityForWPUser() { UserHandle mainUser = mUserManager.getMainUser(); if (mainUser == null) { @@ -205,9 +221,13 @@ public class AppClipsTrampolineActivity extends Activity { return; } - // Start the activity as the main user with activity result forwarding. + // Start the activity as the main user with activity result forwarding. Set the intent extra + // so that the newly started trampoline activity starts the actual app clips activity as the + // work profile user. Starting the app clips activity as the work profile user is required + // to save the screenshot in work profile user storage and grant read permission to the URI. startActivityAsUser( new Intent(this, AppClipsTrampolineActivity.class) + .putExtra(EXTRA_USE_WP_USER, /* value= */ true) .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser); } @@ -221,7 +241,7 @@ public class AppClipsTrampolineActivity extends Activity { int callingPackageUid = 0; try { callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName, - APPLICATION_INFO_FLAGS, mUserTracker.getUserId()).uid; + APPLICATION_INFO_FLAGS, mNotesAppUser.getIdentifier()).uid; } catch (NameNotFoundException e) { Log.d(TAG, "Couldn't find notes app UID " + e); } @@ -254,14 +274,14 @@ public class AppClipsTrampolineActivity extends Activity { if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) { Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class); - convertedData.setData(uri).addFlags(FLAG_GRANT_READ_URI_PERMISSION); + convertedData.setData(uri); } // Broadcast no longer required, setting it to null. mKillAppClipsBroadcastIntent = null; // Expand the note bubble before returning the result. - mNoteTaskController.showNoteTask(NoteTaskEntryPoint.APP_CLIPS); + mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mNotesAppUser); setResult(RESULT_OK, convertedData); finish(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index 4cbca28a4032..b0e4cc978ae2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -26,7 +26,7 @@ import android.graphics.Rect; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Process; +import android.os.UserHandle; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; @@ -110,16 +110,14 @@ final class AppClipsViewModel extends ViewModel { * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to * {@link LiveData}. */ - void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) { + void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds, UserHandle user) { mBgExecutor.execute(() -> { // Render the screenshot bitmap in background. Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds); // Export and save the screenshot in background. - // TODO(b/267310185): Save to work profile UserHandle. ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export( - mBgExecutor, UUID.randomUUID(), screenshotBitmap, - Process.myUserHandle()); + mBgExecutor, UUID.randomUUID(), screenshotBitmap, user); // Get the result and update state on main thread. exportFuture.addListener(() -> { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 46f12105e032..0c800d456f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -333,7 +333,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mLpChanged.preferredMinDisplayRefreshRate = 0; } Trace.setCounter("display_set_preferred_refresh_rate", - (long) mKeyguardPreferredRefreshRate); + (long) mLpChanged.preferredMaxDisplayRefreshRate); } else if (mKeyguardMaxRefreshRate > 0) { boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled() && state.statusBarState == StatusBarState.KEYGUARD diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt new file mode 100644 index 000000000000..dcae258ed76f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.ui.viewmodel + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Models UI state and handles user input for the shade scene. */ +class ShadeSceneViewModel +@AssistedInject +constructor( + @Application private val applicationScope: CoroutineScope, + lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory, + @Assisted private val containerName: String, +) { + private val lockScreenInteractor: LockScreenSceneInteractor = + lockScreenSceneInteractorFactory.create(containerName) + + /** The key of the scene we should switch to when swiping up. */ + val upDestinationSceneKey: StateFlow<SceneKey> = + lockScreenInteractor.isDeviceLocked + .map { isLocked -> upDestinationSceneKey(isLocked = isLocked) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = + upDestinationSceneKey( + isLocked = lockScreenInteractor.isDeviceLocked.value, + ), + ) + + /** Notifies that some content in the shade was clicked. */ + fun onContentClicked() { + lockScreenInteractor.dismissLockScreen() + } + + private fun upDestinationSceneKey( + isLocked: Boolean, + ): SceneKey { + return if (isLocked) SceneKey.LockScreen else SceneKey.Gone + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): ShadeSceneViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt index 43f78c3166e4..e5849c05a534 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt @@ -87,7 +87,8 @@ class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : Status } } -class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { +/** open only for testing purposes. (See [FakeStatusEvent.kt]) */ +open class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { override var contentDescription: String? = null override val priority = 100 override var forceVisible = true @@ -107,9 +108,9 @@ class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { } override fun shouldUpdateFromEvent(other: StatusEvent?): Boolean { - return other is PrivacyEvent && - (other.privacyItems != privacyItems || - other.contentDescription != contentDescription) + return other is PrivacyEvent && (other.privacyItems != privacyItems || + other.contentDescription != contentDescription || + (other.forceVisible && !forceVisible)) } override fun updateFromEvent(other: StatusEvent?) { @@ -122,5 +123,7 @@ class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { privacyChip?.contentDescription = other.contentDescription privacyChip?.privacyList = other.privacyItems + + if (other.forceVisible) forceVisible = true } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt index f7a4feafee25..0a18f2d89d87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt @@ -153,6 +153,7 @@ constructor( ) } currentlyDisplayedEvent?.updateFromEvent(event) + if (event.forceVisible) hasPersistentDot = true } else if (scheduledEvent.value?.shouldUpdateFromEvent(event) == true) { if (DEBUG) { Log.d( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt index 18ee4816fac9..59fc387c4608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt @@ -67,7 +67,10 @@ class ShadeViewDiffer( fun getViewLabel(view: View): String = nodes.values.firstOrNull { node -> node.view === view }?.label ?: view.toString() - private fun detachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { + private fun detachChildren( + parentNode: ShadeNode, + specMap: Map<NodeController, NodeSpec> + ) = traceSection("detachChildren") { val views = nodes.values.associateBy { it.view } fun detachRecursively(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { val parentSpec = specMap[parentNode.controller] @@ -124,7 +127,10 @@ class ShadeViewDiffer( } } - private fun attachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { + private fun attachChildren( + parentNode: ShadeNode, + specMap: Map<NodeController, NodeSpec> + ): Unit = traceSection("attachChildren") { val parentSpec = checkNotNull(specMap[parentNode.controller]) for ((index, childSpec) in parentSpec.children.withIndex()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index bd7840d447cf..2d8f371aadac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -469,13 +469,13 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp case MODE_DISMISS_BOUNCER: Trace.beginSection("MODE_DISMISS_BOUNCER"); mKeyguardViewController.notifyKeyguardAuthenticated( - false /* strongAuth */); + false /* primaryAuth */); Trace.endSection(); break; case MODE_UNLOCK_COLLAPSING: Trace.beginSection("MODE_UNLOCK_COLLAPSING"); mKeyguardViewController.notifyKeyguardAuthenticated( - false /* strongAuth */); + false /* primaryAuth */); Trace.endSection(); break; case MODE_SHOW_BOUNCER: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt new file mode 100644 index 000000000000..ce88a5f5e55f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone.ongoingcall + +import android.content.Context +import android.util.AttributeSet +import com.android.systemui.animation.view.LaunchableLinearLayout + +/** + * A container view for the ongoing call chip background. Needed so that we can limit the height of + * the background when the font size is very large (200%), in which case the background would go + * past the bounds of the status bar. + */ +class OngoingCallBackgroundContainer(context: Context, attrs: AttributeSet) : + LaunchableLinearLayout(context, attrs) { + + /** Sets where this view should fetch its max height from. */ + var maxHeightFetcher: (() -> Int)? = null + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val maxHeight = maxHeightFetcher?.invoke() + val chosenHeight = + if (maxHeight != null) { + // Give 1 extra px of space (without it, the background could still be cut off) + measuredHeight.coerceAtMost(maxHeight - 1) + } else { + measuredHeight + } + setMeasuredDimension(measuredWidth, chosenHeight) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index c73cc9efe85c..b3af91d9fb5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -136,6 +136,9 @@ class OngoingCallController @Inject constructor( fun setChipView(chipView: View) { tearDownChipView() this.chipView = chipView + val backgroundView: OngoingCallBackgroundContainer? = + chipView.findViewById(R.id.ongoing_call_chip_background) + backgroundView?.maxHeightFetcher = { statusBarWindowController.get().statusBarHeight } if (hasOngoingCall()) { updateChip() } diff --git a/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt index 8e2b05cd9526..b9c24871eac8 100644 --- a/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt @@ -22,14 +22,21 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.UserHandle +import android.telecom.TelecomManager import android.util.Log import android.view.WindowManager import com.android.internal.app.AlertActivity import com.android.systemui.R +import javax.inject.Inject /** Dialog shown to the user to switch to managed profile for making a call using work SIM. */ -class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.OnClickListener { +class SwitchToManagedProfileForCallActivity +@Inject +constructor( + private val telecomManager: TelecomManager?, +) : AlertActivity(), DialogInterface.OnClickListener { private lateinit var phoneNumber: Uri + private lateinit var positiveActionIntent: Intent private var managedProfileUserId = UserHandle.USER_NULL override fun onCreate(savedInstanceState: Bundle?) { @@ -37,8 +44,7 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS ) super.onCreate(savedInstanceState) - - phoneNumber = intent.getData() + phoneNumber = intent.data ?: Uri.EMPTY managedProfileUserId = intent.getIntExtra( "android.telecom.extra.MANAGED_PROFILE_USER_ID", @@ -48,11 +54,31 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O mAlertParams.apply { mTitle = getString(R.string.call_from_work_profile_title) mMessage = getString(R.string.call_from_work_profile_text) - mPositiveButtonText = getString(R.string.call_from_work_profile_action) mNegativeButtonText = getString(R.string.call_from_work_profile_close) mPositiveButtonListener = this@SwitchToManagedProfileForCallActivity mNegativeButtonListener = this@SwitchToManagedProfileForCallActivity } + + // A dialer app may not be available in the managed profile. We try to handle that + // gracefully by redirecting the user to the app market to install a suitable app. + val defaultDialerPackageName: String? = + telecomManager?.getDefaultDialerPackage(UserHandle.of(managedProfileUserId)) + + val (intent, positiveButtonText) = + defaultDialerPackageName?.let { + Intent( + Intent.ACTION_CALL, + phoneNumber, + ) to R.string.call_from_work_profile_action + } + ?: Intent( + Intent.ACTION_VIEW, + Uri.parse(APP_STORE_DIALER_QUERY), + ) to R.string.install_dialer_on_work_profile_action + + positiveActionIntent = intent + mAlertParams.apply { mPositiveButtonText = getString(positiveButtonText) } + setupAlert() } @@ -66,7 +92,7 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O private fun switchToManagedProfile() { try { applicationContext.startActivityAsUser( - Intent(Intent.ACTION_CALL, phoneNumber), + positiveActionIntent, ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(), UserHandle.of(managedProfileUserId) ) @@ -77,5 +103,6 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O companion object { private const val TAG = "SwitchToManagedProfileForCallActivity" + private const val APP_STORE_DIALER_QUERY = "market://search?q=dialer" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java index 08dbb816b5e9..08b0c647628c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java +++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java @@ -32,11 +32,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; -import com.android.settingslib.users.EditUserInfoController; -import com.android.settingslib.users.GrantAdminDialogController; +import com.android.settingslib.users.CreateUserDialogController; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.user.utils.MultiUserActionsEvent; import javax.inject.Inject; @@ -61,20 +59,18 @@ public class CreateUserActivity extends Activity { private static final String EXTRA_IS_KEYGUARD_SHOWING = "extra_is_keyguard_showing"; private final UserCreator mUserCreator; - private final EditUserInfoController mEditUserInfoController; + private CreateUserDialogController mCreateUserDialogController; private final IActivityManager mActivityManager; private final ActivityStarter mActivityStarter; private final UiEventLogger mUiEventLogger; - private Dialog mGrantAdminDialog; private Dialog mSetupUserDialog; private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; - private boolean mGrantAdminRights; @Inject public CreateUserActivity(UserCreator userCreator, - EditUserInfoController editUserInfoController, IActivityManager activityManager, + CreateUserDialogController createUserDialogController, IActivityManager activityManager, ActivityStarter activityStarter, UiEventLogger uiEventLogger) { mUserCreator = userCreator; - mEditUserInfoController = editUserInfoController; + mCreateUserDialogController = createUserDialogController; mActivityManager = activityManager; mActivityStarter = activityStarter; mUiEventLogger = uiEventLogger; @@ -86,19 +82,10 @@ public class CreateUserActivity extends Activity { setShowWhenLocked(true); setContentView(R.layout.activity_create_new_user); if (savedInstanceState != null) { - mEditUserInfoController.onRestoreInstanceState(savedInstanceState); - } - boolean isKeyguardShowing = getIntent().getBooleanExtra(EXTRA_IS_KEYGUARD_SHOWING, true); - // Display grant admin dialog only on unlocked device to admin users if multiple admins - // are allowed on this device. - if (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin() - && !isKeyguardShowing) { - mGrantAdminDialog = buildGrantAdminDialog(); - mGrantAdminDialog.show(); - } else { - mSetupUserDialog = createDialog(); - mSetupUserDialog.show(); + mCreateUserDialogController.onRestoreInstanceState(savedInstanceState); } + mSetupUserDialog = createDialog(); + mSetupUserDialog.show(); getOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback); @@ -110,7 +97,7 @@ public class CreateUserActivity extends Activity { outState.putBundle(DIALOG_STATE_KEY, mSetupUserDialog.onSaveInstanceState()); } - mEditUserInfoController.onSaveInstanceState(outState); + mCreateUserDialogController.onSaveInstanceState(outState); super.onSaveInstanceState(outState); } @@ -125,48 +112,21 @@ public class CreateUserActivity extends Activity { private Dialog createDialog() { String defaultUserName = getString(com.android.settingslib.R.string.user_new_user_name); - - return mEditUserInfoController.createDialog( + boolean isKeyguardShowing = getIntent().getBooleanExtra(EXTRA_IS_KEYGUARD_SHOWING, true); + return mCreateUserDialogController.createDialog( this, this::startActivity, - null, - defaultUserName, - getString(com.android.settingslib.R.string.user_add_user), + (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin() + && !isKeyguardShowing), this::addUserNow, this::finish ); } - /** - * Returns dialog that allows to grant user admin rights. - */ - private Dialog buildGrantAdminDialog() { - return new GrantAdminDialogController().createDialog( - this, - (grantAdminRights) -> { - mGrantAdminDialog.dismiss(); - mGrantAdminRights = grantAdminRights; - if (mGrantAdminRights) { - mUiEventLogger.log(MultiUserActionsEvent - .GRANT_ADMIN_FROM_USER_SWITCHER_CREATION_DIALOG); - } else { - mUiEventLogger.log(MultiUserActionsEvent - .NOT_GRANT_ADMIN_FROM_USER_SWITCHER_CREATION_DIALOG); - } - mSetupUserDialog = createDialog(); - mSetupUserDialog.show(); - }, - () -> { - mGrantAdminRights = false; - finish(); - } - ); - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - mEditUserInfoController.onActivityResult(requestCode, resultCode, data); + mCreateUserDialogController.onActivityResult(requestCode, resultCode, data); } @Override @@ -178,9 +138,6 @@ public class CreateUserActivity extends Activity { if (mSetupUserDialog != null) { mSetupUserDialog.dismiss(); } - if (mGrantAdminDialog != null) { - mGrantAdminDialog.dismiss(); - } finish(); } @@ -190,7 +147,7 @@ public class CreateUserActivity extends Activity { super.onDestroy(); } - private void addUserNow(String userName, Drawable userIcon) { + private void addUserNow(String userName, Drawable userIcon, Boolean isAdmin) { mSetupUserDialog.dismiss(); userName = (userName == null || userName.trim().isEmpty()) ? getString(com.android.settingslib.R.string.user_new_user_name) @@ -198,7 +155,7 @@ public class CreateUserActivity extends Activity { mUserCreator.createUser(userName, userIcon, userInfo -> { - if (mGrantAdminRights) { + if (isAdmin) { mUserCreator.setUserAdmin(userInfo.id); } switchToUser(userInfo.id); @@ -230,7 +187,7 @@ public class CreateUserActivity extends Activity { */ private void startActivity(Intent intent, int requestCode) { mActivityStarter.dismissKeyguardThenExecute(() -> { - mEditUserInfoController.startingActivityForResult(); + mCreateUserDialogController.startingActivityForResult(); startActivityForResult(intent, requestCode); return true; }, /* cancel= */ null, /* afterKeyguardGone= */ true); diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index b2bf9727b534..d8ee686ea60f 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -18,6 +18,7 @@ package com.android.systemui.user; import android.os.UserHandle; +import com.android.settingslib.users.CreateUserDialogController; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule; @@ -45,6 +46,12 @@ public abstract class UserModule { return new EditUserInfoController(FILE_PROVIDER_AUTHORITY); } + /** Provides {@link CreateUserDialogController} */ + @Provides + public static CreateUserDialogController provideCreateUserDialogController() { + return new CreateUserDialogController(FILE_PROVIDER_AUTHORITY); + } + /** * Provides the {@link UserHandle} for the user associated with this System UI process. * diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index c2922c4d6f34..27c348ba329a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -50,6 +50,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.CreateUserActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord @@ -455,13 +456,16 @@ constructor( UserActionModel.ADD_USER -> { uiEventLogger.log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) val currentUser = repository.getSelectedUserInfo() - showDialog( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = currentUser.userHandle, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, - dialogShower = dialogShower, - ) + dismissDialog() + activityStarter.startActivity( + CreateUserActivity.createIntentForStart( + applicationContext, + keyguardInteractor.isKeyguardShowing() + ), + /* dismissShade= */ true, + /* animationController */ null, + /* showOverLockscreenWhenLocked */ true, + /* userHandle */ currentUser.getUserHandle(), ) } UserActionModel.ADD_SUPERVISED_USER -> { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java index 30e3d09299f2..b3496967f525 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.hardware.display.DisplayManagerGlobal; import android.testing.AndroidTestingRunner; @@ -38,6 +39,7 @@ import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.systemui.SysuiTestCase; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.settings.FakeDisplayTracker; +import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.Before; import org.junit.Test; @@ -58,6 +60,10 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory; @Mock private KeyguardDisplayManager.KeyguardPresentation mKeyguardPresentation; + @Mock + private KeyguardDisplayManager.DeviceStateHelper mDeviceStateHelper; + @Mock + private KeyguardStateController mKeyguardStateController; private Executor mMainExecutor = Runnable::run; private Executor mBackgroundExecutor = Runnable::run; @@ -76,7 +82,7 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController, mKeyguardStatusViewComponentFactory, mDisplayTracker, mMainExecutor, - mBackgroundExecutor)); + mBackgroundExecutor, mDeviceStateHelper, mKeyguardStateController)); doReturn(mKeyguardPresentation).when(mManager).createPresentation(any()); mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY, @@ -123,4 +129,13 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { mManager.show(); verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay)); } + + @Test + public void testShow_concurrentDisplayActive_occluded() { + mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay}); + + when(mDeviceStateHelper.isConcurrentDisplayActive(mSecondaryDisplay)).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); + verify(mManager, never()).createPresentation(eq(mSecondaryDisplay)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index 30cbc5242a81..665246bd7f7d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java @@ -17,7 +17,6 @@ package com.android.systemui.accessibility; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; -import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -74,10 +73,9 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { @Test public void testShowSettingsPanel() { - final int mode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; - mMagnificationSettingsController.showMagnificationSettings(mode); + mMagnificationSettingsController.showMagnificationSettings(); - verify(mWindowMagnificationSettings).showSettingPanel(eq(mode)); + verify(mWindowMagnificationSettings).showSettingPanel(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java index c08b5b47cb06..ce96708039ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java @@ -16,7 +16,10 @@ package com.android.systemui.accessibility; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static com.google.common.truth.Truth.assertThat; @@ -24,12 +27,17 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.IdRes; import android.content.Context; import android.content.pm.ActivityInfo; +import android.database.ContentObserver; +import android.os.UserHandle; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -102,7 +110,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void showSettingPanel_hasAccessibilityWindowTitle() { - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); final WindowManager.LayoutParams layoutPrams = mWindowManager.getLayoutParamsFromAttachedView(); @@ -114,7 +125,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void showSettingPanel_windowMode_showEditButtonAndDiagonalView() { - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); final Button editButton = getInternalView(R.id.magnifier_edit_button); assertEquals(editButton.getVisibility(), View.VISIBLE); @@ -125,7 +139,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void showSettingPanel_fullScreenMode_hideEditButtonAndDiagonalView() { - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + mWindowMagnificationSettings.showSettingPanel(); final Button editButton = getInternalView(R.id.magnifier_edit_button); assertEquals(editButton.getVisibility(), View.INVISIBLE); @@ -135,9 +152,22 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { } @Test + public void showSettingPanel_windowOnlyCapability_hideFullscreenButton() { + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); + + final View fullscreenButton = getInternalView(R.id.magnifier_full_button); + assertThat(fullscreenButton.getVisibility()).isEqualTo(View.GONE); + } + + @Test public void performClick_smallSizeButton_changeMagnifierSizeSmallAndSwitchToWindowMode() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); verifyOnSetMagnifierSizeAndOnModeSwitch( R.id.magnifier_small_button, MAGNIFICATION_SIZE_SMALL); @@ -145,8 +175,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void performClick_mediumSizeButton_changeMagnifierSizeMediumAndSwitchToWindowMode() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); verifyOnSetMagnifierSizeAndOnModeSwitch( R.id.magnifier_medium_button, MAGNIFICATION_SIZE_MEDIUM); @@ -154,8 +186,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void performClick_largeSizeButton_changeMagnifierSizeLargeAndSwitchToWindowMode() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); verifyOnSetMagnifierSizeAndOnModeSwitch( R.id.magnifier_large_button, MAGNIFICATION_SIZE_LARGE); @@ -178,8 +212,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { View fullScreenModeButton = getInternalView(R.id.magnifier_full_button); getInternalView(R.id.magnifier_panel_view); - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); // Perform click fullScreenModeButton.performClick(); @@ -192,8 +228,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { public void performClick_editButton_setEditMagnifierSizeMode() { View editButton = getInternalView(R.id.magnifier_edit_button); - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); // Perform click editButton.performClick(); @@ -208,8 +246,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { getInternalView(R.id.magnifier_horizontal_lock_switch); final boolean currentCheckedState = diagonalScrollingSwitch.isChecked(); - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); // Perform click diagonalScrollingSwitch.performClick(); @@ -219,8 +259,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void onConfigurationChanged_selectedButtonIsStillSelected() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); View magnifierMediumButton = getInternalView(R.id.magnifier_medium_button); magnifierMediumButton.performClick(); @@ -232,9 +274,46 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { assertThat(magnifierMediumButton.isSelected()).isTrue(); } + @Test + public void showSettingsPanel_observerRegistered() { + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + + mWindowMagnificationSettings.showSettingPanel(); + + verify(mSecureSettings).registerContentObserverForUser( + eq(ACCESSIBILITY_MAGNIFICATION_CAPABILITY), + any(ContentObserver.class), + eq(UserHandle.USER_CURRENT)); + } + + @Test + public void hideSettingsPanel_observerUnregistered() { + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + + mWindowMagnificationSettings.showSettingPanel(); + mWindowMagnificationSettings.hideSettingPanel(); + + verify(mSecureSettings).unregisterContentObserver(any(ContentObserver.class)); + } + private <T extends View> T getInternalView(@IdRes int idRes) { T view = mSettingView.findViewById(idRes); assertNotNull(view); return view; } + + private void setupMagnificationCapabilityAndMode(int capability, int mode) { + when(mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT)).thenReturn(capability); + when(mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT)).thenReturn(mode); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java index 239b5bd39430..086f6285f8cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java @@ -184,8 +184,7 @@ public class WindowMagnificationTest extends SysuiTestCase { mWindowMagnification.mWindowMagnifierCallback.onClickSettingsButton(TEST_DISPLAY); waitForIdleSync(); - verify(mMagnificationSettingsController).showMagnificationSettings( - eq(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)); + verify(mMagnificationSettingsController).showMagnificationSettings(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt new file mode 100644 index 000000000000..2e62bebfe3f1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.authentication.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepository +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class AuthenticationInteractorTest : SysuiTestCase() { + + private val testScope = TestScope() + private val repository: AuthenticationRepository = AuthenticationRepositoryImpl() + private val underTest = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = repository, + ) + + @Test + fun authMethod() = + testScope.runTest { + val authMethod by collectLastValue(underTest.authenticationMethod) + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234)) + + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password")) + } + + @Test + fun isUnlocked_whenAuthMethodIsNone_isTrue() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + underTest.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(isUnlocked).isTrue() + } + + @Test + fun unlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + underTest.unlockDevice() + runCurrent() + + assertThat(isUnlocked).isTrue() + } + + @Test + fun biometricUnlock() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + underTest.biometricUnlock() + runCurrent() + + assertThat(isUnlocked).isTrue() + } + + @Test + fun toggleBypassEnabled() = + testScope.runTest { + val isBypassEnabled by collectLastValue(underTest.isBypassEnabled) + assertThat(isBypassEnabled).isFalse() + + underTest.toggleBypassEnabled() + assertThat(isBypassEnabled).isTrue() + + underTest.toggleBypassEnabled() + assertThat(isBypassEnabled).isFalse() + } + + @Test + fun isAuthenticationRequired_lockedAndSecured_true() = + testScope.runTest { + underTest.lockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + + assertThat(underTest.isAuthenticationRequired()).isTrue() + } + + @Test + fun isAuthenticationRequired_lockedAndNotSecured_false() = + testScope.runTest { + underTest.lockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(underTest.isAuthenticationRequired()).isFalse() + } + + @Test + fun isAuthenticationRequired_unlockedAndSecured_false() = + testScope.runTest { + underTest.unlockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + + assertThat(underTest.isAuthenticationRequired()).isFalse() + } + + @Test + fun isAuthenticationRequired_unlockedAndNotSecured_false() = + testScope.runTest { + underTest.unlockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(underTest.isAuthenticationRequired()).isFalse() + } + + @Test + fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() + assertThat(isUnlocked).isTrue() + } + + @Test + fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse() + assertThat(isUnlocked).isFalse() + } + + @Test + fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate("password".toList())).isTrue() + assertThat(isUnlocked).isTrue() + } + + @Test + fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate("alohomora".toList())).isFalse() + assertThat(isUnlocked).isFalse() + } + + @Test + fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod( + AuthenticationMethodModel.Pattern( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 2, + ), + ) + ) + ) + assertThat(isUnlocked).isFalse() + + assertThat( + underTest.authenticate( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 2, + ), + ) + ) + ) + .isTrue() + assertThat(isUnlocked).isTrue() + } + + @Test + fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod( + AuthenticationMethodModel.Pattern( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 2, + ), + ) + ) + ) + assertThat(isUnlocked).isFalse() + + assertThat( + underTest.authenticate( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 2, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 2, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 2, + y = 2, + ), + ) + ) + ) + .isFalse() + assertThat(isUnlocked).isFalse() + } + + @Test + fun unlocksDevice_whenAuthMethodBecomesNone() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + repository.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(isUnlocked).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java index b8bca3a403e1..38c9caf085e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationManager; import android.hardware.biometrics.BiometricFaceConstants; -import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricSourceType; import android.os.Handler; import android.testing.AndroidTestingRunner; @@ -51,6 +50,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Optional; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -64,11 +65,16 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { KeyguardStateController mKeyguardStateController; @Mock NotificationManager mNotificationManager; + @Mock + Optional<FingerprintReEnrollNotification> mFingerprintReEnrollNotificationOptional; + @Mock + FingerprintReEnrollNotification mFingerprintReEnrollNotification; private static final String TAG = "BiometricNotificationService"; private static final int FACE_NOTIFICATION_ID = 1; private static final int FINGERPRINT_NOTIFICATION_ID = 2; private static final long SHOW_NOTIFICATION_DELAY_MS = 5_000L; // 5 seconds + private static final int FINGERPRINT_ACQUIRED_RE_ENROLL = 0; private final ArgumentCaptor<Notification> mNotificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class); @@ -78,6 +84,11 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { @Before public void setUp() { + when(mFingerprintReEnrollNotificationOptional.orElse(any())) + .thenReturn(mFingerprintReEnrollNotification); + when(mFingerprintReEnrollNotification.isFingerprintReEnrollRequired( + FINGERPRINT_ACQUIRED_RE_ENROLL)).thenReturn(true); + mLooper = TestableLooper.get(this); Handler handler = new Handler(mLooper.getLooper()); BiometricNotificationDialogFactory dialogFactory = new BiometricNotificationDialogFactory(); @@ -87,7 +98,8 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { new BiometricNotificationService(mContext, mKeyguardUpdateMonitor, mKeyguardStateController, handler, mNotificationManager, - broadcastReceiver); + broadcastReceiver, + mFingerprintReEnrollNotificationOptional); biometricNotificationService.start(); ArgumentCaptor<KeyguardUpdateMonitorCallback> updateMonitorCallbackArgumentCaptor = @@ -108,8 +120,8 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { public void testShowFingerprintReEnrollNotification() { when(mKeyguardStateController.isShowing()).thenReturn(false); - mKeyguardUpdateMonitorCallback.onBiometricError( - BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL, + mKeyguardUpdateMonitorCallback.onBiometricHelp( + FINGERPRINT_ACQUIRED_RE_ENROLL, "Testing Fingerprint Re-enrollment" /* errString */, BiometricSourceType.FINGERPRINT ); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt index 4f89b69108f4..da55d5a099b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt @@ -33,7 +33,7 @@ class BoundingBoxOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { @Test fun isGoodOverlap() { val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) - val actual = underTest.isGoodOverlap(touchData, SENSOR) + val actual = underTest.isGoodOverlap(touchData, SENSOR, OVERLAY) assertThat(actual).isEqualTo(testCase.expected) } @@ -50,8 +50,10 @@ class BoundingBoxOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) ), genNegativeTestCases( - invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1), - invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + invalidXs = + listOf(SENSOR.left - 1, SENSOR.right + 1, OVERLAY.left, OVERLAY.right), + invalidYs = + listOf(SENSOR.top - 1, SENSOR.bottom + 1, OVERLAY.top, OVERLAY.bottom), validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) ) @@ -82,6 +84,7 @@ private val TOUCH_DATA = ) private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 500 /* bottom */) +private val OVERLAY = Rect(0 /* left */, 100 /* top */, 400 /* right */, 600 /* bottom */) private fun genTestCases( xs: List<Int>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt index fb3c1854e996..317141ba42dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt @@ -43,7 +43,7 @@ class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { minor = testCase.minor, major = testCase.major ) - val actual = underTest.isGoodOverlap(touchData, SENSOR) + val actual = underTest.isGoodOverlap(touchData, SENSOR, OVERLAY) assertThat(actual).isEqualTo(testCase.expected) } @@ -71,8 +71,10 @@ class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { expected = true ), genNegativeTestCase( - outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), - outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + outerXs = + listOf(SENSOR.left - 1, SENSOR.right + 1, OVERLAY.left, OVERLAY.right), + outerYs = + listOf(SENSOR.top - 1, SENSOR.bottom + 1, OVERLAY.top, OVERLAY.bottom), minor = 100f, major = 100f, expected = false @@ -104,6 +106,7 @@ private val TOUCH_DATA = ) private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */) +private val OVERLAY = Rect(0 /* left */, 100 /* top */, 400 /* right */, 600 /* bottom */) private fun genPositiveTestCases( innerXs: List<Int>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt index 834d0a69e427..3e5c43a33474 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt @@ -16,7 +16,7 @@ class NormalizedTouchDataTest(val testCase: TestCase) : SysuiTestCase() { @Test fun isWithinSensor() { val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) - val actual = touchData.isWithinSensor(SENSOR) + val actual = touchData.isWithinBounds(SENSOR) assertThat(actual).isEqualTo(testCase.expected) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt new file mode 100644 index 000000000000..7dd376ede361 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class BouncerInteractorTest : SysuiTestCase() { + + private val testScope = TestScope() + private val authenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + private val sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val underTest = + BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = "container1", + ) + + @Before + fun setUp() { + overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN) + overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD) + overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN) + overrideResource(R.string.kg_wrong_pin, MESSAGE_WRONG_PIN) + overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD) + overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN) + } + + @Test + fun pinAuthMethod() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + underTest.showOrUnlockDevice("container1") + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) + + underTest.clearMessage() + assertThat(message).isNull() + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) + + // Wrong input. + underTest.authenticate(listOf(9, 8, 7)) + assertThat(message).isEqualTo(MESSAGE_WRONG_PIN) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) + + // Correct input. + underTest.authenticate(listOf(1, 2, 3, 4)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun passwordAuthMethod() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + underTest.showOrUnlockDevice("container1") + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) + + underTest.clearMessage() + assertThat(message).isNull() + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) + + // Wrong input. + underTest.authenticate("alohamora".toList()) + assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) + + // Correct input. + underTest.authenticate("password".toList()) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun patternAuthMethod() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(emptyList()) + ) + authenticationInteractor.lockDevice() + underTest.showOrUnlockDevice("container1") + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) + + underTest.clearMessage() + assertThat(message).isNull() + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) + + // Wrong input. + underTest.authenticate( + listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4)) + ) + assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) + + // Correct input. + underTest.authenticate(emptyList()) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun showOrUnlockDevice_notLocked_switchesToGoneScene() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() + runCurrent() + + underTest.showOrUnlockDevice("container1") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + authenticationInteractor.lockDevice() + + underTest.showOrUnlockDevice("container1") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun showOrUnlockDevice_customMessageShown() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + + val customMessage = "Hello there!" + underTest.showOrUnlockDevice("container1", customMessage) + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(customMessage) + } + + companion object { + private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN" + private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password" + private const val MESSAGE_ENTER_YOUR_PATTERN = "Enter your pattern" + private const val MESSAGE_WRONG_PIN = "Wrong PIN" + private const val MESSAGE_WRONG_PASSWORD = "Wrong password" + private const val MESSAGE_WRONG_PATTERN = "Wrong pattern" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 1a620d266e42..fcd6568de9f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -70,7 +70,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.doAnswer import org.mockito.Mockito.doReturn +import org.mockito.Mockito.isNull import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify @@ -104,6 +106,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { private lateinit var parent: FrameLayout private lateinit var underTest: ControlsUiControllerImpl + private var isKeyguardDismissed: Boolean = true + private var isRemoveAppDialogCreated: Boolean = false + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -140,11 +145,23 @@ class ControlsUiControllerImplTest : SysuiTestCase() { authorizedPanelsRepository, preferredPanelRepository, featureFlags, - ControlsDialogsFactory { fakeDialogController.dialog }, + ControlsDialogsFactory { + isRemoveAppDialogCreated = true + fakeDialogController.dialog + }, dumpManager, ) `when`(userTracker.userId).thenReturn(0) `when`(userTracker.userHandle).thenReturn(UserHandle.of(0)) + doAnswer { + if (isKeyguardDismissed) { + it.getArgument<ActivityStarter.OnDismissAction>(0).onDismiss() + } else { + it.getArgument<Runnable?>(1)?.run() + } + } + .whenever(activityStarter) + .dismissKeyguardThenExecute(any(), isNull(), any()) } @Test @@ -414,6 +431,26 @@ class ControlsUiControllerImplTest : SysuiTestCase() { } @Test + fun testKeyguardRemovingAppsNotShowingDialog() { + isKeyguardDismissed = false + val componentName = ComponentName(context, "cls") + whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true) + val panel = SelectedItem.PanelItem("App name", componentName) + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(panel) + ) + underTest.show(parent, {}, context) + underTest.startRemovingApp(componentName, "Test App") + + assertThat(isRemoveAppDialogCreated).isFalse() + verify(controlsController, never()).removeFavorites(eq(componentName)) + assertThat(underTest.getPreferredSelectedItem(emptyList())).isEqualTo(panel) + assertThat(preferredPanelRepository.shouldAddDefaultComponent()).isTrue() + assertThat(preferredPanelRepository.getSelectedComponent()) + .isEqualTo(SelectedComponentRepository.SelectedComponent(panel)) + } + + @Test fun testCancelRemovingAppsDoesntRemoveFavorite() { val componentName = ComponentName(context, "cls") whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt index 47df64fc33e2..0a9618c2d568 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt @@ -113,8 +113,8 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { @After fun tearDown() { - keyguardUnlockAnimationController.surfaceBehindEntryAnimator.cancel() - keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.cancel() + keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(true) + keyguardUnlockAnimationController.wallpaperAlphaAnimator.cancel() } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 83c89f1e6855..f31ac00051f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -95,6 +95,7 @@ import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.DeviceConfigProxyFake; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import com.android.wm.shell.keyguard.KeyguardTransitions; import org.junit.Before; import org.junit.Test; @@ -135,6 +136,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock ScreenOffAnimationController mScreenOffAnimationController; private @Mock InteractionJankMonitor mInteractionJankMonitor; private @Mock ScreenOnCoordinator mScreenOnCoordinator; + private @Mock KeyguardTransitions mKeyguardTransitions; private @Mock ShadeController mShadeController; private NotificationShadeWindowController mNotificationShadeWindowController; private @Mock DreamOverlayStateController mDreamOverlayStateController; @@ -519,6 +521,12 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } @Test + public void testWakeAndUnlocking() { + mViewMediator.onWakeAndUnlocking(); + verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); + } + + @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testDoKeyguardWhileInteractive_resets() { mViewMediator.setShowingLocked(true); @@ -614,6 +622,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mScreenOffAnimationController, () -> mNotificationShadeDepthController, mScreenOnCoordinator, + mKeyguardTransitions, mInteractionJankMonitor, mDreamOverlayStateController, () -> mShadeController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt new file mode 100644 index 000000000000..749e7a0481eb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class LockScreenSceneInteractorTest : SysuiTestCase() { + + private val testScope = TestScope() + private val sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + private val underTest = + LockScreenSceneInteractor( + applicationScope = testScope.backgroundScope, + authenticationInteractor = mAuthenticationInteractor, + bouncerInteractorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = containerName, + ) + } + }, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + + @Test + fun isDeviceLocked() = + testScope.runTest { + val isDeviceLocked by collectLastValue(underTest.isDeviceLocked) + + mAuthenticationInteractor.lockDevice() + assertThat(isDeviceLocked).isTrue() + + mAuthenticationInteractor.unlockDevice() + assertThat(isDeviceLocked).isFalse() + } + + @Test + fun isSwipeToDismissEnabled_deviceLockedAndAuthMethodSwipe_true() = + testScope.runTest { + val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled) + + mAuthenticationInteractor.lockDevice() + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(isSwipeToDismissEnabled).isTrue() + } + + @Test + fun isSwipeToDismissEnabled_deviceUnlockedAndAuthMethodSwipe_false() = + testScope.runTest { + val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled) + + mAuthenticationInteractor.unlockDevice() + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(isSwipeToDismissEnabled).isFalse() + } + + @Test + fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.lockDevice() + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + underTest.dismissLockScreen() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun dismissLockScreen_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.unlockDevice() + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + underTest.dismissLockScreen() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.lockDevice() + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + underTest.dismissLockScreen() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + runCurrent() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + runCurrent() + mAuthenticationInteractor.unlockDevice() + runCurrent() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + + mAuthenticationInteractor.lockDevice() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + } + + @Test + fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) + if (!mAuthenticationInteractor.isBypassEnabled.value) { + mAuthenticationInteractor.toggleBypassEnabled() + } + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + mAuthenticationInteractor.biometricUnlock() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) + if (mAuthenticationInteractor.isBypassEnabled.value) { + mAuthenticationInteractor.toggleBypassEnabled() + } + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + mAuthenticationInteractor.biometricUnlock() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + } + + @Test + fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + assertThat(isUnlocked).isFalse() + + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + + assertThat(isUnlocked).isTrue() + } + + @Test + fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(isUnlocked).isFalse() + + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + + assertThat(isUnlocked).isFalse() + } + + @Test + fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + runCurrent() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Shade)) + runCurrent() + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + runCurrent() + assertThat(isUnlocked).isFalse() + + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + + assertThat(isUnlocked).isFalse() + } + + @Test + fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + runCurrent() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.QuickSettings)) + runCurrent() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings)) + + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings)) + } + + companion object { + private const val CONTAINER_NAME = "container1" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt new file mode 100644 index 000000000000..d335b09b196a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class LockScreenSceneViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + + private val underTest = + LockScreenSceneViewModel( + applicationScope = testScope.backgroundScope, + interactorFactory = + object : LockScreenSceneInteractor.Factory { + override fun create(containerName: String): LockScreenSceneInteractor { + return LockScreenSceneInteractor( + applicationScope = testScope.backgroundScope, + authenticationInteractor = mAuthenticationInteractor, + bouncerInteractorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = containerName, + ) + } + }, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + } + }, + containerName = CONTAINER_NAME + ) + + @Test + fun lockButtonIcon_whenLocked() = + testScope.runTest { + val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + mAuthenticationInteractor.lockDevice() + + assertThat((lockButtonIcon as? Icon.Resource)?.res) + .isEqualTo(R.drawable.ic_device_lock_on) + } + + @Test + fun lockButtonIcon_whenUnlocked() = + testScope.runTest { + val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + mAuthenticationInteractor.unlockDevice() + + assertThat((lockButtonIcon as? Icon.Resource)?.res) + .isEqualTo(R.drawable.ic_device_lock_off) + } + + @Test + fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + mAuthenticationInteractor.lockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test + fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.lockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) + } + + @Test + fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.lockDevice() + runCurrent() + + underTest.onLockButtonClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onContentClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.unlockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.lockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onLockButtonClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.unlockDevice() + runCurrent() + + underTest.onLockButtonClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + companion object { + private const val CONTAINER_NAME = "container1" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index faca8a91d6b3..9b26d9c1a249 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -347,10 +347,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void onBindViewHolder_isMutingExpectedDevice_verifyView() { + public void advanced_onBindViewHolder_isMutingExpectedDevice_verifyView() { + when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true); when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false); when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index bd7898a5d986..c582cfc93012 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -25,8 +25,11 @@ import android.app.role.RoleManager.ROLE_NOTES import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.ACTION_CREATE_NOTE import android.content.Intent.ACTION_MAIN +import android.content.Intent.ACTION_MANAGE_DEFAULT_APP import android.content.Intent.CATEGORY_HOME +import android.content.Intent.EXTRA_USE_STYLUS_MODE import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -47,8 +50,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskController.Companion.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE import com.android.systemui.notetask.NoteTaskController.Companion.SHORTCUT_ID import com.android.systemui.notetask.NoteTaskEntryPoint.APP_CLIPS +import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON +import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity @@ -221,31 +226,22 @@ internal class NoteTaskControllerTest : SysuiTestCase() { // region showNoteTask @Test fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() { - val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = true, - ) + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) - createNoteTaskController() - .showNoteTask( - entryPoint = expectedInfo.entryPoint!!, - ) + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) - .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) - .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskOpened(expectedInfo) @@ -256,32 +252,23 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun showNoteTaskWithUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() { val user10 = UserHandle.of(/* userId= */ 10) val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = true, - user = user10, - ) + NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true, user = user10) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) createNoteTaskController() - .showNoteTaskAsUser( - entryPoint = expectedInfo.entryPoint!!, - user = user10, - ) + .showNoteTaskAsUser(entryPoint = expectedInfo.entryPoint!!, user = user10) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) - .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) - .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } assertThat(userCaptor.value).isEqualTo(user10) verify(eventLogger).logNoteTaskOpened(expectedInfo) @@ -290,11 +277,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_keyguardIsLocked_noteIsOpen_shouldCloseActivityAndLogUiEvent() { - val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = true, - ) + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) whenever(activityManager.getRunningTasks(anyInt())) @@ -305,10 +288,10 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(ACTION_MAIN) - assertThat(intent.categories).contains(CATEGORY_HOME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) + assertThat(intentCaptor.value).run { + hasAction(ACTION_MAIN) + categories().contains(CATEGORY_HOME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) } assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskClosed(expectedInfo) @@ -317,18 +300,11 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesWithoutLoggingUiEvent() { - val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = false, - ) + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = false) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) - createNoteTaskController() - .showNoteTask( - entryPoint = expectedInfo.entryPoint!!, - ) + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) // Context package name used to create bubble icon from drawable resource id verify(context).packageName @@ -338,10 +314,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_bubblesIsNull_shouldDoNothing() { - createNoteTaskController(bubbles = null) - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - ) + createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -352,7 +325,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val noteTaskController = spy(createNoteTaskController()) doNothing().whenever(noteTaskController).showNoDefaultNotesAppToast() - noteTaskController.showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON) + noteTaskController.showNoteTask(entryPoint = TAIL_BUTTON) verify(noteTaskController).showNoDefaultNotesAppToast() verifyZeroInteractions(context, bubbles, eventLogger) @@ -360,10 +333,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_flagDisabled_shouldDoNothing() { - createNoteTaskController(isEnabled = false) - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - ) + createNoteTaskController(isEnabled = false).showNoteTask(entryPoint = TAIL_BUTTON) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -372,10 +342,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun showNoteTask_userIsLocked_shouldDoNothing() { whenever(userManager.isUserUnlocked).thenReturn(false) - createNoteTaskController() - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - ) + createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -383,30 +350,22 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_keyboardShortcut_shouldStartActivity() { val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.KEYBOARD_SHORTCUT, - isKeyguardLocked = true, - ) + NOTE_TASK_INFO.copy(entryPoint = KEYBOARD_SHORTCUT, isKeyguardLocked = true) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) - createNoteTaskController() - .showNoteTask( - entryPoint = expectedInfo.entryPoint!!, - ) + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) - .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) - .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, true)).isFalse() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + extras().bool(EXTRA_USE_STYLUS_MODE).isFalse() } assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskOpened(expectedInfo) @@ -583,7 +542,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON) + createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle) } @@ -593,8 +552,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController() - .showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT) + createNoteTaskController().showNoteTask(entryPoint = WIDGET_PICKER_SHORTCUT) verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) } @@ -604,7 +562,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.APP_CLIPS) + createNoteTaskController().showNoteTask(entryPoint = APP_CLIPS) verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) } @@ -615,13 +573,13 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val iconCaptor = argumentCaptor<Icon>() verify(bubbles) .showOrHideAppBubble(capture(intentCaptor), eq(userHandle), capture(iconCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } - iconCaptor.value.let { icon -> + iconCaptor.value?.let { icon -> assertThat(icon).isNotNull() assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) } @@ -679,9 +637,10 @@ internal class NoteTaskControllerTest : SysuiTestCase() { verify(shortcutManager).updateShortcuts(actualShortcuts.capture()) val actualShortcut = actualShortcuts.value.first() assertThat(actualShortcut.id).isEqualTo(SHORTCUT_ID) - assertThat(actualShortcut.intent?.component?.className) - .isEqualTo(LaunchNoteTaskActivity::class.java.name) - assertThat(actualShortcut.intent?.action).isEqualTo(Intent.ACTION_CREATE_NOTE) + assertThat(actualShortcut.intent).run { + hasComponentClass(LaunchNoteTaskActivity::class.java) + hasAction(ACTION_CREATE_NOTE) + } assertThat(actualShortcut.shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL) assertThat(actualShortcut.isLongLived).isEqualTo(true) assertThat(actualShortcut.icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) @@ -737,12 +696,9 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() verify(context).startActivityAsUser(intentCaptor.capture(), eq(user0)) - intentCaptor.value.let { intent -> - assertThat(intent) - .hasComponent( - ComponentName(context, LaunchNoteTaskManagedProfileProxyActivity::class.java) - ) - assertThat(intent).hasFlags(FLAG_ACTIVITY_NEW_TASK) + assertThat(intentCaptor.value).run { + hasComponentClass(LaunchNoteTaskManagedProfileProxyActivity::class.java) + hasFlags(FLAG_ACTIVITY_NEW_TASK) } } // endregion @@ -817,9 +773,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(workUserInfo.id)) } @@ -833,9 +787,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) } @@ -848,9 +800,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) } @@ -863,9 +813,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) } // endregion diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java index 35c8cc70953d..87892539ccfe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java @@ -28,8 +28,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.test.suitebuilder.annotation.SmallTest; +import android.testing.TestableLooper; +import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import androidx.test.runner.AndroidJUnit4; @@ -48,6 +51,7 @@ import org.mockito.Mockito; @SmallTest @RunWith(AndroidJUnit4.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class TileLayoutTest extends SysuiTestCase { private Resources mResources; private int mLayoutSizeForOneTile; @@ -228,4 +232,53 @@ public class TileLayoutTest extends SysuiTestCase { assertEquals(false, mTileLayout.updateResources()); } + + @Test + public void fontScalingChanged_updateResources_cellHeightEnoughForTileContent() { + final float originalFontScale = mContext.getResources().getConfiguration().fontScale; + float[] testScales = {0.8f, 1.0f, 1.4f, 1.6f, 2.0f}; + for (float scale: testScales) { + changeFontScaling_updateResources_cellHeightEnoughForTileContent(scale); + } + + changeFontScaling(originalFontScale); + } + + private void changeFontScaling_updateResources_cellHeightEnoughForTileContent(float scale) { + changeFontScaling(scale); + + QSPanelControllerBase.TileRecord tileRecord = createTileRecord(); + mTileLayout.addTile(tileRecord); + + FakeTileView tileView = new FakeTileView(mContext); + QSTile.State state = new QSTile.State(); + state.label = "TEST LABEL"; + state.secondaryLabel = "TEST SECONDARY LABEL"; + tileView.changeState(state); + + mTileLayout.updateResources(); + + int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + tileView.measure(spec, spec); + assertTrue(mTileLayout.getCellHeight() >= tileView.getMeasuredHeight()); + + mTileLayout.removeTile(tileRecord); + } + + private static class FakeTileView extends QSTileViewImpl { + FakeTileView(Context context) { + super(context, new QSIconViewImpl(context), /* collapsed= */ false); + } + + void changeState(QSTile.State state) { + handleStateChanged(state); + } + } + + private void changeFontScaling(float scale) { + Configuration configuration = new Configuration(mContext.getResources().getConfiguration()); + configuration.fontScale = scale; + // updateConfiguration could help update on both resource configuration and displayMetrics + mContext.getResources().updateConfiguration(configuration, null, null); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt new file mode 100644 index 000000000000..e8875bee5276 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class QuickSettingsSceneViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + + private val underTest = + QuickSettingsSceneViewModel( + lockScreenSceneInteractorFactory = + object : LockScreenSceneInteractor.Factory { + override fun create(containerName: String): LockScreenSceneInteractor { + return LockScreenSceneInteractor( + applicationScope = testScope.backgroundScope, + authenticationInteractor = mAuthenticationInteractor, + bouncerInteractorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = containerName, + ) + } + }, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + } + }, + containerName = CONTAINER_NAME + ) + + @Test + fun onContentClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.unlockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.lockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + companion object { + private const val CONTAINER_NAME = "container1" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java index 9acd47e4378f..55813f60aecd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java @@ -17,8 +17,10 @@ package com.android.systemui.reardisplay; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertTrue; +import android.content.res.Configuration; import android.hardware.devicestate.DeviceStateManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -68,6 +70,27 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { } @Test + public void testClosedDialogIsRefreshedOnConfigurationChange() { + RearDisplayDialogController controller = new RearDisplayDialogController(mContext, + mCommandQueue, mFakeExecutor); + controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); + controller.setFoldedStates(new int[]{0}); + controller.setAnimationRepeatCount(0); + + controller.showRearDisplayDialog(CLOSED_BASE_STATE); + assertTrue(controller.mRearDisplayEducationDialog.isShowing()); + TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + R.id.rear_display_title_text_view); + + controller.onConfigurationChanged(new Configuration()); + assertTrue(controller.mRearDisplayEducationDialog.isShowing()); + TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById( + R.id.rear_display_title_text_view); + + assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2); + } + + @Test public void testOpenDialogIsShown() { RearDisplayDialogController controller = new RearDisplayDialogController(mContext, mCommandQueue, mFakeExecutor); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java index 3c08d58cbb67..27eec801ef62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java @@ -37,8 +37,8 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; +import android.os.Process; import android.os.ResultReceiver; -import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.widget.ImageView; @@ -120,7 +120,8 @@ public final class AppClipsActivityTest extends SysuiTestCase { ImageExporter.Result result = new ImageExporter.Result(); result.uri = TEST_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), any(Bitmap.class), - any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); + eq(Process.myUserHandle()))) + .thenReturn(Futures.immediateFuture(result)); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java index cbd9dba3cdbf..e9007ff84f13 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java @@ -27,6 +27,7 @@ import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_USE_WP_USER; import static com.google.common.truth.Truth.assertThat; @@ -74,6 +75,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; import java.util.Optional; @RunWith(AndroidTestingRunner.class) @@ -82,7 +84,6 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { private static final String TEST_URI_STRING = "www.test-uri.com"; private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING); private static final int TEST_UID = 42; - private static final int TEST_USER_ID = 43; private static final String TEST_CALLING_PACKAGE = "test-calling-package"; @Mock @@ -287,6 +288,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualIntent.getComponent()).isEqualTo( new ComponentName(mContext, AppClipsTrampolineActivity.class)); assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + assertThat(actualIntent.getBooleanExtra(EXTRA_USE_WP_USER, false)).isTrue(); assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM); } @@ -313,13 +315,13 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { when(mOptionalBubbles.get()).thenReturn(mBubbles); when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true); when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); - when(mUserTracker.getUserId()).thenReturn(TEST_USER_ID); + when(mUserTracker.getUserProfiles()).thenReturn(List.of()); ApplicationInfo testApplicationInfo = new ApplicationInfo(); testApplicationInfo.uid = TEST_UID; when(mPackageManager.getApplicationInfoAsUser(eq(TEST_CALLING_PACKAGE), any(ApplicationInfoFlags.class), - eq(TEST_USER_ID))).thenReturn(testApplicationInfo); + eq(mContext.getUser().getIdentifier()))).thenReturn(testApplicationInfo); } public static final class AppClipsTrampolineActivityTestable extends diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java index e7c3c0578627..b7b8b11ba887 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java @@ -31,6 +31,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.net.Uri; +import android.os.Process; import android.os.UserHandle; import androidx.test.runner.AndroidJUnit4; @@ -57,10 +58,10 @@ public final class AppClipsViewModelTest extends SysuiTestCase { private static final Drawable FAKE_DRAWABLE = new ShapeDrawable(); private static final Rect FAKE_RECT = new Rect(); private static final Uri FAKE_URI = Uri.parse("www.test-uri.com"); + private static final UserHandle USER_HANDLE = Process.myUserHandle(); @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; @Mock private ImageExporter mImageExporter; - private AppClipsViewModel mViewModel; @Before @@ -99,10 +100,10 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_throwsError_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn( + eq(USER_HANDLE))).thenReturn( Futures.immediateFailedFuture(new ExecutionException(new Throwable()))); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()) @@ -113,10 +114,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_failsSilently_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn( - Futures.immediateFuture(new ImageExporter.Result())); + eq(USER_HANDLE))).thenReturn(Futures.immediateFuture(new ImageExporter.Result())); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()) @@ -129,9 +129,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { ImageExporter.Result result = new ImageExporter.Result(); result.uri = FAKE_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); + eq(USER_HANDLE))).thenReturn(Futures.immediateFuture(result)); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()).isNull(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt new file mode 100644 index 000000000000..688cce83a55d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class ShadeSceneViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + + private val underTest = + ShadeSceneViewModel( + applicationScope = testScope.backgroundScope, + lockScreenSceneInteractorFactory = + object : LockScreenSceneInteractor.Factory { + override fun create(containerName: String): LockScreenSceneInteractor { + return LockScreenSceneInteractor( + applicationScope = testScope.backgroundScope, + authenticationInteractor = mAuthenticationInteractor, + bouncerInteractorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = containerName, + ) + } + }, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + } + }, + containerName = CONTAINER_NAME + ) + + @Test + fun upTransitionSceneKey_deviceLocked_lockScreen() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.lockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.LockScreen) + } + + @Test + fun upTransitionSceneKey_deviceUnlocked_gone() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.unlockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test + fun onContentClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.unlockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.lockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + companion object { + private const val CONTAINER_NAME = "container1" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java index de5824d1f463..b7241759ca24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java @@ -32,6 +32,7 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.log.TableLogBufferBase; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -44,6 +45,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import kotlinx.coroutines.CoroutineScope; @@ -59,6 +61,8 @@ public class ConditionMonitorTest extends SysuiTestCase { @Mock private CoroutineScope mScope; + @Mock + private TableLogBufferBase mLogBuffer; private Monitor mConditionMonitor; @@ -630,4 +634,42 @@ public class ConditionMonitorTest extends SysuiTestCase { verify(callback).onActiveChanged(eq(true)); verify(callback).onConditionsChanged(eq(true)); } + + @Test + public void testLoggingCallback() { + final Monitor monitor = new Monitor(mExecutor, Collections.emptySet(), mLogBuffer); + + final FakeCondition condition = new FakeCondition(mScope); + final FakeCondition overridingCondition = new FakeCondition( + mScope, + /* initialValue= */ false, + /* overriding= */ true); + + final Monitor.Callback callback = mock(Monitor.Callback.class); + monitor.addSubscription(getDefaultBuilder(callback) + .addCondition(condition) + .addCondition(overridingCondition) + .build()); + mExecutor.runAllReady(); + + // condition set to true + condition.fakeUpdateCondition(true); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition", "True"); + + // condition set to false + condition.fakeUpdateCondition(false); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition", "False"); + + // condition unset + condition.fakeClearCondition(); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition", "Invalid"); + + // overriding condition set to true + overridingCondition.fakeUpdateCondition(true); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition[OVRD]", "True"); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java index a325cbf25ffe..5416536305fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java @@ -47,4 +47,8 @@ public class FakeCondition extends Condition { public void fakeUpdateCondition(boolean isConditionMet) { updateCondition(isConditionMet); } + + public void fakeClearCondition() { + clearCondition(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt index cd0646543e69..839770267c74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt @@ -27,3 +27,9 @@ class FakeStatusEvent( override val showAnimation: Boolean = true, override var contentDescription: String? = "", ) : StatusEvent + +class FakePrivacyStatusEvent( + override val viewCreator: ViewCreator, + override val showAnimation: Boolean = true, + override var contentDescription: String? = "", +) : PrivacyEvent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 08a9f3139d71..39ed5535ff3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -380,6 +380,53 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { } @Test + fun testPrivacyEvent_forceVisibleIsUpdated_whenRescheduledDuringQueuedState() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule privacy event + createAndScheduleFakePrivacyEvent() + // request removal of persistent dot (sets forceVisible to false) + systemStatusAnimationScheduler.removePersistentDot() + // create and schedule a privacy event again (resets forceVisible to true) + createAndScheduleFakePrivacyEvent() + + // skip chip animation lifecycle and fast forward to SHOWING_PERSISTENT_DOT state + fastForwardAnimationToState(SHOWING_PERSISTENT_DOT) + + // verify that we reach SHOWING_PERSISTENT_DOT and that listener callback is invoked + assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + } + + @Test + fun testPrivacyEvent_forceVisibleIsUpdated_whenRescheduledDuringAnimatingState() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule privacy event + createAndScheduleFakePrivacyEvent() + // request removal of persistent dot (sets forceVisible to false) + systemStatusAnimationScheduler.removePersistentDot() + fastForwardAnimationToState(RUNNING_CHIP_ANIM) + + // create and schedule a privacy event again (resets forceVisible to true) + createAndScheduleFakePrivacyEvent() + + // skip status chip display time + advanceTimeBy(DISPLAY_LENGTH + 1) + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean()) + + // skip disappear animation + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + + // verify that we reach SHOWING_PERSISTENT_DOT and that listener callback is invoked + assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + } + + @Test fun testNewEvent_isScheduled_whenPostedDuringRemovalAnimation() = runTest { // Instantiate class under test with TestScope from runTest initializeSystemStatusAnimationScheduler(testScope = this) @@ -440,8 +487,7 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { private fun createAndScheduleFakePrivacyEvent(): OngoingPrivacyChip { val privacyChip = OngoingPrivacyChip(mContext) - val fakePrivacyStatusEvent = - FakeStatusEvent(viewCreator = { privacyChip }, priority = 100, forceVisible = true) + val fakePrivacyStatusEvent = FakePrivacyStatusEvent(viewCreator = { privacyChip }) systemStatusAnimationScheduler.onStatusEvent(fakePrivacyStatusEvent) return privacyChip } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt new file mode 100644 index 000000000000..ec074d76da10 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone.ongoingcall + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class OngoingCallBackgroundContainerTest : SysuiTestCase() { + + private lateinit var underTest: OngoingCallBackgroundContainer + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + TestableLooper.get(this).runWithLooper { + val chipView = LayoutInflater.from(context).inflate(R.layout.ongoing_call_chip, null) + underTest = chipView.requireViewById(R.id.ongoing_call_chip_background) + } + } + + @Test + fun onMeasure_maxHeightFetcherNotSet_usesDesired() { + underTest.maxHeightFetcher = null + + underTest.measure( + WIDTH_SPEC, + View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY), + ) + + assertThat(underTest.measuredHeight).isEqualTo(123) + } + + @Test + fun onMeasure_maxLargerThanDesired_usesDesired() { + underTest.maxHeightFetcher = { 234 } + + underTest.measure( + WIDTH_SPEC, + View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY), + ) + + assertThat(underTest.measuredHeight).isEqualTo(123) + } + + @Test + fun onMeasure_desiredLargerThanMax_usesMaxMinusOne() { + underTest.maxHeightFetcher = { 234 } + + underTest.measure( + WIDTH_SPEC, + View.MeasureSpec.makeMeasureSpec(567, View.MeasureSpec.EXACTLY), + ) + + // We use the max - 1 to give a bit extra space + assertThat(underTest.measuredHeight).isEqualTo(233) + } + + private companion object { + val WIDTH_SPEC = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt index 2b86cfdec04e..6db35ae94a3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt @@ -6,6 +6,7 @@ import android.testing.TestableLooper import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever @@ -27,9 +28,7 @@ class CreateUserActivityTest : SysuiTestCase() { createDialog( /* activity = */ nullable(), /* activityStarter = */ nullable(), - /* oldUserIcon = */ nullable(), - /* defaultUserName = */ nullable(), - /* title = */ nullable(), + /* isMultipleAdminsEnabled = */ any(), /* successCallback = */ nullable(), /* cancelCallback = */ nullable() ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index d252d5317d06..ca83d49b19ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -482,30 +482,17 @@ class UserInteractorTest : SysuiTestCase() { } @Test - fun executeAction_addUser_dialogShown() = + fun executeAction_addUser_dismissesDialogAndStartsActivity() = testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) keyguardRepository.setKeyguardShowing(false) - val dialogRequest = collectLastValue(underTest.dialogShowRequests) - val dialogShower: UserSwitchDialogController.DialogShower = mock() - underTest.executeAction(UserActionModel.ADD_USER, dialogShower) + underTest.executeAction(UserActionModel.ADD_USER) verify(uiEventLogger, times(1)) .log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) - assertThat(dialogRequest()) - .isEqualTo( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = userInfos[0].userHandle, - isKeyguardShowing = false, - showEphemeralMessage = false, - dialogShower = dialogShower, - ) - ) - underTest.onDialogShown() - assertThat(dialogRequest()).isNull() } @Test @@ -862,7 +849,7 @@ class UserInteractorTest : SysuiTestCase() { // Dialog is shown. assertThat(dialogRequest()) - .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) underTest.onDialogShown() assertThat(dialogRequest()).isNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/TraceUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TraceUtilsTest.kt new file mode 100644 index 000000000000..6aad0ad46c2f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/TraceUtilsTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.util + +import android.os.Handler +import android.os.Looper +import android.os.Trace.TRACE_TAG_APP +import android.testing.AndroidTestingRunner +import android.util.Log +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.After +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TraceUtilsTest : SysuiTestCase() { + + companion object { + private const val TAG = "TraceUtilsTest" + private const val TEST_FAIL_TIMEOUT = 5000L + + // A string that is 128 characters long + private const val SECTION_NAME_THATS_TOO_LONG = + "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_" + + "123456789_123456789_123456789_123456789_12345678" + } + + @Before + fun setUp() { + // Enable tracing via atrace in order to see the expected IllegalArgumentException. Trace + // sections won't run if tracing is disabled. + uiDevice.executeShellCommand("atrace --async_start -a com.android.*") + } + + @After + fun tearDown() { + uiDevice.executeShellCommand("atrace --async_stop") + } + + @Test + fun testLongTraceSection_throws_whenUsingPublicAPI() { + // Expects: "java.lang.IllegalArgumentException: sectionName is too long" + assertThrows(IllegalArgumentException::class.java) { + android.os.Trace.beginSection(SECTION_NAME_THATS_TOO_LONG) + } + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsingPrivateAPI() { + android.os.Trace.traceBegin(TRACE_TAG_APP, SECTION_NAME_THATS_TOO_LONG) + } + + @Test + @Ignore("b/267482189 - Enable once androidx.tracing >= 1.2.0-beta04") + fun testLongTraceSection_doesNotThrow_whenUsingAndroidX() { + androidx.tracing.Trace.beginSection(SECTION_NAME_THATS_TOO_LONG) + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsingHelper() { + traceSection(SECTION_NAME_THATS_TOO_LONG) { + Log.v(TAG, "com.android.systemui.util.traceSection() block.") + } + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsedAsTraceNameSupplier() { + Handler(Looper.getMainLooper()) + .runWithScissors( + TraceUtils.namedRunnable(SECTION_NAME_THATS_TOO_LONG) { + Log.v(TAG, "TraceUtils.namedRunnable() block.") + }, + TEST_FAIL_TIMEOUT + ) + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsingTraceRunnable() { + TraceUtils.traceRunnable(SECTION_NAME_THATS_TOO_LONG) { + Log.v(TAG, "TraceUtils.traceRunnable() block.") + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt index 1bdee3667d04..e3e7933405a4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt @@ -21,7 +21,11 @@ import android.graphics.Rect class FakeOverlapDetector : OverlapDetector { var shouldReturn: Map<Int, Boolean> = mapOf() - override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { + override fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect + ): Boolean { return shouldReturn[touchData.pointerId] ?: error("Unexpected PointerId not declared in TestCase currentPointers") } diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index a324b2ff88e8..e89e33f3c24b 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -81,7 +81,6 @@ import android.view.MagnificationSpec; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; -import android.view.WindowInfo; import android.view.accessibility.AccessibilityCache; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -103,7 +102,6 @@ import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection; import com.android.server.accessibility.magnification.MagnificationProcessor; import com.android.server.inputmethod.InputMethodManagerInternal; -import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; @@ -2081,7 +2079,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ IAccessibilityInteractionConnectionCallback callback, int fetchFlags, long interrogatingTid) { RemoteAccessibilityConnection connection; - IBinder activityToken = null; + IBinder windowToken = null; synchronized (mLock) { connection = mA11yWindowManager.getConnectionLocked(userId, resolvedWindowId); if (connection == null) { @@ -2090,9 +2088,8 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ final boolean isA11yFocusAction = (action == ACTION_ACCESSIBILITY_FOCUS) || (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS); if (!isA11yFocusAction) { - final WindowInfo windowInfo = - mA11yWindowManager.findWindowInfoByIdLocked(resolvedWindowId); - if (windowInfo != null) activityToken = windowInfo.activityToken; + windowToken = mA11yWindowManager.getWindowTokenForUserAndWindowIdLocked( + userId, resolvedWindowId); } final AccessibilityWindowInfo a11yWindowInfo = mA11yWindowManager.findA11yWindowInfoByIdLocked(resolvedWindowId); @@ -2113,9 +2110,8 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ if (action == ACTION_CLICK || action == ACTION_LONG_CLICK) { mA11yWindowManager.notifyOutsideTouch(userId, resolvedWindowId); } - if (activityToken != null) { - LocalServices.getService(ActivityTaskManagerInternal.class) - .setFocusedActivity(activityToken); + if (windowToken != null) { + mWindowManagerService.requestWindowFocus(windowToken); } if (intConnTracingEnabled()) { logTraceIntConn("performAccessibilityAction", diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index af6b24e1c332..2d60716104c1 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -558,10 +558,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku try { final Intent onClickIntent; - if (provider.maskedBySuspendedPackage) { + if (provider.maskedByQuietProfile) { + showBadge = true; + onClickIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(appUserId); + } else if (provider.maskedBySuspendedPackage) { showBadge = mUserManager.hasBadge(appUserId); final String suspendingPackage = mPackageManagerInternal.getSuspendingPackage( appInfo.packageName, appUserId); + // TODO(b/281839596): don't rely on platform always meaning suspended by admin. if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) { onClickIntent = mDevicePolicyManagerInternal.createShowAdminSupportIntent( appUserId, true); @@ -575,9 +579,6 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku appInfo.packageName, suspendingPackage, dialogInfo, null, null, appUserId); } - } else if (provider.maskedByQuietProfile) { - showBadge = true; - onClickIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(appUserId); } else /* provider.maskedByLockedProfile */ { showBadge = true; onClickIntent = mKeyguardManager diff --git a/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java b/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java index b8bac61b346b..ea31074ff5d2 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.os.Build; import android.os.ICancellationSignal; import android.os.RemoteException; import android.os.SystemClock; @@ -155,7 +156,19 @@ final class RemoteFieldClassificationService public void onSuccess(FieldClassificationResponse response) { logLatency(startTime); if (sDebug) { - Log.d(TAG, "onSuccess Response: " + response); + if (Build.IS_DEBUGGABLE) { + Slog.d(TAG, "onSuccess Response: " + response); + } else { + String msg = ""; + if (response == null + || response.getClassifications() == null) { + msg = "null response"; + } else { + msg = "size: " + + response.getClassifications().size(); + } + Slog.d(TAG, "onSuccess " + msg); + } } fieldClassificationServiceCallbacks .onClassificationRequestSuccess(response); @@ -165,7 +178,7 @@ final class RemoteFieldClassificationService public void onFailure() { logLatency(startTime); if (sDebug) { - Log.d(TAG, "onFailure"); + Slog.d(TAG, "onFailure"); } fieldClassificationServiceCallbacks .onClassificationRequestFailure(0, null); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 7975e49541a0..ed61d645dc6e 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -306,6 +306,7 @@ public class CompanionDeviceManagerService extends SystemService { } else if (phase == PHASE_BOOT_COMPLETED) { // Run the Inactive Association Removal job service daily. InactiveAssociationsRemovalService.schedule(getContext()); + mCrossDeviceSyncController.onBootCompleted(); } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index c511429a37d8..04fbab434d1f 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -26,6 +26,7 @@ import android.os.ShellCommand; import android.util.proto.ProtoOutputStream; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; +import com.android.server.companion.datatransfer.contextsync.BitmapUtils; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.transport.CompanionTransportManager; @@ -168,13 +169,17 @@ class CompanionDeviceShellCommand extends ShellCommand { case "send-context-sync-call-facilitators-message": { associationId = getNextIntArgRequired(); int numberOfFacilitators = getNextIntArgRequired(); + String facilitatorName = getNextArgRequired(); + String facilitatorId = getNextArgRequired(); final ProtoOutputStream pos = new ProtoOutputStream(); pos.write(ContextSyncMessage.VERSION, 1); final long telecomToken = pos.start(ContextSyncMessage.TELECOM); for (int i = 0; i < numberOfFacilitators; i++) { final long facilitatorsToken = pos.start(Telecom.FACILITATORS); - pos.write(Telecom.CallFacilitator.NAME, "Call Facilitator App #" + i); - pos.write(Telecom.CallFacilitator.IDENTIFIER, "com.android.test" + i); + pos.write(Telecom.CallFacilitator.NAME, + numberOfFacilitators == 1 ? facilitatorName : facilitatorName + i); + pos.write(Telecom.CallFacilitator.IDENTIFIER, + numberOfFacilitators == 1 ? facilitatorId : facilitatorId + i); pos.end(facilitatorsToken); } pos.end(telecomToken); @@ -188,6 +193,15 @@ class CompanionDeviceShellCommand extends ShellCommand { associationId = getNextIntArgRequired(); String callId = getNextArgRequired(); String facilitatorId = getNextArgRequired(); + int status = getNextIntArgRequired(); + boolean acceptControl = getNextBooleanArgRequired(); + boolean rejectControl = getNextBooleanArgRequired(); + boolean silenceControl = getNextBooleanArgRequired(); + boolean muteControl = getNextBooleanArgRequired(); + boolean unmuteControl = getNextBooleanArgRequired(); + boolean endControl = getNextBooleanArgRequired(); + boolean holdControl = getNextBooleanArgRequired(); + boolean unholdControl = getNextBooleanArgRequired(); final ProtoOutputStream pos = new ProtoOutputStream(); pos.write(ContextSyncMessage.VERSION, 1); final long telecomToken = pos.start(ContextSyncMessage.TELECOM); @@ -195,15 +209,40 @@ class CompanionDeviceShellCommand extends ShellCommand { pos.write(Telecom.Call.ID, callId); final long originToken = pos.start(Telecom.Call.ORIGIN); pos.write(Telecom.Call.Origin.CALLER_ID, "Caller Name"); + pos.write(Telecom.Call.Origin.APP_ICON, BitmapUtils.renderDrawableToByteArray( + mService.getContext().getPackageManager().getApplicationIcon( + facilitatorId))); final long facilitatorToken = pos.start( Telecom.Request.CreateAction.FACILITATOR); pos.write(Telecom.CallFacilitator.NAME, "Test App Name"); pos.write(Telecom.CallFacilitator.IDENTIFIER, facilitatorId); pos.end(facilitatorToken); pos.end(originToken); - pos.write(Telecom.Call.STATUS, Telecom.Call.RINGING); - pos.write(Telecom.Call.CONTROLS, Telecom.ACCEPT); - pos.write(Telecom.Call.CONTROLS, Telecom.REJECT); + pos.write(Telecom.Call.STATUS, status); + if (acceptControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.ACCEPT); + } + if (rejectControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.REJECT); + } + if (silenceControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.SILENCE); + } + if (muteControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.MUTE); + } + if (unmuteControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.UNMUTE); + } + if (endControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.END); + } + if (holdControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.PUT_ON_HOLD); + } + if (unholdControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.TAKE_OFF_HOLD); + } pos.end(callsToken); pos.end(telecomToken); mTransportManager.createEmulatedTransport(associationId) diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/BitmapUtils.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/BitmapUtils.java new file mode 100644 index 000000000000..829041af2198 --- /dev/null +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/BitmapUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.datatransfer.contextsync; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import java.io.ByteArrayOutputStream; + +/** Provides bitmap utility operations for rendering drawables to byte arrays. */ +public class BitmapUtils { + + private static final int APP_ICON_BITMAP_DIMENSION = 256; + + /** Render a drawable to a bitmap, which is then reformatted as a byte array. */ + public static byte[] renderDrawableToByteArray(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + // Can't recycle the drawable's bitmap, so handle separately + final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); + if (bitmap.getWidth() > APP_ICON_BITMAP_DIMENSION + || bitmap.getHeight() > APP_ICON_BITMAP_DIMENSION) { + // Downscale, as the original drawable bitmap is too large. + final Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, + APP_ICON_BITMAP_DIMENSION, APP_ICON_BITMAP_DIMENSION, /* filter= */ true); + final byte[] renderedBitmap = renderBitmapToByteArray(scaledBitmap); + scaledBitmap.recycle(); + return renderedBitmap; + } + return renderBitmapToByteArray(bitmap); + } + final Bitmap bitmap = Bitmap.createBitmap(APP_ICON_BITMAP_DIMENSION, + APP_ICON_BITMAP_DIMENSION, + Bitmap.Config.ARGB_8888); + try { + final Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); + drawable.draw(canvas); + return renderBitmapToByteArray(bitmap); + } finally { + bitmap.recycle(); + } + } + + private static byte[] renderBitmapToByteArray(Bitmap bitmap) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(bitmap.getByteCount()); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); + return baos.toByteArray(); + } +} diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java index 168068ec6497..de7bf4022e04 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java @@ -19,10 +19,6 @@ package com.android.server.companion.datatransfer.contextsync; import android.annotation.NonNull; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.telecom.Call; import android.telecom.CallAudioState; import android.telecom.VideoProfile; @@ -30,7 +26,6 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; -import java.io.ByteArrayOutputStream; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -42,7 +37,6 @@ public class CrossDeviceCall { public static final String EXTRA_CALL_ID = "com.android.companion.datatransfer.contextsync.extra.CALL_ID"; - private static final int APP_ICON_BITMAP_DIMENSION = 256; private final String mId; private Call mCall; @@ -80,7 +74,7 @@ public class CrossDeviceCall { .getApplicationInfo(mCallingAppPackageName, PackageManager.ApplicationInfoFlags.of(0)); mCallingAppName = packageManager.getApplicationLabel(applicationInfo).toString(); - mCallingAppIcon = renderDrawableToByteArray( + mCallingAppIcon = BitmapUtils.renderDrawableToByteArray( packageManager.getApplicationIcon(applicationInfo)); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "Could not get application info for package " + mCallingAppPackageName, e); @@ -89,40 +83,6 @@ public class CrossDeviceCall { updateCallDetails(callDetails); } - private byte[] renderDrawableToByteArray(Drawable drawable) { - if (drawable instanceof BitmapDrawable) { - // Can't recycle the drawable's bitmap, so handle separately - final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); - if (bitmap.getWidth() > APP_ICON_BITMAP_DIMENSION - || bitmap.getHeight() > APP_ICON_BITMAP_DIMENSION) { - // Downscale, as the original drawable bitmap is too large. - final Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, - APP_ICON_BITMAP_DIMENSION, APP_ICON_BITMAP_DIMENSION, /* filter= */ true); - final byte[] renderedBitmap = renderBitmapToByteArray(scaledBitmap); - scaledBitmap.recycle(); - return renderedBitmap; - } - return renderBitmapToByteArray(bitmap); - } - final Bitmap bitmap = Bitmap.createBitmap(APP_ICON_BITMAP_DIMENSION, - APP_ICON_BITMAP_DIMENSION, - Bitmap.Config.ARGB_8888); - try { - final Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); - drawable.draw(canvas); - return renderBitmapToByteArray(bitmap); - } finally { - bitmap.recycle(); - } - } - - private byte[] renderBitmapToByteArray(Bitmap bitmap) { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(bitmap.getByteCount()); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); - return baos.toByteArray(); - } - /** * Update the mute state of this call. No-op if the call is not capable of being muted. * diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java index 937d7fed3542..8c6ff86d6d63 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java @@ -146,6 +146,31 @@ public class CrossDeviceSyncController { mPhoneAccountManager = new PhoneAccountManager(mContext); } + /** Invoke set-up tasks that happen when boot is completed. */ + public void onBootCompleted() { + if (!CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + return; + } + + mPhoneAccountManager.onBootCompleted(); + + final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class); + if (telecomManager.getCallCapablePhoneAccounts().size() != 0) { + final PhoneAccountHandle defaultOutgoingTelAccountHandle = + telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL); + if (defaultOutgoingTelAccountHandle != null) { + final PhoneAccount defaultOutgoingTelAccount = telecomManager.getPhoneAccount( + defaultOutgoingTelAccountHandle); + if (defaultOutgoingTelAccount != null) { + mCallFacilitators.add( + new CallMetadataSyncData.CallFacilitator( + defaultOutgoingTelAccount.getLabel().toString(), + FACILITATOR_ID_SYSTEM)); + } + } + } + } + private void processCallCreateRequests(int associationId, CallMetadataSyncData callMetadataSyncData) { final Iterator<CallMetadataSyncData.CallCreateRequest> iterator = @@ -546,6 +571,10 @@ public class CrossDeviceSyncController { CallMetadataSyncConnectionService.class); } + void onBootCompleted() { + mTelecomManager.clearPhoneAccounts(); + } + PhoneAccountHandle getPhoneAccountHandle(int associationId, String appIdentifier) { return mPhoneAccountHandles.get( new PhoneAccountHandleIdentifier(associationId, appIdentifier)); diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index 949f39ae1609..277bd88e21ca 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -68,24 +68,10 @@ class SecureTransport extends Transport implements SecureChannel.Callback { } @Override - public Future<byte[]> requestForResponse(int message, byte[] data) { - // Check if channel is secured and start securing - if (!mShouldProcessRequests) { - Slog.d(TAG, "Establishing secure connection."); - try { - mSecureChannel.establishSecureConnection(); - } catch (Exception e) { - Slog.w(TAG, "Failed to initiate secure channel handshake.", e); - onError(e); - } - } - - return super.requestForResponse(message, data); - } - - @Override protected void sendMessage(int message, int sequence, @NonNull byte[] data) throws IOException { + establishSecureConnection(); + if (DEBUG) { Slog.d(TAG, "Queueing message 0x" + Integer.toHexString(message) + " sequence " + sequence + " length " + data.length @@ -103,6 +89,19 @@ class SecureTransport extends Transport implements SecureChannel.Callback { } } + private void establishSecureConnection() { + // Check if channel is secured and start securing + if (!mShouldProcessRequests) { + Slog.d(TAG, "Establishing secure connection."); + try { + mSecureChannel.establishSecureConnection(); + } catch (Exception e) { + Slog.w(TAG, "Failed to initiate secure channel handshake.", e); + onError(e); + } + } + } + @Override public void onSecureConnection() { mShouldProcessRequests = true; diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index ad4c0bf26d62..e9b9980b93eb 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -55,6 +55,7 @@ import android.util.SparseArray; import android.view.Display; import android.widget.Toast; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; @@ -156,7 +157,7 @@ public class VirtualDeviceManagerService extends SystemService { VirtualDeviceImpl virtualDevice = virtualDevicesSnapshot.get(i); virtualDevice.showToastWhereUidIsRunning(appUid, getContext().getString( - com.android.internal.R.string.vdm_camera_access_denied, + R.string.vdm_camera_access_denied, virtualDevice.getDisplayName()), Toast.LENGTH_LONG, Looper.myLooper()); } @@ -623,6 +624,18 @@ public class VirtualDeviceManagerService extends SystemService { } @Override + public void onAuthenticationPrompt(int uid) { + synchronized (mVirtualDeviceManagerLock) { + for (int i = 0; i < mVirtualDevices.size(); i++) { + VirtualDeviceImpl device = mVirtualDevices.valueAt(i); + device.showToastWhereUidIsRunning(uid, + R.string.app_streaming_blocked_message_for_fingerprint_dialog, + Toast.LENGTH_LONG, Looper.getMainLooper()); + } + } + } + + @Override public int getBaseVirtualDisplayFlags(IVirtualDevice virtualDevice) { return ((VirtualDeviceImpl) virtualDevice).getBaseVirtualDisplayFlags(); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 96766a20c803..392b5df6f7da 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -4504,10 +4504,11 @@ public final class ActiveServices { + ", uid=" + callingUid + " requires " + r.permission); return new ServiceLookupResult(r.permission); - } else if (Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE.equals(r.permission) + } else if ((Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE.equals(r.permission) + || Manifest.permission.BIND_VISUAL_QUERY_DETECTION_SERVICE.equals(r.permission)) && callingUid != Process.SYSTEM_UID) { - // Hotword detection must run in its own sandbox, and we don't even trust - // its enclosing application to bind to it - only the system. + // Hotword detection and visual query detection must run in its own sandbox, and we + // don't even trust its enclosing application to bind to it - only the system. // TODO(b/185746653) remove this special case and generalize Slog.w(TAG, "Permission Denial: Accessing service " + r.shortInstanceName + " from pid=" + callingPid @@ -5243,6 +5244,7 @@ public final class ActiveServices { final IApplicationThread thread = app.getThread(); final int pid = app.getPid(); final UidRecord uidRecord = app.getUidRecord(); + r.isolationHostProc = app; if (thread != null) { try { if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 658e6649b46d..544828afed64 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1175,17 +1175,21 @@ public class ActivityManagerService extends IActivityManager.Stub static final class StickyBroadcast { public Intent intent; public boolean deferUntilActive; + public int originalCallingUid; - public static StickyBroadcast create(Intent intent, boolean deferUntilActive) { + public static StickyBroadcast create(Intent intent, boolean deferUntilActive, + int originalCallingUid) { final StickyBroadcast b = new StickyBroadcast(); b.intent = intent; b.deferUntilActive = deferUntilActive; + b.originalCallingUid = originalCallingUid; return b; } @Override public String toString() { - return "{intent=" + intent + ", defer=" + deferUntilActive + "}"; + return "{intent=" + intent + ", defer=" + deferUntilActive + ", originalCallingUid=" + + originalCallingUid + "}"; } } @@ -11119,6 +11123,9 @@ public class ActivityManagerService extends IActivityManager.Stub pw.print(" [D]"); } pw.println(); + pw.print(" originalCallingUid: "); + pw.println(broadcasts.get(i).originalCallingUid); + pw.println(); Bundle bundle = intent.getExtras(); if (bundle != null) { pw.print(" extras: "); @@ -14008,16 +14015,25 @@ public class ActivityManagerService extends IActivityManager.Stub if (allSticky != null) { ArrayList receivers = new ArrayList(); receivers.add(bf); + sticky = null; final int stickyCount = allSticky.size(); for (int i = 0; i < stickyCount; i++) { final StickyBroadcast broadcast = allSticky.get(i); + final int originalStickyCallingUid = allSticky.get(i).originalCallingUid; + // TODO(b/281889567): consider using checkComponentPermission instead of + // canAccessUnexportedComponents + if (sticky == null && (exported || originalStickyCallingUid == callingUid + || ActivityManager.canAccessUnexportedComponents( + originalStickyCallingUid))) { + sticky = broadcast.intent; + } BroadcastQueue queue = broadcastQueueForIntent(broadcast.intent); BroadcastRecord r = new BroadcastRecord(queue, broadcast.intent, null, null, null, -1, -1, false, null, null, null, null, OP_NONE, BroadcastOptions.makeWithDeferUntilActive(broadcast.deferUntilActive), receivers, null, null, 0, null, null, false, true, true, -1, - BackgroundStartPrivileges.NONE, + originalStickyCallingUid, BackgroundStartPrivileges.NONE, false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */, null /* filterExtrasForReceiver */); queue.enqueueBroadcastLocked(r); @@ -14895,12 +14911,13 @@ public class ActivityManagerService extends IActivityManager.Stub for (i = 0; i < stickiesCount; i++) { if (intent.filterEquals(list.get(i).intent)) { // This sticky already exists, replace it. - list.set(i, StickyBroadcast.create(new Intent(intent), deferUntilActive)); + list.set(i, StickyBroadcast.create(new Intent(intent), deferUntilActive, + callingUid)); break; } } if (i >= stickiesCount) { - list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive)); + list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive, callingUid)); } } diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 8759e3f207c4..979874e49ad1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -918,6 +918,7 @@ final class ActivityManagerShellCommand extends ShellCommand { } int runSendBroadcast(PrintWriter pw) throws RemoteException { + pw = new PrintWriter(new TeeWriter(LOG_WRITER_INFO, pw)); Intent intent; try { intent = makeIntent(UserHandle.USER_CURRENT); @@ -931,9 +932,10 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println("Broadcasting: " + intent); pw.flush(); Bundle bundle = mBroadcastOptions == null ? null : mBroadcastOptions.toBundle(); - mInterface.broadcastIntentWithFeature(null, null, intent, null, receiver, 0, null, null, - requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, bundle, true, - false, mUserId); + final int result = mInterface.broadcastIntentWithFeature(null, null, intent, null, + receiver, 0, null, null, requiredPermissions, null, null, + android.app.AppOpsManager.OP_NONE, bundle, true, false, mUserId); + Slogf.i(TAG, "Broadcasted %s: " + result, intent); if (!mAsync) { receiver.waitForFinish(); } diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index a92744086f70..a402db945321 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -79,6 +79,9 @@ final class BroadcastRecord extends Binder { final @Nullable String callerFeatureId; // which feature in the package sent this final int callingPid; // the pid of who sent this final int callingUid; // the uid of who sent this + + final int originalStickyCallingUid; + // if this is a sticky broadcast, the Uid of the original sender final boolean callerInstantApp; // caller is an Instant App? final boolean callerInstrumented; // caller is being instrumented? final boolean ordered; // serialize the send to receivers? @@ -330,7 +333,8 @@ final class BroadcastRecord extends Binder { pw.print(prefix); pw.print("resultAbort="); pw.print(resultAbort); pw.print(" ordered="); pw.print(ordered); pw.print(" sticky="); pw.print(sticky); - pw.print(" initialSticky="); pw.println(initialSticky); + pw.print(" initialSticky="); pw.print(initialSticky); + pw.print(" originalStickyCallingUid="); pw.println(originalStickyCallingUid); } if (nextReceiver != 0) { pw.print(prefix); pw.print("nextReceiver="); pw.println(nextReceiver); @@ -399,6 +403,27 @@ final class BroadcastRecord extends Binder { } } + BroadcastRecord(BroadcastQueue queue, + Intent intent, ProcessRecord callerApp, String callerPackage, + @Nullable String callerFeatureId, int callingPid, int callingUid, + boolean callerInstantApp, String resolvedType, + String[] requiredPermissions, String[] excludedPermissions, + String[] excludedPackages, int appOp, + BroadcastOptions options, List receivers, + ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, + String resultData, Bundle resultExtras, boolean serialized, boolean sticky, + boolean initialSticky, int userId, + @NonNull BackgroundStartPrivileges backgroundStartPrivileges, + boolean timeoutExempt, + @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) { + this(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, + callingUid, callerInstantApp, resolvedType, requiredPermissions, + excludedPermissions, excludedPackages, appOp, options, receivers, resultToApp, + resultTo, resultCode, resultData, resultExtras, serialized, sticky, + initialSticky, userId, -1, backgroundStartPrivileges, timeoutExempt, + filterExtrasForReceiver); + } + BroadcastRecord(BroadcastQueue _queue, Intent _intent, ProcessRecord _callerApp, String _callerPackage, @Nullable String _callerFeatureId, int _callingPid, int _callingUid, @@ -408,7 +433,7 @@ final class BroadcastRecord extends Binder { BroadcastOptions _options, List _receivers, ProcessRecord _resultToApp, IIntentReceiver _resultTo, int _resultCode, String _resultData, Bundle _resultExtras, boolean _serialized, boolean _sticky, - boolean _initialSticky, int _userId, + boolean _initialSticky, int _userId, int originalStickyCallingUid, @NonNull BackgroundStartPrivileges backgroundStartPrivileges, boolean timeoutExempt, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) { @@ -460,6 +485,7 @@ final class BroadcastRecord extends Binder { interactive = options != null && options.isInteractive(); shareIdentity = options != null && options.isShareIdentityEnabled(); this.filterExtrasForReceiver = filterExtrasForReceiver; + this.originalStickyCallingUid = originalStickyCallingUid; } /** @@ -524,6 +550,7 @@ final class BroadcastRecord extends Binder { shareIdentity = from.shareIdentity; urgent = from.urgent; filterExtrasForReceiver = from.filterExtrasForReceiver; + originalStickyCallingUid = from.originalStickyCallingUid; } /** diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 6718319c7ac6..5f918cfa3478 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -563,14 +563,15 @@ public class BroadcastSkipPolicy { // Ensure that broadcasts are only sent to other apps if they are explicitly marked as // exported, or are System level broadcasts + final int originalCallingUid = r.sticky ? r.originalStickyCallingUid : r.callingUid; if (!filter.exported && checkComponentPermission(null, r.callingPid, - r.callingUid, filter.receiverList.uid, filter.exported) + originalCallingUid, filter.receiverList.uid, filter.exported) != PackageManager.PERMISSION_GRANTED) { return "Exported Denial: sending " + r.intent.toString() + ", action: " + r.intent.getAction() + " from " + r.callerPackage - + " (uid=" + r.callingUid + ")" + + " (uid=" + originalCallingUid + ")" + " due to receiver " + filter.receiverList.app + " (uid " + filter.receiverList.uid + ")" + " not specifying RECEIVER_EXPORTED"; diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index cb26c134f74a..04db6c02d071 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -348,21 +348,22 @@ public final class PendingIntentRecord extends IIntentSender.Stub { * use caller's BAL permission. */ public static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller( - @Nullable ActivityOptions activityOptions, int callingUid) { + @Nullable ActivityOptions activityOptions, int callingUid, + @Nullable String callingPackage) { if (activityOptions == null) { // since the ActivityOptions were not created by the app itself, determine the default // for the app - return getDefaultBackgroundStartPrivileges(callingUid); + return getDefaultBackgroundStartPrivileges(callingUid, callingPackage); } return getBackgroundStartPrivilegesAllowedByCaller(activityOptions.toBundle(), - callingUid); + callingUid, callingPackage); } private static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller( - @Nullable Bundle options, int callingUid) { + @Nullable Bundle options, int callingUid, @Nullable String callingPackage) { if (options == null || !options.containsKey( ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)) { - return getDefaultBackgroundStartPrivileges(callingUid); + return getDefaultBackgroundStartPrivileges(callingUid, callingPackage); } return options.getBoolean(ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED) ? BackgroundStartPrivileges.ALLOW_BAL @@ -381,8 +382,10 @@ public final class PendingIntentRecord extends IIntentSender.Stub { android.Manifest.permission.LOG_COMPAT_CHANGE }) public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges( - int callingUid) { - boolean isChangeEnabledForApp = CompatChanges.isChangeEnabled( + int callingUid, @Nullable String callingPackage) { + boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled( + DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage, + UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled( DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingUid); if (isChangeEnabledForApp) { return BackgroundStartPrivileges.ALLOW_FGS; @@ -638,7 +641,7 @@ public final class PendingIntentRecord extends IIntentSender.Stub { // temporarily allow receivers and services to open activities from background if the // PendingIntent.send() caller was foreground at the time of sendInner() call if (uid != callingUid && controller.mAtmInternal.isUidForeground(callingUid)) { - return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid); + return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid, null); } return BackgroundStartPrivileges.NONE; } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index c393213c5e85..fbe7e704a653 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -107,6 +107,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.storage.StorageManagerInternal; +import android.provider.DeviceConfig; import android.system.Os; import android.system.OsConstants; import android.text.TextUtils; @@ -181,6 +182,8 @@ public final class ProcessList { static final String ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY = "persist.sys.vold_app_data_isolation_enabled"; + private static final String APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = ":isSdkSandboxNext"; + // OOM adjustments for processes in various states: // Uninitialized value for any major or minor adj fields @@ -538,6 +541,78 @@ public final class ProcessList { ActivityManagerGlobalLock mProcLock; + private static final String PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = + "apply_sdk_sandbox_next_restrictions"; + private static final boolean DEFAULT_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = false; + + @GuardedBy("mService") + private ProcessListSettingsListener mProcessListSettingsListener; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + ProcessListSettingsListener getProcessListSettingsListener() { + synchronized (mService) { + if (mProcessListSettingsListener == null) { + mProcessListSettingsListener = new ProcessListSettingsListener(mService.mContext); + mProcessListSettingsListener.registerObserver(); + } + return mProcessListSettingsListener; + } + } + + static class ProcessListSettingsListener implements DeviceConfig.OnPropertiesChangedListener { + + private final Context mContext; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private boolean mSdkSandboxApplyRestrictionsNext = + DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, + PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, + DEFAULT_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + + ProcessListSettingsListener(Context context) { + mContext = context; + } + + private void registerObserver() { + DeviceConfig.addOnPropertiesChangedListener( + DeviceConfig.NAMESPACE_ADSERVICES, mContext.getMainExecutor(), this); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + void unregisterObserver() { + DeviceConfig.removeOnPropertiesChangedListener(this); + } + + boolean applySdkSandboxRestrictionsNext() { + synchronized (mLock) { + return mSdkSandboxApplyRestrictionsNext; + } + } + + @Override + public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) { + synchronized (mLock) { + for (String name : properties.getKeyset()) { + if (name == null) { + continue; + } + + switch (name) { + case PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS: + mSdkSandboxApplyRestrictionsNext = + properties.getBoolean( + PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, + DEFAULT_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + break; + default: + } + } + } + } + } + final class IsolatedUidRange { @VisibleForTesting public final int mFirstUid; @@ -1883,8 +1958,9 @@ public final class ProcessList { new IllegalStateException("SELinux tag not defined for " + app.info.packageName + " (uid " + app.uid + ")")); } - final String seInfo = app.info.seInfo - + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser); + + String seInfo = updateSeInfo(app); + // Start the process. It will either succeed and return a result containing // the PID of the new process, or else throw a RuntimeException. final String entryPoint = "android.app.ActivityThread"; @@ -1907,6 +1983,21 @@ public final class ProcessList { } } + @VisibleForTesting + @GuardedBy("mService") + String updateSeInfo(ProcessRecord app) { + String extraInfo = ""; + // By the time the first the SDK sandbox process is started, device config service + // should be available. + if (app.isSdkSandbox + && getProcessListSettingsListener().applySdkSandboxRestrictionsNext()) { + extraInfo = APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS; + } + + return app.info.seInfo + + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser) + extraInfo; + } + @GuardedBy("mService") boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, ProcessRecord app, diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java index f520f6a9ef49..4dfd9b076354 100644 --- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java @@ -19,6 +19,7 @@ package com.android.server.appop; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; +import static android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT; import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES; import static android.app.AppOpsManager.opRestrictsRead; import static android.app.AppOpsManager.opToDefaultMode; @@ -41,6 +42,7 @@ import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; +import android.permission.PermissionManager; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; @@ -107,7 +109,7 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface * {@link #upgradeLocked(int)} below. The first version was 1. */ @VisibleForTesting - static final int CURRENT_VERSION = 3; + static final int CURRENT_VERSION = 4; /** * This stores the version of appops.xml seen at boot. If this is smaller than @@ -1074,7 +1076,12 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface upgradeScheduleExactAlarmLocked(); // fall through case 2: - // for future upgrades + // split the appops.xml into appops.xml to store appop state and apppops_access.xml + // to store app-op access. + // fall through + case 3: + resetUseFullScreenIntentLocked(); + // fall through } scheduleFastWriteLocked(); } @@ -1145,6 +1152,38 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface } } + /** + * A cleanup step for U Beta 2 that reverts the OP_USE_FULL_SCREEN_INTENT's mode to MODE_DEFAULT + * if the permission flags for the USE_FULL_SCREEN_INTENT permission does not have USER_SET. + */ + @VisibleForTesting + @GuardedBy("mLock") + void resetUseFullScreenIntentLocked() { + final PermissionManagerServiceInternal pmsi = LocalServices.getService( + PermissionManagerServiceInternal.class); + final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class); + final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + final PermissionManager permissionManager = + mContext.getSystemService(PermissionManager.class); + + final String permissionName = AppOpsManager.opToPermission(OP_USE_FULL_SCREEN_INTENT); + final String[] packagesDeclaringPermission = + pmsi.getAppOpPermissionPackages(permissionName); + final int[] userIds = umi.getUserIds(); + + for (final String pkg : packagesDeclaringPermission) { + for (int userId : userIds) { + final int uid = pmi.getPackageUid(pkg, 0, userId); + final int flags = permissionManager.getPermissionFlags(pkg, permissionName, + UserHandle.of(userId)); + if ((flags & PackageManager.FLAG_PERMISSION_USER_SET) == 0) { + setUidMode(uid, OP_USE_FULL_SCREEN_INTENT, + AppOpsManager.opToDefaultMode(OP_USE_FULL_SCREEN_INTENT)); + } + } + } + } + @VisibleForTesting List<Integer> getUidsWithNonDefaultModes() { List<Integer> result = new ArrayList<>(); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index bc4e8df2a4ad..71401f437232 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -209,7 +209,6 @@ import java.util.concurrent.atomic.AtomicBoolean; private void init() { setupMessaging(mContext); - initAudioHalBluetoothState(); initRoutingStrategyIds(); mPreferredCommunicationDevice = null; updateActiveCommunicationDevice(); @@ -865,100 +864,10 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - // Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn(). - @GuardedBy("mDeviceStateLock") + /** + * Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn(). + */ private boolean mBluetoothScoOn; - // value of BT_SCO parameter currently applied to audio HAL. - @GuardedBy("mDeviceStateLock") - private boolean mBluetoothScoOnApplied; - - // A2DP suspend state requested by AudioManager.setA2dpSuspended() API. - @GuardedBy("mDeviceStateLock") - private boolean mBluetoothA2dpSuspendedExt; - // A2DP suspend state requested by AudioDeviceInventory. - @GuardedBy("mDeviceStateLock") - private boolean mBluetoothA2dpSuspendedInt; - // value of BT_A2dpSuspendedSCO parameter currently applied to audio HAL. - @GuardedBy("mDeviceStateLock") - private boolean mBluetoothA2dpSuspendedApplied; - - // LE Audio suspend state requested by AudioManager.setLeAudioSuspended() API. - @GuardedBy("mDeviceStateLock") - private boolean mBluetoothLeSuspendedExt; - // LE Audio suspend state requested by AudioDeviceInventory. - @GuardedBy("mDeviceStateLock") - private boolean mBluetoothLeSuspendedInt; - // value of LeAudioSuspended parameter currently applied to audio HAL. - @GuardedBy("mDeviceStateLock") - private boolean mBluetoothLeSuspendedApplied; - - private void initAudioHalBluetoothState() { - mBluetoothScoOnApplied = false; - AudioSystem.setParameters("BT_SCO=off"); - mBluetoothA2dpSuspendedApplied = false; - AudioSystem.setParameters("A2dpSuspended=false"); - mBluetoothLeSuspendedApplied = false; - AudioSystem.setParameters("LeAudioSuspended=false"); - } - - @GuardedBy("mDeviceStateLock") - private void updateAudioHalBluetoothState() { - if (mBluetoothScoOn != mBluetoothScoOnApplied) { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothScoOn: " - + mBluetoothScoOn + ", mBluetoothScoOnApplied: " + mBluetoothScoOnApplied); - } - if (mBluetoothScoOn) { - if (!mBluetoothA2dpSuspendedApplied) { - AudioSystem.setParameters("A2dpSuspended=true"); - mBluetoothA2dpSuspendedApplied = true; - } - if (!mBluetoothLeSuspendedApplied) { - AudioSystem.setParameters("LeAudioSuspended=true"); - mBluetoothLeSuspendedApplied = true; - } - AudioSystem.setParameters("BT_SCO=on"); - } else { - AudioSystem.setParameters("BT_SCO=off"); - } - mBluetoothScoOnApplied = mBluetoothScoOn; - } - if (!mBluetoothScoOnApplied) { - if ((mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt) - != mBluetoothA2dpSuspendedApplied) { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothA2dpSuspendedExt: " - + mBluetoothA2dpSuspendedExt - + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt - + ", mBluetoothA2dpSuspendedApplied: " - + mBluetoothA2dpSuspendedApplied); - } - mBluetoothA2dpSuspendedApplied = - mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt; - if (mBluetoothA2dpSuspendedApplied) { - AudioSystem.setParameters("A2dpSuspended=true"); - } else { - AudioSystem.setParameters("A2dpSuspended=false"); - } - } - if ((mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt) - != mBluetoothLeSuspendedApplied) { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothLeSuspendedExt: " - + mBluetoothLeSuspendedExt - + ", mBluetoothLeSuspendedInt: " + mBluetoothLeSuspendedInt - + ", mBluetoothLeSuspendedApplied: " + mBluetoothLeSuspendedApplied); - } - mBluetoothLeSuspendedApplied = - mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt; - if (mBluetoothLeSuspendedApplied) { - AudioSystem.setParameters("LeAudioSuspended=true"); - } else { - AudioSystem.setParameters("LeAudioSuspended=false"); - } - } - } - } /*package*/ void setBluetoothScoOn(boolean on, String eventSource) { if (AudioService.DEBUG_COMM_RTE) { @@ -966,76 +875,10 @@ import java.util.concurrent.atomic.AtomicBoolean; } synchronized (mDeviceStateLock) { mBluetoothScoOn = on; - updateAudioHalBluetoothState(); postUpdateCommunicationRouteClient(eventSource); } } - /*package*/ void postSetA2dpSuspended(boolean enable, String eventSource) { - sendILMsgNoDelay(MSG_IL_SET_A2DP_SUSPENDED, SENDMSG_QUEUE, (enable ? 1 : 0), eventSource); - } - - /*package*/ void setA2dpSuspended(boolean enable, boolean internal, String eventSource) { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "setA2dpSuspended source: " + eventSource + ", enable: " - + enable + ", internal: " + internal - + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt - + ", mBluetoothA2dpSuspendedExt: " + mBluetoothA2dpSuspendedExt); - } - synchronized (mDeviceStateLock) { - if (internal) { - mBluetoothA2dpSuspendedInt = enable; - } else { - mBluetoothA2dpSuspendedExt = enable; - } - updateAudioHalBluetoothState(); - } - } - - /*package*/ void clearA2dpSuspended() { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "clearA2dpSuspended"); - } - synchronized (mDeviceStateLock) { - mBluetoothA2dpSuspendedInt = false; - mBluetoothA2dpSuspendedExt = false; - updateAudioHalBluetoothState(); - } - } - - /*package*/ void postSetLeAudioSuspended(boolean enable, String eventSource) { - sendILMsgNoDelay( - MSG_IL_SET_LEAUDIO_SUSPENDED, SENDMSG_QUEUE, (enable ? 1 : 0), eventSource); - } - - /*package*/ void setLeAudioSuspended(boolean enable, boolean internal, String eventSource) { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "setLeAudioSuspended source: " + eventSource + ", enable: " - + enable + ", internal: " + internal - + ", mBluetoothLeSuspendedInt: " + mBluetoothA2dpSuspendedInt - + ", mBluetoothLeSuspendedExt: " + mBluetoothA2dpSuspendedExt); - } - synchronized (mDeviceStateLock) { - if (internal) { - mBluetoothLeSuspendedInt = enable; - } else { - mBluetoothLeSuspendedExt = enable; - } - updateAudioHalBluetoothState(); - } - } - - /*package*/ void clearLeAudioSuspended() { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "clearLeAudioSuspended"); - } - synchronized (mDeviceStateLock) { - mBluetoothLeSuspendedInt = false; - mBluetoothLeSuspendedExt = false; - updateAudioHalBluetoothState(); - } - } - /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) { synchronized (mDeviceStateLock) { return mDeviceInventory.startWatchingRoutes(observer); @@ -1821,12 +1664,6 @@ import java.util.concurrent.atomic.AtomicBoolean; final int capturePreset = msg.arg1; mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset); } break; - case MSG_IL_SET_A2DP_SUSPENDED: { - setA2dpSuspended((msg.arg1 == 1), false /*internal*/, (String) msg.obj); - } break; - case MSG_IL_SET_LEAUDIO_SUSPENDED: { - setLeAudioSuspended((msg.arg1 == 1), false /*internal*/, (String) msg.obj); - } break; case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: { final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); @@ -1905,8 +1742,6 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47; private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48; private static final int MSG_IL_BTLEAUDIO_TIMEOUT = 49; - private static final int MSG_IL_SET_A2DP_SUSPENDED = 50; - private static final int MSG_IL_SET_LEAUDIO_SUSPENDED = 51; private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52; @@ -2185,6 +2020,12 @@ import java.util.concurrent.atomic.AtomicBoolean; "updateCommunicationRoute, preferredCommunicationDevice: " + preferredCommunicationDevice + " eventSource: " + eventSource))); + if (preferredCommunicationDevice == null + || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { + AudioSystem.setParameters("BT_SCO=off"); + } else { + AudioSystem.setParameters("BT_SCO=on"); + } if (preferredCommunicationDevice == null) { AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice(); if (defaultDevice != null) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 1d8bef1c6732..b70c3e48b6e6 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -1478,7 +1478,7 @@ public class AudioDeviceInventory { } // Reset A2DP suspend state each time a new sink is connected - mDeviceBroker.clearA2dpSuspended(); + mAudioSystem.setParameters("A2dpSuspended=false"); // The convention for head tracking sensors associated with A2DP devices is to // use a UUID derived from the MAC address as follows: @@ -1751,8 +1751,7 @@ public class AudioDeviceInventory { private void makeA2dpDeviceUnavailableLater(String address, int delayMs) { // prevent any activity on the A2DP audio output to avoid unwanted // reconnection of the sink. - mDeviceBroker.setA2dpSuspended( - true /*enable*/, true /*internal*/, "makeA2dpDeviceUnavailableLater"); + mAudioSystem.setParameters("A2dpSuspended=true"); // retrieve DeviceInfo before removing device final String deviceKey = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); @@ -1899,7 +1898,7 @@ public class AudioDeviceInventory { "LE Audio device addr=" + address + " now available").printLog(TAG)); } // Reset LEA suspend state each time a new sink is connected - mDeviceBroker.clearLeAudioSuspended(); + mAudioSystem.setParameters("LeAudioSuspended=false"); UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada); mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address), @@ -1954,8 +1953,7 @@ public class AudioDeviceInventory { private void makeLeAudioDeviceUnavailableLater(String address, int device, int delayMs) { // prevent any activity on the LEA output to avoid unwanted // reconnection of the sink. - mDeviceBroker.setLeAudioSuspended( - true /*enable*/, true /*internal*/, "makeLeAudioDeviceUnavailableLater"); + mAudioSystem.setParameters("LeAudioSuspended=true"); // the device will be made unavailable later, so consider it disconnected right away mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address)); // send the delayed message to make the device unavailable later diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a72187399acd..24eba76ccf66 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6412,26 +6412,6 @@ public class AudioService extends IAudioService.Stub mDeviceBroker.setBluetoothScoOn(on, eventSource); } - /** @see AudioManager#setA2dpSuspended(boolean) */ - @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK) - public void setA2dpSuspended(boolean enable) { - super.setA2dpSuspended_enforcePermission(); - final String eventSource = new StringBuilder("setA2dpSuspended(").append(enable) - .append(") from u/pid:").append(Binder.getCallingUid()).append("/") - .append(Binder.getCallingPid()).toString(); - mDeviceBroker.postSetA2dpSuspended(enable, eventSource); - } - - /** @see AudioManager#setA2dpSuspended(boolean) */ - @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK) - public void setLeAudioSuspended(boolean enable) { - super.setLeAudioSuspended_enforcePermission(); - final String eventSource = new StringBuilder("setLeAudioSuspended(").append(enable) - .append(") from u/pid:").append(Binder.getCallingUid()).append("/") - .append(Binder.getCallingPid()).toString(); - mDeviceBroker.postSetLeAudioSuspended(enable, eventSource); - } - /** @see AudioManager#isBluetoothScoOn() * Note that it doesn't report internal state, but state seen by apps (which may have * called setBluetoothScoOn() */ diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index e46c3cc4a285..bfa6c36e1c57 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -445,8 +445,8 @@ public class BtHelper { /*package*/ synchronized void resetBluetoothSco() { mScoAudioState = SCO_STATE_INACTIVE; broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - mDeviceBroker.clearA2dpSuspended(); - mDeviceBroker.clearLeAudioSuspended(); + AudioSystem.setParameters("A2dpSuspended=false"); + AudioSystem.setParameters("LeAudioSuspended=false"); mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco"); } diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index f8cb9e98c714..4538cad513d6 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -70,6 +70,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.SystemService; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import java.util.ArrayList; import java.util.Arrays; @@ -262,6 +263,11 @@ public class AuthService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { + VirtualDeviceManagerInternal vdm = getLocalService( + VirtualDeviceManagerInternal.class); + if (vdm != null) { + vdm.onAuthenticationPrompt(callingUid); + } return mBiometricService.authenticate( token, sessionId, userId, receiver, opPackageName, promptInfo); } finally { diff --git a/services/core/java/com/android/server/biometrics/BiometricSensor.java b/services/core/java/com/android/server/biometrics/BiometricSensor.java index bac44809883f..937e3f8f8668 100644 --- a/services/core/java/com/android/server/biometrics/BiometricSensor.java +++ b/services/core/java/com/android/server/biometrics/BiometricSensor.java @@ -22,20 +22,14 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.BiometricConstants; -import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricSensorReceiver; -import android.hardware.biometrics.SensorPropertiesInternal; import android.os.IBinder; import android.os.RemoteException; -import android.text.TextUtils; -import android.util.IndentingPrintWriter; import android.util.Slog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Collections; -import java.util.List; /** * Wraps IBiometricAuthenticator implementation and stores information about the authenticator, @@ -73,7 +67,6 @@ public abstract class BiometricSensor { public final int id; public final @Authenticators.Types int oemStrength; // strength as configured by the OEM public final int modality; - @NonNull public final List<ComponentInfoInternal> componentInfo; public final IBiometricAuthenticator impl; private @Authenticators.Types int mUpdatedStrength; // updated by BiometricStrengthController @@ -93,16 +86,15 @@ public abstract class BiometricSensor { */ abstract boolean confirmationSupported(); - BiometricSensor(@NonNull Context context, int modality, @NonNull SensorPropertiesInternal props, - IBiometricAuthenticator impl) { + BiometricSensor(@NonNull Context context, int id, int modality, + @Authenticators.Types int strength, IBiometricAuthenticator impl) { this.mContext = context; - this.id = props.sensorId; + this.id = id; this.modality = modality; - this.oemStrength = Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); - this.componentInfo = Collections.unmodifiableList(props.componentInfo); + this.oemStrength = strength; this.impl = impl; - mUpdatedStrength = oemStrength; + mUpdatedStrength = strength; goToStateUnknown(); } @@ -186,25 +178,8 @@ public abstract class BiometricSensor { return "ID(" + id + ")" + ", oemStrength: " + oemStrength + ", updatedStrength: " + mUpdatedStrength - + ", modality: " + modality + + ", modality " + modality + ", state: " + mSensorState + ", cookie: " + mCookie; } - - protected void dump(@NonNull IndentingPrintWriter pw) { - pw.println(TextUtils.formatSimple("ID: %d", id)); - pw.increaseIndent(); - pw.println(TextUtils.formatSimple("oemStrength: %d", oemStrength)); - pw.println(TextUtils.formatSimple("updatedStrength: %d", mUpdatedStrength)); - pw.println(TextUtils.formatSimple("modality: %d", modality)); - pw.println("componentInfo:"); - for (ComponentInfoInternal info : componentInfo) { - pw.increaseIndent(); - info.dump(pw); - pw.decreaseIndent(); - } - pw.println(TextUtils.formatSimple("state: %d", mSensorState)); - pw.println(TextUtils.formatSimple("cookie: %d", mCookie)); - pw.decreaseIndent(); - } } diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 0ab74b8580c1..448843477ecd 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -64,7 +64,6 @@ import android.provider.Settings; import android.security.KeyStore; import android.text.TextUtils; import android.util.ArraySet; -import android.util.IndentingPrintWriter; import android.util.Pair; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -642,16 +641,13 @@ public class BiometricService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override - public synchronized void registerAuthenticator(int modality, - @NonNull SensorPropertiesInternal props, + public synchronized void registerAuthenticator(int id, int modality, + @Authenticators.Types int strength, @NonNull IBiometricAuthenticator authenticator) { super.registerAuthenticator_enforcePermission(); - @Authenticators.Types final int strength = - Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); - - Slog.d(TAG, "Registering ID: " + props.sensorId + Slog.d(TAG, "Registering ID: " + id + " Modality: " + modality + " Strength: " + strength); @@ -672,12 +668,12 @@ public class BiometricService extends SystemService { } for (BiometricSensor sensor : mSensors) { - if (sensor.id == props.sensorId) { + if (sensor.id == id) { throw new IllegalStateException("Cannot register duplicate authenticator"); } } - mSensors.add(new BiometricSensor(getContext(), modality, props, authenticator) { + mSensors.add(new BiometricSensor(getContext(), id, modality, strength, authenticator) { @Override boolean confirmationAlwaysRequired(int userId) { return mSettingObserver.getConfirmationAlwaysRequired(modality, userId); @@ -1376,17 +1372,13 @@ public class BiometricService extends SystemService { return null; } - private void dumpInternal(PrintWriter printWriter) { - IndentingPrintWriter pw = new IndentingPrintWriter(printWriter); - + private void dumpInternal(PrintWriter pw) { pw.println("Legacy Settings: " + mSettingObserver.mUseLegacyFaceOnlySettings); pw.println(); pw.println("Sensors:"); for (BiometricSensor sensor : mSensors) { - pw.increaseIndent(); - sensor.dump(pw); - pw.decreaseIndent(); + pw.println(" " + sensor); } pw.println(); pw.println("CurrentSession: " + mAuthSession); diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index fb978b2ba4b9..b474cad962c3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -555,7 +555,7 @@ public class BiometricScheduler { for (BiometricSchedulerOperation pendingOperation : mPendingOperations) { Slog.d(getTag(), "[Watchdog cancelling pending] " + pendingOperation.getClientMonitor()); - pendingOperation.markCanceling(); + pendingOperation.markCancelingForWatchdog(); } Slog.d(getTag(), "[Watchdog cancelling current] " + mCurrentOperation.getClientMonitor()); diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index 4825f1dea66f..57feedc0e68e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -267,13 +267,17 @@ public class BiometricSchedulerOperation { /** Flags this operation as canceled, if possible, but does not cancel it until started. */ public boolean markCanceling() { - if (mState == STATE_WAITING_IN_QUEUE) { + if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) { mState = STATE_WAITING_IN_QUEUE_CANCELING; return true; } return false; } + @VisibleForTesting void markCancelingForWatchdog() { + mState = STATE_WAITING_IN_QUEUE_CANCELING; + } + /** * Cancel the operation now. * diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java index d43045b4450f..0f0a81d24473 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java @@ -20,6 +20,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.IBiometricService; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; @@ -27,6 +28,7 @@ import android.hardware.face.IFaceService; import android.os.RemoteException; import android.util.Slog; +import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BiometricServiceRegistry; import java.util.List; @@ -51,8 +53,10 @@ public class FaceServiceRegistry extends BiometricServiceRegistry<ServiceProvide @Override protected void registerService(@NonNull IBiometricService service, @NonNull FaceSensorPropertiesInternal props) { + @BiometricManager.Authenticators.Types final int strength = + Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); try { - service.registerAuthenticator(TYPE_FACE, props, + service.registerAuthenticator(props.sensorId, TYPE_FACE, strength, new FaceAuthenticator(mService, props.sensorId)); } catch (RemoteException e) { Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 6c26e2b0ce99..ece35c522ec7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -91,6 +91,7 @@ import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; import com.android.server.biometrics.sensors.fingerprint.hidl.Fingerprint21; import com.android.server.biometrics.sensors.fingerprint.hidl.Fingerprint21UdfpsMock; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.google.android.collect.Lists; @@ -329,6 +330,16 @@ public class FingerprintService extends SystemService { return -1; } } + final long identity2 = Binder.clearCallingIdentity(); + try { + VirtualDeviceManagerInternal vdm = getLocalService( + VirtualDeviceManagerInternal.class); + if (vdm != null) { + vdm.onAuthenticationPrompt(callingUid); + } + } finally { + Binder.restoreCallingIdentity(identity2); + } return provider.second.scheduleAuthenticate(token, operationId, 0 /* cookie */, new ClientMonitorCallbackConverter(receiver), options, restricted, statsClient, isKeyguard); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java index 6d210eac542b..33810b764f23 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java @@ -20,6 +20,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRIN import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.IBiometricService; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; @@ -27,6 +28,7 @@ import android.hardware.fingerprint.IFingerprintService; import android.os.RemoteException; import android.util.Slog; +import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BiometricServiceRegistry; import java.util.List; @@ -51,8 +53,10 @@ public class FingerprintServiceRegistry extends BiometricServiceRegistry<Service @Override protected void registerService(@NonNull IBiometricService service, @NonNull FingerprintSensorPropertiesInternal props) { + @BiometricManager.Authenticators.Types final int strength = + Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); try { - service.registerAuthenticator(TYPE_FINGERPRINT, props, + service.registerAuthenticator(props.sensorId, TYPE_FINGERPRINT, strength, new FingerprintAuthenticator(mService, props.sensorId)); } catch (RemoteException e) { Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId); diff --git a/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java b/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java index f27761fd644c..35ea36c5d56f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java +++ b/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java @@ -16,10 +16,12 @@ package com.android.server.biometrics.sensors.iris; +import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_IRIS; import android.annotation.NonNull; import android.content.Context; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.SensorPropertiesInternal; import android.hardware.iris.IIrisService; @@ -31,6 +33,7 @@ import android.util.Slog; import com.android.server.ServiceThread; import com.android.server.SystemService; +import com.android.server.biometrics.Utils; import java.util.List; @@ -72,12 +75,17 @@ public class IrisService extends SystemService { ServiceManager.getService(Context.BIOMETRIC_SERVICE)); for (SensorPropertiesInternal hidlSensor : hidlSensors) { + final int sensorId = hidlSensor.sensorId; + final @BiometricManager.Authenticators.Types int strength = + Utils.propertyStrengthToAuthenticatorStrength( + hidlSensor.sensorStrength); + final IrisAuthenticator authenticator = new IrisAuthenticator(mServiceWrapper, + sensorId); try { - biometricService.registerAuthenticator(TYPE_IRIS, hidlSensor, - new IrisAuthenticator(mServiceWrapper, hidlSensor.sensorId)); + biometricService.registerAuthenticator(sensorId, TYPE_IRIS, strength, + authenticator); } catch (RemoteException e) { - Slog.e(TAG, "Remote exception when registering sensorId: " - + hidlSensor.sensorId); + Slog.e(TAG, "Remote exception when registering sensorId: " + sensorId); } } }); diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index a33533841e89..4d12574fb6b0 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -70,6 +70,11 @@ public abstract class VirtualDeviceManagerInternal { public abstract void onAppsOnVirtualDeviceChanged(); /** + * Notifies that an authentication prompt is about to be shown for an app with the given uid. + */ + public abstract void onAuthenticationPrompt(int uid); + + /** * Gets the owner uid for a deviceId. * * @param deviceId which device we're asking about diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index c678a92af13a..4208a12f91d4 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -3677,8 +3677,11 @@ public class Vpn { encapType = IkeSessionParams.ESP_ENCAP_TYPE_NONE; break; default: - ipVersion = IkeSessionParams.ESP_IP_VERSION_AUTO; - encapType = IkeSessionParams.ESP_ENCAP_TYPE_AUTO; + // By default, PREFERRED_IKE_PROTOCOL_IPV4_UDP is used for safety. This is + // because some carriers' networks do not support IPv6 very well, and using + // IPv4 can help to prevent problems. + ipVersion = IkeSessionParams.ESP_IP_VERSION_IPV4; + encapType = IkeSessionParams.ESP_ENCAP_TYPE_UDP; break; } return new CarrierConfigInfo(mccMnc, natKeepalive, encapType, ipVersion); diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 964569008acc..5d0177be18b3 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -17,6 +17,7 @@ package com.android.server.devicestate; import static android.Manifest.permission.CONTROL_DEVICE_STATE; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE; @@ -73,6 +74,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.WeakHashMap; @@ -162,7 +164,7 @@ public final class DeviceStateManagerService extends SystemService { @GuardedBy("mLock") private final SparseArray<ProcessRecord> mProcessRecords = new SparseArray<>(); - private Set<Integer> mDeviceStatesAvailableForAppRequests; + private Set<Integer> mDeviceStatesAvailableForAppRequests = new HashSet<>(); private Set<Integer> mFoldedDeviceStates; @@ -879,8 +881,16 @@ public final class DeviceStateManagerService extends SystemService { * @param callingPid Process ID that is requesting this state change * @param state state that is being requested. */ - private void assertCanRequestDeviceState(int callingPid, int state) { - if (!isTopApp(callingPid) || !isStateAvailableForAppRequests(state)) { + private void assertCanRequestDeviceState(int callingPid, int callingUid, int state) { + final boolean isTopApp = isTopApp(callingPid); + final boolean isForegroundApp = isForegroundApp(callingPid, callingUid); + final boolean isStateAvailableForAppRequests = isStateAvailableForAppRequests(state); + + final boolean canRequestState = isTopApp + && isForegroundApp + && isStateAvailableForAppRequests; + + if (!canRequestState) { getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, "Permission required to request device state, " + "or the call must come from the top app " @@ -893,15 +903,43 @@ public final class DeviceStateManagerService extends SystemService { * not the top app, then check if this process holds the CONTROL_DEVICE_STATE permission. * * @param callingPid Process ID that is requesting this state change + * @param callingUid UID that is requesting this state change */ - private void assertCanControlDeviceState(int callingPid) { - if (!isTopApp(callingPid)) { + private void assertCanControlDeviceState(int callingPid, int callingUid) { + final boolean isTopApp = isTopApp(callingPid); + final boolean isForegroundApp = isForegroundApp(callingPid, callingUid); + + final boolean canControlState = isTopApp && isForegroundApp; + + if (!canControlState) { getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, "Permission required to request device state, " + "or the call must come from the top app."); } } + /** + * Checks if the caller is in the foreground. Note that callers may be the top app as returned + * from {@link #isTopApp(int)}, but not be in the foreground. For example, keyguard may be on + * top of the top app. + */ + private boolean isForegroundApp(int callingPid, int callingUid) { + try { + final List<ActivityManager.RunningAppProcessInfo> procs = + ActivityManager.getService().getRunningAppProcesses(); + for (int i = 0; i < procs.size(); i++) { + ActivityManager.RunningAppProcessInfo proc = procs.get(i); + if (proc.pid == callingPid && proc.uid == callingUid + && proc.importance <= IMPORTANCE_FOREGROUND) { + return true; + } + } + } catch (RemoteException e) { + Slog.w(TAG, "am.getRunningAppProcesses() failed", e); + } + return false; + } + private boolean isTopApp(int callingPid) { final WindowProcessController topApp = mActivityTaskManagerInternal.getTopApp(); return topApp != null && topApp.getPid() == callingPid; @@ -918,7 +956,6 @@ public final class DeviceStateManagerService extends SystemService { */ @GuardedBy("mLock") private void readStatesAvailableForRequestFromApps() { - mDeviceStatesAvailableForAppRequests = new HashSet<>(); String[] availableAppStatesConfigIdentifiers = getContext().getResources() .getStringArray(R.array.config_deviceStatesAvailableForAppRequests); for (int i = 0; i < availableAppStatesConfigIdentifiers.length; i++) { @@ -1118,7 +1155,7 @@ public final class DeviceStateManagerService extends SystemService { // Allow top processes to request a device state change // If the calling process ID is not the top app, then we check if this process // holds a permission to CONTROL_DEVICE_STATE - assertCanRequestDeviceState(callingPid, state); + assertCanRequestDeviceState(callingPid, callingUid, state); if (token == null) { throw new IllegalArgumentException("Request token must not be null."); @@ -1139,10 +1176,11 @@ public final class DeviceStateManagerService extends SystemService { @Override // Binder call public void cancelStateRequest() { final int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); // Allow top processes to cancel a device state change // If the calling process ID is not the top app, then we check if this process // holds a permission to CONTROL_DEVICE_STATE - assertCanControlDeviceState(callingPid); + assertCanControlDeviceState(callingPid, callingUid); final long callingIdentity = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java index c4b98c21b608..6147c10f85d5 100644 --- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java +++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java @@ -172,7 +172,7 @@ final class NewDeviceAction extends HdmiCecFeatureAction { return; } if (mDisplayName == null) { - mDisplayName = HdmiUtils.getDefaultDeviceName(mDeviceLogicalAddress); + mDisplayName = ""; } HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder() .setLogicalAddress(mDeviceLogicalAddress) diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 02b705344cb9..47f648547a88 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -33,6 +33,8 @@ import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -74,6 +76,7 @@ import android.util.Log; import android.view.KeyEvent; import com.android.server.LocalServices; +import com.android.server.uri.UriGrantsManagerInternal; import java.io.PrintWriter; import java.util.ArrayList; @@ -101,6 +104,10 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR static final long THROW_FOR_INVALID_BROADCAST_RECEIVER = 270049379L; private static final String TAG = "MediaSessionRecord"; + private static final String[] ART_URIS = new String[] { + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI}; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** @@ -154,6 +161,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR private final SessionStub mSession; private final SessionCb mSessionCb; private final MediaSessionService mService; + private final UriGrantsManagerInternal mUgmInternal; private final Context mContext; private final boolean mVolumeAdjustmentForRemoteGroupSessions; @@ -215,6 +223,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mAudioAttrs = DEFAULT_ATTRIBUTES; mPolicies = policies; + mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class); mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean( com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); @@ -1080,21 +1089,45 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR public void setMetadata(MediaMetadata metadata, long duration, String metadataDescription) throws RemoteException { synchronized (mLock) { - MediaMetadata temp = metadata == null ? null : new MediaMetadata.Builder(metadata) - .build(); - // This is to guarantee that the underlying bundle is unparceled - // before we set it to prevent concurrent reads from throwing an - // exception - if (temp != null) { - temp.size(); - } - mMetadata = temp; mDuration = duration; mMetadataDescription = metadataDescription; + mMetadata = sanitizeMediaMetadata(metadata); } mHandler.post(MessageHandler.MSG_UPDATE_METADATA); } + private MediaMetadata sanitizeMediaMetadata(MediaMetadata metadata) { + if (metadata == null) { + return null; + } + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(metadata); + for (String key: ART_URIS) { + String uriString = metadata.getString(key); + if (TextUtils.isEmpty(uriString)) { + continue; + } + Uri uri = Uri.parse(uriString); + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + continue; + } + try { + mUgmInternal.checkGrantUriPermission(getUid(), + getPackageName(), + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, getUserId())); + } catch (SecurityException e) { + metadataBuilder.putString(key, null); + } + } + MediaMetadata sanitizedMetadata = metadataBuilder.build(); + // sanitizedMetadata.size() guarantees that the underlying bundle is unparceled + // before we set it to prevent concurrent reads from throwing an + // exception + sanitizedMetadata.size(); + return sanitizedMetadata; + } + @Override public void setPlaybackState(PlaybackState state) throws RemoteException { int oldState = mPlaybackState == null diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java index 8a2888881e59..5b3f7a58e653 100644 --- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java +++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java @@ -382,7 +382,11 @@ public class DefaultCrossProfileIntentFiltersUtils { return filters; } - /** Call intent with tel scheme exclusively handled my managed profile. */ + /** Call intent with tel scheme exclusively handled my managed profile. + * Note that work profile telephony relies on this intent filter to redirect intents to + * the IntentForwarderActivity. Work profile telephony error handling must be updated in + * the Telecomm package CallsManager if this filter is changed. + */ private static final DefaultCrossProfileIntentFilter CALL_MANAGED_PROFILE = new DefaultCrossProfileIntentFilter.Builder( DefaultCrossProfileIntentFilter.Direction.TO_PROFILE, diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 5979b12e4a30..e18b2e87222d 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -280,7 +280,7 @@ final class InstallPackageHelper { SharedUserSetting requestSharedUserSetting = mPm.mSettings.getSharedUserSettingLPr( request.getScanRequestPackageSetting()); SharedUserSetting resultSharedUserSetting = mPm.mSettings.getSharedUserSettingLPr( - request.getScanRequestPackageSetting()); + request.getScannedPackageSetting()); if (requestSharedUserSetting != null && requestSharedUserSetting != resultSharedUserSetting) { // shared user changed, remove from old shared user diff --git a/services/core/java/com/android/server/pm/UserJourneyLogger.java b/services/core/java/com/android/server/pm/UserJourneyLogger.java index f48a1669c259..895edce093b5 100644 --- a/services/core/java/com/android/server/pm/UserJourneyLogger.java +++ b/services/core/java/com/android/server/pm/UserJourneyLogger.java @@ -99,6 +99,8 @@ public class UserJourneyLogger { FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN; public static final int USER_JOURNEY_REVOKE_ADMIN = FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN; + public static final int USER_JOURNEY_USER_LIFECYCLE = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_LIFECYCLE; @IntDef(prefix = {"USER_JOURNEY"}, value = { USER_JOURNEY_UNKNOWN, @@ -109,7 +111,8 @@ public class UserJourneyLogger { USER_JOURNEY_USER_CREATE, USER_JOURNEY_USER_REMOVE, USER_JOURNEY_GRANT_ADMIN, - USER_JOURNEY_REVOKE_ADMIN + USER_JOURNEY_REVOKE_ADMIN, + USER_JOURNEY_USER_LIFECYCLE }) public @interface UserJourney { } @@ -272,11 +275,12 @@ public class UserJourneyLogger { int userType, int userFlags, @UserJourneyErrorCode int errorCode) { if (session == null) { writeUserLifecycleJourneyReported(-1, journey, originalUserId, targetUserId, - userType, userFlags, ERROR_CODE_INVALID_SESSION_ID); + userType, userFlags, ERROR_CODE_INVALID_SESSION_ID, -1); } else { + final long elapsedTime = System.currentTimeMillis() - session.mStartTimeInMills; writeUserLifecycleJourneyReported( session.mSessionId, journey, originalUserId, targetUserId, userType, userFlags, - errorCode); + errorCode, elapsedTime); } } @@ -285,10 +289,10 @@ public class UserJourneyLogger { */ @VisibleForTesting public void writeUserLifecycleJourneyReported(long sessionId, int journey, int originalUserId, - int targetUserId, int userType, int userFlags, int errorCode) { + int targetUserId, int userType, int userFlags, int errorCode, long elapsedTime) { FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, sessionId, journey, originalUserId, targetUserId, userType, userFlags, - errorCode); + errorCode, elapsedTime); } /** @@ -452,6 +456,29 @@ public class UserJourneyLogger { } /** + * Log user journey event and report finishing with error + */ + public UserJourneySession logDelayedUserJourneyFinishWithError(@UserIdInt int originalUserId, + UserInfo targetUser, @UserJourney int journey, @UserJourneyErrorCode int errorCode) { + synchronized (mLock) { + final int key = getUserJourneyKey(targetUser.id, journey); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleJourneyReported( + userJourneySession, + journey, originalUserId, targetUser.id, + getUserTypeForStatsd(targetUser.userType), + targetUser.flags, + errorCode); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** * Log event and report finish when user is null. This is edge case when UserInfo * can not be passed because it is null, therefore all information are passed as arguments. */ @@ -533,6 +560,23 @@ public class UserJourneyLogger { } /** + * This keeps the start time when finishing extensively long journey was began. + * For instance full user lifecycle ( from creation to deletion )when user is about to delete + * we need to get user creation time before it was deleted. + */ + public UserJourneySession startSessionForDelayedJourney(@UserIdInt int targetId, + @UserJourney int journey, long startTime) { + final long newSessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); + synchronized (mLock) { + final int key = getUserJourneyKey(targetId, journey); + final UserJourneySession userJourneySession = + new UserJourneySession(newSessionId, journey, startTime); + mUserIdToUserJourneyMap.append(key, userJourneySession); + return userJourneySession; + } + } + + /** * Helper class to store user journey and session id. * * <p> User journey tracks a chain of user lifecycle events occurring during different user @@ -542,11 +586,19 @@ public class UserJourneyLogger { public final long mSessionId; @UserJourney public final int mJourney; + public long mStartTimeInMills; @VisibleForTesting public UserJourneySession(long sessionId, @UserJourney int journey) { mJourney = journey; mSessionId = sessionId; + mStartTimeInMills = System.currentTimeMillis(); + } + @VisibleForTesting + public UserJourneySession(long sessionId, @UserJourney int journey, long startTimeInMills) { + mJourney = journey; + mSessionId = sessionId; + mStartTimeInMills = startTimeInMills; } } -} +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 4ef68d8f5aaf..4e043aae4f09 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -30,6 +30,7 @@ import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_IS_NOT_AN_ import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_LIFECYCLE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; import android.Manifest; @@ -3332,7 +3333,7 @@ public class UserManagerService extends IUserManager.Stub { } /** - * Enforces that only the system UID or root's UID or apps that have the + * Enforces that only the system UID or root's UID or apps that have the * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or * {@link android.Manifest.permission#QUERY_USERS QUERY_USERS} * can make certain calls to the UserManager. @@ -5535,6 +5536,8 @@ public class UserManagerService extends IUserManager.Stub { } mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_USER_REMOVE); + mUserJourneyLogger.startSessionForDelayedJourney(userId, + USER_JOURNEY_USER_LIFECYCLE, userData.info.creationTime); try { mAppOpsService.removeUser(userId); @@ -5560,6 +5563,10 @@ public class UserManagerService extends IUserManager.Stub { mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, userData.info, USER_JOURNEY_USER_REMOVE, ERROR_CODE_UNSPECIFIED); + mUserJourneyLogger + .logDelayedUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_LIFECYCLE, + ERROR_CODE_UNSPECIFIED); } @Override public void userStopAborted(int userIdParam) { @@ -5567,6 +5574,10 @@ public class UserManagerService extends IUserManager.Stub { mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, userData.info, USER_JOURNEY_USER_REMOVE, ERROR_CODE_ABORTED); + mUserJourneyLogger + .logDelayedUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_LIFECYCLE, + ERROR_CODE_ABORTED); } }); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index 4d2b119d7800..951052922586 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -216,6 +216,7 @@ final class DefaultPermissionGrantPolicy { STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_AUDIO); STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_VIDEO); STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_IMAGES); + STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED); } private static final Set<String> NEARBY_DEVICES_PERMISSIONS = new ArraySet<>(); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 93d6676dd929..4a57592aa1ae 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -1675,6 +1675,7 @@ public class BatteryStatsImpl extends BatteryStats { String mLastWakeupReason = null; long mLastWakeupUptimeMs = 0; + long mLastWakeupElapsedTimeMs = 0; private final HashMap<String, SamplingTimer> mWakeupReasonStats = new HashMap<>(); public Map<String, ? extends Timer> getWakeupReasonStats() { @@ -5048,7 +5049,7 @@ public class BatteryStatsImpl extends BatteryStats { SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason); timer.add(deltaUptimeMs * 1000, 1, elapsedRealtimeMs); // time in in microseconds FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason, - /* duration_usec */ deltaUptimeMs * 1000); + /* duration_usec */ deltaUptimeMs * 1000, mLastWakeupElapsedTimeMs); mLastWakeupReason = null; } } @@ -5059,6 +5060,7 @@ public class BatteryStatsImpl extends BatteryStats { mHistory.recordWakeupEvent(elapsedRealtimeMs, uptimeMs, reason); mLastWakeupReason = reason; mLastWakeupUptimeMs = uptimeMs; + mLastWakeupElapsedTimeMs = elapsedRealtimeMs; } @GuardedBy("this") diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java index a694f5f1b629..ecd5bd22cd03 100644 --- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java +++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java @@ -274,13 +274,14 @@ public final class SensorPrivacyService extends SystemService { mHandler = new SensorPrivacyHandler(FgThread.get().getLooper(), mContext); mSensorPrivacyStateController = SensorPrivacyStateController.getInstance(); + correctStateIfNeeded(); + int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_CAMERA, OP_PHONE_CALL_CAMERA, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO}; mAppOpsManager.startWatchingNoted(micAndCameraOps, this); mAppOpsManager.startWatchingStarted(micAndCameraOps, this); - mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -313,6 +314,20 @@ public final class SensorPrivacyService extends SystemService { userId, toggleType, sensor, state.isEnabled())); } + // If sensor privacy is enabled for a sensor, but the device doesn't support sensor privacy + // for that sensor, then disable privacy + private void correctStateIfNeeded() { + mSensorPrivacyStateController.forEachState((type, user, sensor, state) -> { + if (type != TOGGLE_TYPE_SOFTWARE) { + return; + } + if (!supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor) && state.isEnabled()) { + setToggleSensorPrivacyUnchecked( + TOGGLE_TYPE_SOFTWARE, user, OTHER, sensor, false); + } + }); + } + @Override public void onUserRestrictionsChanged(int userId, Bundle newRestrictions, Bundle prevRestrictions) { @@ -721,15 +736,30 @@ public final class SensorPrivacyService extends SystemService { if (userId == UserHandle.USER_CURRENT) { userId = mCurrentUser; } + if (!canChangeToggleSensorPrivacy(userId, sensor)) { return; } + if (enable && !supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor)) { + // Do not enable sensor privacy if the device doesn't support it + return; + } setToggleSensorPrivacyUnchecked(TOGGLE_TYPE_SOFTWARE, userId, source, sensor, enable); } private void setToggleSensorPrivacyUnchecked(int toggleType, int userId, int source, int sensor, boolean enable) { + if (DEBUG) { + Log.d(TAG, "callingUid=" + Binder.getCallingUid() + + " callingPid=" + Binder.getCallingPid() + + " setToggleSensorPrivacyUnchecked(" + + "userId=" + userId + + " source=" + source + + " sensor=" + sensor + + " enable=" + enable + + ")"); + } final long[] lastChange = new long[1]; mSensorPrivacyStateController.atomic(() -> { SensorState sensorState = mSensorPrivacyStateController diff --git a/services/core/java/com/android/server/tv/TvInputHal.java b/services/core/java/com/android/server/tv/TvInputHal.java index 4bbca335b8e3..c05d148d50d8 100644 --- a/services/core/java/com/android/server/tv/TvInputHal.java +++ b/services/core/java/com/android/server/tv/TvInputHal.java @@ -19,6 +19,7 @@ package com.android.server.tv; import android.hardware.tv.input.V1_0.Constants; import android.media.tv.TvInputHardwareInfo; import android.media.tv.TvStreamConfig; +import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.MessageQueue; @@ -45,12 +46,15 @@ final class TvInputHal implements Handler.Callback { Constants.EVENT_STREAM_CONFIGURATIONS_CHANGED; public static final int EVENT_FIRST_FRAME_CAPTURED = 4; + public static final int EVENT_TV_MESSAGE = 5; + public interface Callback { void onDeviceAvailable(TvInputHardwareInfo info, TvStreamConfig[] configs); void onDeviceUnavailable(int deviceId); void onStreamConfigurationChanged(int deviceId, TvStreamConfig[] configs, int cableConnectionStatus); void onFirstFrameCaptured(int deviceId, int streamId); + void onTvMessage(int deviceId, int type, Bundle data); } private native long nativeOpen(MessageQueue queue); @@ -171,6 +175,10 @@ final class TvInputHal implements Handler.Callback { mHandler.obtainMessage(EVENT_STREAM_CONFIGURATION_CHANGED, deviceId, streamId)); } + private void tvMessageReceivedFromNative(int deviceId, int type, Bundle data) { + mHandler.obtainMessage(EVENT_TV_MESSAGE, deviceId, type, data).sendToTarget(); + } + // Handler.Callback implementation @Override @@ -221,6 +229,13 @@ final class TvInputHal implements Handler.Callback { break; } + case EVENT_TV_MESSAGE: { + int deviceId = msg.arg1; + int type = msg.arg2; + Bundle data = (Bundle) msg.obj; + mCallback.onTvMessage(deviceId, type, data); + } + default: Slog.e(TAG, "Unknown event: " + msg); return false; diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java index 077f8d527ab5..9cdceef006d9 100755 --- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java +++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java @@ -48,6 +48,7 @@ import android.media.tv.TvInputService.PriorityHintUseCaseType; import android.media.tv.TvStreamConfig; import android.media.tv.tunerresourcemanager.ResourceClientProfile; import android.media.tv.tunerresourcemanager.TunerResourceManager; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; @@ -59,6 +60,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.Surface; +import com.android.internal.os.SomeArgs; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.SystemService; @@ -264,6 +266,17 @@ class TvInputHardwareManager implements TvInputHal.Callback { } } + @Override + public void onTvMessage(int deviceId, int type, Bundle data) { + synchronized (mLock) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = mHardwareInputIdMap.get(deviceId); + args.arg2 = data; + mHandler.obtainMessage(ListenerHandler.TV_MESSAGE_RECEIVED, type, 0, args) + .sendToTarget(); + } + } + public List<TvInputHardwareInfo> getHardwareList() { synchronized (mLock) { return Collections.unmodifiableList(mHardwareList); @@ -1224,6 +1237,7 @@ class TvInputHardwareManager implements TvInputHal.Callback { void onHdmiDeviceAdded(HdmiDeviceInfo device); void onHdmiDeviceRemoved(HdmiDeviceInfo device); void onHdmiDeviceUpdated(String inputId, HdmiDeviceInfo device); + void onTvMessage(String inputId, int type, Bundle data); } private class ListenerHandler extends Handler { @@ -1234,6 +1248,7 @@ class TvInputHardwareManager implements TvInputHal.Callback { private static final int HDMI_DEVICE_REMOVED = 5; private static final int HDMI_DEVICE_UPDATED = 6; private static final int TVINPUT_INFO_ADDED = 7; + private static final int TV_MESSAGE_RECEIVED = 8; @Override public final void handleMessage(Message msg) { @@ -1303,6 +1318,15 @@ class TvInputHardwareManager implements TvInputHal.Callback { } break; } + case TV_MESSAGE_RECEIVED: { + SomeArgs args = (SomeArgs) msg.obj; + String inputId = (String) args.arg1; + Bundle data = (Bundle) args.arg2; + int type = msg.arg1; + mListener.onTvMessage(inputId, type, data); + args.recycle(); + break; + } default: { Slog.w(TAG, "Unhandled message: " + msg); break; diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 1f5c1cf6a959..9cfdd5fb42e7 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -4146,6 +4146,28 @@ public final class TvInputManagerService extends SystemService { } } } + + @Override + public void onTvMessage(String inputId, int type, Bundle data) { + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(mCurrentUserId); + TvInputState inputState = userState.inputMap.get(inputId); + ServiceState serviceState = userState.serviceStateMap.get(inputState.info + .getComponent()); + for (IBinder token : serviceState.sessionTokens) { + try { + SessionState sessionState = getSessionStateLocked(token, + Binder.getCallingUid(), mCurrentUserId); + if (!sessionState.isRecordingSession + && sessionState.hardwareSessionToken != null) { + sessionState.session.notifyTvMessage(type, data); + } + } catch (RemoteException e) { + Slog.e(TAG, "error in onTvMessage", e); + } + } + } + } } private static class SessionNotFoundException extends IllegalArgumentException { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 1e50f3d2b67e..d93778e18748 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -78,7 +78,6 @@ import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_IF_ALLOWLISTED; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_NEVER; -import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_EXCLUDE_PORTRAIT_FULLSCREEN; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; @@ -9323,8 +9322,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (info.applicationInfo == null) { return info.getMinAspectRatio(); } - - if (!info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO)) { + if (!mLetterboxUiController.shouldOverrideMinAspectRatio()) { return info.getMinAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 7926216fe15d..e07c65426278 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -262,12 +262,6 @@ public abstract class ActivityTaskManagerInternal { */ public abstract void setVr2dDisplayId(int vr2dDisplayId); - /** - * Set focus on an activity. - * @param token The activity token. - */ - public abstract void setFocusedActivity(IBinder token); - public abstract void registerScreenObserver(ScreenObserver observer); /** @@ -585,6 +579,11 @@ public abstract class ActivityTaskManagerInternal { public abstract void setDeviceOwnerUid(int uid); /** + * Called by DevicePolicyManagerService to set the uids of the profile owners. + */ + public abstract void setProfileOwnerUids(Set<Integer> uids); + + /** * Set all associated companion app that belongs to a userId. * @param userId * @param companionAppUids ActivityTaskManager will take ownership of this Set, the caller diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f35343c98711..b8ae33032a6c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -158,6 +158,7 @@ import android.app.PictureInPictureUiState; import android.app.ProfilerInfo; import android.app.WaitResult; import android.app.admin.DevicePolicyCache; +import android.app.admin.DeviceStateCache; import android.app.assist.ActivityId; import android.app.assist.AssistContent; import android.app.assist.AssistStructure; @@ -783,6 +784,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private int mDeviceOwnerUid = Process.INVALID_UID; + private Set<Integer> mProfileOwnerUids = new ArraySet<Integer>(); + private final class SettingObserver extends ContentObserver { private final Uri mFontScaleUri = Settings.System.getUriFor(FONT_SCALE); private final Uri mHideErrorDialogsUri = Settings.Global.getUriFor(HIDE_ERROR_DIALOGS); @@ -5334,15 +5337,54 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return null; } - WindowProcessController getProcessController(int pid, int uid) { + /** + * Returns the {@link WindowProcessController} for the app process for the given uid and pid. + * + * If no such {@link WindowProcessController} is found, it does not belong to an app, or the + * pid does not match the uid {@code null} is returned. + */ + @Nullable WindowProcessController getProcessController(int pid, int uid) { + return UserHandle.isApp(uid) ? getProcessControllerInternal(pid, uid) : null; + } + + /** + * Returns the {@link WindowProcessController} for the given uid and pid. + * + * If no such {@link WindowProcessController} is found or the pid does not match the uid + * {@code null} is returned. + */ + private @Nullable WindowProcessController getProcessControllerInternal(int pid, int uid) { final WindowProcessController proc = mProcessMap.getProcess(pid); - if (proc == null) return null; - if (UserHandle.isApp(uid) && proc.mUid == uid) { + if (proc == null) { + return null; + } + if (proc.mUid == uid) { return proc; } return null; } + /** + * Returns the package name if (and only if) the package name can be uniquely determined. + * Otherwise returns {@code null}. + * + * The provided pid must match the provided uid, otherwise this also returns null. + */ + @Nullable String getPackageNameIfUnique(int uid, int pid) { + WindowProcessController processController = getProcessControllerInternal(pid, uid); + if (processController == null) { + Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") has no WPC"); + return null; + } + List<String> realCallingPackages = processController.getPackageList(); + if (realCallingPackages.size() == 1) { + return realCallingPackages.get(0); + } + Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") is ambiguous: " + + realCallingPackages); + return null; + } + /** A uid is considered to be foreground if it has a visible non-toast window. */ @HotPath(caller = HotPath.START_SERVICE) boolean hasActiveVisibleWindow(int uid) { @@ -5360,6 +5402,15 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mDeviceOwnerUid = uid; } + boolean isAffiliatedProfileOwner(int uid) { + return uid >= 0 && mProfileOwnerUids.contains(uid) + && DeviceStateCache.getInstance().hasAffiliationWithDevice(UserHandle.getUserId(uid)); + } + + void setProfileOwnerUids(Set<Integer> uids) { + mProfileOwnerUids = uids; + } + /** * Saves the current activity manager state and includes the saved state in the next dump of * activity manager. @@ -5806,26 +5857,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public void setFocusedActivity(IBinder token) { - synchronized (mGlobalLock) { - final ActivityRecord r = ActivityRecord.forTokenLocked(token); - if (r == null) { - throw new IllegalArgumentException( - "setFocusedActivity: No activity record matching token=" + token); - } - if (r.moveFocusableActivityToTop("setFocusedActivity")) { - mRootWindowContainer.resumeFocusedTasksTopActivities(); - } - } - } - - @Override public int getDisplayId(IBinder token) { synchronized (mGlobalLock) { ActivityRecord r = ActivityRecord.forTokenLocked(token); if (r == null) { throw new IllegalArgumentException( - "setFocusedActivity: No activity record matching token=" + token); + "getDisplayId: No activity record matching token=" + token); } return r.getDisplayId(); } @@ -6916,6 +6953,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public void setProfileOwnerUids(Set<Integer> uids) { + synchronized (mGlobalLock) { + ActivityTaskManagerService.this.setProfileOwnerUids(uids); + } + } + + @Override public void setCompanionAppUids(int userId, Set<Integer> companionAppUids) { synchronized (mGlobalLock) { mCompanionAppUidsMap.put(userId, companionAppUids); diff --git a/services/core/java/com/android/server/wm/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java index 90ec964e2f0f..2df601fd1d3b 100644 --- a/services/core/java/com/android/server/wm/AnrController.java +++ b/services/core/java/com/android/server/wm/AnrController.java @@ -34,6 +34,7 @@ import android.util.SparseArray; import android.view.InputApplicationHandle; import com.android.internal.os.TimeoutRecord; +import com.android.server.FgThread; import com.android.server.am.StackTracesDumpHelper; import com.android.server.criticalevents.CriticalEventLog; @@ -68,7 +69,9 @@ class AnrController { TimeoutRecord timeoutRecord) { try { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "notifyAppUnresponsive()"); + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowStarted(); preDumpIfLockTooSlow(); + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowEnded(); final ActivityRecord activity; timeoutRecord.mLatencyTracker.waitingOnGlobalLockStarted(); boolean blamePendingFocusRequest = false; @@ -108,7 +111,7 @@ class AnrController { if (!blamePendingFocusRequest) { Slog.i(TAG_WM, "ANR in " + activity.getName() + ". Reason: " + timeoutRecord.mReason); - dumpAnrStateLocked(activity, null /* windowState */, timeoutRecord.mReason); + dumpAnrStateAsync(activity, null /* windowState */, timeoutRecord.mReason); mUnresponsiveAppByDisplay.put(activity.getDisplayId(), activity); } } @@ -159,7 +162,9 @@ class AnrController { */ private boolean notifyWindowUnresponsive(@NonNull IBinder inputToken, TimeoutRecord timeoutRecord) { + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowStarted(); preDumpIfLockTooSlow(); + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowEnded(); final int pid; final boolean aboveSystem; final ActivityRecord activity; @@ -178,7 +183,7 @@ class AnrController { ? windowState.mActivityRecord : null; Slog.i(TAG_WM, "ANR in " + target + ". Reason:" + timeoutRecord.mReason); aboveSystem = isWindowAboveSystem(windowState); - dumpAnrStateLocked(activity, windowState, timeoutRecord.mReason); + dumpAnrStateAsync(activity, windowState, timeoutRecord.mReason); } if (activity != null) { activity.inputDispatchingTimedOut(timeoutRecord, pid); @@ -197,7 +202,7 @@ class AnrController { timeoutRecord.mLatencyTracker.waitingOnGlobalLockStarted(); synchronized (mService.mGlobalLock) { timeoutRecord.mLatencyTracker.waitingOnGlobalLockEnded(); - dumpAnrStateLocked(null /* activity */, null /* windowState */, timeoutRecord.mReason); + dumpAnrStateAsync(null /* activity */, null /* windowState */, timeoutRecord.mReason); } // We cannot determine the z-order of the window, so place the anr dialog as high @@ -351,15 +356,23 @@ class AnrController { } - private void dumpAnrStateLocked(ActivityRecord activity, WindowState windowState, - String reason) { - try { - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "dumpAnrStateLocked()"); - mService.saveANRStateLocked(activity, windowState, reason); - mService.mAtmService.saveANRState(reason); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - } + /** + * Executes asynchronously on the fg thread not to block the stack dump for + * the ANRing processes. + */ + private void dumpAnrStateAsync(ActivityRecord activity, WindowState windowState, + String reason) { + FgThread.getExecutor().execute(() -> { + try { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "dumpAnrStateLocked()"); + synchronized (mService.mGlobalLock) { + mService.saveANRStateLocked(activity, windowState, reason); + mService.mAtmService.saveANRState(reason); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + } + }); } private boolean isWindowAboveSystem(@NonNull WindowState windowState) { diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 1a3d6730fe20..b216578262b4 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -180,7 +180,8 @@ public class BackgroundActivityStartController { Intent intent, ActivityOptions checkedOptions) { return checkBackgroundActivityStart(callingUid, callingPid, callingPackage, - realCallingUid, realCallingPid, callerApp, originatingPendingIntent, + realCallingUid, realCallingPid, + callerApp, originatingPendingIntent, backgroundStartPrivileges, intent, checkedOptions) == BAL_BLOCK; } @@ -288,11 +289,13 @@ public class BackgroundActivityStartController { } } + String realCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid); + // Legacy behavior allows to use caller foreground state to bypass BAL restriction. // The options here are the options passed by the sender and not those on the intent. final BackgroundStartPrivileges balAllowedByPiSender = PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller( - checkedOptions, realCallingUid); + checkedOptions, realCallingUid, realCallingPackage); final boolean logVerdictChangeByPiDefaultChange = checkedOptions == null || checkedOptions.getPendingIntentBackgroundActivityStartMode() @@ -342,6 +345,14 @@ public class BackgroundActivityStartController { /*background*/ true, callingUid, realCallingUid, intent, "Device Owner"); } + // don't abort if the callingUid is a affiliated profile owner + if (mService.isAffiliatedProfileOwner(callingUid)) { + return logStartAllowedAndReturnCode( + BAL_ALLOW_ALLOWLISTED_COMPONENT, + resultIfPiSenderAllowsBal, balAllowedByPiSender, + /*background*/ true, callingUid, realCallingUid, + intent, "Affiliated Profile Owner"); + } // don't abort if the callingUid has companion device final int callingUserId = UserHandle.getUserId(callingUid); if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) { @@ -452,8 +463,11 @@ public class BackgroundActivityStartController { // If we are here, it means all exemptions not based on PI sender failed, so we'll block // unless resultIfPiSenderAllowsBal is an allow and the PI sender allows BAL - String realCallingPackage = callingUid == realCallingUid ? callingPackage : - mService.mContext.getPackageManager().getNameForUid(realCallingUid); + if (realCallingPackage == null) { + realCallingPackage = (callingUid == realCallingUid ? callingPackage : + mService.mContext.getPackageManager().getNameForUid(realCallingUid)) + + "[debugOnly]"; + } String stateDumpLog = " [callingPackage: " + callingPackage + "; callingUid: " + callingUid diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index fa5da306d5f5..251a087916e3 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2962,6 +2962,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(mDisplayId, mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight); mDisplayRotation.physicalDisplayChanged(); + mDisplayPolicy.physicalDisplayChanged(); } // If there is an override set for base values - use it, otherwise use new values. @@ -2993,6 +2994,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp reconfigureDisplayLocked(); if (physicalDisplayChanged) { + mDisplayPolicy.physicalDisplayUpdated(); mDisplaySwitchTransitionLauncher.onDisplayUpdated(currentRotation, getRotation(), getDisplayAreaInfo()); } @@ -3042,7 +3044,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp + mBaseDisplayHeight + " on display:" + getDisplayId()); } } - if (mDisplayReady) { + if (mDisplayReady && !mDisplayPolicy.shouldKeepCurrentDecorInsets()) { mDisplayPolicy.mDecorInsets.invalidate(); } } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index db64b43bdf8c..899c36c4a1a7 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -174,6 +174,10 @@ public class DisplayPolicy { private static final int INSETS_OVERRIDE_INDEX_INVALID = -1; + // TODO(b/266197298): Remove this by a more general protocol from the insets providers. + private static final boolean USE_CACHED_INSETS_FOR_DISPLAY_SWITCH = + SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", false); + private final WindowManagerService mService; private final Context mContext; private final Context mUiContext; @@ -218,6 +222,8 @@ public class DisplayPolicy { private SystemGesturesPointerEventListener mSystemGestures; final DecorInsets mDecorInsets; + /** Currently it can only be non-null when physical display switch happens. */ + private DecorInsets.Cache mCachedDecorInsets; private volatile int mLidState = LID_ABSENT; private volatile int mDockMode = Intent.EXTRA_DOCK_STATE_UNDOCKED; @@ -1707,11 +1713,7 @@ public class DisplayPolicy { * Called when the configuration has changed, and it's safe to load new values from resources. */ public void onConfigurationChanged() { - final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); - final Resources res = getCurrentUserResources(); - final int portraitRotation = displayRotation.getPortraitRotation(); - mNavBarOpacityMode = res.getInteger(R.integer.config_navBarOpacityMode); mLeftGestureInset = mGestureNavigationSettingsObserver.getLeftSensitivity(res); mRightGestureInset = mGestureNavigationSettingsObserver.getRightSensitivity(res); @@ -1882,10 +1884,11 @@ public class DisplayPolicy { @Override public String toString() { - return "{nonDecorInsets=" + mNonDecorInsets - + ", configInsets=" + mConfigInsets - + ", nonDecorFrame=" + mNonDecorFrame - + ", configFrame=" + mConfigFrame + '}'; + final StringBuilder tmpSb = new StringBuilder(32); + return "{nonDecorInsets=" + mNonDecorInsets.toShortString(tmpSb) + + ", configInsets=" + mConfigInsets.toShortString(tmpSb) + + ", nonDecorFrame=" + mNonDecorFrame.toShortString(tmpSb) + + ", configFrame=" + mConfigFrame.toShortString(tmpSb) + '}'; } } @@ -1923,6 +1926,39 @@ public class DisplayPolicy { info.mNeedUpdate = true; } } + + void setTo(DecorInsets src) { + for (int i = mInfoForRotation.length - 1; i >= 0; i--) { + mInfoForRotation[i].set(src.mInfoForRotation[i]); + } + } + + void dump(String prefix, PrintWriter pw) { + for (int rotation = 0; rotation < mInfoForRotation.length; rotation++) { + final DecorInsets.Info info = mInfoForRotation[rotation]; + pw.println(prefix + Surface.rotationToString(rotation) + "=" + info); + } + } + + private static class Cache { + /** + * If {@link #mPreserveId} is this value, it is in the middle of updating display + * configuration before a transition is started. Then the active cache should be used. + */ + static final int ID_UPDATING_CONFIG = -1; + final DecorInsets mDecorInsets; + int mPreserveId; + boolean mActive; + + Cache(DisplayContent dc) { + mDecorInsets = new DecorInsets(dc); + } + + boolean canPreserve() { + return mPreserveId == ID_UPDATING_CONFIG || mDecorInsets.mDisplayContent + .mTransitionController.inTransition(mPreserveId); + } + } } /** @@ -1930,6 +1966,9 @@ public class DisplayPolicy { * call {@link DisplayContent#sendNewConfiguration()} if this method returns {@code true}. */ boolean updateDecorInsetsInfo() { + if (shouldKeepCurrentDecorInsets()) { + return false; + } final DisplayFrames displayFrames = mDisplayContent.mDisplayFrames; final int rotation = displayFrames.mRotation; final int dw = displayFrames.mWidth; @@ -1940,6 +1979,10 @@ public class DisplayPolicy { if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)) { return false; } + if (mCachedDecorInsets != null && !mCachedDecorInsets.canPreserve() + && !mDisplayContent.isSleeping()) { + mCachedDecorInsets = null; + } mDecorInsets.invalidate(); mDecorInsets.mInfoForRotation[rotation].set(newInfo); return true; @@ -1949,6 +1992,71 @@ public class DisplayPolicy { return mDecorInsets.get(rotation, w, h); } + /** Returns {@code true} to trust that {@link #mDecorInsets} already has the expected state. */ + boolean shouldKeepCurrentDecorInsets() { + return mCachedDecorInsets != null && mCachedDecorInsets.mActive + && mCachedDecorInsets.canPreserve(); + } + + void physicalDisplayChanged() { + if (USE_CACHED_INSETS_FOR_DISPLAY_SWITCH) { + updateCachedDecorInsets(); + } + } + + /** + * Caches the current insets and switches current insets to previous cached insets. This is to + * reduce multiple display configuration changes if there are multiple insets provider windows + * which may trigger {@link #updateDecorInsetsInfo()} individually. + */ + @VisibleForTesting + void updateCachedDecorInsets() { + DecorInsets prevCache = null; + if (mCachedDecorInsets == null) { + mCachedDecorInsets = new DecorInsets.Cache(mDisplayContent); + } else { + prevCache = new DecorInsets(mDisplayContent); + prevCache.setTo(mCachedDecorInsets.mDecorInsets); + } + // Set a special id to preserve it before a real id is available from transition. + mCachedDecorInsets.mPreserveId = DecorInsets.Cache.ID_UPDATING_CONFIG; + // Cache the current insets. + mCachedDecorInsets.mDecorInsets.setTo(mDecorInsets); + // Switch current to previous cache. + if (prevCache != null) { + mDecorInsets.setTo(prevCache); + mCachedDecorInsets.mActive = true; + } + } + + /** + * Called after the display configuration is updated according to the physical change. Suppose + * there should be a display change transition, so associate the cached decor insets with the + * transition to limit the lifetime of using the cache. + */ + void physicalDisplayUpdated() { + if (mCachedDecorInsets == null) { + return; + } + if (!mDisplayContent.mTransitionController.isCollecting()) { + // Unable to know when the display switch is finished. + mCachedDecorInsets = null; + return; + } + mCachedDecorInsets.mPreserveId = + mDisplayContent.mTransitionController.getCollectingTransitionId(); + // The validator will run after the transition is finished. So if the insets are changed + // during the transition, it can update to the latest state. + mDisplayContent.mTransitionController.mStateValidators.add(() -> { + // The insets provider client may defer to change its window until screen is on. So + // only validate when awake to avoid the cache being always dropped. + if (!mDisplayContent.isSleeping() && updateDecorInsetsInfo()) { + Slog.d(TAG, "Insets changed after display switch transition"); + mDisplayContent.sendNewConfiguration(); + } + }); + } + @NavigationBarPosition int navigationBarPosition(int displayRotation) { if (mNavigationBar != null) { @@ -2626,9 +2734,10 @@ public class DisplayPolicy { pw.print(prefix); pw.print("mRemoteInsetsControllerControlsSystemBars="); pw.println(mRemoteInsetsControllerControlsSystemBars); pw.print(prefix); pw.println("mDecorInsetsInfo:"); - for (int rotation = 0; rotation < mDecorInsets.mInfoForRotation.length; rotation++) { - final DecorInsets.Info info = mDecorInsets.mInfoForRotation[rotation]; - pw.println(prefixInner + Surface.rotationToString(rotation) + "=" + info); + mDecorInsets.dump(prefixInner, pw); + if (mCachedDecorInsets != null) { + pw.print(prefix); pw.println("mCachedDecorInsets:"); + mCachedDecorInsets.mDecorInsets.dump(prefixInner, pw); } if (!CLIENT_TRANSIENT) { mSystemGestures.dump(pw, prefix); diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index ae93a9496f7c..1fbf59301191 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -223,7 +223,7 @@ final class DisplayRotationCompatPolicy { try { activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true); ProtoLog.v(WM_DEBUG_STATES, - "Refershing activity for camera compatibility treatment, " + "Refreshing activity for camera compatibility treatment, " + "activityRecord=%s", activity); final ClientTransaction transaction = ClientTransaction.obtain( activity.app.getThread(), activity.token); @@ -245,7 +245,7 @@ final class DisplayRotationCompatPolicy { } /** - * Notifies that animation in {@link ScreenAnimationRotation} has finished. + * Notifies that animation in {@link ScreenRotationAnimation} has finished. * * <p>This class uses this signal as a trigger for notifying the user about forced rotation * reason with the {@link Toast}. @@ -311,11 +311,14 @@ final class DisplayRotationCompatPolicy { } } - // Refreshing only when configuration changes after rotation. + // Refreshing only when configuration changes after rotation or camera split screen aspect ratio + // treatment is enabled private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig, Configuration lastReportedConfig) { - return newConfig.windowConfiguration.getDisplayRotation() - != lastReportedConfig.windowConfiguration.getDisplayRotation() + final boolean displayRotationChanged = (newConfig.windowConfiguration.getDisplayRotation() + != lastReportedConfig.windowConfiguration.getDisplayRotation()); + return (displayRotationChanged + || activity.mLetterboxUiController.isCameraCompatSplitScreenAspectRatioAllowed()) && isTreatmentEnabledForActivity(activity) && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat(); } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index f492bab0728f..a93cb8ad2d97 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -214,7 +214,7 @@ final class LetterboxConfiguration { // otherwise the apps get blacked out when they are resumed and do not have focus yet. private boolean mIsCompatFakeFocusEnabled; - // Whether should use split screen aspect ratio for the activity when camera compat treatment + // Whether we should use split screen aspect ratio for the activity when camera compat treatment // is enabled and activity is connected to the camera in fullscreen. private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled; @@ -1118,7 +1118,7 @@ final class LetterboxConfiguration { } /** - * Whether should use split screen aspect ratio for the activity when camera compat treatment + * Whether we should use split screen aspect ratio for the activity when camera compat treatment * is enabled and activity is connected to the camera in fullscreen. */ boolean isCameraCompatSplitScreenAspectRatioEnabled() { diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 0288e4bbb26e..b557b2e1410f 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -25,6 +25,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE; +import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR; @@ -49,6 +50,7 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; @@ -185,10 +187,15 @@ final class LetterboxUiController { // when dealing with translucent activities. private final List<LetterboxUiController> mDestroyListeners = new ArrayList<>(); + // Corresponds to OVERRIDE_MIN_ASPECT_RATIO + private final boolean mIsOverrideMinAspectRatio; + @Nullable private final Boolean mBooleanPropertyAllowOrientationOverride; @Nullable private final Boolean mBooleanPropertyAllowDisplayOrientationOverride; + @Nullable + private final Boolean mBooleanPropertyAllowMinAspectRatioOverride; /* * WindowContainerListener responsible to make translucent activities inherit @@ -300,6 +307,10 @@ final class LetterboxUiController { readComponentProperty(packageManager, mActivityRecord.packageName, /* gatingCondition */ null, PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE); + mBooleanPropertyAllowMinAspectRatioOverride = + readComponentProperty(packageManager, mActivityRecord.packageName, + /* gatingCondition */ null, + PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION); mIsOverrideToPortraitOrientationEnabled = @@ -330,6 +341,7 @@ final class LetterboxUiController { mIsOverrideEnableCompatFakeFocusEnabled = isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS); + mIsOverrideMinAspectRatio = isCompatChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO); } /** @@ -502,6 +514,25 @@ final class LetterboxUiController { } /** + * Whether we should apply the min aspect ratio per-app override. When this override is applied + * the min aspect ratio given in the app's manifest will be overridden to the largest enabled + * aspect ratio treatment unless the app's manifest value is higher. The treatment will also + * apply if no value is provided in the manifest. + * + * <p>This method returns {@code true} when the following conditions are met: + * <ul> + * <li>Opt-out component property isn't enabled + * <li>Per-app override is enabled + * </ul> + */ + boolean shouldOverrideMinAspectRatio() { + return shouldEnableWithOptInOverrideAndOptOutProperty( + /* gatingCondition */ () -> true, + mIsOverrideMinAspectRatio, + mBooleanPropertyAllowMinAspectRatioOverride); + } + + /** * Sets whether an activity is relaunching after the app has called {@link * android.app.Activity#setRequestedOrientation}. */ @@ -958,7 +989,7 @@ final class LetterboxUiController { * Whether we use split screen aspect ratio for the activity when camera compat treatment * is active because the corresponding config is enabled and activity supports resizing. */ - private boolean isCameraCompatSplitScreenAspectRatioAllowed() { + boolean isCameraCompatSplitScreenAspectRatioAllowed() { return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled() && !mActivityRecord.shouldCreateCompatDisplayInsets(); } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 5e60e92150a9..e68bb71f6fb2 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1194,6 +1194,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (asyncRotationController != null && containsChangeFor(dc, mTargets)) { asyncRotationController.onTransitionFinished(); } + if (hasParticipatedDisplay && dc.mDisplayRotationCompatPolicy != null) { + final ChangeInfo changeInfo = mChanges.get(dc); + if (changeInfo != null + && changeInfo.mRotation != dc.getWindowConfiguration().getRotation()) { + dc.mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished(); + } + } if (mTransientLaunches != null) { InsetsControlTarget prevImeTarget = dc.getImeTarget( DisplayContent.IME_TARGET_CONTROL); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index b697ab158003..e228e7ef056a 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -431,6 +431,19 @@ class TransitionController { return inCollectingTransition(wc) || inPlayingTransition(wc); } + /** Returns {@code true} if the id matches a collecting or playing transition. */ + boolean inTransition(int syncId) { + if (mCollectingTransition != null && mCollectingTransition.getSyncId() == syncId) { + return true; + } + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + if (mPlayingTransitions.get(i).getSyncId() == syncId) { + return true; + } + } + return false; + } + /** @return {@code true} if wc is in a participant subtree */ boolean isTransitionOnDisplay(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc)) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 792ec2e92083..92d4cec80cc9 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -449,6 +449,19 @@ public abstract class WindowManagerInternal { public abstract void moveDisplayToTopIfAllowed(int displayId); /** + * Request to move window input focus to the window with the provided window token. + * + * <p> + * It is necessary to move window input focus before certain actions on views in a window can + * be performed, such as opening an IME. Input normally requests to move focus on window touch + * so this method should not be necessary in most cases; only features that bypass normal touch + * behavior (like Accessibility actions) require this method. + * </p> + * @param windowToken The window token. + */ + public abstract void requestWindowFocus(IBinder windowToken); + + /** * @return Whether the keyguard is engaged. */ public abstract boolean isKeyguardLocked(); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 25b7df4eda08..322c11a7cc4b 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -5652,7 +5652,7 @@ public class WindowManagerService extends IWindowManager.Stub case ON_POINTER_DOWN_OUTSIDE_FOCUS: { synchronized (mGlobalLock) { final IBinder touchedToken = (IBinder) msg.obj; - onPointerDownOutsideFocusLocked(touchedToken); + onPointerDownOutsideFocusLocked(getInputTargetFromToken(touchedToken)); } break; } @@ -7752,6 +7752,15 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public void requestWindowFocus(IBinder windowToken) { + synchronized (mGlobalLock) { + final InputTarget inputTarget = + WindowManagerService.this.getInputTargetFromWindowTokenLocked(windowToken); + WindowManagerService.this.onPointerDownOutsideFocusLocked(inputTarget); + } + } + + @Override public boolean isKeyguardLocked() { return WindowManagerService.this.isKeyguardLocked(); } @@ -8590,8 +8599,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - private void onPointerDownOutsideFocusLocked(IBinder touchedToken) { - InputTarget t = getInputTargetFromToken(touchedToken); + private void onPointerDownOutsideFocusLocked(InputTarget t) { if (t == null || !t.receiveFocusFromTapOutside()) { // If the window that received the input event cannot receive keys, don't move the // display it's on to the top since that window won't be able to get focus anyway. @@ -9014,7 +9022,7 @@ public class WindowManagerService extends IWindowManager.Stub final SurfaceControl mirror = SurfaceControl.mirrorSurface(displaySc); outSurfaceControl.copyFrom(mirror, "WMS.mirrorDisplay"); - + mirror.release(); return true; } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index dbd9e4b8ea68..3672820c13ad 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -721,6 +721,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } + List<String> getPackageList() { + synchronized (mPkgList) { + return new ArrayList<>(mPkgList); + } + } + void addActivityIfNeeded(ActivityRecord r) { // even if we already track this activity, note down that it has been launched setLastActivityLaunchTime(r); diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 2ce86add4e58..d5217c8295bd 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -109,6 +109,7 @@ cc_defaults { "libchrome", "libcutils", "libcrypto", + "libfmq", "liblog", "libgraphicsenv", "libgralloctypes", @@ -157,6 +158,7 @@ cc_defaults { "android.hardware.broadcastradio@1.0", "android.hardware.broadcastradio@1.1", "android.hardware.contexthub@1.0", + "android.hardware.common.fmq-V1-ndk", "android.hardware.gnss-V3-cpp", "android.hardware.gnss@1.0", "android.hardware.gnss@1.1", diff --git a/services/core/jni/com_android_server_tv_TvInputHal.cpp b/services/core/jni/com_android_server_tv_TvInputHal.cpp index a8806b5ef643..1e6384031f9a 100644 --- a/services/core/jni/com_android_server_tv_TvInputHal.cpp +++ b/services/core/jni/com_android_server_tv_TvInputHal.cpp @@ -20,6 +20,7 @@ #include "tvinput/JTvInputHal.h" +gBundleClassInfoType gBundleClassInfo; gTvInputHalClassInfoType gTvInputHalClassInfo; gTvStreamConfigClassInfoType gTvStreamConfigClassInfo; gTvStreamConfigBuilderClassInfoType gTvStreamConfigBuilderClassInfo; @@ -133,10 +134,22 @@ int register_android_server_tv_TvInputHal(JNIEnv* env) { GET_METHOD_ID( gTvInputHalClassInfo.firstFrameCaptured, clazz, "firstFrameCapturedFromNative", "(II)V"); + GET_METHOD_ID(gTvInputHalClassInfo.tvMessageReceived, clazz, "tvMessageReceivedFromNative", + "(IILandroid/os/Bundle;)V"); FIND_CLASS(gTvStreamConfigClassInfo.clazz, "android/media/tv/TvStreamConfig"); gTvStreamConfigClassInfo.clazz = jclass(env->NewGlobalRef(gTvStreamConfigClassInfo.clazz)); + FIND_CLASS(gBundleClassInfo.clazz, "android/os/Bundle"); + gBundleClassInfo.clazz = jclass(env->NewGlobalRef(gBundleClassInfo.clazz)); + GET_METHOD_ID(gBundleClassInfo.constructor, gBundleClassInfo.clazz, "<init>", "()V"); + GET_METHOD_ID(gBundleClassInfo.putByteArray, gBundleClassInfo.clazz, "putByteArray", + "(Ljava/lang/String;[B)V"); + GET_METHOD_ID(gBundleClassInfo.putString, gBundleClassInfo.clazz, "putString", + "(Ljava/lang/String;Ljava/lang/String;)V"); + GET_METHOD_ID(gBundleClassInfo.putInt, gBundleClassInfo.clazz, "putInt", + "(Ljava/lang/String;I)V"); + FIND_CLASS(gTvStreamConfigBuilderClassInfo.clazz, "android/media/tv/TvStreamConfig$Builder"); gTvStreamConfigBuilderClassInfo.clazz = jclass(env->NewGlobalRef(gTvStreamConfigBuilderClassInfo.clazz)); diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp index 6bb52174fcb2..c494044e8e18 100644 --- a/services/core/jni/tvinput/JTvInputHal.cpp +++ b/services/core/jni/tvinput/JTvInputHal.cpp @@ -275,6 +275,26 @@ void JTvInputHal::onStreamConfigurationsChanged(int deviceId, int cableConnectio cableConnectionStatus); } +void JTvInputHal::onTvMessage(int deviceId, int streamId, AidlTvMessageEventType type, + AidlTvMessage& message, signed char data[], int dataLength) { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + jobject bundle = env->NewObject(gBundleClassInfo.clazz, gBundleClassInfo.constructor); + const jsize len = static_cast<jsize>(dataLength); + jbyteArray convertedData = env->NewByteArray(len); + env->SetByteArrayRegion(convertedData, 0, len, reinterpret_cast<const jbyte*>(data)); + env->CallObjectMethod(bundle, gBundleClassInfo.putString, + "android.media.tv.TvInputManager.subtype", message.subType.c_str()); + env->CallObjectMethod(bundle, gBundleClassInfo.putByteArray, + "android.media.tv.TvInputManager.raw_data", convertedData); + env->CallObjectMethod(bundle, gBundleClassInfo.putInt, + "android.media.tv.TvInputManager.group_id", message.groupId); + env->CallObjectMethod(bundle, gBundleClassInfo.putInt, + "android.media.tv.TvInputManager.stream_id", streamId); + env->CallVoidMethod(mThiz, gTvInputHalClassInfo.tvMessageReceived, deviceId, + static_cast<int>(type), bundle); + env->DeleteLocalRef(convertedData); +} + void JTvInputHal::onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded) { sp<BufferProducerThread> thread; { @@ -316,6 +336,17 @@ JTvInputHal::TvInputEventWrapper JTvInputHal::TvInputEventWrapper::createEventWr return event; } +JTvInputHal::TvMessageEventWrapper JTvInputHal::TvMessageEventWrapper::createEventWrapper( + const AidlTvMessageEvent& aidlTvMessageEvent) { + TvMessageEventWrapper event; + event.messages.insert(event.messages.begin(), std::begin(aidlTvMessageEvent.messages) + 1, + std::end(aidlTvMessageEvent.messages)); + event.streamId = aidlTvMessageEvent.streamId; + event.deviceId = aidlTvMessageEvent.messages[0].groupId; + event.type = aidlTvMessageEvent.type; + return event; +} + JTvInputHal::NotifyHandler::NotifyHandler(JTvInputHal* hal, const TvInputEventWrapper& event) { mHal = hal; mEvent = event; @@ -338,6 +369,41 @@ void JTvInputHal::NotifyHandler::handleMessage(const Message& message) { } } +JTvInputHal::NotifyTvMessageHandler::NotifyTvMessageHandler(JTvInputHal* hal, + const TvMessageEventWrapper& event) { + mHal = hal; + mEvent = event; +} + +void JTvInputHal::NotifyTvMessageHandler::handleMessage(const Message& message) { + std::shared_ptr<AidlMessageQueue<int8_t, SynchronizedReadWrite>> queue = + mHal->mQueueMap[mEvent.deviceId][mEvent.streamId]; + for (AidlTvMessage item : mEvent.messages) { + if (queue == NULL || !queue->isValid() || queue->availableToRead() < item.dataLengthBytes) { + MQDescriptor<int8_t, SynchronizedReadWrite> queueDesc; + if (mHal->mTvInput->getTvMessageQueueDesc(&queueDesc, mEvent.deviceId, mEvent.streamId) + .isOk()) { + queue = std::make_shared<AidlMessageQueue<int8_t, SynchronizedReadWrite>>(queueDesc, + false); + } + if (queue == NULL || !queue->isValid() || + queue->availableToRead() < item.dataLengthBytes) { + ALOGE("Incomplete TvMessageQueue data or missing queue"); + return; + } + mHal->mQueueMap[mEvent.deviceId][mEvent.streamId] = queue; + } + signed char* buffer = new signed char[item.dataLengthBytes]; + if (queue->read(buffer, item.dataLengthBytes)) { + mHal->onTvMessage(mEvent.deviceId, mEvent.streamId, mEvent.type, item, buffer, + item.dataLengthBytes); + } else { + ALOGE("Failed to read from TvMessageQueue"); + } + delete[] buffer; + } +} + JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) { mHal = hal; } @@ -351,7 +417,15 @@ JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) { ::ndk::ScopedAStatus JTvInputHal::TvInputCallback::notifyTvMessageEvent( const AidlTvMessageEvent& event) { - // TODO: Implement this + const std::string DEVICE_ID_SUBTYPE = "device_id"; + if (sizeof(event.messages) > 0 && event.messages[0].subType == DEVICE_ID_SUBTYPE) { + mHal->mLooper + ->sendMessage(new NotifyTvMessageHandler(mHal, + TvMessageEventWrapper::createEventWrapper( + event)), + static_cast<int>(event.type)); + } + return ::ndk::ScopedAStatus::ok(); } @@ -406,4 +480,14 @@ JTvInputHal::ITvInputWrapper::ITvInputWrapper(std::shared_ptr<AidlITvInput>& aid } } +::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::getTvMessageQueueDesc( + MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId, + int32_t in_streamId) { + if (mIsHidl) { + return ::ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION); + } else { + return mAidlTvInput->getTvMessageQueueDesc(out_queue, in_deviceId, in_streamId); + } +} + } // namespace android diff --git a/services/core/jni/tvinput/JTvInputHal.h b/services/core/jni/tvinput/JTvInputHal.h index e29da79d62dd..e8b1c9931372 100644 --- a/services/core/jni/tvinput/JTvInputHal.h +++ b/services/core/jni/tvinput/JTvInputHal.h @@ -24,6 +24,7 @@ #include <aidl/android/media/audio/common/AudioDevice.h> #include <aidlcommonsupport/NativeHandle.h> #include <android/binder_manager.h> +#include <fmq/AidlMessageQueue.h> #include <nativehelper/JNIHelp.h> #include <utils/Errors.h> #include <utils/KeyedVector.h> @@ -40,6 +41,10 @@ #include "android_runtime/android_view_Surface.h" #include "tvinput/jstruct.h" +using ::android::AidlMessageQueue; + +using ::aidl::android::hardware::common::fmq::MQDescriptor; +using ::aidl::android::hardware::common::fmq::SynchronizedReadWrite; using ::aidl::android::hardware::tv::input::BnTvInputCallback; using ::aidl::android::hardware::tv::input::CableConnectionStatus; using ::aidl::android::hardware::tv::input::TvInputEventType; @@ -54,10 +59,16 @@ using AidlITvInput = ::aidl::android::hardware::tv::input::ITvInput; using AidlNativeHandle = ::aidl::android::hardware::common::NativeHandle; using AidlTvInputDeviceInfo = ::aidl::android::hardware::tv::input::TvInputDeviceInfo; using AidlTvInputEvent = ::aidl::android::hardware::tv::input::TvInputEvent; +using AidlTvMessage = ::aidl::android::hardware::tv::input::TvMessage; using AidlTvMessageEvent = ::aidl::android::hardware::tv::input::TvMessageEvent; using AidlTvMessageEventType = ::aidl::android::hardware::tv::input::TvMessageEventType; using AidlTvStreamConfig = ::aidl::android::hardware::tv::input::TvStreamConfig; +using AidlMessageQueueMap = std::unordered_map< + int, + std::unordered_map<int, std::shared_ptr<AidlMessageQueue<int8_t, SynchronizedReadWrite>>>>; + +extern gBundleClassInfoType gBundleClassInfo; extern gTvInputHalClassInfoType gTvInputHalClassInfo; extern gTvStreamConfigClassInfoType gTvStreamConfigClassInfo; extern gTvStreamConfigBuilderClassInfoType gTvStreamConfigBuilderClassInfo; @@ -121,6 +132,19 @@ private: TvInputDeviceInfoWrapper deviceInfo; }; + class TvMessageEventWrapper { + public: + TvMessageEventWrapper() {} + + static TvMessageEventWrapper createEventWrapper( + const AidlTvMessageEvent& aidlTvMessageEvent); + + int streamId; + int deviceId; + std::vector<AidlTvMessage> messages; + AidlTvMessageEventType type; + }; + class NotifyHandler : public MessageHandler { public: NotifyHandler(JTvInputHal* hal, const TvInputEventWrapper& event); @@ -132,6 +156,17 @@ private: JTvInputHal* mHal; }; + class NotifyTvMessageHandler : public MessageHandler { + public: + NotifyTvMessageHandler(JTvInputHal* hal, const TvMessageEventWrapper& event); + + void handleMessage(const Message& message) override; + + private: + TvMessageEventWrapper mEvent; + JTvInputHal* mHal; + }; + class TvInputCallback : public HidlITvInputCallback, public BnTvInputCallback { public: explicit TvInputCallback(JTvInputHal* hal); @@ -156,6 +191,9 @@ private: ::ndk::ScopedAStatus closeStream(int32_t in_deviceId, int32_t in_streamId); ::ndk::ScopedAStatus setTvMessageEnabled(int32_t deviceId, int32_t streamId, TvMessageEventType in_type, bool enabled); + ::ndk::ScopedAStatus getTvMessageQueueDesc( + MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId, + int32_t in_streamId); private: ::ndk::ScopedAStatus hidlSetCallback(const std::shared_ptr<TvInputCallback>& in_callback); @@ -178,11 +216,14 @@ private: void onDeviceUnavailable(int deviceId); void onStreamConfigurationsChanged(int deviceId, int cableConnectionStatus); void onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded); + void onTvMessage(int deviceId, int streamId, AidlTvMessageEventType type, + AidlTvMessage& message, signed char data[], int dataLength); Mutex mLock; Mutex mStreamLock; jweak mThiz; sp<Looper> mLooper; + AidlMessageQueueMap mQueueMap; KeyedVector<int, KeyedVector<int, Connection> > mConnections; diff --git a/services/core/jni/tvinput/jstruct.h b/services/core/jni/tvinput/jstruct.h index 0a4a64dbb40c..23cf4aeaffa9 100644 --- a/services/core/jni/tvinput/jstruct.h +++ b/services/core/jni/tvinput/jstruct.h @@ -23,6 +23,7 @@ typedef struct { jmethodID deviceUnavailable; jmethodID streamConfigsChanged; jmethodID firstFrameCaptured; + jmethodID tvMessageReceived; } gTvInputHalClassInfoType; typedef struct { @@ -31,6 +32,14 @@ typedef struct { typedef struct { jclass clazz; + jmethodID constructor; + jmethodID putByteArray; + jmethodID putString; + jmethodID putInt; +} gBundleClassInfoType; + +typedef struct { + jclass clazz; jmethodID constructor; jmethodID streamId; diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index fe8a8c8979f7..69a5e5c3a901 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -93,6 +93,7 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta public void onFinalResponseReceived( ComponentName componentName, Void response) { + mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.updateMetricsOnResponseReceived(mProviders, componentName, isPrimaryProviderViaProviderInfo(componentName)); respondToClientWithResponseAndFinish(null); diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 20dd1816bf44..c37a038d8ef7 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -64,7 +64,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR RequestInfo.TYPE_CREATE, callingAppInfo, enabledProviders, cancellationSignal, startedTimestamp); mRequestSessionMetric.collectCreateFlowInitialMetricInfo( - /*origin=*/request.getOrigin() != null); + /*origin=*/request.getOrigin() != null, request); mPrimaryProviders = primaryProviders; } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index 25561ed25f20..272452e1b2ae 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -33,7 +33,6 @@ import android.os.IBinder; import android.os.Looper; import android.os.ResultReceiver; import android.service.credentials.CredentialProviderInfoFactory; -import android.util.Log; import android.util.Slog; import java.util.ArrayList; @@ -72,7 +71,6 @@ public class CredentialManagerUi { }; private void handleUiResult(int resultCode, Bundle resultData) { - Log.i("reemademo", "handleUiResult with resultCOde: " + resultCode); switch (resultCode) { case UserSelectionDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION: @@ -86,13 +84,11 @@ public class CredentialManagerUi { } break; case UserSelectionDialogResult.RESULT_CODE_DIALOG_USER_CANCELED: - Log.i("reemademo", "RESULT_CODE_DIALOG_USER_CANCELED"); mStatus = UiStatus.TERMINATED; mCallbacks.onUiCancellation(/* isUserCancellation= */ true); break; case UserSelectionDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS: - Log.i("reemademo", "RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS"); mStatus = UiStatus.TERMINATED; mCallbacks.onUiCancellation(/* isUserCancellation= */ false); @@ -102,7 +98,6 @@ public class CredentialManagerUi { mCallbacks.onUiSelectorInvocationFailure(); break; default: - Log.i("reemademo", "Unknown error code returned from the UI"); mStatus = UiStatus.IN_PROGRESS; mCallbacks.onUiSelectorInvocationFailure(); break; diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index f95ed6adc8f6..aee4f583eec9 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -133,6 +133,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, public void onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response) { Slog.i(TAG, "onFinalResponseReceived from: " + componentName.flattenToString()); + mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.updateMetricsOnResponseReceived(mProviders, componentName, isPrimaryProviderViaProviderInfo(componentName)); if (response != null) { @@ -158,7 +159,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override public void onUiCancellation(boolean isUserCancellation) { - String exception = GetCredentialException.TYPE_NO_CREDENTIAL; + String exception = GetCredentialException.TYPE_USER_CANCELED; String message = "User cancelled the selector"; if (!isUserCancellation) { exception = GetCredentialException.TYPE_INTERRUPTED; diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index 64a73c92ca1f..b36de0b03fa8 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -39,7 +39,6 @@ import java.util.Map; /** * For all future metric additions, this will contain their names for local usage after importing * from {@link com.android.internal.util.FrameworkStatsLog}. - * TODO(b/271135048) - Emit all atoms, including all V4 atoms (specifically the rest of track 1). */ public class MetricUtilities { private static final boolean LOG_FLAG = true; @@ -101,7 +100,9 @@ public class MetricUtilities { */ protected static int getMetricTimestampDifferenceMicroseconds(long t2, long t1) { if (t2 - t1 > Integer.MAX_VALUE) { - throw new ArithmeticException("Input timestamps are too far apart and unsupported"); + Slog.i(TAG, "Input timestamps are too far apart and unsupported, " + + "falling back to default int"); + return DEFAULT_INT_32; } if (t2 < t1) { Slog.i(TAG, "The timestamps aren't in expected order, falling back to default int"); @@ -229,7 +230,7 @@ public class MetricUtilities { authenticationMetric.isAuthReturned() ); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during candidate get metric logging: " + e); + Slog.w(TAG, "Unexpected error during candidate auth metric logging: " + e); } } @@ -252,22 +253,18 @@ public class MetricUtilities { } var sessions = providers.values(); for (var session : sessions) { - try { - var metric = session.getProviderSessionMetric() - .getCandidatePhasePerProviderMetric(); - FrameworkStatsLog.write( - FrameworkStatsLog.CREDENTIAL_MANAGER_GET_REPORTED, - /* session_id */ metric.getSessionIdProvider(), - /* sequence_num */ emitSequenceId, - /* candidate_provider_uid */ metric.getCandidateUid(), - /* response_unique_classtypes */ - metric.getResponseCollective().getUniqueResponseStrings(), - /* per_classtype_counts */ - metric.getResponseCollective().getUniqueResponseCounts() - ); - } catch (Exception e) { - Slog.w(TAG, "Unexpected exception during get metric logging" + e); - } + var metric = session.getProviderSessionMetric() + .getCandidatePhasePerProviderMetric(); + FrameworkStatsLog.write( + FrameworkStatsLog.CREDENTIAL_MANAGER_GET_REPORTED, + /* session_id */ metric.getSessionIdProvider(), + /* sequence_num */ emitSequenceId, + /* candidate_provider_uid */ metric.getCandidateUid(), + /* response_unique_classtypes */ + metric.getResponseCollective().getUniqueResponseStrings(), + /* per_classtype_counts */ + metric.getResponseCollective().getUniqueResponseCounts() + ); } } catch (Exception e) { Slog.w(TAG, "Unexpected error during candidate get metric logging: " + e); @@ -399,7 +396,7 @@ public class MetricUtilities { /* caller_uid */ callingUid, /* api_status */ apiStatus.getMetricCode()); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during simple v2 metric logging: " + e); } } @@ -505,7 +502,7 @@ public class MetricUtilities { candidateAggregateMetric.isAuthReturned() ); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during total candidate metric logging: " + e); } } @@ -570,7 +567,7 @@ public class MetricUtilities { /* primary_indicated */ finalPhaseMetric.isPrimary() ); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during final no uid metric logging: " + e); } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 25f20caee16d..6f79852df02f 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -54,7 +54,7 @@ public final class ProviderCreateSession extends ProviderSession< private static final String TAG = "ProviderCreateSession"; // Key to be used as an entry key for a save entry - private static final String SAVE_ENTRY_KEY = "save_entry_key"; + public static final String SAVE_ENTRY_KEY = "save_entry_key"; // Key to be used as an entry key for a remote entry private static final String REMOTE_ENTRY_KEY = "remote_entry_key"; @@ -193,11 +193,13 @@ public final class ProviderCreateSession extends ProviderSession< mProviderResponseDataHandler.addResponseContent(response.getCreateEntries(), response.getRemoteCreateEntry()); if (mProviderResponseDataHandler.isEmptyResponse(response)) { - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + ((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric()); updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } else { - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + ((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric()); updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 14260f0e1ad4..9b0a1cb0c57c 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -435,7 +435,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential BeginGetCredentialResponse response = PendingIntentResultHandler .extractResponseContent(providerPendingIntentResponse .getResultData()); - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/true); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/true, null); if (response != null && !mProviderResponseDataHandler.isEmptyResponse(response)) { addToInitialRemoteResponse(response, /*isInitialResponse=*/ false); // Additional content received is in the form of new response content. @@ -473,12 +473,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential addToInitialRemoteResponse(response, /*isInitialResponse=*/true); // Log the data. if (mProviderResponseDataHandler.isEmptyResponse(response)) { - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + null); updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE, /*source=*/ CredentialsSource.REMOTE_PROVIDER); return; } - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + null); updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 83f21afa1a32..f2055d0595be 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -215,8 +215,10 @@ public abstract class ProviderSession<T, R> CredentialsSource source) { setStatus(status); boolean isPrimary = mProviderInfo != null && mProviderInfo.isPrimary(); - mProviderSessionMetric.collectCandidateMetricUpdate(isTerminatingStatus(status), - isCompletionStatus(status), mProviderSessionUid, + mProviderSessionMetric.collectCandidateMetricUpdate(isTerminatingStatus(status) + || isStatusWaitingForRemoteResponse(status), + isCompletionStatus(status) || isUiInvokingStatus(status), + mProviderSessionUid, /*isAuthEntry*/source == CredentialsSource.AUTH_ENTRY, /*isPrimary*/isPrimary); mCallbacks.onProviderStatusChanged(status, mComponentName, source); diff --git a/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java b/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java index 62b8f24eb201..1ac8a71e745e 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java @@ -43,9 +43,6 @@ public class BrowsedAuthenticationMetric { // Indicates if this provider returned from the authentication entry query, default false private boolean mAuthReturned = false; - // TODO(b/271135048) - Match the atom and provide a clean per provider session metric - // encapsulation. - public BrowsedAuthenticationMetric(int sessionIdProvider) { mSessionIdProvider = sessionIdProvider; } diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java index 339c221b0ccc..91547788a23b 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java @@ -16,6 +16,7 @@ package com.android.server.credentials.metrics; +import com.android.server.credentials.MetricUtilities; import com.android.server.credentials.ProviderSession; import com.android.server.credentials.metrics.shared.ResponseCollective; @@ -29,7 +30,7 @@ import java.util.Map; */ public class CandidateAggregateMetric { - private static final String TAG = "CandidateProviderMetric"; + private static final String TAG = "CandidateTotalMetric"; // The session id of this provider metric private final int mSessionIdProvider; // Indicates if this provider returned from the candidate query phase, @@ -74,8 +75,6 @@ public class CandidateAggregateMetric { /** * This will take all the candidate data captured and aggregate that information. - * TODO(b/271135048) : Add on authentication entry outputs from track 2 here as well once - * generated * @param providers the providers associated with the candidate flow */ public void collectAverages(Map<String, ProviderSession> providers) { @@ -88,11 +87,15 @@ public class CandidateAggregateMetric { Map<String, Integer> responseCountQuery = new LinkedHashMap<>(); Map<EntryEnum, Integer> entryCountQuery = new LinkedHashMap<>(); var providerSessions = providers.values(); - long min_query_start = Integer.MAX_VALUE; - long max_query_end = Integer.MIN_VALUE; + long min_query_start = Long.MAX_VALUE; + long max_query_end = Long.MIN_VALUE; for (var session : providerSessions) { var sessionMetric = session.getProviderSessionMetric(); var candidateMetric = sessionMetric.getCandidatePhasePerProviderMetric(); + if (candidateMetric.getCandidateUid() == MetricUtilities.DEFAULT_INT_32) { + mNumProviders--; + continue; // Do not aggregate this one and reduce the size of actual candidates + } if (mServiceBeganTimeNanoseconds == -1) { mServiceBeganTimeNanoseconds = candidateMetric.getServiceBeganTimeNanoseconds(); } @@ -119,15 +122,17 @@ public class CandidateAggregateMetric { } private void collectAuthAggregates(Map<String, ProviderSession> providers) { - mNumProviders = providers.size(); Map<String, Integer> responseCountAuth = new LinkedHashMap<>(); Map<EntryEnum, Integer> entryCountAuth = new LinkedHashMap<>(); var providerSessions = providers.values(); for (var session : providerSessions) { var sessionMetric = session.getProviderSessionMetric(); var authMetrics = sessionMetric.getBrowsedAuthenticationMetric(); - mNumAuthEntriesTapped += authMetrics.size(); for (var authMetric : authMetrics) { + if (authMetric.getProviderUid() == MetricUtilities.DEFAULT_INT_32) { + continue; // skip this unfilled base auth entry + } + mNumAuthEntriesTapped++; mAuthReturned = mAuthReturned || authMetric.isAuthReturned(); ResponseCollective authCollective = authMetric.getAuthEntryCollective(); ResponseCollective.combineTypeCountMaps(responseCountAuth, diff --git a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java index 530f01cbdfc5..226cd2c11b15 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java +++ b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java @@ -28,6 +28,8 @@ import static com.android.server.credentials.ProviderGetSession.REMOTE_ENTRY_KEY import android.util.Slog; +import com.android.server.credentials.ProviderCreateSession; + import java.util.AbstractMap; import java.util.Map; @@ -52,6 +54,8 @@ public enum EntryEnum { new AbstractMap.SimpleEntry<>(REMOTE_ENTRY_KEY, REMOTE_ENTRY.mInnerMetricCode), new AbstractMap.SimpleEntry<>(CREDENTIAL_ENTRY_KEY, + CREDENTIAL_ENTRY.mInnerMetricCode), + new AbstractMap.SimpleEntry<>(ProviderCreateSession.SAVE_ENTRY_KEY, CREDENTIAL_ENTRY.mInnerMetricCode) ); diff --git a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java index 44d845eaaf43..c1f6b4779ed4 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java @@ -20,6 +20,7 @@ import static com.android.server.credentials.MetricUtilities.DELTA_RESPONSES_CUT import static com.android.server.credentials.MetricUtilities.generateMetricKey; import android.annotation.NonNull; +import android.annotation.Nullable; import android.service.credentials.BeginCreateCredentialResponse; import android.service.credentials.BeginGetCredentialResponse; import android.service.credentials.CredentialEntry; @@ -205,18 +206,22 @@ public class ProviderSessionMetric { * * @param response contains entries and data from the candidate provider responses * @param isAuthEntry indicates if this is an auth entry collection or not + * @param initialPhaseMetric for create flows, this helps identify the response type, which + * will identify the *type* of create flow, especially important in + * track 2. This is expected to be null in get flows. * @param <R> the response type associated with the API flow in progress */ - public <R> void collectCandidateEntryMetrics(R response, boolean isAuthEntry) { + public <R> void collectCandidateEntryMetrics(R response, boolean isAuthEntry, + @Nullable InitialPhaseMetric initialPhaseMetric) { try { if (response instanceof BeginGetCredentialResponse) { beginGetCredentialResponseCollectionCandidateEntryMetrics( (BeginGetCredentialResponse) response, isAuthEntry); } else if (response instanceof BeginCreateCredentialResponse) { beginCreateCredentialResponseCollectionCandidateEntryMetrics( - (BeginCreateCredentialResponse) response); + (BeginCreateCredentialResponse) response, initialPhaseMetric); } else { - Slog.i(TAG, "Your response type is unsupported for metric logging"); + Slog.i(TAG, "Your response type is unsupported for candidate metric logging"); } } catch (Exception e) { Slog.i(TAG, "Unexpected error during candidate entry metric logging: " + e); @@ -245,7 +250,6 @@ public class ProviderSessionMetric { String entryKey = generateMetricKey(entry.getType(), DELTA_RESPONSES_CUT); responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1); }); - ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); } @@ -262,19 +266,21 @@ public class ProviderSessionMetric { } private void beginCreateCredentialResponseCollectionCandidateEntryMetrics( - BeginCreateCredentialResponse response) { + BeginCreateCredentialResponse response, InitialPhaseMetric initialPhaseMetric) { Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); var createEntries = response.getCreateEntries(); - int numRemoteEntry = response.getRemoteCreateEntry() != null ? MetricUtilities.ZERO : + int numRemoteEntry = response.getRemoteCreateEntry() == null ? MetricUtilities.ZERO : MetricUtilities.UNIT; int numCreateEntries = createEntries.size(); entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry); entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCreateEntries); Map<String, Integer> responseCounts = new LinkedHashMap<>(); - responseCounts.put(MetricUtilities.DEFAULT_STRING, numCreateEntries); - // We don't store create response because it's directly related to the request - // We do still store the count, however + String[] requestStrings = initialPhaseMetric == null ? new String[0] : + initialPhaseMetric.getUniqueRequestStrings(); + if (requestStrings.length > 0) { + responseCounts.put(requestStrings[0], initialPhaseMetric.getUniqueRequestCounts()[0]); + } ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index 281f3cc705be..83b57c4f78c2 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -26,13 +26,17 @@ import static com.android.server.credentials.MetricUtilities.logApiCalledCandida import static com.android.server.credentials.MetricUtilities.logApiCalledCandidatePhase; import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPhase; import static com.android.server.credentials.MetricUtilities.logApiCalledNoUidFinal; +import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL; +import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY; import android.annotation.NonNull; import android.content.ComponentName; +import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; import android.credentials.ui.UserSelectionDialogResult; import android.util.Slog; +import com.android.server.credentials.MetricUtilities; import com.android.server.credentials.ProviderSession; import java.util.ArrayList; @@ -112,7 +116,7 @@ public class RequestSessionMetric { mInitialPhaseMetric.setCallerUid(mCallingUid); mInitialPhaseMetric.setApiName(metricCode); } catch (Exception e) { - Slog.i(TAG, "Unexpected error collecting initial metrics: " + e); + Slog.i(TAG, "Unexpected error collecting initial phase metric start info: " + e); } } @@ -177,9 +181,12 @@ public class RequestSessionMetric { * * @param origin indicates if an origin was passed in or not */ - public void collectCreateFlowInitialMetricInfo(boolean origin) { + public void collectCreateFlowInitialMetricInfo(boolean origin, + CreateCredentialRequest request) { try { mInitialPhaseMetric.setOriginSpecified(origin); + mInitialPhaseMetric.setRequestCounts(Map.of(generateMetricKey(request.getType(), + DELTA_RESPONSES_CUT), MetricUtilities.UNIT)); } catch (Exception e) { Slog.i(TAG, "Unexpected error collecting create flow metric: " + e); } @@ -195,7 +202,7 @@ public class RequestSessionMetric { 0) + 1); }); } catch (Exception e) { - Slog.i(TAG, "Unexpected error during get request metric logging: " + e); + Slog.i(TAG, "Unexpected error during get request count map metric logging: " + e); } return uniqueRequestCounts; } @@ -210,7 +217,7 @@ public class RequestSessionMetric { mInitialPhaseMetric.setOriginSpecified(request.getOrigin() != null); mInitialPhaseMetric.setRequestCounts(getRequestCountMap(request)); } catch (Exception e) { - Slog.i(TAG, "Unexpected error collecting get flow metric: " + e); + Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e); } } @@ -277,7 +284,7 @@ public class RequestSessionMetric { mChosenProviderFinalPhaseMetric.setChosenProviderStatus( finalStatus.getMetricCode()); } catch (Exception e) { - Slog.i(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during final phase provider status metric logging: " + e); } } @@ -367,7 +374,11 @@ public class RequestSessionMetric { public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) { try { logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric); - logApiCalledCandidateGetMetric(providers, mSequenceCounter); + if (mInitialPhaseMetric.getApiName() == GET_CREDENTIAL.getMetricCode() + || mInitialPhaseMetric.getApiName() == GET_CREDENTIAL_VIA_REGISTRY + .getMetricCode()) { + logApiCalledCandidateGetMetric(providers, mSequenceCounter); + } } catch (Exception e) { Slog.i(TAG, "Unexpected error during candidate metric emit: " + e); } @@ -405,7 +416,7 @@ public class RequestSessionMetric { } logApiCalledAuthenticationMetric(browsedAuthenticationMetric, ++mSequenceCounter); } catch (Exception e) { - Slog.i(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during auth entry metric emit: " + e); } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java b/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java index 0958a841fe7e..ceb957100aec 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java +++ b/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java @@ -18,6 +18,7 @@ package com.android.server.credentials.metrics.shared; import android.annotation.NonNull; +import com.android.server.credentials.MetricUtilities; import com.android.server.credentials.metrics.EntryEnum; import java.util.Collections; @@ -121,7 +122,7 @@ public class ResponseCollective { * @return a count of this particular entry enum stored by this provider */ public int getCountForEntry(EntryEnum e) { - return mEntryCounts.get(e); + return mEntryCounts.getOrDefault(e, MetricUtilities.ZERO); } /** @@ -167,7 +168,7 @@ public class ResponseCollective { public static <T> Map<T, Integer> combineTypeCountMaps(Map<T, Integer> first, Map<T, Integer> second) { for (T response : second.keySet()) { - first.merge(response, first.getOrDefault(response, 0), Integer::sum); + first.put(response, first.getOrDefault(response, 0) + second.get(response)); } return first; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index 1392d0232262..1322225d8e93 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -56,6 +56,7 @@ import android.util.Xml; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.utils.Slogf; import libcore.io.IoUtils; @@ -1511,7 +1512,7 @@ final class DevicePolicyEngine { readInner(parser); } catch (XmlPullParserException | IOException | ClassNotFoundException e) { - Log.e(TAG, "Error parsing resources file", e); + Slogf.wtf(TAG, "Error parsing resources file", e); } finally { IoUtils.closeQuietly(input); } @@ -1533,7 +1534,7 @@ final class DevicePolicyEngine { readEnforcingAdminsInner(parser); break; default: - Log.e(TAG, "Unknown tag " + tag); + Slogf.wtf(TAG, "Unknown tag " + tag); } } } @@ -1554,7 +1555,7 @@ final class DevicePolicyEngine { policyState = PolicyState.readFromXml(parser); break; default: - Log.e(TAG, "Unknown tag for local policy entry" + tag); + Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag); } } @@ -1564,7 +1565,9 @@ final class DevicePolicyEngine { } mLocalPolicies.get(userId).put(policyKey, policyState); } else { - Log.e(TAG, "Error parsing local policy"); + Slogf.wtf(TAG, "Error parsing local policy, policyKey is " + + (policyKey == null ? "null" : policyKey) + ", and policyState is " + + (policyState == null ? "null" : policyState) + "."); } } @@ -1583,20 +1586,26 @@ final class DevicePolicyEngine { policyState = PolicyState.readFromXml(parser); break; default: - Log.e(TAG, "Unknown tag for local policy entry" + tag); + Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag); } } if (policyKey != null && policyState != null) { mGlobalPolicies.put(policyKey, policyState); } else { - Log.e(TAG, "Error parsing global policy"); + Slogf.wtf(TAG, "Error parsing global policy, policyKey is " + + (policyKey == null ? "null" : policyKey) + ", and policyState is " + + (policyState == null ? "null" : policyState) + "."); } } private void readEnforcingAdminsInner(TypedXmlPullParser parser) throws XmlPullParserException { EnforcingAdmin admin = EnforcingAdmin.readFromXml(parser); + if (admin == null) { + Slogf.wtf(TAG, "Error parsing enforcingAdmins, EnforcingAdmin is null."); + return; + } if (!mEnforcingAdmins.contains(admin.getUserId())) { mEnforcingAdmins.put(admin.getUserId(), new HashSet<>()); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 9d84f8366a23..4f29abff69ee 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -5544,7 +5544,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (isPermissionCheckFlagEnabled()) { CallerIdentity caller = getCallerIdentity(who, callerPackageName); ap = enforcePermissionAndGetEnforcingAdmin( - who, MANAGE_DEVICE_POLICY_WIPE_DATA, + who, + /*permission=*/ MANAGE_DEVICE_POLICY_WIPE_DATA, + /* adminPolicy=*/ DeviceAdminInfo.USES_POLICY_WIPE_DATA, caller.getPackageName(), affectedUserId).getActiveAdmin(); } else { // This API can only be called by an active device admin, @@ -5826,9 +5828,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ActiveAdmin ap; if (isPermissionCheckFlagEnabled()) { CallerIdentity caller = getCallerIdentity(who, callerPackageName); - // TODO: Allow use of USES_POLICY_FORCE_LOCK ap = enforcePermissionAndGetEnforcingAdmin( - who, MANAGE_DEVICE_POLICY_LOCK, caller.getPackageName(), + who, + /*permission=*/ MANAGE_DEVICE_POLICY_LOCK, + /*AdminPolicy=*/DeviceAdminInfo.USES_POLICY_FORCE_LOCK, + caller.getPackageName(), affectedUserId).getActiveAdmin(); } else { ap = getActiveAdminForCallerLocked( @@ -10315,6 +10319,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { policy.mSecondaryLockscreenEnabled = false; policy.mUserProvisioningState = DevicePolicyManager.STATE_USER_UNMANAGED; policy.mAffiliationIds.clear(); + resetAffiliationCacheLocked(); policy.mLockTaskPackages.clear(); if (!isPolicyEngineForFinanceFlagEnabled()) { updateLockTaskPackagesLocked(mContext, policy.mLockTaskPackages, userId); @@ -11800,9 +11805,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (isPermissionCheckFlagEnabled()) { CallerIdentity caller = getCallerIdentity(admin, callerPackageName); int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle; - // TODO: Support USES_POLICY_DISABLE_KEYGUARD_FEATURES ap = enforcePermissionAndGetEnforcingAdmin( - admin, MANAGE_DEVICE_POLICY_KEYGUARD, + admin, + /*permission=*/MANAGE_DEVICE_POLICY_KEYGUARD, + /*adminPolicy=*/DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES, caller.getPackageName(), affectedUserId).getActiveAdmin(); } else { ap = getActiveAdminForCallerLocked(admin, @@ -13348,23 +13354,23 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller = getCallerIdentity(who); } int userId = caller.getUserId(); + int affectedUserId = parent ? getProfileParentId(userId) : userId; checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_USER_RESTRICTION); if (isPolicyEngineForFinanceFlagEnabled()) { if (!isDeviceOwner(caller) && !isProfileOwner(caller)) { + EnforcingAdmin admin = enforcePermissionForUserRestriction( + who, + key, + caller.getPackageName(), + affectedUserId); if (!mInjector.isChangeEnabled(ENABLE_COEXISTENCE_CHANGE, callerPackage, userId)) { throw new IllegalStateException("Calling package is not targeting Android U."); } if (!UserRestrictionsUtils.isValidRestriction(key)) { throw new IllegalArgumentException("Invalid restriction key: " + key); } - int affectedUserId = parent ? getProfileParentId(userId) : userId; - EnforcingAdmin admin = enforcePermissionForUserRestriction( - who, - key, - caller.getPackageName(), - affectedUserId); PolicyDefinition<Boolean> policyDefinition = PolicyDefinition.getPolicyDefinitionForUserRestriction(key); if (enabledFromThisOwner) { @@ -15127,7 +15133,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return; } - if ((flags == 0) || ((flags & ~(allowedFlags)) != 0)) { + if ((flags & ~(allowedFlags)) != 0) { throw new SecurityException( "Permitted lock task features when managing a financed device: " + "LOCK_TASK_FEATURE_SYSTEM_INFO, LOCK_TASK_FEATURE_KEYGUARD, " @@ -18021,10 +18027,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { synchronized (getLockObject()) { getUserData(callingUserId).mAffiliationIds = affiliationIds; saveSettingsLocked(callingUserId); - if (callingUserId != UserHandle.USER_SYSTEM && isDeviceOwner(admin, callingUserId)) { + mStateCache.setHasAffiliationWithDevice(callingUserId, + isUserAffiliatedWithDeviceLocked(callingUserId)); + if (callingUserId == UserHandle.USER_SYSTEM) { + resetAffiliationCacheLocked(); + } else if (callingUserId != UserHandle.USER_SYSTEM && isDeviceOwner(admin, + callingUserId)) { // Affiliation ids specified by the device owner are additionally stored in // UserHandle.USER_SYSTEM's DevicePolicyData. getUserData(UserHandle.USER_SYSTEM).mAffiliationIds = affiliationIds; + mStateCache.setHasAffiliationWithDevice(UserHandle.USER_SYSTEM, true); saveSettingsLocked(UserHandle.USER_SYSTEM); } @@ -18038,6 +18050,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private void resetAffiliationCacheLocked() { + mInjector.binderWithCleanCallingIdentity(() -> { + for (UserInfo user : mUserManager.getUsers()) { + mStateCache.setHasAffiliationWithDevice(user.id, + isUserAffiliatedWithDeviceLocked(user.id)); + } + }); + } + @Override public List<String> getAffiliationIds(ComponentName admin) { if (!mHasFeature) { @@ -23002,6 +23023,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_FACTORY_RESET, MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES, MANAGE_DEVICE_POLICY_KEYGUARD, + MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, MANAGE_DEVICE_POLICY_LOCK_TASK, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS, @@ -23009,7 +23031,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE, MANAGE_DEVICE_POLICY_TIME, MANAGE_DEVICE_POLICY_USERS, - MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS + MANAGE_DEVICE_POLICY_WIPE_DATA ); /** @@ -23529,14 +23551,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { * * @param callerPackageName The package name of the calling application. * @param adminPolicy The admin policy that should grant holders permission. - * @param permission The name of the permission being checked. + * @param permissions The names of the permissions being checked. * @param targetUserId The userId of the user which the caller needs permission to act on. * @throws SecurityException if the caller has not been granted the given permission, * the associated cross-user permission if the caller's user is different to the target user. */ private void enforcePermissions(String[] permissions, int adminPolicy, String callerPackageName, int targetUserId) throws SecurityException { - if (hasAdminPolicy(adminPolicy, callerPackageName)) { + if (hasAdminPolicy(adminPolicy, callerPackageName) + && mInjector.userHandleGetCallingUserId() == targetUserId) { return; } enforcePermissions(permissions, callerPackageName, targetUserId); @@ -23565,8 +23588,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private boolean hasAdminPolicy(int adminPolicy, String callerPackageName) { CallerIdentity caller = getCallerIdentity(callerPackageName); - ActiveAdmin deviceAdmin = getActiveAdminForCaller(null, caller); - return deviceAdmin != null && deviceAdmin.info.usesPolicy(adminPolicy); + ActiveAdmin deviceAdmin = getActiveAdminWithPolicyForUidLocked( + null, adminPolicy, caller.getUid()); + return deviceAdmin != null; } /** diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java index 011a282ead7a..47607d7426e8 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java @@ -42,6 +42,8 @@ public class DeviceStateCacheImpl extends DeviceStateCache { private AtomicInteger mDeviceOwnerType = new AtomicInteger(NO_DEVICE_OWNER); private Map<Integer, Boolean> mHasProfileOwner = new ConcurrentHashMap<>(); + private Map<Integer, Boolean> mAffiliationWithDevice = new ConcurrentHashMap<>(); + @GuardedBy("mLock") private boolean mIsDeviceProvisioned = false; @@ -70,6 +72,19 @@ public class DeviceStateCacheImpl extends DeviceStateCache { } } + void setHasAffiliationWithDevice(int userId, Boolean hasAffiliateProfileOwner) { + if (hasAffiliateProfileOwner) { + mAffiliationWithDevice.put(userId, true); + } else { + mAffiliationWithDevice.remove(userId); + } + } + + @Override + public boolean hasAffiliationWithDevice(int userId) { + return mAffiliationWithDevice.getOrDefault(userId, false); + } + @Override public boolean isUserOrganizationManaged(@UserIdInt int userHandle) { if (mHasProfileOwner.getOrDefault(userHandle, false) diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java index 3ed2d34be6df..5243d14af1ab 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java @@ -30,6 +30,7 @@ import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.role.RoleManagerLocal; import com.android.server.LocalManagerRegistry; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParserException; @@ -51,6 +52,9 @@ import java.util.Set; * */ final class EnforcingAdmin { + + static final String TAG = "EnforcingAdmin"; + static final String ROLE_AUTHORITY_PREFIX = "role:"; static final String DPC_AUTHORITY = "enterprise"; static final String DEVICE_ADMIN_AUTHORITY = "device_admin"; @@ -286,6 +290,7 @@ final class EnforcingAdmin { } } + @Nullable static EnforcingAdmin readFromXml(TypedXmlPullParser parser) throws XmlPullParserException { String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME); @@ -294,13 +299,25 @@ final class EnforcingAdmin { int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID); if (isRoleAuthority) { + if (packageName == null) { + Slogf.wtf(TAG, "Error parsing EnforcingAdmin with RoleAuthority, packageName is " + + "null."); + return null; + } // TODO(b/281697976): load active admin return new EnforcingAdmin(packageName, userId, null); } else { + if (packageName == null || authoritiesStr == null) { + Slogf.wtf(TAG, "Error parsing EnforcingAdmin, packageName is " + + (packageName == null ? "null" : packageName) + ", and authorities is " + + (authoritiesStr == null ? "null" : authoritiesStr) + "."); + return null; + } String className = parser.getAttributeValue(/* namespace= */ null, ATTR_CLASS_NAME); ComponentName componentName = className == null ? null : new ComponentName(packageName, className); Set<String> authorities = Set.of(authoritiesStr.split(ATTR_AUTHORITIES_SEPARATOR)); + // TODO(b/281697976): load active admin return new EnforcingAdmin(packageName, componentName, authorities, userId, null); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index 194647fda92c..0c1c406dd7e2 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -131,7 +131,8 @@ class Owners { } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -163,11 +164,16 @@ class Owners { } @GuardedBy("mData") - private void pushToActivityTaskManagerLocked() { + private void pushDeviceOwnerUidToActivityTaskManagerLocked() { mActivityTaskManagerInternal.setDeviceOwnerUid(getDeviceOwnerUidLocked()); } @GuardedBy("mData") + private void pushProfileOwnerUidsToActivityTaskManagerLocked() { + mActivityTaskManagerInternal.setProfileOwnerUids(getProfileOwnerUidsLocked()); + } + + @GuardedBy("mData") private void pushToActivityManagerLocked() { mActivityManagerInternal.setDeviceOwnerUid(getDeviceOwnerUidLocked()); @@ -196,6 +202,11 @@ class Owners { } } + @GuardedBy("mData") + Set<Integer> getProfileOwnerUidsLocked() { + return mData.mProfileOwners.keySet(); + } + String getDeviceOwnerPackageName() { synchronized (mData) { return mData.mDeviceOwner != null ? mData.mDeviceOwner.packageName : null; @@ -263,7 +274,7 @@ class Owners { } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); } } @@ -282,7 +293,7 @@ class Owners { mUserManagerInternal.setDeviceManaged(false); } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); } } @@ -302,6 +313,7 @@ class Owners { mUserManagerInternal.setUserManaged(userId, true); } notifyChangeLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -317,6 +329,7 @@ class Owners { mUserManagerInternal.setUserManaged(userId, false); } notifyChangeLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -328,6 +341,7 @@ class Owners { ownerInfo.isOrganizationOwnedDevice); mData.mProfileOwners.put(userId, newOwnerInfo); notifyChangeLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -345,7 +359,7 @@ class Owners { mData.mDeviceOwner.packageName, previousDeviceOwnerType); } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java index 63b250d4acfc..37d4f95cac29 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java @@ -457,6 +457,7 @@ class OwnersData { case TAG_POLICY_ENGINE_MIGRATION: mMigratedToPolicyEngine = parser.getAttributeBoolean( null, ATTR_MIGRATED_TO_POLICY_ENGINE, false); + break; default: Slog.e(TAG, "Unexpected tag: " + tag); return false; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 7e5bb0bb40ba..7e48407fc911 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -42,6 +42,7 @@ import android.os.UserManager; import com.android.internal.util.function.QuadFunction; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParserException; @@ -53,6 +54,9 @@ import java.util.Map; import java.util.Set; final class PolicyDefinition<V> { + + static final String TAG = "PolicyDefinition"; + private static final int POLICY_FLAG_NONE = 0; // Only use this flag if a policy can not be applied locally. @@ -596,22 +600,40 @@ final class PolicyDefinition<V> { mPolicyKey.saveToXml(serializer); } + @Nullable static <V> PolicyDefinition<V> readFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { // TODO: can we avoid casting? PolicyKey policyKey = readPolicyKeyFromXml(parser); + if (policyKey == null) { + Slogf.wtf(TAG, "Error parsing PolicyDefinition, PolicyKey is null."); + return null; + } PolicyDefinition<V> genericPolicyDefinition = (PolicyDefinition<V>) POLICY_DEFINITIONS.get(policyKey.getIdentifier()); + if (genericPolicyDefinition == null) { + Slogf.wtf(TAG, "Unknown generic policy key: " + policyKey); + return null; + } return genericPolicyDefinition.createPolicyDefinition(policyKey); } + @Nullable static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { // TODO: can we avoid casting? PolicyKey policyKey = PolicyKey.readGenericPolicyKeyFromXml(parser); + if (policyKey == null) { + Slogf.wtf(TAG, "Error parsing PolicyKey, GenericPolicyKey is null"); + return null; + } PolicyDefinition<PolicyValue<V>> genericPolicyDefinition = (PolicyDefinition<PolicyValue<V>>) POLICY_DEFINITIONS.get( policyKey.getIdentifier()); + if (genericPolicyDefinition == null) { + Slogf.wtf(TAG, "Error parsing PolicyKey, Unknown generic policy key: " + policyKey); + return null; + } return genericPolicyDefinition.mPolicyKey.readFromXml(parser); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java index dd4c6afdcfb6..599c4a7441c5 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java @@ -19,11 +19,11 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.PolicyValue; -import android.util.Log; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParserException; @@ -224,6 +224,7 @@ final class PolicyState<V> { } } + @Nullable static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser) throws IOException, XmlPullParserException { @@ -245,33 +246,55 @@ final class PolicyState<V> { switch (adminPolicyTag) { case TAG_ENFORCING_ADMIN_ENTRY: admin = EnforcingAdmin.readFromXml(parser); + if (admin == null) { + Slogf.wtf(TAG, "Error Parsing TAG_ENFORCING_ADMIN_ENTRY, " + + "EnforcingAdmin is null"); + } break; case TAG_POLICY_VALUE_ENTRY: value = policyDefinition.readPolicyValueFromXml(parser); + if (value == null) { + Slogf.wtf(TAG, "Error Parsing TAG_POLICY_VALUE_ENTRY, " + + "PolicyValue is null"); + } break; } } if (admin != null) { policiesSetByAdmins.put(admin, value); } else { - Log.e(TAG, "Error Parsing TAG_ADMIN_POLICY_ENTRY"); + Slogf.wtf(TAG, "Error Parsing TAG_ADMIN_POLICY_ENTRY, EnforcingAdmin " + + "is null"); } break; case TAG_POLICY_DEFINITION_ENTRY: policyDefinition = PolicyDefinition.readFromXml(parser); + if (policyDefinition == null) { + Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, " + + "PolicyDefinition is null"); + } break; case TAG_RESOLVED_VALUE_ENTRY: + if (policyDefinition == null) { + Slogf.wtf(TAG, "Error Parsing TAG_RESOLVED_VALUE_ENTRY, " + + "policyDefinition is null"); + break; + } currentResolvedPolicy = policyDefinition.readPolicyValueFromXml(parser); + if (currentResolvedPolicy == null) { + Slogf.wtf(TAG, "Error Parsing TAG_RESOLVED_VALUE_ENTRY, " + + "currentResolvedPolicy is null"); + } break; default: - Log.e(TAG, "Unknown tag: " + tag); + Slogf.wtf(TAG, "Unknown tag: " + tag); } } if (policyDefinition != null) { return new PolicyState<V>(policyDefinition, policiesSetByAdmins, currentResolvedPolicy); } else { - Log.e("PolicyState", "Error parsing policyState"); + Slogf.wtf(TAG, "Error parsing policyState, policyDefinition is null"); return null; } } diff --git a/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-3.xml b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-3.xml new file mode 100644 index 000000000000..1363bf783ac2 --- /dev/null +++ b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-3.xml @@ -0,0 +1,801 @@ +<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<app-ops v="3"> + <uid n="1001"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="15" m="0" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="1002"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="15" m="0" /> + <op n="87" m="1" /> + </uid> + <uid n="10077"> + <op n="87" m="1" /> + </uid> + <uid n="10079"> + <op n="116" m="1" /> + </uid> + <uid n="10080"> + <op n="87" m="1" /> + </uid> + <uid n="10081"> + <op n="87" m="1" /> + </uid> + <uid n="10086"> + <op n="87" m="1" /> + </uid> + <uid n="10087"> + <op n="59" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10090"> + <op n="0" m="4" /> + <op n="1" m="4" /> + </uid> + <uid n="10096"> + <op n="0" m="1" /> + <op n="1" m="1" /> + </uid> + <uid n="10112"> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10113"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="51" m="1" /> + <op n="62" m="1" /> + </uid> + <uid n="10114"> + <op n="4" m="1" /> + </uid> + <uid n="10115"> + <op n="0" m="1" /> + </uid> + <uid n="10116"> + <op n="0" m="1" /> + </uid> + <uid n="10117"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="13" m="1" /> + <op n="14" m="1" /> + <op n="16" m="1" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + </uid> + <uid n="10118"> + <op n="51" m="1" /> + </uid> + <uid n="10119"> + <op n="11" m="1" /> + <op n="77" m="1" /> + <op n="111" m="1" /> + </uid> + <uid n="10120"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="6" m="1" /> + <op n="7" m="1" /> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="54" m="1" /> + <op n="59" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10121"> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10122"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10123"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10124"> + <op n="11" m="1" /> + <op n="26" m="1" /> + </uid> + <uid n="10125"> + <op n="11" m="1" /> + </uid> + <uid n="10127"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="65" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="111" m="1" /> + </uid> + <uid n="10129"> + <op n="0" m="1" /> + <op n="1" m="1" /> + </uid> + <uid n="10130"> + <op n="51" m="1" /> + </uid> + <uid n="10131"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10132"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="69" m="1" /> + <op n="79" m="1" /> + </uid> + <uid n="10133"> + <op n="11" m="1" /> + </uid> + <uid n="10136"> + <op n="0" m="1" /> + <op n="4" m="1" /> + <op n="77" m="1" /> + <op n="87" m="1" /> + <op n="111" m="1" /> + <op n="114" m="1" /> + </uid> + <uid n="10137"> + <op n="62" m="1" /> + </uid> + <uid n="10138"> + <op n="26" m="4" /> + </uid> + <uid n="10140"> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10141"> + <op n="11" m="1" /> + <op n="27" m="1" /> + <op n="111" m="1" /> + </uid> + <uid n="10142"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + </uid> + <uid n="10144"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="27" m="4" /> + <op n="111" m="1" /> + </uid> + <uid n="10145"> + <op n="11" m="1" /> + </uid> + <uid n="10149"> + <op n="11" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10150"> + <op n="51" m="1" /> + </uid> + <uid n="10151"> + <op n="11" m="1" /> + </uid> + <uid n="10152"> + <op n="11" m="1" /> + </uid> + <uid n="10154"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="27" m="4" /> + </uid> + <uid n="10155"> + <op n="0" m="1" /> + <op n="1" m="1" /> + </uid> + <uid n="10157"> + <op n="13" m="1" /> + </uid> + <uid n="10158"> + <op n="11" m="1" /> + </uid> + <uid n="10160"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10161"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="77" m="1" /> + <op n="87" m="1" /> + <op n="111" m="1" /> + <op n="114" m="1" /> + </uid> + <uid n="10162"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="15" m="0" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + <op n="89" m="0" /> + </uid> + <uid n="10163"> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="56" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10164"> + <op n="26" m="1" /> + <op n="27" m="4" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="69" m="1" /> + <op n="79" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10169"> + <op n="11" m="1" /> + </uid> + <uid n="10170"> + <op n="87" m="1" /> + </uid> + <uid n="10171"> + <op n="26" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10172"> + <op n="11" m="1" /> + </uid> + <uid n="10173"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="23" m="0" /> + <op n="26" m="1" /> + <op n="51" m="1" /> + <op n="62" m="1" /> + <op n="65" m="1" /> + </uid> + <uid n="10175"> + <op n="0" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + </uid> + <uid n="10178"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="27" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10179"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10180"> + <op n="11" m="1" /> + </uid> + <uid n="10181"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="1110181"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="107" m="2" /> + </uid> + <uid n="10182"> + <op n="27" m="4" /> + </uid> + <uid n="10183"> + <op n="11" m="1" /> + <op n="26" m="4" /> + </uid> + <uid n="10184"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="26" m="1" /> + <op n="27" m="4" /> + </uid> + <uid n="10185"> + <op n="8" m="1" /> + <op n="59" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10187"> + <op n="11" m="1" /> + </uid> + <uid n="10189"> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="51" m="1" /> + </uid> + <uid n="10190"> + <op n="0" m="1" /> + <op n="13" m="1" /> + </uid> + <uid n="10191"> + <op n="11" m="1" /> + </uid> + <uid n="10192"> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10193"> + <op n="11" m="1" /> + </uid> + <uid n="10197"> + <op n="11" m="1" /> + </uid> + <uid n="10198"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="77" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="90" m="1" /> + <op n="107" m="0" /> + <op n="111" m="1" /> + <op n="133" m="0" /> + </uid> + <uid n="10199"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10200"> + <op n="11" m="1" /> + <op n="65" m="1" /> + <op n="107" m="1" /> + <op n="133" m="1" /> + </uid> + <uid n="1110200"> + <op n="11" m="1" /> + <op n="65" m="1" /> + <op n="107" m="2" /> + <op n="133" m="2"/> + </uid> + <uid n="10201"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="62" m="1" /> + <op n="84" m="0" /> + <op n="86" m="0" /> + <op n="87" m="1" /> + </uid> + <uid n="10206"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="26" m="4" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10209"> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10210"> + <op n="51" m="1" /> + </uid> + <uid n="10212"> + <op n="11" m="1" /> + <op n="62" m="1" /> + </uid> + <uid n="10214"> + <op n="26" m="4" /> + </uid> + <uid n="10216"> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10225"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10229"> + <op n="11" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10231"> + <op n="51" m="1" /> + </uid> + <uid n="10232"> + <op n="51" m="1" /> + </uid> + <uid n="10234"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="20" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10235"> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10237"> + <op n="0" m="4" /> + <op n="1" m="4" /> + </uid> + <uid n="10238"> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10240"> + <op n="112" m="1" /> + </uid> + <uid n="10241"> + <op n="59" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10245"> + <op n="13" m="1" /> + <op n="51" m="1" /> + </uid> + <uid n="10247"> + <op n="0" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="0" /> + <op n="90" m="1" /> + </uid> + <uid n="10254"> + <op n="11" m="1" /> + </uid> + <uid n="10255"> + <op n="11" m="1" /> + </uid> + <uid n="10256"> + <op n="87" m="1" /> + </uid> + <uid n="10258"> + <op n="11" m="1" /> + </uid> + <uid n="10260"> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10262"> + <op n="15" m="0" /> + </uid> + <uid n="10266"> + <op n="0" m="4" /> + </uid> + <uid n="10267"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="77" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="107" m="2" /> + <op n="111" m="1" /> + <op n="133" m="2" /> + </uid> + <uid n="10268"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="62" m="1" /> + </uid> + <uid n="10269"> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <pkg n="com.google.android.iwlan"> + <uid n="0"> + <op n="1" /> + <op n="75" m="0" /> + </uid> + </pkg> + <pkg n="com.android.phone"> + <uid n="0"> + <op n="1" /> + <op n="75" m="0" /> + </uid> + </pkg> + <pkg n="android"> + <uid n="1000"> + <op n="0"> + <st n="214748364801" t="1670287941040" /> + </op> + <op n="4"> + <st n="214748364801" t="1670289665522" /> + </op> + <op n="6"> + <st n="214748364801" t="1670287946650" /> + </op> + <op n="8"> + <st n="214748364801" t="1670289624396" /> + </op> + <op n="14"> + <st n="214748364801" t="1670287951031" /> + </op> + <op n="40"> + <st n="214748364801" t="1670291786337" d="156" /> + </op> + <op n="41"> + <st id="SensorNotificationService" n="214748364801" t="1670287585567" d="4251183" /> + <st id="CountryDetector" n="214748364801" t="1670287583306" d="6700" /> + </op> + <op n="43"> + <st n="214748364801" r="1670291755062" /> + </op> + <op n="61"> + <st n="214748364801" r="1670291754997" /> + </op> + <op n="105"> + <st n="214748364801" r="1670291473903" /> + <st id="GnssService" n="214748364801" r="1670288044920" /> + </op> + <op n="111"> + <st n="214748364801" t="1670291441554" /> + </op> + </uid> + </pkg> + <pkg n="com.android.server.telecom"> + <uid n="1000"> + <op n="6"> + <st n="214748364801" t="1670287609092" /> + </op> + <op n="111"> + <st n="214748364801" t="1670287583728" /> + </op> + </uid> + </pkg> + <pkg n="com.android.settings"> + <uid n="1000"> + <op n="43"> + <st n="214748364801" r="1670291447349" /> + </op> + <op n="105"> + <st n="214748364801" r="1670291399231" /> + </op> + <op n="111"> + <st n="214748364801" t="1670291756910" /> + </op> + </uid> + </pkg> + <pkg n="com.android.phone"> + <uid n="1001"> + <op n="15"> + <st n="214748364801" t="1670287951022" /> + </op> + <op n="40"> + <st n="214748364801" t="1670291786177" /> + </op> + <op n="105"> + <st n="214748364801" r="1670291756403" /> + </op> + </uid> + </pkg> + <pkg n="com.android.bluetooth"> + <uid n="1002"> + <op n="4"> + <st n="214748364801" t="1670289671076" /> + </op> + <op n="40"> + <st n="214748364801" t="1670287585676" d="8" /> + </op> + <op n="43"> + <st n="214748364801" r="1670287585818" /> + </op> + <op n="77"> + <st n="214748364801" t="1670288037629" /> + </op> + <op n="111"> + <st n="214748364801" t="1670287592081" /> + </op> + </uid> + </pkg> + <pkg n="com.android.vending"> + <uid n="10136"> + <op n="40"> + <st n="429496729601" t="1670289621210" d="114" /> + <st n="858993459201" t="1670289879730" d="349" /> + <st n="1288490188801" t="1670287942622" d="937" /> + </op> + <op n="43"> + <st n="429496729601" r="1670289755305" /> + <st n="858993459201" r="1670288019246" /> + <st n="1073741824001" r="1670289571783" /> + <st n="1288490188801" r="1670289373336" /> + </op> + <op n="76"> + <st n="429496729601" t="1670289748735" d="15991" /> + <st n="858993459201" t="1670291395180" d="79201" /> + <st n="1073741824001" t="1670291395168" d="12" /> + <st n="1288490188801" t="1670291526029" d="3" /> + <st n="1503238553601" t="1670291526032" d="310718" /> + </op> + <op n="105"> + <st n="429496729601" r="1670289538910" /> + <st n="858993459201" r="1670288054519" /> + <st n="1073741824001" r="1670287599379" /> + <st n="1288490188801" r="1670289526854" /> + <st n="1503238553601" r="1670289528242" /> + </op> + </uid> + </pkg> + <pkg n="com.android.nfc"> + <uid n="1027"> + <op n="40"> + <st n="214748364801" t="1670291786330" d="22" /> + </op> + </uid> + </pkg> +</app-ops>
\ No newline at end of file diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index f82d246d68bd..a614c4dd1d61 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -37,6 +37,7 @@ import static com.android.server.am.ProcessList.NETWORK_STATE_BLOCK; import static com.android.server.am.ProcessList.NETWORK_STATE_NO_CHANGE; import static com.android.server.am.ProcessList.NETWORK_STATE_UNBLOCK; +import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; @@ -60,6 +61,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.Manifest; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.BroadcastOptions; @@ -82,13 +84,16 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; import android.util.IntArray; import android.util.Log; import android.util.Pair; import androidx.test.filters.MediumTest; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService.StickyBroadcast; import com.android.server.am.ProcessList.IsolatedUidRange; @@ -106,6 +111,7 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; import java.io.File; import java.util.ArrayList; @@ -137,7 +143,9 @@ public class ActivityManagerServiceTest { private static final String TEST_EXTRA_KEY1 = "com.android.server.am.TEST_EXTRA_KEY1"; private static final String TEST_EXTRA_VALUE1 = "com.android.server.am.TEST_EXTRA_VALUE1"; - + private static final String PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = + "apply_sdk_sandbox_next_restrictions"; + private static final String APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = ":isSdkSandboxNext"; private static final int TEST_UID = 11111; private static final int USER_ID = 666; @@ -154,6 +162,7 @@ public class ActivityManagerServiceTest { }; private static PackageManagerInternal sPackageManagerInternal; + private static ProcessList.ProcessListSettingsListener sProcessListSettingsListener; @BeforeClass public static void setUpOnce() { @@ -181,7 +190,6 @@ public class ActivityManagerServiceTest { private ActivityManagerService mAms; private HandlerThread mHandlerThread; private TestHandler mHandler; - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -203,6 +211,15 @@ public class ActivityManagerServiceTest { mAms.mActivityTaskManager.initialize(null, null, mHandler.getLooper()); mHandler.setRunnablesToIgnore( List.of(mAms.mUidObserverController.getDispatchRunnableForTest())); + + // Required for updating DeviceConfig. + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity( + Manifest.permission.READ_DEVICE_CONFIG, + Manifest.permission.WRITE_DEVICE_CONFIG); + sProcessListSettingsListener = mAms.mProcessList.getProcessListSettingsListener(); + assertThat(sProcessListSettingsListener).isNotNull(); } private void mockNoteOperation() { @@ -216,6 +233,12 @@ public class ActivityManagerServiceTest { @After public void tearDown() { mHandlerThread.quit(); + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + if (sProcessListSettingsListener != null) { + sProcessListSettingsListener.unregisterObserver(); + } } @SuppressWarnings("GuardedBy") @@ -268,6 +291,77 @@ public class ActivityManagerServiceTest { false); // expectNotify } + @SuppressWarnings("GuardedBy") + @SmallTest + @Test + public void defaultSdkSandboxNextRestrictions() throws Exception { + sProcessListSettingsListener.onPropertiesChanged( + new DeviceConfig.Properties( + DeviceConfig.NAMESPACE_ADSERVICES, + Map.of(PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, ""))); + assertThat( + sProcessListSettingsListener.applySdkSandboxRestrictionsNext()) + .isFalse(); + } + + @SuppressWarnings("GuardedBy") + @SmallTest + @Test + public void doNotApplySdkSandboxNextRestrictions() throws Exception { + MockitoSession mockitoSession = + ExtendedMockito.mockitoSession().spyStatic(Process.class).startMocking(); + try { + sProcessListSettingsListener.onPropertiesChanged( + new DeviceConfig.Properties( + DeviceConfig.NAMESPACE_ADSERVICES, + Map.of(PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, "false"))); + assertThat( + sProcessListSettingsListener.applySdkSandboxRestrictionsNext()) + .isFalse(); + ExtendedMockito.doReturn(true).when(() -> Process.isSdkSandboxUid(anyInt())); + ApplicationInfo info = new ApplicationInfo(); + info.packageName = "com.android.sdksandbox"; + info.seInfo = "default:targetSdkVersion=34:complete"; + final ProcessRecord appRec = new ProcessRecord( + mAms, info, TAG, Process.FIRST_SDK_SANDBOX_UID, + /* sdkSandboxClientPackageName= */ "com.example.client", + /* definingUid= */ 0, /* definingProcessName= */ ""); + assertThat(mAms.mProcessList.updateSeInfo(appRec)).doesNotContain( + APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + } finally { + mockitoSession.finishMocking(); + } + } + @SuppressWarnings("GuardedBy") + @SmallTest + @Test + public void applySdkSandboxNextRestrictions() throws Exception { + MockitoSession mockitoSession = + ExtendedMockito.mockitoSession().spyStatic(Process.class).startMocking(); + try { + sProcessListSettingsListener.onPropertiesChanged( + new DeviceConfig.Properties( + DeviceConfig.NAMESPACE_ADSERVICES, + Map.of(PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, "true"))); + assertThat( + sProcessListSettingsListener.applySdkSandboxRestrictionsNext()) + .isTrue(); + ExtendedMockito.doReturn(true).when(() -> Process.isSdkSandboxUid(anyInt())); + ApplicationInfo info = new ApplicationInfo(); + info.packageName = "com.android.sdksandbox"; + info.seInfo = "default:targetSdkVersion=34:complete"; + final ProcessRecord appRec = new ProcessRecord( + mAms, info, TAG, Process.FIRST_SDK_SANDBOX_UID, + /* sdkSandboxClientPackageName= */ "com.example.client", + /* definingUid= */ 0, /* definingProcessName= */ ""); + assertThat(mAms.mProcessList.updateSeInfo(appRec)).contains( + APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + } finally { + mockitoSession.finishMocking(); + } + } + + private UidRecord addUidRecord(int uid) { final UidRecord uidRec = new UidRecord(uid, mAms); uidRec.procStateSeqWaitingForNetwork = 1; @@ -648,24 +742,24 @@ public class ActivityManagerServiceTest { broadcastIntent(intent1, null, true); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER), - StickyBroadcast.create(intent1, false)); + StickyBroadcast.create(intent1, false, Process.myUid())); assertNull(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER)); assertNull(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER)); broadcastIntent(intent2, options.toBundle(), true); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER), - StickyBroadcast.create(intent1, false)); + StickyBroadcast.create(intent1, false, Process.myUid())); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER), - StickyBroadcast.create(intent2, true)); + StickyBroadcast.create(intent2, true, Process.myUid())); assertNull(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER)); broadcastIntent(intent3, null, true); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER), - StickyBroadcast.create(intent1, false)); + StickyBroadcast.create(intent1, false, Process.myUid())); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER), - StickyBroadcast.create(intent2, true)); + StickyBroadcast.create(intent2, true, Process.myUid())); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER), - StickyBroadcast.create(intent3, false)); + StickyBroadcast.create(intent3, false, Process.myUid())); } @SuppressWarnings("GuardedBy") @@ -698,6 +792,9 @@ public class ActivityManagerServiceTest { if (a.deferUntilActive != b.deferUntilActive) { return false; } + if (a.originalCallingUid != b.originalCallingUid) { + return false; + } return true; } diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java index 5474c2092785..92d1118d0f1e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java @@ -17,6 +17,7 @@ package com.android.server.appop; import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; +import static android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT; import static android.app.AppOpsManager._NUM_OP; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; @@ -31,6 +32,7 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; @@ -44,6 +46,7 @@ import android.content.pm.UserPackage; import android.content.res.AssetManager; import android.os.Handler; import android.os.UserHandle; +import android.permission.PermissionManager; import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; @@ -87,6 +90,10 @@ public class AppOpsUpgradeTest { "AppOpsUpgradeTest/appops-unversioned.xml"; private static final String APP_OPS_VERSION_1_ASSET_PATH = "AppOpsUpgradeTest/appops-version-1.xml"; + + private static final String APP_OPS_VERSION_3_ASSET_PATH = + "AppOpsUpgradeTest/appops-version-3.xml"; + private static final String APP_OPS_FILENAME = "appops-test.xml"; private static final Context sContext = InstrumentationRegistry.getTargetContext(); @@ -105,6 +112,8 @@ public class AppOpsUpgradeTest { private PermissionManagerServiceInternal mPermissionManagerInternal; @Mock private Handler mHandler; + @Mock + private PermissionManager mPermissionManager; private Object mLock = new Object(); private SparseArray<int[]> mSwitchedOps; @@ -211,7 +220,7 @@ public class AppOpsUpgradeTest { } } - private static int getModeInFile(int uid) { + private static int getModeInFile(int uid, int op) { switch (uid) { case 10198: return 0; @@ -222,7 +231,7 @@ public class AppOpsUpgradeTest { case 1110181: return 2; default: - return AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM); + return AppOpsManager.opToDefaultMode(op); } } @@ -258,7 +267,7 @@ public class AppOpsUpgradeTest { for (int userId : userIds) { for (int appId : appIds) { final int uid = UserHandle.getUid(userId, appId); - final int previousMode = getModeInFile(uid); + final int previousMode = getModeInFile(uid, OP_SCHEDULE_EXACT_ALARM); final int expectedMode; if (previousMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) { @@ -281,6 +290,55 @@ public class AppOpsUpgradeTest { } @Test + public void resetUseFullScreenIntent() { + extractAppOpsFile(APP_OPS_VERSION_3_ASSET_PATH); + + String[] packageNames = {"p1", "package2", "pkg3", "package.4", "pkg-5", "pkg.6"}; + int[] appIds = {10267, 10181, 10198, 10199, 10200, 4213}; + int[] userIds = {0, 10, 11}; + int flag = 0; + + doReturn(userIds).when(mUserManagerInternal).getUserIds(); + + doReturn(packageNames).when(mPermissionManagerInternal).getAppOpPermissionPackages( + AppOpsManager.opToPermission(OP_USE_FULL_SCREEN_INTENT)); + + doReturn(mPermissionManager).when(mTestContext).getSystemService(PermissionManager.class); + + doReturn(flag).when(mPackageManager).getPermissionFlags( + anyString(), anyString(), isA(UserHandle.class)); + + doAnswer(invocation -> { + String pkg = invocation.getArgument(0); + int index = ArrayUtils.indexOf(packageNames, pkg); + if (index < 0) { + return index; + } + int userId = invocation.getArgument(2); + return UserHandle.getUid(userId, appIds[index]); + }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt()); + + AppOpsCheckingServiceImpl testService = new AppOpsCheckingServiceImpl(sAppOpsFile, mLock, + mHandler, mTestContext, mSwitchedOps); + testService.readState(); + + synchronized (testService) { + testService.resetUseFullScreenIntentLocked(); + } + + for (int userId : userIds) { + for (int appId : appIds) { + final int uid = UserHandle.getUid(userId, appId); + final int expectedMode = AppOpsManager.opToDefaultMode(OP_USE_FULL_SCREEN_INTENT); + synchronized (testService) { + int mode = testService.getUidMode(uid, OP_USE_FULL_SCREEN_INTENT); + assertEquals(expectedMode, mode); + } + } + } + } + + @Test public void upgradeFromNoFile() { assertFalse(sAppOpsFile.exists()); @@ -290,12 +348,14 @@ public class AppOpsUpgradeTest { doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); // trigger upgrade testService.systemReady(); verify(testService, never()).upgradeRunAnyInBackgroundLocked(); verify(testService, never()).upgradeScheduleExactAlarmLocked(); + verify(testService, never()).resetUseFullScreenIntentLocked(); testService.writeState(); @@ -319,12 +379,14 @@ public class AppOpsUpgradeTest { doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); // trigger upgrade testService.systemReady(); verify(testService).upgradeRunAnyInBackgroundLocked(); verify(testService).upgradeScheduleExactAlarmLocked(); + verify(testService).resetUseFullScreenIntentLocked(); testService.writeState(); assertTrue(parser.parse()); @@ -344,12 +406,40 @@ public class AppOpsUpgradeTest { doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); // trigger upgrade testService.systemReady(); verify(testService, never()).upgradeRunAnyInBackgroundLocked(); verify(testService).upgradeScheduleExactAlarmLocked(); + verify(testService).resetUseFullScreenIntentLocked(); + + testService.writeState(); + assertTrue(parser.parse()); + assertEquals(AppOpsCheckingServiceImpl.CURRENT_VERSION, parser.mVersion); + } + + @Test + public void resetFromVersion3() { + extractAppOpsFile(APP_OPS_VERSION_3_ASSET_PATH); + AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile); + assertTrue(parser.parse()); + assertEquals(3, parser.mVersion); + + AppOpsCheckingServiceImpl testService = spy(new AppOpsCheckingServiceImpl(sAppOpsFile, + mLock, mHandler, mTestContext, mSwitchedOps)); + testService.readState(); + + doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); + doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); + + testService.systemReady(); + + verify(testService, never()).upgradeRunAnyInBackgroundLocked(); + verify(testService, never()).upgradeScheduleExactAlarmLocked(); + verify(testService).resetUseFullScreenIntentLocked(); testService.writeState(); assertTrue(parser.parse()); diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceMockingTest.java new file mode 100644 index 000000000000..a8853071abe8 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceMockingTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.virtual; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Looper; +import android.platform.test.annotations.Presubmit; +import android.testing.TestableContext; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class VirtualDeviceManagerServiceMockingTest { + private static final int UID_1 = 0; + private static final int DEVICE_ID_1 = 42; + private static final int DEVICE_ID_2 = 43; + + @Rule + public final TestableContext mContext = new TestableContext( + InstrumentationRegistry.getContext()); + + private VirtualDeviceManagerService mVdms; + private VirtualDeviceManagerInternal mLocalService; + + @Before + public void setUp() { + mVdms = new VirtualDeviceManagerService(mContext); + mLocalService = mVdms.getLocalServiceInstance(); + } + + @Test + public void onAuthenticationPrompt_noDevices_noCrash() { + // This should not crash + mLocalService.onAuthenticationPrompt(UID_1); + } + + @Test + public void onAuthenticationPrompt_oneDevice_showToastWhereUidIsRunningIsCalled() { + VirtualDeviceImpl device = mock(VirtualDeviceImpl.class); + mVdms.addVirtualDevice(device); + + mLocalService.onAuthenticationPrompt(UID_1); + + verify(device).showToastWhereUidIsRunning(eq(UID_1), + eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(), + any(Looper.class)); + } + + @Test + public void onAuthenticationPrompt_twoDevices_showToastWhereUidIsRunningIsCalledOnBoth() { + VirtualDeviceImpl device1 = mock(VirtualDeviceImpl.class); + VirtualDeviceImpl device2 = mock(VirtualDeviceImpl.class); + when(device1.getDeviceId()).thenReturn(DEVICE_ID_1); + when(device2.getDeviceId()).thenReturn(DEVICE_ID_2); + mVdms.addVirtualDevice(device1); + mVdms.addVirtualDevice(device2); + + mLocalService.onAuthenticationPrompt(UID_1); + + verify(device1).showToastWhereUidIsRunning(eq(UID_1), + eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(), + any(Looper.class)); + verify(device2).showToastWhereUidIsRunning(eq(UID_1), + eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(), + any(Looper.class)); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index c040b1928ce4..05780ebe6c4b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -582,6 +582,49 @@ public class JobStatusTest { } @Test + public void testModifyingInternalFlags() { + final JobInfo jobInfo = + new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setExpedited(true) + .build(); + JobStatus job = createJobStatus(jobInfo); + + assertEquals(0, job.getInternalFlags()); + + // Add single flag + job.addInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION); + assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION, job.getInternalFlags()); + + // Add multiple flags + job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, + job.getInternalFlags()); + + // Add flag that's already set + job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, + job.getInternalFlags()); + + // Remove multiple + job.removeInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + assertEquals(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, job.getInternalFlags()); + + // Remove one that isn't set + job.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + assertEquals(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, job.getInternalFlags()); + + // Remove final flag. + job.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertEquals(0, job.getInternalFlags()); + } + + @Test public void testShouldTreatAsUserInitiated() { JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) .setUserInitiated(false) @@ -619,6 +662,9 @@ public class JobStatusTest { rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, 0, 0, 0, 0, 0); assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob()); + + rescheduledJob.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob()); } @Test @@ -641,6 +687,9 @@ public class JobStatusTest { rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, 0, 0, 0, 0, 0); assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob()); + + rescheduledJob.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob()); } /** diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index 85d159c25be2..f88afe7839c3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -58,6 +58,8 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.server.LocalServices; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import org.junit.Before; import org.junit.Rule; @@ -102,6 +104,8 @@ public class AuthServiceTest { IFaceService mFaceService; @Mock AppOpsManager mAppOpsManager; + @Mock + private VirtualDeviceManagerInternal mVdmInternal; @Captor private ArgumentCaptor<List<FingerprintSensorPropertiesInternal>> mFingerprintPropsCaptor; @Captor @@ -115,6 +119,8 @@ public class AuthServiceTest { "1:4:15", // ID1:Iris:Strong "2:8:15", // ID2:Face:Strong }; + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + LocalServices.addService(VirtualDeviceManagerInternal.class, mVdmInternal); when(mResources.getIntArray(eq(R.array.config_udfps_sensor_props))).thenReturn(new int[0]); when(mResources.getBoolean(eq(R.bool.config_is_powerbutton_fps))).thenReturn(false); @@ -152,7 +158,8 @@ public class AuthServiceTest { verify(mBiometricService, never()).registerAuthenticator( anyInt(), - any(), + anyInt(), + anyInt(), any()); } @@ -272,6 +279,47 @@ public class AuthServiceTest { } @Test + public void testAuthenticate_noVdmInternalService_noCrash() throws Exception { + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + final Binder token = new Binder(); + + // This should not crash + mAuthService.mImpl.authenticate( + token, + 0, /* sessionId */ + 0, /* userId */ + mReceiver, + TEST_OP_PACKAGE_NAME, + new PromptInfo()); + waitForIdle(); + } + + @Test + public void testAuthenticate_callsVirtualDeviceManagerOnAuthenticationPrompt() + throws Exception { + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + final Binder token = new Binder(); + + mAuthService.mImpl.authenticate( + token, + 0, /* sessionId */ + 0, /* userId */ + mReceiver, + TEST_OP_PACKAGE_NAME, + new PromptInfo()); + waitForIdle(); + + ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mVdmInternal).onAuthenticationPrompt(uidCaptor.capture()); + assertEquals((int) (uidCaptor.getValue()), Binder.getCallingUid()); + } + + @Test public void testAuthenticate_throwsWhenUsingTestConfigurations() { final PromptInfo promptInfo = mock(PromptInfo.class); when(promptInfo.containsPrivateApiConfigurations()).thenReturn(false); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 26a3ae110525..154aa7d4e78e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -45,14 +45,13 @@ import android.app.trust.ITrustManager; import android.content.Context; import android.content.res.Resources; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricSensorReceiver; import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorProperties; -import android.hardware.face.FaceSensorProperties; -import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; @@ -490,16 +489,9 @@ public class AuthSessionTest { IBiometricAuthenticator fingerprintAuthenticator = mock(IBiometricAuthenticator.class); when(fingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(fingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - id, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, type, - false /* resetLockoutRequiresHardwareAuthToken */); - mFingerprintSensorProps.add(props); - - mSensors.add(new BiometricSensor(mContext, + mSensors.add(new BiometricSensor(mContext, id, TYPE_FINGERPRINT /* modality */, - props, + Authenticators.BIOMETRIC_STRONG /* strength */, fingerprintAuthenticator) { @Override boolean confirmationAlwaysRequired(int userId) { @@ -512,6 +504,21 @@ public class AuthSessionTest { } }); + final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); + componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */)); + componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */)); + + mFingerprintSensorProps.add(new FingerprintSensorPropertiesInternal(id, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + componentInfo, + type, + false /* resetLockoutRequiresHardwareAuthToken */)); + when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); } @@ -519,13 +526,9 @@ public class AuthSessionTest { IBiometricAuthenticator authenticator) throws RemoteException { when(authenticator.isHardwareDetected(any())).thenReturn(true); when(authenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - mSensors.add(new BiometricSensor(mContext, + mSensors.add(new BiometricSensor(mContext, id, TYPE_FACE /* modality */, - new FaceSensorPropertiesInternal(id, - SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FaceSensorProperties.TYPE_UNKNOWN, - true /* supportsFace Detection */, true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + Authenticators.BIOMETRIC_STRONG /* strength */, authenticator) { @Override boolean confirmationAlwaysRequired(int userId) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 46fa3abe5122..520e1c84c74e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -19,7 +19,6 @@ package com.android.server.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; -import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI; @@ -70,11 +69,7 @@ import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.display.DisplayManagerGlobal; -import android.hardware.face.FaceSensorProperties; -import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; @@ -124,7 +119,6 @@ public class BiometricServiceTest { private static final int SENSOR_ID_FINGERPRINT = 0; private static final int SENSOR_ID_FACE = 1; - private FingerprintSensorPropertiesInternal mFingerprintProps; private BiometricService mBiometricService; @@ -207,11 +201,6 @@ public class BiometricServiceTest { }; when(mInjector.getConfiguration(any())).thenReturn(config); - - mFingerprintProps = new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT, - STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */); } @Test @@ -347,7 +336,8 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(0 /* id */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, @@ -419,7 +409,8 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(0 /* id */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, @@ -1351,13 +1342,9 @@ public class BiometricServiceTest { for (int i = 0; i < testCases.length; i++) { final BiometricSensor sensor = - new BiometricSensor(mContext, + new BiometricSensor(mContext, 0 /* id */, TYPE_FINGERPRINT, - new FingerprintSensorPropertiesInternal(i /* id */, - Utils.authenticatorStrengthToPropertyStrength(testCases[i][0]), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), + testCases[i][0], mock(IBiometricAuthenticator.class)) { @Override boolean confirmationAlwaysRequired(int userId) { @@ -1385,7 +1372,8 @@ public class BiometricServiceTest { when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(0 /* testId */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); verify(mBiometricService.mBiometricStrengthController).updateStrengths(); @@ -1396,14 +1384,15 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); + final int testId = 0; + when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); - - final int testId = SENSOR_ID_FINGERPRINT; - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(testId /* id */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); // Downgrade the authenticator @@ -1503,9 +1492,11 @@ public class BiometricServiceTest { mBiometricService.onStart(); mBiometricService.mImpl.registerAuthenticator( - 2 /* modality */, mFingerprintProps, mFingerprintAuthenticator); + 0 /* id */, 2 /* modality */, 15 /* strength */, + mFingerprintAuthenticator); mBiometricService.mImpl.registerAuthenticator( - 2 /* modality */, mFingerprintProps, mFingerprintAuthenticator); + 0 /* id */, 2 /* modality */, 15 /* strength */, + mFingerprintAuthenticator); } @Test(expected = IllegalArgumentException.class) @@ -1515,7 +1506,9 @@ public class BiometricServiceTest { mBiometricService.onStart(); mBiometricService.mImpl.registerAuthenticator( - 2 /* modality */, mFingerprintProps, null /* authenticator */); + 0 /* id */, 2 /* modality */, + Authenticators.BIOMETRIC_STRONG /* strength */, + null /* authenticator */); } @Test @@ -1526,13 +1519,8 @@ public class BiometricServiceTest { for (String s : mInjector.getConfiguration(null)) { SensorConfig config = new SensorConfig(s); - mBiometricService.mImpl.registerAuthenticator(config.modality, - new FingerprintSensorPropertiesInternal(config.id, - Utils.authenticatorStrengthToPropertyStrength(config.strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), - mFingerprintAuthenticator); + mBiometricService.mImpl.registerAuthenticator(config.id, config.modality, + config.strength, mFingerprintAuthenticator); } } @@ -1656,12 +1644,7 @@ public class BiometricServiceTest { when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(LockoutTracker.LOCKOUT_NONE); - mBiometricService.mImpl.registerAuthenticator(modality, - new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FINGERPRINT, modality, strength, mFingerprintAuthenticator); } @@ -1670,13 +1653,7 @@ public class BiometricServiceTest { when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFaceAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(LockoutTracker.LOCKOUT_NONE); - mBiometricService.mImpl.registerAuthenticator(modality, - new FaceSensorPropertiesInternal(SENSOR_ID_FACE, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FaceSensorProperties.TYPE_UNKNOWN, true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FACE, modality, strength, mFaceAuthenticator); } } @@ -1699,27 +1676,15 @@ public class BiometricServiceTest { when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); - mBiometricService.mImpl.registerAuthenticator(modality, - new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), - mFingerprintAuthenticator); + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FINGERPRINT, modality, + strength, mFingerprintAuthenticator); } if ((modality & BiometricAuthenticator.TYPE_FACE) != 0) { when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); - mBiometricService.mImpl.registerAuthenticator(modality, - new FaceSensorPropertiesInternal(SENSOR_ID_FACE, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FaceSensorProperties.TYPE_UNKNOWN, - true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), - mFaceAuthenticator); + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FACE, modality, + strength, mFaceAuthenticator); } } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java index f7539bd27c9d..ee5ab92065ee 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java @@ -16,9 +16,6 @@ package com.android.server.biometrics; -import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; -import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -30,13 +27,9 @@ import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IInvalidationCallback; -import android.hardware.biometrics.SensorPropertiesInternal; -import android.hardware.face.FaceSensorProperties; -import android.hardware.face.FaceSensorPropertiesInternal; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -49,7 +42,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.List; @Presubmit @SmallTest @@ -67,54 +59,26 @@ public class InvalidationTrackerTest { public void testCallbackReceived_whenAllStrongSensorsInvalidated() throws Exception { final IBiometricAuthenticator authenticator1 = mock(IBiometricAuthenticator.class); when(authenticator1.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor1 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FINGERPRINT, - new FingerprintSensorPropertiesInternal(0 /* id */, - STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor1 = new TestSensor(mContext, 0 /* id */, + BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, authenticator1); final IBiometricAuthenticator authenticator2 = mock(IBiometricAuthenticator.class); when(authenticator2.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor2 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FINGERPRINT, - new FingerprintSensorPropertiesInternal(1 /* id */, - STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_REAR, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor2 = new TestSensor(mContext, 1 /* id */, + BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, authenticator2); final IBiometricAuthenticator authenticator3 = mock(IBiometricAuthenticator.class); when(authenticator3.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor3 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FACE, - new FaceSensorPropertiesInternal(2 /* id */, - STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FaceSensorProperties.TYPE_RGB, - true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor3 = new TestSensor(mContext, 2 /* id */, + BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG, authenticator3); final IBiometricAuthenticator authenticator4 = mock(IBiometricAuthenticator.class); when(authenticator4.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor4 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FACE, - new FaceSensorPropertiesInternal(3 /* id */, - STRENGTH_WEAK, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FaceSensorProperties.TYPE_IR, - true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor4 = new TestSensor(mContext, 3 /* id */, + BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_WEAK, authenticator4); final ArrayList<BiometricSensor> sensors = new ArrayList<>(); @@ -149,9 +113,9 @@ public class InvalidationTrackerTest { private static class TestSensor extends BiometricSensor { - TestSensor(@NonNull Context context, int modality, @NonNull SensorPropertiesInternal props, + TestSensor(@NonNull Context context, int id, int modality, int strength, @NonNull IBiometricAuthenticator impl) { - super(context, modality, props, impl); + super(context, id, modality, strength, impl); } @Override diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java index 3c77a3593001..527bc5b0c811 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java @@ -62,18 +62,15 @@ public class BiometricSchedulerOperationTest { super(null, null, null, null, 0, null, 0, 0, mock(BiometricLogger.class), mock(BiometricContext.class)); } - - @Override - public boolean isInterruptable() { - return true; - } } @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Mock - private InterruptableMonitor<FakeHal> mClientMonitor; + private InterruptableMonitor<FakeHal> mInterruptableClientMonitor; + @Mock + private BaseClientMonitor mNonInterruptableClientMonitor; @Mock private ClientMonitorCallback mClientCallback; @Mock @@ -84,149 +81,159 @@ public class BiometricSchedulerOperationTest { ArgumentCaptor<ClientMonitorCallback> mStartedCallbackCaptor; private Handler mHandler; - private BiometricSchedulerOperation mOperation; + private BiometricSchedulerOperation mInterruptableOperation; + private BiometricSchedulerOperation mNonInterruptableOperation; private boolean mIsDebuggable; @Before public void setUp() { mHandler = new Handler(TestableLooper.get(this).getLooper()); mIsDebuggable = false; - mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback, - () -> mIsDebuggable); + mInterruptableOperation = new BiometricSchedulerOperation(mInterruptableClientMonitor, + mClientCallback, () -> mIsDebuggable); + mNonInterruptableOperation = new BiometricSchedulerOperation(mNonInterruptableClientMonitor, + mClientCallback, () -> mIsDebuggable); + + when(mInterruptableClientMonitor.isInterruptable()).thenReturn(true); + when(mNonInterruptableClientMonitor.isInterruptable()).thenReturn(false); } @Test public void testStartWithCookie() { final int cookie = 200; - when(mClientMonitor.getCookie()).thenReturn(cookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - assertThat(mOperation.isReadyToStart(mOnStartCallback)).isEqualTo(cookie); - assertThat(mOperation.isStarted()).isFalse(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); - verify(mClientMonitor).waitForCookie(any()); + assertThat(mInterruptableOperation.isReadyToStart(mOnStartCallback)).isEqualTo(cookie); + assertThat(mInterruptableOperation.isStarted()).isFalse(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); + verify(mInterruptableClientMonitor).waitForCookie(any()); - final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); assertThat(started).isTrue(); - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); - assertThat(mOperation.isStarted()).isTrue(); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); + assertThat(mInterruptableOperation.isStarted()).isTrue(); } @Test public void testNoStartWithoutCookie() { final int goodCookie = 20; final int badCookie = 22; - when(mClientMonitor.getCookie()).thenReturn(goodCookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(goodCookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - assertThat(mOperation.isReadyToStart(mOnStartCallback)).isEqualTo(goodCookie); - final boolean started = mOperation.startWithCookie(mOnStartCallback, badCookie); + assertThat(mInterruptableOperation.isReadyToStart(mOnStartCallback)).isEqualTo(goodCookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, + badCookie); assertThat(started).isFalse(); - assertThat(mOperation.isStarted()).isFalse(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); - verify(mClientMonitor).waitForCookie(any()); - verify(mClientMonitor, never()).start(any()); + assertThat(mInterruptableOperation.isStarted()).isFalse(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); + verify(mInterruptableClientMonitor).waitForCookie(any()); + verify(mInterruptableClientMonitor, never()).start(any()); } @Test public void testSecondStartWithCookieCrashesWhenDebuggable() { final int cookie = 5; mIsDebuggable = true; - when(mClientMonitor.getCookie()).thenReturn(cookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); assertThat(started).isTrue(); assertThrows(IllegalStateException.class, - () -> mOperation.startWithCookie(mOnStartCallback, cookie)); + () -> mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)); } @Test public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() { final int cookie = 5; mIsDebuggable = false; - when(mClientMonitor.getCookie()).thenReturn(cookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); assertThat(started).isTrue(); - final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean startedAgain = mInterruptableOperation.startWithCookie(mOnStartCallback, + cookie); assertThat(startedAgain).isFalse(); } @Test public void startsWhenReadyAndHalAvailable() { - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); + mInterruptableOperation.start(mOnStartCallback); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); - assertThat(mOperation.isStarted()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); + assertThat(mInterruptableOperation.isStarted()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); - verify(mClientCallback).onClientStarted(eq(mClientMonitor)); - verify(mOnStartCallback).onClientStarted(eq(mClientMonitor)); + verify(mClientCallback).onClientStarted(eq(mInterruptableClientMonitor)); + verify(mOnStartCallback).onClientStarted(eq(mInterruptableClientMonitor)); verify(mClientCallback, never()).onClientFinished(any(), anyBoolean()); verify(mOnStartCallback, never()).onClientFinished(any(), anyBoolean()); - mStartedCallbackCaptor.getValue().onClientFinished(mClientMonitor, true); + mStartedCallbackCaptor.getValue().onClientFinished(mInterruptableClientMonitor, + true); - assertThat(mOperation.isFinished()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - verify(mClientMonitor).destroy(); - verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(true)); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + verify(mInterruptableClientMonitor).destroy(); + verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(true)); } @Test public void startFailsWhenReadyButHalNotAvailable() { - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(null); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(null); - mOperation.start(mOnStartCallback); - verify(mClientMonitor, never()).start(any()); + mInterruptableOperation.start(mOnStartCallback); + verify(mInterruptableClientMonitor, never()).start(any()); - assertThat(mOperation.isStarted()).isFalse(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isStarted()).isFalse(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); - verify(mClientCallback, never()).onClientStarted(eq(mClientMonitor)); - verify(mOnStartCallback, never()).onClientStarted(eq(mClientMonitor)); - verify(mClientCallback).onClientFinished(eq(mClientMonitor), eq(false)); - verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(false)); + verify(mClientCallback, never()).onClientStarted(eq(mInterruptableClientMonitor)); + verify(mOnStartCallback, never()).onClientStarted(eq(mInterruptableClientMonitor)); + verify(mClientCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false)); + verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false)); } @Test public void secondStartCrashesWhenDebuggable() { mIsDebuggable = true; - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.start(mOnStartCallback); + final boolean started = mInterruptableOperation.start(mOnStartCallback); assertThat(started).isTrue(); - assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback)); + assertThrows(IllegalStateException.class, () -> mInterruptableOperation.start( + mOnStartCallback)); } @Test public void secondStartFailsNicelyWhenNotDebuggable() { mIsDebuggable = false; - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.start(mOnStartCallback); + final boolean started = mInterruptableOperation.start(mOnStartCallback); assertThat(started).isTrue(); - final boolean startedAgain = mOperation.start(mOnStartCallback); + final boolean startedAgain = mInterruptableOperation.start(mOnStartCallback); assertThat(startedAgain).isFalse(); } @@ -234,77 +241,78 @@ public class BiometricSchedulerOperationTest { public void doesNotStartWithCookie() { // This class only throws exceptions when debuggable. mIsDebuggable = true; - when(mClientMonitor.getCookie()).thenReturn(9); + when(mInterruptableClientMonitor.getCookie()).thenReturn(9); assertThrows(IllegalStateException.class, - () -> mOperation.start(mock(ClientMonitorCallback.class))); + () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); } @Test public void cannotRestart() { // This class only throws exceptions when debuggable. mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); assertThrows(IllegalStateException.class, - () -> mOperation.start(mock(ClientMonitorCallback.class))); + () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); } @Test public void abortsNotRunning() { // This class only throws exceptions when debuggable. mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.abort(); + mInterruptableOperation.abort(); - assertThat(mOperation.isFinished()).isTrue(); - verify(mClientMonitor).unableToStart(); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + verify(mInterruptableClientMonitor).unableToStart(); + verify(mInterruptableClientMonitor).destroy(); assertThrows(IllegalStateException.class, - () -> mOperation.start(mock(ClientMonitorCallback.class))); + () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); } @Test public void abortCrashesWhenDebuggableIfOperationIsRunning() { mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); - assertThrows(IllegalStateException.class, () -> mOperation.abort()); + assertThrows(IllegalStateException.class, () -> mInterruptableOperation.abort()); } @Test public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() { mIsDebuggable = false; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); - mOperation.abort(); + mInterruptableOperation.abort(); } @Test public void cancel() { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - mOperation.start(mOnStartCallback); - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); - mOperation.cancel(mHandler, cancelCb); + mInterruptableOperation.start(mOnStartCallback); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); + mInterruptableOperation.cancel(mHandler, cancelCb); - assertThat(mOperation.isCanceling()).isTrue(); - verify(mClientMonitor).cancel(); - verify(mClientMonitor, never()).destroy(); + assertThat(mInterruptableOperation.isCanceling()).isTrue(); + verify(mInterruptableClientMonitor).cancel(); + verify(mInterruptableClientMonitor, never()).destroy(); - mStartedCallbackCaptor.getValue().onClientFinished(mClientMonitor, true); + mStartedCallbackCaptor.getValue().onClientFinished(mInterruptableClientMonitor, + true); - assertThat(mOperation.isFinished()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + verify(mInterruptableClientMonitor).destroy(); // should be unused since the operation was started verify(cancelCb, never()).onClientStarted(any()); @@ -313,61 +321,84 @@ public class BiometricSchedulerOperationTest { @Test public void cancelWithoutStarting() { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - mOperation.cancel(mHandler, cancelCb); + mInterruptableOperation.cancel(mHandler, cancelCb); - assertThat(mOperation.isCanceling()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isTrue(); ArgumentCaptor<ClientMonitorCallback> cbCaptor = ArgumentCaptor.forClass(ClientMonitorCallback.class); - verify(mClientMonitor).cancelWithoutStarting(cbCaptor.capture()); + verify(mInterruptableClientMonitor).cancelWithoutStarting(cbCaptor.capture()); - cbCaptor.getValue().onClientFinished(mClientMonitor, true); - verify(cancelCb).onClientFinished(eq(mClientMonitor), eq(true)); - verify(mClientMonitor, never()).start(any()); - verify(mClientMonitor, never()).cancel(); - verify(mClientMonitor).destroy(); + cbCaptor.getValue().onClientFinished(mInterruptableClientMonitor, true); + verify(cancelCb).onClientFinished(eq(mInterruptableClientMonitor), eq(true)); + verify(mInterruptableClientMonitor, never()).start(any()); + verify(mInterruptableClientMonitor, never()).cancel(); + verify(mInterruptableClientMonitor).destroy(); } @Test public void cancelCrashesWhenDebuggableIfOperationIsFinished() { mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.abort(); - assertThat(mOperation.isFinished()).isTrue(); + mInterruptableOperation.abort(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb)); + assertThrows(IllegalStateException.class, () -> mInterruptableOperation.cancel(mHandler, + cancelCb)); } @Test public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() { mIsDebuggable = false; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.abort(); - assertThat(mOperation.isFinished()).isTrue(); + mInterruptableOperation.abort(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - mOperation.cancel(mHandler, cancelCb); + mInterruptableOperation.cancel(mHandler, cancelCb); } @Test - public void markCanceling() { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); - - mOperation.markCanceling(); - - assertThat(mOperation.isMarkedCanceling()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); - verify(mClientMonitor, never()).start(any()); - verify(mClientMonitor, never()).cancel(); - verify(mClientMonitor, never()).cancelWithoutStarting(any()); - verify(mClientMonitor, never()).unableToStart(); - verify(mClientMonitor, never()).destroy(); + public void markCanceling_interruptableClient() { + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mInterruptableOperation.markCanceling(); + + assertThat(mInterruptableOperation.isMarkedCanceling()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); + verify(mInterruptableClientMonitor, never()).start(any()); + verify(mInterruptableClientMonitor, never()).cancel(); + verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any()); + verify(mInterruptableClientMonitor, never()).unableToStart(); + verify(mInterruptableClientMonitor, never()).destroy(); + } + + @Test + public void markCanceling_nonInterruptableClient() { + mNonInterruptableOperation.markCanceling(); + + assertThat(mNonInterruptableOperation.isMarkedCanceling()).isFalse(); + assertThat(mNonInterruptableOperation.isCanceling()).isFalse(); + assertThat(mNonInterruptableOperation.isFinished()).isFalse(); + verify(mNonInterruptableClientMonitor, never()).start(any()); + verify(mNonInterruptableClientMonitor, never()).cancel(); + verify(mNonInterruptableClientMonitor, never()).cancelWithoutStarting(any()); + verify(mNonInterruptableClientMonitor, never()).destroy(); + } + + @Test + public void markCancelingForWatchdog() { + mNonInterruptableOperation.markCancelingForWatchdog(); + mInterruptableOperation.markCancelingForWatchdog(); + + assertThat(mInterruptableOperation.isMarkedCanceling()).isTrue(); + assertThat(mNonInterruptableOperation.isMarkedCanceling()).isTrue(); } @Test @@ -381,26 +412,26 @@ public class BiometricSchedulerOperationTest { } private void markCancellingAndStart(Integer withCookie) { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); if (withCookie != null) { - when(mClientMonitor.getCookie()).thenReturn(withCookie); + when(mInterruptableClientMonitor.getCookie()).thenReturn(withCookie); } - mOperation.markCanceling(); + mInterruptableOperation.markCanceling(); final ClientMonitorCallback cb = mock(ClientMonitorCallback.class); if (withCookie != null) { - mOperation.startWithCookie(cb, withCookie); + mInterruptableOperation.startWithCookie(cb, withCookie); } else { - mOperation.start(cb); + mInterruptableOperation.start(cb); } - assertThat(mOperation.isFinished()).isTrue(); - verify(cb).onClientFinished(eq(mClientMonitor), eq(true)); - verify(mClientMonitor, never()).start(any()); - verify(mClientMonitor, never()).cancel(); - verify(mClientMonitor, never()).cancelWithoutStarting(any()); - verify(mClientMonitor, never()).unableToStart(); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + verify(cb).onClientFinished(eq(mInterruptableClientMonitor), eq(true)); + verify(mInterruptableClientMonitor, never()).start(any()); + verify(mInterruptableClientMonitor, never()).cancel(); + verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any()); + verify(mInterruptableClientMonitor, never()).unableToStart(); + verify(mInterruptableClientMonitor).destroy(); } @Test @@ -414,23 +445,23 @@ public class BiometricSchedulerOperationTest { } private void cancelWatchdog(boolean start) { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); if (start) { - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); } - mOperation.cancel(mHandler, mock(ClientMonitorCallback.class)); + mInterruptableOperation.cancel(mHandler, mock(ClientMonitorCallback.class)); - assertThat(mOperation.isCanceling()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isTrue(); // omit call to onClientFinished and trigger watchdog - mOperation.mCancelWatchdog.run(); + mInterruptableOperation.mCancelWatchdog.run(); - assertThat(mOperation.isFinished()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(false)); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false)); + verify(mInterruptableClientMonitor).destroy(); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java index d3f04dfcfa17..903ed9082481 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java @@ -17,6 +17,8 @@ package com.android.server.biometrics.sensors.face; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; @@ -68,7 +70,9 @@ public class FaceServiceRegistryTest { @Mock private ServiceProvider mProvider2; @Captor - private ArgumentCaptor<FaceSensorPropertiesInternal> mPropsCaptor; + private ArgumentCaptor<Integer> mIdCaptor; + @Captor + private ArgumentCaptor<Integer> mStrengthCaptor; private FaceSensorPropertiesInternal mProvider1Props; private FaceSensorPropertiesInternal mProvider2Props; @@ -78,13 +82,13 @@ public class FaceServiceRegistryTest { public void setup() { mProvider1Props = new FaceSensorPropertiesInternal(SENSOR_ID_1, STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FaceSensorProperties.TYPE_RGB, + List.of(), FaceSensorProperties.TYPE_RGB, true /* supportsFace Detection */, true /* supportsSelfIllumination */, false /* resetLockoutRequiresHardwareAuthToken */); mProvider2Props = new FaceSensorPropertiesInternal(SENSOR_ID_2, STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FaceSensorProperties.TYPE_IR, + List.of(), FaceSensorProperties.TYPE_IR, true /* supportsFace Detection */, true /* supportsSelfIllumination */, false /* resetLockoutRequiresHardwareAuthToken */); @@ -103,9 +107,10 @@ public class FaceServiceRegistryTest { assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2); assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props); verify(mBiometricService, times(2)).registerAuthenticator( - eq(TYPE_FACE), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()) - .containsExactly(mProvider1Props, mProvider2Props); + mIdCaptor.capture(), eq(TYPE_FACE), mStrengthCaptor.capture(), any()); + assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2); + assertThat(mStrengthCaptor.getAllValues()) + .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java index 6e09069e654b..13c3f64fec93 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java @@ -17,6 +17,8 @@ package com.android.server.biometrics.sensors.fingerprint; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; @@ -68,7 +70,9 @@ public class FingerprintServiceRegistryTest { @Mock private ServiceProvider mProvider2; @Captor - private ArgumentCaptor<FingerprintSensorPropertiesInternal> mPropsCaptor; + private ArgumentCaptor<Integer> mIdCaptor; + @Captor + private ArgumentCaptor<Integer> mStrengthCaptor; private FingerprintSensorPropertiesInternal mProvider1Props; private FingerprintSensorPropertiesInternal mProvider2Props; @@ -78,11 +82,11 @@ public class FingerprintServiceRegistryTest { public void setup() { mProvider1Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_1, STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + List.of(), FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, false /* resetLockoutRequiresHardwareAuthToken */); mProvider2Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_2, STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FingerprintSensorProperties.TYPE_UNKNOWN, + List.of(), FingerprintSensorProperties.TYPE_UNKNOWN, false /* resetLockoutRequiresHardwareAuthToken */); when(mProvider1.getSensorProperties()).thenReturn(List.of(mProvider1Props)); @@ -99,9 +103,10 @@ public class FingerprintServiceRegistryTest { assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2); assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props); verify(mBiometricService, times(2)).registerAuthenticator( - eq(TYPE_FINGERPRINT), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()) - .containsExactly(mProvider1Props, mProvider2Props); + mIdCaptor.capture(), eq(TYPE_FINGERPRINT), mStrengthCaptor.capture(), any()); + assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2); + assertThat(mStrengthCaptor.getAllValues()) + .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java index 1089c07e6787..2aa62d96168d 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java @@ -27,6 +27,8 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFP import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -44,6 +46,7 @@ import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.IFingerprintServiceReceiver; +import android.os.Binder; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.provider.Settings; @@ -54,8 +57,10 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; +import com.android.server.LocalServices; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import org.junit.Before; import org.junit.Rule; @@ -103,6 +108,8 @@ public class FingerprintServiceTest { private IFingerprintServiceReceiver mServiceReceiver; @Mock private IBinder mToken; + @Mock + private VirtualDeviceManagerInternal mVdmInternal; @Captor private ArgumentCaptor<FingerprintAuthenticateOptions> mAuthenticateOptionsCaptor; @@ -110,21 +117,22 @@ public class FingerprintServiceTest { private final FingerprintSensorPropertiesInternal mSensorPropsDefault = new FingerprintSensorPropertiesInternal(ID_DEFAULT, STRENGTH_STRONG, 2 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, + List.of(), TYPE_REAR, false /* resetLockoutRequiresHardwareAuthToken */); private final FingerprintSensorPropertiesInternal mSensorPropsVirtual = new FingerprintSensorPropertiesInternal(ID_VIRTUAL, STRENGTH_STRONG, 2 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, + List.of(), TYPE_UDFPS_OPTICAL, false /* resetLockoutRequiresHardwareAuthToken */); - @Captor - private ArgumentCaptor<FingerprintSensorPropertiesInternal> mPropsCaptor; private FingerprintService mService; @Before public void setup() throws Exception { + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + LocalServices.addService(VirtualDeviceManagerInternal.class, mVdmInternal); + when(mFingerprintDefault.getSensorProperties()).thenReturn(List.of(mSensorPropsDefault)); when(mFingerprintVirtual.getSensorProperties()).thenReturn(List.of(mSensorPropsVirtual)); when(mFingerprintDefault.containsSensor(anyInt())) @@ -168,8 +176,7 @@ public class FingerprintServiceTest { mService.mServiceWrapper.registerAuthenticators(HIDL_AUTHENTICATORS); waitForRegistration(); - verify(mIBiometricService).registerAuthenticator(anyInt(), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()).containsExactly(mSensorPropsDefault); + verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), anyInt(), anyInt(), any()); } @Test @@ -181,8 +188,7 @@ public class FingerprintServiceTest { mService.mServiceWrapper.registerAuthenticators(HIDL_AUTHENTICATORS); waitForRegistration(); - verify(mIBiometricService).registerAuthenticator(anyInt(), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()).containsExactly(mSensorPropsVirtual); + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); } @Test @@ -192,8 +198,7 @@ public class FingerprintServiceTest { mService.mServiceWrapper.registerAuthenticators(HIDL_AUTHENTICATORS); waitForRegistration(); - verify(mIBiometricService).registerAuthenticator(anyInt(), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()).containsExactly(mSensorPropsVirtual); + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); } private void waitForRegistration() throws Exception { @@ -225,6 +230,36 @@ public class FingerprintServiceTest { verifyNoAuthenticate(mFingerprintVirtual); } + @Test + public void testAuthenticate_noVdmInternalService_noCrash() throws Exception { + initServiceWithAndWait(NAME_DEFAULT, NAME_VIRTUAL); + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + + final long operationId = 2; + + // This should not crash + mService.mServiceWrapper.authenticate(mToken, operationId, mServiceReceiver, + new FingerprintAuthenticateOptions.Builder() + .setSensorId(SENSOR_ID_ANY) + .build()); + } + + @Test + public void testAuthenticate_callsVirtualDeviceManagerOnAuthenticationPrompt() + throws Exception { + initServiceWithAndWait(NAME_DEFAULT, NAME_VIRTUAL); + + final long operationId = 2; + mService.mServiceWrapper.authenticate(mToken, operationId, mServiceReceiver, + new FingerprintAuthenticateOptions.Builder() + .setSensorId(SENSOR_ID_ANY) + .build()); + + ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mVdmInternal).onAuthenticationPrompt(uidCaptor.capture()); + assertEquals((int) (uidCaptor.getValue()), Binder.getCallingUid()); + } + private FingerprintAuthenticateOptions verifyAuthenticateWithNewRequestId( FingerprintProvider provider, long operationId) { diff --git a/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java index 20e2692cb747..bfd407216c3b 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java @@ -27,6 +27,7 @@ import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_FINISH; import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_NONE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_LIFECYCLE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_START; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_STOP; @@ -499,6 +500,27 @@ public class UserJourneyLoggerTest { 0x00000402, ERROR_CODE_UNSPECIFIED, 3); } + @Test + public void testUserLifecycleJourney() { + final long startTime = System.currentTimeMillis(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .startSessionForDelayedJourney(10, USER_JOURNEY_USER_LIFECYCLE, startTime); + + + final UserLifecycleJourneyReportedCaptor report = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logDelayedUserJourneyFinishWithError(0, targetUser, + USER_JOURNEY_USER_LIFECYCLE, ERROR_CODE_UNSPECIFIED); + + + report.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_LIFECYCLE, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + assertThat(report.mElapsedTime.getValue() > 0L).isTrue(); + } + static class UserLifecycleJourneyReportedCaptor { ArgumentCaptor<Long> mSessionId = ArgumentCaptor.forClass(Long.class); ArgumentCaptor<Integer> mJourney = ArgumentCaptor.forClass(Integer.class); @@ -507,6 +529,7 @@ public class UserJourneyLoggerTest { ArgumentCaptor<Integer> mUserType = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> mUserFlags = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> mErrorCode = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Long> mElapsedTime = ArgumentCaptor.forClass(Long.class); public void captureAndAssert(UserJourneyLogger mUserJourneyLogger, long sessionId, int journey, int originalUserId, @@ -518,7 +541,8 @@ public class UserJourneyLoggerTest { mTargetUserId.capture(), mUserType.capture(), mUserFlags.capture(), - mErrorCode.capture()); + mErrorCode.capture(), + mElapsedTime.capture()); assertThat(mSessionId.getValue()).isEqualTo(sessionId); assertThat(mJourney.getValue()).isEqualTo(journey); @@ -577,4 +601,4 @@ public class UserJourneyLoggerTest { state, errorCode, 1); } } -} +}
\ No newline at end of file diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 95fc0faf50ba..2b589bf59682 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -645,7 +645,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("allowed_noStartsAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -659,7 +659,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_unsupportedUsecase_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -673,7 +673,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingUidProcessStateTop_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -687,7 +687,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_realCallingUidProcessStateTop_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -701,7 +701,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_hasForegroundActivities_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - true, false, false, false, false, false, false); + true, false, false, false, false, false, false, false); } /** @@ -715,7 +715,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_pinned_singleinstance_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, true, false); + false, false, false, false, false, false, true, false); } /** @@ -729,7 +729,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("disallowed_rootUid_notAborted", false, Process.ROOT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -743,7 +743,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("disallowed_systemUid_notAborted", false, Process.SYSTEM_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -757,7 +757,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("disallowed_nfcUid_notAborted", false, Process.NFC_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -772,7 +772,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingUidHasVisibleWindow_notAborted", false, UNIMPORTANT_UID, true, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -788,7 +788,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_realCallingUidHasVisibleWindow_abortedInU", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, true, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -803,7 +803,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callerIsRecents_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false); + false, true, false, false, false, false, false, false); } /** @@ -818,7 +818,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callerIsAllowed_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, true, false, false, false, false); + false, false, true, false, false, false, false, false); } /** @@ -834,7 +834,7 @@ public class ActivityStarterTests extends WindowTestsBase { false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, true, false, false, false); + false, false, false, true, false, false, false, false); } /** @@ -850,7 +850,23 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingPackageNameIsDeviceOwner_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, true, false, false); + false, false, false, false, true, false, false, false); + } + + /** + * This test ensures that supported usecases aren't aborted when background starts are + * disallowed. Each scenarios tests one condition that makes them supported in isolation. In + * this case the caller is a affiliated profile owner. + */ + @Test + public void + testBackgroundActivityStartsDisallowed_isAffiliatedProfileOwnerNotAborted() { + doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled(); + runAndVerifyBackgroundActivityStartsSubtest( + "disallowed_callingUidIsAffiliatedProfileOwner_notAborted", false, + UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, + UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, + false, false, false, false, false, true, false, false); } /** @@ -865,7 +881,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callerHasSystemExemptAppOpNotAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, true); + false, false, false, false, false, false, false, true); } /** @@ -881,7 +897,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingPackageNameIsIme_notAborted", false, CURRENT_IME_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -902,7 +918,7 @@ public class ActivityStarterTests extends WindowTestsBase { "allowed_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false); + false, true, false, false, false, false, false, false); verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, "", // activity name BackgroundActivityStartController.BAL_ALLOW_PERMISSION, @@ -928,12 +944,12 @@ public class ActivityStarterTests extends WindowTestsBase { anyInt(), anyInt())); doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when( () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller( - anyObject(), anyInt())); + anyObject(), anyInt(), anyObject())); runAndVerifyBackgroundActivityStartsSubtest( "allowed_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false); + false, true, false, false, false, false, false, false); verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, DEFAULT_COMPONENT_PACKAGE_NAME + "/" + DEFAULT_COMPONENT_PACKAGE_NAME, BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT, @@ -949,6 +965,7 @@ public class ActivityStarterTests extends WindowTestsBase { boolean callerIsTempAllowed, boolean callerIsInstrumentingWithBackgroundActivityStartPrivileges, boolean isCallingUidDeviceOwner, + boolean isCallingUidAffiliatedProfileOwner, boolean isPinnedSingleInstance, boolean hasSystemExemptAppOp) { // window visibility @@ -982,6 +999,9 @@ public class ActivityStarterTests extends WindowTestsBase { callerIsInstrumentingWithBackgroundActivityStartPrivileges); // callingUid is the device owner doReturn(isCallingUidDeviceOwner).when(mAtm).isDeviceOwner(callingUid); + // callingUid is the affiliated profile owner + doReturn(isCallingUidAffiliatedProfileOwner).when(mAtm) + .isAffiliatedProfileOwner(callingUid); // caller has OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop doReturn(hasSystemExemptAppOp ? AppOpsManager.MODE_ALLOWED diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index 8ac6b0f65b72..5ec36048234b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -301,6 +301,33 @@ public class DisplayPolicyTests extends WindowTestsBase { } @Test + public void testSwitchDecorInsets() { + createNavBarWithProvidedInsets(mDisplayContent); + final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy(); + final DisplayInfo info = mDisplayContent.getDisplayInfo(); + final int w = info.logicalWidth; + final int h = info.logicalHeight; + displayPolicy.updateDecorInsetsInfo(); + final Rect prevConfigFrame = new Rect(displayPolicy.getDecorInsetsInfo(info.rotation, + info.logicalWidth, info.logicalHeight).mConfigFrame); + + displayPolicy.updateCachedDecorInsets(); + mDisplayContent.updateBaseDisplayMetrics(w / 2, h / 2, + info.logicalDensityDpi, info.physicalXDpi, info.physicalYDpi); + // There is no previous cache. But the current state will be cached. + assertFalse(displayPolicy.shouldKeepCurrentDecorInsets()); + + // Switch to original state. + displayPolicy.updateCachedDecorInsets(); + mDisplayContent.updateBaseDisplayMetrics(w, h, + info.logicalDensityDpi, info.physicalXDpi, info.physicalYDpi); + assertTrue(displayPolicy.shouldKeepCurrentDecorInsets()); + // The current insets are restored from cache directly. + assertEquals(prevConfigFrame, displayPolicy.getDecorInsetsInfo(info.rotation, + info.logicalWidth, info.logicalHeight).mConfigFrame); + } + + @Test public void testUpdateDisplayConfigurationByDecor() { doReturn(NO_CUTOUT).when(mDisplayContent).calculateDisplayCutoutForRotation(anyInt()); final WindowState navbar = createNavBarWithProvidedInsets(mDisplayContent); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index a3117269eb01..3ca35ef7cc26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -479,6 +479,8 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); + doReturn(false).when(mActivity.mLetterboxUiController) + .isCameraCompatSplitScreenAspectRatioAllowed(); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false); @@ -487,6 +489,19 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test + public void testOnActivityConfigurationChanging_splitScreenAspectRatioAllowed_refresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + doReturn(true).when(mActivity.mLetterboxUiController) + .isCameraCompatSplitScreenAspectRatioAllowed(); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false); + + assertActivityRefreshRequested(/* refreshRequested */ true); + } + + @Test public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 03c93e976059..81e0fb3c7996 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -24,6 +24,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE; +import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT; @@ -40,6 +41,7 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; @@ -861,6 +863,62 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_overrideEnabled_returnsTrue() { + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyTrue_overrideEnabled_returnsTrue() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyTrue_overrideDisabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_overrideDisabled_returnsFalse() { + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyFalse_overrideEnabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyFalse_noOverride_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test public void testgetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() { doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration) .isCameraCompatTreatmentEnabled(anyBoolean()); diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java index f1e1a5adc2c3..dc5f6e960a2b 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java @@ -1252,6 +1252,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { mEventLogger.enqueue(new SessionEvent(Type.RESUME_FAILED, modelData.getModelId(), String.valueOf(status)) .printLog(ALOGW, TAG)); + modelData.setRequested(false); callback.onResumeFailed(status); } catch (RemoteException e) { mEventLogger.enqueue(new SessionEvent(Type.RESUME_FAILED, @@ -1300,6 +1301,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { mEventLogger.enqueue(new SessionEvent(Type.PAUSE_FAILED, modelData.getModelId(), String.valueOf(status)) .printLog(ALOGW, TAG)); + modelData.setRequested(false); callback.onPauseFailed(status); } catch (RemoteException e) { mEventLogger.enqueue(new SessionEvent(Type.PAUSE_FAILED, @@ -1453,6 +1455,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } synchronized void setNotLoaded() { + mRecognitionToken = null; mModelState = MODEL_NOTLOADED; } @@ -1462,6 +1465,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { synchronized void clearState() { mModelState = MODEL_NOTLOADED; + mRecognitionToken = null; mRecognitionConfig = null; mRequested = false; mCallback = null; diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java index f70268e1848a..e793f317d41f 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -168,9 +168,18 @@ class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.Glo * Attached to the HAL service via factory. */ private void attachToHal() { - mHalService = new SoundTriggerHalEnforcer( - new SoundTriggerHalWatchdog( - new SoundTriggerDuplicateModelHandler(mHalFactory.create()))); + mHalService = null; + while (mHalService == null) { + try { + mHalService = new SoundTriggerHalEnforcer( + new SoundTriggerHalWatchdog( + new SoundTriggerDuplicateModelHandler(mHalFactory.create()))); + } catch (RuntimeException e) { + if (!(e.getCause() instanceof RemoteException)) { + throw e; + } + } + } mHalService.linkToDeath(this); mHalService.registerCallback(this); mProperties = mHalService.getProperties(); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java index cd29dace2263..3a651042ea71 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java @@ -47,7 +47,6 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPH import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_UNEXPECTED_CALLBACK; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK; import static com.android.server.voiceinteraction.HotwordDetectionConnection.ENFORCE_HOTWORD_PHRASE_ID; -import static com.android.server.voiceinteraction.SoundTriggerSessionPermissionsDecorator.enforcePermissionForPreflight; import android.annotation.NonNull; import android.annotation.Nullable; @@ -743,7 +742,14 @@ abstract class DetectorSession { void enforcePermissionsForDataDelivery() { Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - enforcePermissionForPreflight(mContext, mVoiceInteractorIdentity, RECORD_AUDIO); + int result = PermissionChecker.checkPermissionForPreflight( + mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid, + mVoiceInteractorIdentity.packageName); + if (result != PermissionChecker.PERMISSION_GRANTED) { + throw new SecurityException( + "Failed to obtain permission RECORD_AUDIO for identity " + + mVoiceInteractorIdentity); + } int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD); mAppOpsManager.noteOpNoThrow(hotwordOp, mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName, @@ -770,7 +776,7 @@ abstract class DetectorSession { throw new SecurityException( TextUtils.formatSimple("Failed to obtain permission %s for identity %s", permission, - SoundTriggerSessionPermissionsDecorator.toString(identity))); + identity)); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java deleted file mode 100644 index 0ef2f06b6684..000000000000 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.voiceinteraction; - -import android.hardware.soundtrigger.SoundTrigger; -import android.os.RemoteException; - -import com.android.internal.app.IHotwordRecognitionStatusCallback; -import com.android.internal.app.IVoiceInteractionSoundTriggerSession; - -/** - * A remote object that simply proxies calls to a real {@link IVoiceInteractionSoundTriggerSession} - * implementation. This design pattern allows us to add decorators to the core implementation - * (simply wrapping a binder object does not work). - */ -final class SoundTriggerSessionBinderProxy extends IVoiceInteractionSoundTriggerSession.Stub { - - private final IVoiceInteractionSoundTriggerSession mDelegate; - - SoundTriggerSessionBinderProxy(IVoiceInteractionSoundTriggerSession delegate) { - mDelegate = delegate; - } - - @Override - public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException { - return mDelegate.getDspModuleProperties(); - } - - @Override - public int startRecognition(int i, String s, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback, - SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException { - return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback, - recognitionConfig, b); - } - - @Override - public int stopRecognition(int i, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback) - throws RemoteException { - return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback); - } - - @Override - public int setParameter(int i, int i1, int i2) throws RemoteException { - return mDelegate.setParameter(i, i1, i2); - } - - @Override - public int getParameter(int i, int i1) throws RemoteException { - return mDelegate.getParameter(i, i1); - } - - @Override - public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException { - return mDelegate.queryParameter(i, i1); - } - - @Override - public void detach() throws RemoteException { - mDelegate.detach(); - } -} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java deleted file mode 100644 index 0f8a945ec461..000000000000 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.voiceinteraction; - -import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; -import static android.Manifest.permission.RECORD_AUDIO; - -import static com.android.server.voiceinteraction.HotwordDetectionConnection.DEBUG; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.PermissionChecker; -import android.hardware.soundtrigger.SoundTrigger; -import android.media.permission.Identity; -import android.media.permission.PermissionUtil; -import android.os.IBinder; -import android.os.RemoteException; -import android.text.TextUtils; -import android.util.Slog; - -import com.android.internal.app.IHotwordRecognitionStatusCallback; -import com.android.internal.app.IVoiceInteractionSoundTriggerSession; - -/** - * Decorates {@link IVoiceInteractionSoundTriggerSession} with permission checks for {@link - * android.Manifest.permission#RECORD_AUDIO} and - * {@link android.Manifest.permission#CAPTURE_AUDIO_HOTWORD}. - * <p> - * Does not implement {@link #asBinder()} as it's intended to be wrapped by an - * {@link IVoiceInteractionSoundTriggerSession.Stub} object. - */ -final class SoundTriggerSessionPermissionsDecorator implements - IVoiceInteractionSoundTriggerSession { - static final String TAG = "SoundTriggerSessionPermissionsDecorator"; - - private final IVoiceInteractionSoundTriggerSession mDelegate; - private final Context mContext; - private final Identity mOriginatorIdentity; - - SoundTriggerSessionPermissionsDecorator(IVoiceInteractionSoundTriggerSession delegate, - Context context, Identity originatorIdentity) { - mDelegate = delegate; - mContext = context; - mOriginatorIdentity = originatorIdentity; - } - - @Override - public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException { - // No permission needed here (the app must have the Assistant Role to retrieve the session). - return mDelegate.getDspModuleProperties(); - } - - @Override - public int startRecognition(int i, String s, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback, - SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException { - if (DEBUG) { - Slog.d(TAG, "startRecognition"); - } - if (!isHoldingPermissions()) { - return SoundTrigger.STATUS_PERMISSION_DENIED; - } - return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback, - recognitionConfig, b); - } - - @Override - public int stopRecognition(int i, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback) - throws RemoteException { - // Stopping a model does not require special permissions. Having a handle to the session is - // sufficient. - return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback); - } - - @Override - public int setParameter(int i, int i1, int i2) throws RemoteException { - if (!isHoldingPermissions()) { - return SoundTrigger.STATUS_PERMISSION_DENIED; - } - return mDelegate.setParameter(i, i1, i2); - } - - @Override - public int getParameter(int i, int i1) throws RemoteException { - // No permission needed here (the app must have the Assistant Role to retrieve the session). - return mDelegate.getParameter(i, i1); - } - - @Override - public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException { - // No permission needed here (the app must have the Assistant Role to retrieve the session). - return mDelegate.queryParameter(i, i1); - } - - @Override - public IBinder asBinder() { - throw new UnsupportedOperationException( - "This object isn't intended to be used as a Binder."); - } - - @Override - public void detach() { - try { - mDelegate.detach(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - } - - // TODO: Share this code with SoundTriggerMiddlewarePermission. - private boolean isHoldingPermissions() { - try { - enforcePermissionForPreflight(mContext, mOriginatorIdentity, RECORD_AUDIO); - enforcePermissionForPreflight(mContext, mOriginatorIdentity, CAPTURE_AUDIO_HOTWORD); - return true; - } catch (SecurityException e) { - Slog.e(TAG, e.toString()); - return false; - } - } - - /** - * Throws a {@link SecurityException} if originator permanently doesn't have the given - * permission. - * Soft (temporary) denials are considered OK for preflight purposes. - * - * @param context A {@link Context}, used for permission checks. - * @param identity The identity to check. - * @param permission The identifier of the permission we want to check. - */ - static void enforcePermissionForPreflight(@NonNull Context context, - @NonNull Identity identity, @NonNull String permission) { - final int status = PermissionUtil.checkPermissionForPreflight(context, identity, - permission); - switch (status) { - case PermissionChecker.PERMISSION_GRANTED: - case PermissionChecker.PERMISSION_SOFT_DENIED: - return; - case PermissionChecker.PERMISSION_HARD_DENIED: - throw new SecurityException( - TextUtils.formatSimple("Failed to obtain permission %s for identity %s", - permission, toString(identity))); - default: - throw new RuntimeException("Unexpected permission check result."); - } - } - - static String toString(Identity identity) { - return "{uid=" + identity.uid - + " pid=" + identity.pid - + " packageName=" + identity.packageName - + " attributionTag=" + identity.attributionTag - + "}"; - } - - // Temporary hack for using the same status code as SoundTrigger, so we don't change behavior. - // TODO: Reuse SoundTrigger code so we don't need to do this. - private static final int TEMPORARY_PERMISSION_DENIED = 3; -} diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java index 52ff90f38113..b1a7d819cd17 100644 --- a/telecomm/java/android/telecom/CallAttributes.java +++ b/telecomm/java/android/telecom/CallAttributes.java @@ -59,6 +59,9 @@ public final class CallAttributes implements Parcelable { public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities"; /** @hide **/ + public static final String DISPLAY_NAME_KEY = "DisplayName"; + + /** @hide **/ public static final String CALLER_PID_KEY = "CallerPid"; /** @hide **/ |