diff options
380 files changed, 10766 insertions, 5176 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index f779b4d96b45..b0b3b1f3cb5e 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -319,6 +319,8 @@ public class DeviceIdleController extends SystemService private SensorManager mSensorManager; private final boolean mUseMotionSensor; private Sensor mMotionSensor; + private final boolean mIsLocationPrefetchEnabled; + @Nullable private LocationRequest mLocationRequest; private Intent mIdleIntent; private Bundle mIdleIntentOptions; @@ -2460,6 +2462,11 @@ public class DeviceIdleController extends SystemService return null; } + boolean isLocationPrefetchEnabled() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_autoPowerModePrefetchLocation); + } + boolean useMotionSensor() { return mContext.getResources().getBoolean( com.android.internal.R.bool.config_autoPowerModeUseMotionSensor); @@ -2489,6 +2496,7 @@ public class DeviceIdleController extends SystemService mAppStateTracker = mInjector.getAppStateTracker(context, AppSchedulingModuleThread.get().getLooper()); LocalServices.addService(AppStateTracker.class, mAppStateTracker); + mIsLocationPrefetchEnabled = mInjector.isLocationPrefetchEnabled(); mUseMotionSensor = mInjector.useMotionSensor(); } @@ -2602,8 +2610,7 @@ public class DeviceIdleController extends SystemService mMotionSensor = mInjector.getMotionSensor(); } - if (getContext().getResources().getBoolean( - com.android.internal.R.bool.config_autoPowerModePrefetchLocation)) { + if (mIsLocationPrefetchEnabled) { mLocationRequest = new LocationRequest.Builder(/*intervalMillis=*/ 0) .setQuality(LocationRequest.QUALITY_HIGH_ACCURACY) .setMaxUpdates(1) @@ -3779,34 +3786,40 @@ public class DeviceIdleController extends SystemService case STATE_SENSING: cancelSensingTimeoutAlarmLocked(); moveToStateLocked(STATE_LOCATING, reason); - scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT); - LocationManager locationManager = mInjector.getLocationManager(); - if (locationManager != null - && locationManager.getProvider(LocationManager.FUSED_PROVIDER) != null) { - locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER, - mLocationRequest, - AppSchedulingModuleThread.getExecutor(), - mGenericLocationListener); - mLocating = true; - } else { - mHasFusedLocation = false; - } - if (locationManager != null - && locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) { - mHasGps = true; - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 5, - mGpsLocationListener, mHandler.getLooper()); - mLocating = true; + if (mIsLocationPrefetchEnabled) { + scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT); + LocationManager locationManager = mInjector.getLocationManager(); + if (locationManager != null + && locationManager.getProvider(LocationManager.FUSED_PROVIDER) + != null) { + locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER, + mLocationRequest, + AppSchedulingModuleThread.getExecutor(), + mGenericLocationListener); + mLocating = true; + } else { + mHasFusedLocation = false; + } + if (locationManager != null + && locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) { + mHasGps = true; + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, + 1000, 5, mGpsLocationListener, mHandler.getLooper()); + mLocating = true; + } else { + mHasGps = false; + } + // If we have a location provider, we're all set, the listeners will move state + // forward. + if (mLocating) { + break; + } + // Otherwise, we have to move from locating into idle maintenance. } else { - mHasGps = false; - } - // If we have a location provider, we're all set, the listeners will move state - // forward. - if (mLocating) { - break; + mLocating = false; } - // Otherwise, we have to move from locating into idle maintenance. + // We're not doing any locating work, so move on to the next state. case STATE_LOCATING: cancelAlarmLocked(); cancelLocatingLocked(); @@ -5303,15 +5316,19 @@ public class DeviceIdleController extends SystemService pw.print(" "); pw.print(mStationaryListeners.size()); pw.println(" stationary listeners registered"); } - pw.print(" mLocating="); pw.print(mLocating); - pw.print(" mHasGps="); pw.print(mHasGps); - pw.print(" mHasFused="); pw.print(mHasFusedLocation); - pw.print(" mLocated="); pw.println(mLocated); - if (mLastGenericLocation != null) { - pw.print(" mLastGenericLocation="); pw.println(mLastGenericLocation); - } - if (mLastGpsLocation != null) { - pw.print(" mLastGpsLocation="); pw.println(mLastGpsLocation); + if (mIsLocationPrefetchEnabled) { + pw.print(" mLocating="); pw.print(mLocating); + pw.print(" mHasGps="); pw.print(mHasGps); + pw.print(" mHasFused="); pw.print(mHasFusedLocation); + pw.print(" mLocated="); pw.println(mLocated); + if (mLastGenericLocation != null) { + pw.print(" mLastGenericLocation="); pw.println(mLastGenericLocation); + } + if (mLastGpsLocation != null) { + pw.print(" mLastGpsLocation="); pw.println(mLastGpsLocation); + } + } else { + pw.println(" Location prefetching disabled"); } pw.print(" mState="); pw.print(stateToString(mState)); pw.print(" mLightState="); 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 577260e5106f..8a4b4647f94b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1560,7 +1560,8 @@ public class JobSchedulerService extends com.android.server.SystemService jobStatus.getJob().getMinLatencyMillis(), jobStatus.getEstimatedNetworkDownloadBytes(), jobStatus.getEstimatedNetworkUploadBytes(), - jobStatus.getWorkCount()); + jobStatus.getWorkCount(), + ActivityManager.processStateAmToProto(mUidProcStates.get(jobStatus.getUid()))); // If the job is immediately ready to run, then we can just immediately // put it in the pending list and try to schedule it. This is especially @@ -1935,6 +1936,7 @@ public class JobSchedulerService extends com.android.server.SystemService * {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of * currently scheduled jobs. */ + @GuardedBy("mLock") private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob, @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) { if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString()); @@ -1986,7 +1988,8 @@ public class JobSchedulerService extends com.android.server.SystemService cancelled.getJob().getMinLatencyMillis(), cancelled.getEstimatedNetworkDownloadBytes(), cancelled.getEstimatedNetworkUploadBytes(), - cancelled.getWorkCount()); + cancelled.getWorkCount(), + ActivityManager.processStateAmToProto(mUidProcStates.get(cancelled.getUid()))); } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { 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 fb36cdec490f..f95df4471c29 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -25,6 +25,7 @@ import android.Manifest; import android.annotation.BytesLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.Notification; import android.app.compat.CompatChanges; @@ -476,7 +477,8 @@ public final class JobServiceContext implements ServiceConnection { job.getJob().getMinLatencyMillis(), job.getEstimatedNetworkDownloadBytes(), job.getEstimatedNetworkUploadBytes(), - job.getWorkCount()); + job.getWorkCount(), + ActivityManager.processStateAmToProto(mService.getUidProcState(job.getUid()))); final String sourcePackage = job.getSourcePackageName(); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { final String componentPackage = job.getServiceComponent().getPackageName(); @@ -1447,7 +1449,9 @@ public final class JobServiceContext implements ServiceConnection { completedJob.getJob().getMinLatencyMillis(), completedJob.getEstimatedNetworkDownloadBytes(), completedJob.getEstimatedNetworkUploadBytes(), - completedJob.getWorkCount()); + completedJob.getWorkCount(), + ActivityManager + .processStateAmToProto(mService.getUidProcState(completedJob.getUid()))); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler", getId()); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 332c53cb224f..d97f71847592 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3632,18 +3632,12 @@ package android.view.autofill { } public final class AutofillManager { - method public void clearAutofillRequestCallback(); - method @RequiresPermission(android.Manifest.permission.PROVIDE_OWN_AUTOFILL_SUGGESTIONS) public void setAutofillRequestCallback(@NonNull java.util.concurrent.Executor, @NonNull android.view.autofill.AutofillRequestCallback); field public static final String ANY_HINT = "any"; field public static final int FLAG_SMART_SUGGESTION_OFF = 0; // 0x0 field public static final int FLAG_SMART_SUGGESTION_SYSTEM = 1; // 0x1 field public static final int MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS = 120000; // 0x1d4c0 } - public interface AutofillRequestCallback { - method public void onFillRequest(@Nullable android.view.inputmethod.InlineSuggestionsRequest, @NonNull android.os.CancellationSignal, @NonNull android.service.autofill.FillCallback); - } - } package android.view.contentcapture { @@ -3767,11 +3761,6 @@ package android.view.inputmethod { method @NonNull public static android.view.inputmethod.InlineSuggestionInfo newInlineSuggestionInfo(@NonNull android.widget.inline.InlinePresentationSpec, @NonNull String, @Nullable String[], @NonNull String, boolean); } - public static final class InlineSuggestionsRequest.Builder { - method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setClientSupported(boolean); - method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setServiceSupported(boolean); - } - public final class InlineSuggestionsResponse implements android.os.Parcelable { method @NonNull public static android.view.inputmethod.InlineSuggestionsResponse newInlineSuggestionsResponse(@NonNull java.util.List<android.view.inputmethod.InlineSuggestion>); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 521bf05d9d4f..e2ef00525902 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -771,6 +771,7 @@ public class ActivityManager { /** * The set of flags for process capability. + * Keep it in sync with ProcessCapability in atoms.proto. * @hide */ @IntDef(flag = true, prefix = { "PROCESS_CAPABILITY_" }, value = { diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index 99a7fa21b911..705b5ee84d01 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -407,7 +407,7 @@ public final class PendingIntent implements Parcelable { } private static void checkPendingIntent(int flags, @NonNull Intent intent, - @NonNull Context context) { + @NonNull Context context, boolean isActivityResultType) { final boolean isFlagImmutableSet = (flags & PendingIntent.FLAG_IMMUTABLE) != 0; final boolean isFlagMutableSet = (flags & PendingIntent.FLAG_MUTABLE) != 0; final String packageName = context.getPackageName(); @@ -428,11 +428,12 @@ public final class PendingIntent implements Parcelable { throw new IllegalArgumentException(msg); } - // For apps with target SDK < U, warn that creation or retrieval of a mutable - // implicit PendingIntent will be blocked from target SDK U onwards for security - // reasons. The block itself happens on the server side, but this warning has to - // stay here to preserve the client side stack trace for app developers. - if (isNewMutableDisallowedImplicitPendingIntent(flags, intent) + // For apps with target SDK < U, warn that creation or retrieval of a mutable implicit + // PendingIntent that is not of type {@link ActivityManager#INTENT_SENDER_ACTIVITY_RESULT} + // will be blocked from target SDK U onwards for security reasons. The block itself + // happens on the server side, but this warning has to stay here to preserve the client + // side stack trace for app developers. + if (isNewMutableDisallowedImplicitPendingIntent(flags, intent, isActivityResultType) && !Compatibility.isChangeEnabled(BLOCK_MUTABLE_IMPLICIT_PENDING_INTENT)) { String msg = "New mutable implicit PendingIntent: pkg=" + packageName + ", action=" + intent.getAction() @@ -445,7 +446,13 @@ public final class PendingIntent implements Parcelable { /** @hide */ public static boolean isNewMutableDisallowedImplicitPendingIntent(int flags, - @NonNull Intent intent) { + @NonNull Intent intent, boolean isActivityResultType) { + if (isActivityResultType) { + // Pending intents of type {@link ActivityManager#INTENT_SENDER_ACTIVITY_RESULT} + // should be ignored as they are intrinsically tied to a target which means they + // are already explicit. + return false; + } boolean isFlagNoCreateSet = (flags & PendingIntent.FLAG_NO_CREATE) != 0; boolean isFlagMutableSet = (flags & PendingIntent.FLAG_MUTABLE) != 0; boolean isImplicit = (intent.getComponent() == null) && (intent.getPackage() == null); @@ -534,7 +541,7 @@ public final class PendingIntent implements Parcelable { @NonNull Intent intent, int flags, Bundle options, UserHandle user) { String packageName = context.getPackageName(); String resolvedType = intent.resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intent, context); + checkPendingIntent(flags, intent, context, /* isActivityResultType */ false); try { intent.migrateExtraStreamToClipData(context); intent.prepareToLeaveProcess(context); @@ -668,7 +675,7 @@ public final class PendingIntent implements Parcelable { intents[i].migrateExtraStreamToClipData(context); intents[i].prepareToLeaveProcess(context); resolvedTypes[i] = intents[i].resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intents[i], context); + checkPendingIntent(flags, intents[i], context, /* isActivityResultType */ false); } try { IIntentSender target = @@ -721,7 +728,7 @@ public final class PendingIntent implements Parcelable { Intent intent, int flags, UserHandle userHandle) { String packageName = context.getPackageName(); String resolvedType = intent.resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intent, context); + checkPendingIntent(flags, intent, context, /* isActivityResultType */ false); try { intent.prepareToLeaveProcess(context); IIntentSender target = @@ -800,7 +807,7 @@ public final class PendingIntent implements Parcelable { Intent intent, int flags, int serviceKind) { String packageName = context.getPackageName(); String resolvedType = intent.resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intent, context); + checkPendingIntent(flags, intent, context, /* isActivityResultType */ false); try { intent.prepareToLeaveProcess(context); IIntentSender target = diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java index a34a50c4b7b0..be1d8b8ad7d3 100644 --- a/core/java/android/app/WallpaperColors.java +++ b/core/java/android/app/WallpaperColors.java @@ -213,9 +213,17 @@ public final class WallpaperColors implements Parcelable { .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) .generate(); } else { + // in any case, always use between 5 and 128 clusters + int minClusters = 5; + int maxClusters = 128; + + // if the bitmap is very small, use bitmapArea/16 clusters instead of 128 + int minPixelsPerCluster = 16; + int numberOfColors = Math.max(minClusters, + Math.min(maxClusters, bitmapArea / minPixelsPerCluster)); palette = Palette .from(bitmap, new CelebiQuantizer()) - .maximumColorCount(128) + .maximumColorCount(numberOfColors) .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) .generate(); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index de66f050c007..56f6f8206d30 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2072,6 +2072,25 @@ public class PackageInstaller { return new InstallInfo(result); } + /** + * Parse a single APK file passed as an FD to get install relevant information about + * the package wrapped in {@link InstallInfo}. + * @throws PackageParsingException if the package source file(s) provided is(are) not valid, + * or the parser isn't able to parse the supplied source(s). + * @hide + */ + @NonNull + public InstallInfo readInstallInfo(@NonNull ParcelFileDescriptor pfd, + @Nullable String debugPathName, int flags) throws PackageParsingException { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult<PackageLite> result = ApkLiteParseUtils.parseMonolithicPackageLite(input, + pfd.getFileDescriptor(), debugPathName, flags); + if (result.isError()) { + throw new PackageParsingException(result.getErrorCode(), result.getErrorMessage()); + } + return new InstallInfo(result); + } + // (b/239722738) This class serves as a bridge between the PackageLite class, which // is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java) // This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or @@ -2125,6 +2144,21 @@ public class PackageInstaller { public long calculateInstalledSize(@NonNull SessionParams params) throws IOException { return InstallLocationUtils.calculateInstalledSize(mPkg, params.abiOverride); } + + /** + * @param params {@link SessionParams} of the installation + * @param pfd of an APK opened for read + * @return Total disk space occupied by an application after installation. + * Includes the size of the raw APKs, possibly unpacked resources, raw dex metadata files, + * and all relevant native code. + * @throws IOException when size of native binaries cannot be calculated. + * @hide + */ + public long calculateInstalledSize(@NonNull SessionParams params, + @NonNull ParcelFileDescriptor pfd) throws IOException { + return InstallLocationUtils.calculateInstalledSize(mPkg, params.abiOverride, + pfd.getFileDescriptor()); + } } /** diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index a4339d41dfd2..d209b35ac810 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -109,7 +109,7 @@ public class ApkLiteParseUtils { } /** - * Parse lightweight details about a single APK files. + * Parse lightweight details about a single APK file. */ public static ParseResult<PackageLite> parseMonolithicPackageLite(ParseInput input, File packageFile, int flags) { @@ -135,6 +135,33 @@ public class ApkLiteParseUtils { } /** + * Parse lightweight details about a single APK file passed as an FD. + */ + public static ParseResult<PackageLite> parseMonolithicPackageLite(ParseInput input, + FileDescriptor packageFd, String debugPathName, int flags) { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parseApkLite"); + try { + final ParseResult<ApkLite> result = parseApkLite(input, packageFd, debugPathName, + flags); + if (result.isError()) { + return input.error(result); + } + + final ApkLite baseApk = result.getResult(); + final String packagePath = debugPathName; + return input.success( + new PackageLite(packagePath, baseApk.getPath(), baseApk, null /* splitNames */, + null /* isFeatureSplits */, null /* usesSplitNames */, + null /* configForSplit */, null /* splitApkPaths */, + null /* splitRevisionCodes */, baseApk.getTargetSdkVersion(), + null /* requiredSplitTypes */, null, /* splitTypes */ + baseApk.isAllowUpdateOwnership())); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + } + + /** * Parse lightweight details about a directory of APKs. * * @param packageDir is the folder that contains split apks for a regular app diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index 09d2db89a043..9ebb0585afd0 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -30,6 +30,8 @@ import com.android.internal.util.AnnotationValidations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; /** * Contains information about the request that initiated this UX flow. @@ -64,6 +66,9 @@ public final class RequestInfo implements Parcelable { @Nullable private final CreateCredentialRequest mCreateCredentialRequest; + @NonNull + private final List<String> mDefaultProviderIds; + @Nullable private final GetCredentialRequest mGetCredentialRequest; @@ -83,7 +88,8 @@ public final class RequestInfo implements Parcelable { @NonNull String appPackageName) { return new RequestInfo( token, TYPE_CREATE, appPackageName, createCredentialRequest, null, - /*hasPermissionToOverrideDefault=*/ false); + /*hasPermissionToOverrideDefault=*/ false, + /*defaultProviderIds=*/ new ArrayList<>()); } /** @@ -94,10 +100,11 @@ public final class RequestInfo implements Parcelable { @NonNull public static RequestInfo newCreateRequestInfo( @NonNull IBinder token, @NonNull CreateCredentialRequest createCredentialRequest, - @NonNull String appPackageName, boolean hasPermissionToOverrideDefault) { + @NonNull String appPackageName, boolean hasPermissionToOverrideDefault, + @NonNull List<String> defaultProviderIds) { return new RequestInfo( token, TYPE_CREATE, appPackageName, createCredentialRequest, null, - hasPermissionToOverrideDefault); + hasPermissionToOverrideDefault, defaultProviderIds); } /** Creates new {@code RequestInfo} for a get-credential flow. */ @@ -107,7 +114,8 @@ public final class RequestInfo implements Parcelable { @NonNull String appPackageName) { return new RequestInfo( token, TYPE_GET, appPackageName, null, getCredentialRequest, - /*hasPermissionToOverrideDefault=*/ false); + /*hasPermissionToOverrideDefault=*/ false, + /*defaultProviderIds=*/ new ArrayList<>()); } @@ -149,6 +157,20 @@ public final class RequestInfo implements Parcelable { } /** + * Returns default provider identifier (flattened component name) configured from the user + * settings. + * + * Will only be possibly non-empty for the create use case. Not meaningful for the sign-in use + * case. + * + * @hide + */ + @NonNull + public List<String> getDefaultProviderIds() { + return mDefaultProviderIds; + } + + /** * Returns the non-null GetCredentialRequest when the type of the request is {@link * #TYPE_GET}, or null otherwise. */ @@ -161,13 +183,15 @@ public final class RequestInfo implements Parcelable { @NonNull String appPackageName, @Nullable CreateCredentialRequest createCredentialRequest, @Nullable GetCredentialRequest getCredentialRequest, - boolean hasPermissionToOverrideDefault) { + boolean hasPermissionToOverrideDefault, + @NonNull List<String> defaultProviderIds) { mToken = token; mType = type; mAppPackageName = appPackageName; mCreateCredentialRequest = createCredentialRequest; mGetCredentialRequest = getCredentialRequest; mHasPermissionToOverrideDefault = hasPermissionToOverrideDefault; + mDefaultProviderIds = defaultProviderIds == null ? new ArrayList<>() : defaultProviderIds; } private RequestInfo(@NonNull Parcel in) { @@ -188,6 +212,7 @@ public final class RequestInfo implements Parcelable { mCreateCredentialRequest = createCredentialRequest; mGetCredentialRequest = getCredentialRequest; mHasPermissionToOverrideDefault = in.readBoolean(); + mDefaultProviderIds = in.createStringArrayList(); } @Override @@ -198,6 +223,7 @@ public final class RequestInfo implements Parcelable { dest.writeTypedObject(mCreateCredentialRequest, flags); dest.writeTypedObject(mGetCredentialRequest, flags); dest.writeBoolean(mHasPermissionToOverrideDefault); + dest.writeStringList(mDefaultProviderIds); } @Override diff --git a/core/java/android/net/metrics/WakeupStats.java b/core/java/android/net/metrics/WakeupStats.java index bb36536fe2ce..fac747c8f1a3 100644 --- a/core/java/android/net/metrics/WakeupStats.java +++ b/core/java/android/net/metrics/WakeupStats.java @@ -80,18 +80,20 @@ public class WakeupStats { break; } - switch (ev.dstHwAddr.getAddressType()) { - case MacAddress.TYPE_UNICAST: - l2UnicastCount++; - break; - case MacAddress.TYPE_MULTICAST: - l2MulticastCount++; - break; - case MacAddress.TYPE_BROADCAST: - l2BroadcastCount++; - break; - default: - break; + if (ev.dstHwAddr != null) { + switch (ev.dstHwAddr.getAddressType()) { + case MacAddress.TYPE_UNICAST: + l2UnicastCount++; + break; + case MacAddress.TYPE_MULTICAST: + l2MulticastCount++; + break; + case MacAddress.TYPE_BROADCAST: + l2BroadcastCount++; + break; + default: + break; + } } increment(ethertypes, ev.ethertype); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 79e7574647b3..4a46beb670de 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -4677,16 +4677,22 @@ public final class Settings { "display_color_mode_vendor_hint"; /** - * Whether or not the peak refresh rate should be forced. 0=no, 1=yes + * The user selected min refresh rate in frames per second. + * + * If this isn't set, 0 will be used. * @hide */ - public static final String FORCE_PEAK_REFRESH_RATE = "force_peak_refresh_rate"; + @Readable + public static final String MIN_REFRESH_RATE = "min_refresh_rate"; /** - * Whether or not the peak refresh rate should be used for some content. 0=no, 1=yes + * The user selected peak refresh rate in frames per second. + * + * If this isn't set, the system falls back to a device specific default. * @hide */ - public static final String SMOOTH_DISPLAY = "smooth_display"; + @Readable + public static final String PEAK_REFRESH_RATE = "peak_refresh_rate"; /** * The amount of time in milliseconds before the device goes to sleep or begins diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java index 4a848dd86463..8afae74735e2 100644 --- a/core/java/android/service/autofill/FillRequest.java +++ b/core/java/android/service/autofill/FillRequest.java @@ -97,8 +97,6 @@ public final class FillRequest implements Parcelable { */ public static final @RequestFlags int FLAG_VIEW_NOT_FOCUSED = 0x10; - // The flag value 0x20 has been defined in AutofillManager. - /** * Indicates the request supports fill dialog presentation for the fields, the * system will send the request when the activity just started. diff --git a/core/java/android/service/credentials/CredentialProviderInfoFactory.java b/core/java/android/service/credentials/CredentialProviderInfoFactory.java index 1a1df6f5f989..751c675b28f4 100644 --- a/core/java/android/service/credentials/CredentialProviderInfoFactory.java +++ b/core/java/android/service/credentials/CredentialProviderInfoFactory.java @@ -75,12 +75,13 @@ public final class CredentialProviderInfoFactory { /** * Constructs an information instance of the credential provider. * - * @param context the context object + * @param context the context object * @param serviceComponent the serviceComponent of the provider service - * @param userId the android userId for which the current process is running + * @param userId the android userId for which the current process is running * @param isSystemProvider whether this provider is a system provider * @throws PackageManager.NameNotFoundException If provider service is not found - * @throws SecurityException If provider does not require the relevant permission + * @throws SecurityException If provider does not require the relevant + * permission */ public static CredentialProviderInfo create( @NonNull Context context, @@ -99,13 +100,15 @@ public final class CredentialProviderInfoFactory { /** * Constructs an information instance of the credential provider. * - * @param context the context object - * @param serviceInfo the service info for the provider app. This must be retrieved from the - * {@code PackageManager} - * @param isSystemProvider whether the provider app is a system provider + * @param context the context object + * @param serviceInfo the service info for the provider app. This must + * be retrieved from the + * {@code PackageManager} + * @param isSystemProvider whether the provider app is a system provider * @param disableSystemAppVerificationForTests whether to disable system app permission - * verification so that tests can install system providers - * @param isEnabled whether the user enabled this provider + * verification so that tests can install system + * providers + * @param isEnabled whether the user enabled this provider * @throws SecurityException If provider does not require the relevant permission */ public static CredentialProviderInfo create( @@ -374,7 +377,6 @@ public final class CredentialProviderInfoFactory { if (appInfo == null || serviceInfo == null) { continue; } - services.add(serviceInfo); } catch (SecurityException | PackageManager.NameNotFoundException e) { Slog.e(TAG, "Error getting info for " + serviceInfo, e); diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 53a5fd5c634d..cf2e6a639fce 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -18,7 +18,6 @@ package android.service.credentials; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; -import android.Manifest; import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.SdkConstant; @@ -35,7 +34,7 @@ import android.os.ICancellationSignal; import android.os.Looper; import android.os.OutcomeReceiver; import android.os.RemoteException; -import android.util.Log; +import android.util.Slog; import java.util.Objects; @@ -226,7 +225,7 @@ public abstract class CredentialProviderService extends Service { if (SERVICE_INTERFACE.equals(intent.getAction())) { return mInterface.asBinder(); } - Log.d(TAG, "Failed to bind with intent: " + intent); + Slog.w(TAG, "Failed to bind with intent: " + intent); return null; } @@ -252,11 +251,6 @@ public abstract class CredentialProviderService extends Service { GetCredentialException>() { @Override public void onResult(BeginGetCredentialResponse result) { - // If provider service does not possess the HYBRID permission, this - // check will throw an exception in the provider process. - if (result.getRemoteCredentialEntry() != null) { - enforceRemoteEntryPermission(); - } try { callback.onSuccess(result); } catch (RemoteException e) { @@ -274,15 +268,6 @@ public abstract class CredentialProviderService extends Service { } )); } - private void enforceRemoteEntryPermission() { - String permission = - Manifest.permission.PROVIDE_REMOTE_CREDENTIALS; - getApplicationContext().enforceCallingOrSelfPermission( - permission, - String.format("Provider must have %s, in order to set a " - + "remote entry", permission) - ); - } @Override public void onBeginCreateCredential(BeginCreateCredentialRequest request, @@ -305,11 +290,6 @@ public abstract class CredentialProviderService extends Service { BeginCreateCredentialResponse, CreateCredentialException>() { @Override public void onResult(BeginCreateCredentialResponse result) { - // If provider service does not possess the HYBRID permission, this - // check will throw an exception in the provider process. - if (result.getRemoteCreateEntry() != null) { - enforceRemoteEntryPermission(); - } try { callback.onSuccess(result); } catch (RemoteException e) { diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 6061a0f93312..7822ddeb73d8 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -1063,21 +1063,6 @@ public class VoiceInteractionService extends Service { mActiveVisualQueryDetector = null; } mActiveDetectors.remove(detector); - shutdownHotwordDetectionServiceIfRequiredLocked(); - } - } - - private void shutdownHotwordDetectionServiceIfRequiredLocked() { - for (HotwordDetector detector : mActiveDetectors) { - if (detector.isUsingSandboxedDetectionService()) { - return; - } - } - - try { - mSystemService.shutdownHotwordDetectionService(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8f20e2d044ac..153bfde07758 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11325,7 +11325,7 @@ public final class ViewRootImpl implements ViewParent, // to sync the same frame in the same BBQ. That shouldn't be possible, but // if it did happen, invoke markSyncReady so the active SSG doesn't get // stuck. - Log.e(mTag, "Unable to syncNextTransaction. Possibly something else is" + Log.w(mTag, "Unable to syncNextTransaction. Possibly something else is" + " trying to sync?"); surfaceSyncGroup.markSyncReady(); } diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index f7b7d3387938..e39b3a182b28 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -16,7 +16,6 @@ package android.view.autofill; -import static android.Manifest.permission.PROVIDE_OWN_AUTOFILL_SUGGESTIONS; import static android.service.autofill.FillRequest.FLAG_IME_SHOWING; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; @@ -30,7 +29,6 @@ import static android.view.autofill.Helper.sVerbose; import static android.view.autofill.Helper.toList; import android.accessibilityservice.AccessibilityServiceInfo; -import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -52,21 +50,16 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.metrics.LogMaker; -import android.os.Binder; import android.os.Build; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; -import android.os.ICancellationSignal; import android.os.Looper; import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; import android.service.autofill.AutofillService; -import android.service.autofill.FillCallback; import android.service.autofill.FillEventHistory; -import android.service.autofill.IFillCallback; import android.service.autofill.UserData; import android.text.TextUtils; import android.util.ArrayMap; @@ -87,7 +80,6 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityWindowInfo; -import android.view.inputmethod.InlineSuggestionsRequest; import android.view.inputmethod.InputMethodManager; import android.widget.CheckBox; import android.widget.DatePicker; @@ -187,12 +179,6 @@ import sun.misc.Cleaner; * shows an autofill save UI if the value of savable views have changed. If the user selects the * option to Save, the current value of the views is then sent to the autofill service. * - * <p>There is another choice for the application to provide it's datasets to the Autofill framework - * by setting an {@link AutofillRequestCallback} through - * {@link #setAutofillRequestCallback(Executor, AutofillRequestCallback)}. The application can use - * its callback instead of the default {@link AutofillService}. See - * {@link AutofillRequestCallback} for more details. - * * <h3 id="additional-notes">Additional notes</h3> * * <p>It is safe to call <code>AutofillManager</code> methods from any thread. @@ -326,7 +312,6 @@ public final class AutofillManager { /** @hide */ public static final int FLAG_ADD_CLIENT_DEBUG = 0x2; /** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4; /** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY = 0x8; - /** @hide */ public static final int FLAG_ENABLED_CLIENT_SUGGESTIONS = 0x20; // NOTE: flag below is used by the session start receiver only, hence it can have values above /** @hide */ public static final int RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY = 0x1; @@ -653,11 +638,6 @@ public final class AutofillManager { @GuardedBy("mLock") private boolean mEnabledForAugmentedAutofillOnly; - @GuardedBy("mLock") - @Nullable private AutofillRequestCallback mAutofillRequestCallback; - @GuardedBy("mLock") - @Nullable private Executor mRequestCallbackExecutor; - /** * Indicates whether there is already a field to do a fill request after * the activity started. @@ -2338,44 +2318,6 @@ public final class AutofillManager { return new AutofillId(parent.getAutofillViewId(), virtualId); } - /** - * Sets the client's suggestions callback for autofill. - * - * @see AutofillRequestCallback - * - * @param executor specifies the thread upon which the callbacks will be invoked. - * @param callback which handles autofill request to provide client's suggestions. - * - * @hide - */ - @TestApi - @RequiresPermission(PROVIDE_OWN_AUTOFILL_SUGGESTIONS) - public void setAutofillRequestCallback(@NonNull @CallbackExecutor Executor executor, - @NonNull AutofillRequestCallback callback) { - if (mContext.checkSelfPermission(PROVIDE_OWN_AUTOFILL_SUGGESTIONS) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires PROVIDE_OWN_AUTOFILL_SUGGESTIONS permission!"); - } - - synchronized (mLock) { - mRequestCallbackExecutor = executor; - mAutofillRequestCallback = callback; - } - } - - /** - * clears the client's suggestions callback for autofill. - * - * @hide - */ - @TestApi - public void clearAutofillRequestCallback() { - synchronized (mLock) { - mRequestCallbackExecutor = null; - mAutofillRequestCallback = null; - } - } - @GuardedBy("mLock") private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds, @NonNull AutofillValue value, int flags) { @@ -2436,13 +2378,6 @@ public final class AutofillManager { } } - if (mAutofillRequestCallback != null) { - if (sDebug) { - Log.d(TAG, "startSession with the client suggestions provider"); - } - flags |= FLAG_ENABLED_CLIENT_SUGGESTIONS; - } - mService.startSession(client.autofillClientGetActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), mCallback != null, flags, clientActivity, @@ -2796,28 +2731,6 @@ public final class AutofillManager { } } - private void onFillRequest(InlineSuggestionsRequest request, - CancellationSignal cancellationSignal, FillCallback callback) { - final AutofillRequestCallback autofillRequestCallback; - final Executor executor; - synchronized (mLock) { - autofillRequestCallback = mAutofillRequestCallback; - executor = mRequestCallbackExecutor; - } - if (autofillRequestCallback != null && executor != null) { - final long ident = Binder.clearCallingIdentity(); - try { - executor.execute(() -> - autofillRequestCallback.onFillRequest( - request, cancellationSignal, callback)); - } finally { - Binder.restoreCallingIdentity(ident); - } - } else { - callback.onSuccess(null); - } - } - /** @hide */ public static final int SET_STATE_FLAG_ENABLED = 0x01; /** @hide */ @@ -4374,23 +4287,6 @@ public final class AutofillManager { } @Override - public void requestFillFromClient(int id, InlineSuggestionsRequest request, - IFillCallback callback) { - final AutofillManager afm = mAfm.get(); - if (afm != null) { - ICancellationSignal transport = CancellationSignal.createTransport(); - try { - callback.onCancellable(transport); - } catch (RemoteException e) { - Slog.w(TAG, "Error requesting a cancellation", e); - } - - afm.onFillRequest(request, CancellationSignal.fromTransport(transport), - new FillCallback(callback, id)); - } - } - - @Override public void notifyFillDialogTriggerIds(List<AutofillId> ids) { final AutofillManager afm = mAfm.get(); if (afm != null) { diff --git a/core/java/android/view/autofill/AutofillRequestCallback.java b/core/java/android/view/autofill/AutofillRequestCallback.java deleted file mode 100644 index 10a088b4ebfa..000000000000 --- a/core/java/android/view/autofill/AutofillRequestCallback.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view.autofill; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.TestApi; -import android.os.CancellationSignal; -import android.service.autofill.FillCallback; -import android.view.inputmethod.InlineSuggestionsRequest; - -/** - * <p>This class is used to provide some input suggestions to the Autofill framework. - * - * <P>When the user is requested to input something, Autofill will try to query input suggestions - * for the user choosing. If the application want to provide some internal input suggestions, - * implements this callback and register via - * {@link AutofillManager#setAutofillRequestCallback(java.util.concurrent.Executor, - * AutofillRequestCallback)}. Autofill will callback the - * {@link #onFillRequest(InlineSuggestionsRequest, CancellationSignal, FillCallback)} to request - * input suggestions. - * - * <P>To make sure the callback to take effect, must register before the autofill session starts. - * If the autofill session is started, calls {@link AutofillManager#cancel()} to finish current - * session, and then the callback will be used at the next restarted session. - * - * <P>To create a {@link android.service.autofill.FillResponse}, application should fetch - * {@link AutofillId}s from its view structure. Below is an example: - * <pre class="prettyprint"> - * AutofillId usernameId = findViewById(R.id.username).getAutofillId(); - * AutofillId passwordId = findViewById(R.id.password).getAutofillId(); - * </pre> - * To learn more about creating a {@link android.service.autofill.FillResponse}, read - * <a href="/guide/topics/text/autofill-services#fill">Fill out client views</a>. - * - * <P>To fallback to the default {@link android.service.autofill.AutofillService}, just respond - * a null of the {@link android.service.autofill.FillResponse}. And then Autofill will do a fill - * request with the default {@link android.service.autofill.AutofillService}. Or clear the callback - * from {@link AutofillManager} via {@link AutofillManager#clearAutofillRequestCallback()}. If the - * client would like to keep no suggestions for the field, respond with an empty - * {@link android.service.autofill.FillResponse} which has no dataset. - * - * <P>IMPORTANT: This should not be used for displaying anything other than input suggestions, or - * the keyboard may choose to block your app from the inline strip. - * - * @hide - */ -@TestApi -public interface AutofillRequestCallback { - /** - * Called by the Android system to decide if a screen can be autofilled by the callback. - * - * @param inlineSuggestionsRequest the {@link InlineSuggestionsRequest request} to handle if - * currently inline suggestions are supported and can be displayed. - * @param cancellationSignal signal for observing cancellation requests. The system will use - * this to notify you that the fill result is no longer needed and you should stop - * handling this fill request in order to save resources. - * @param callback object used to notify the result of the request. - */ - void onFillRequest(@Nullable InlineSuggestionsRequest inlineSuggestionsRequest, - @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback); -} diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl index 2e5967cc32d1..51afe4cf784d 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -24,11 +24,9 @@ import android.content.Intent; import android.content.IntentSender; import android.graphics.Rect; import android.os.IBinder; -import android.service.autofill.IFillCallback; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; import android.view.autofill.IAutofillWindowPresenter; -import android.view.inputmethod.InlineSuggestionsRequest; import android.view.KeyEvent; import com.android.internal.os.IResultReceiver; @@ -144,12 +142,6 @@ oneway interface IAutoFillManagerClient { void requestShowSoftInput(in AutofillId id); /** - * Requests to determine if a screen can be autofilled by the client app. - */ - void requestFillFromClient(int id, in InlineSuggestionsRequest request, - in IFillCallback callback); - - /** * Notifies autofill ids that require to show the fill dialog. */ void notifyFillDialogTriggerIds(in List<AutofillId> ids); diff --git a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java index 77a2b5b877be..581feca2de55 100644 --- a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java +++ b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java @@ -112,22 +112,6 @@ public final class InlineSuggestionsRequest implements Parcelable { private @Nullable InlinePresentationSpec mInlineTooltipPresentationSpec; /** - * Whether the IME supports inline suggestions from the default Autofill service that - * provides the input view. - * - * Note: The default value is {@code true}. - */ - private boolean mServiceSupported; - - /** - * Whether the IME supports inline suggestions from the application that provides the - * input view. - * - * Note: The default value is {@code true}. - */ - private boolean mClientSupported; - - /** * @hide * @see {@link #mHostInputToken}. */ @@ -221,15 +205,9 @@ public final class InlineSuggestionsRequest implements Parcelable { return Bundle.EMPTY; } - private static boolean defaultServiceSupported() { - return true; - } - - private static boolean defaultClientSupported() { - return true; - } - - /** @hide */ + /** + * @hide + */ abstract static class BaseBuilder { abstract Builder setInlinePresentationSpecs( @NonNull List<android.widget.inline.InlinePresentationSpec> specs); @@ -241,25 +219,14 @@ public final class InlineSuggestionsRequest implements Parcelable { abstract Builder setHostDisplayId(int value); } - /** @hide */ - public boolean isServiceSupported() { - return mServiceSupported; - } - - /** @hide */ - public boolean isClientSupported() { - return mClientSupported; - } - - - // Code below generated by codegen v1.0.22. + // Code below generated by codegen v1.0.23. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code // // To regenerate run: - // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java + // $ codegen $ANDROID_BUILD_TOP/./frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java // // To exclude the generated code from IntelliJ auto-formatting enable (one-time): // Settings > Editor > Code Style > Formatter Control @@ -275,9 +242,7 @@ public final class InlineSuggestionsRequest implements Parcelable { @NonNull Bundle extras, @Nullable IBinder hostInputToken, int hostDisplayId, - @Nullable InlinePresentationSpec inlineTooltipPresentationSpec, - boolean serviceSupported, - boolean clientSupported) { + @Nullable InlinePresentationSpec inlineTooltipPresentationSpec) { this.mMaxSuggestionCount = maxSuggestionCount; this.mInlinePresentationSpecs = inlinePresentationSpecs; com.android.internal.util.AnnotationValidations.validate( @@ -294,8 +259,6 @@ public final class InlineSuggestionsRequest implements Parcelable { this.mHostInputToken = hostInputToken; this.mHostDisplayId = hostDisplayId; this.mInlineTooltipPresentationSpec = inlineTooltipPresentationSpec; - this.mServiceSupported = serviceSupported; - this.mClientSupported = clientSupported; onConstructed(); } @@ -379,9 +342,7 @@ public final class InlineSuggestionsRequest implements Parcelable { } /** - * The {@link InlinePresentationSpec} for the inline suggestion tooltip in the response. - * - * @see android.service.autofill.InlinePresentation#createTooltipPresentation(Slice, InlinePresentationSpec) + * Specifies the UI specification for the inline suggestion tooltip in the response. */ @DataClass.Generated.Member public @Nullable InlinePresentationSpec getInlineTooltipPresentationSpec() { @@ -402,9 +363,7 @@ public final class InlineSuggestionsRequest implements Parcelable { "extras = " + mExtras + ", " + "hostInputToken = " + mHostInputToken + ", " + "hostDisplayId = " + mHostDisplayId + ", " + - "inlineTooltipPresentationSpec = " + mInlineTooltipPresentationSpec + ", " + - "serviceSupported = " + mServiceSupported + ", " + - "clientSupported = " + mClientSupported + + "inlineTooltipPresentationSpec = " + mInlineTooltipPresentationSpec + " }"; } @@ -428,9 +387,7 @@ public final class InlineSuggestionsRequest implements Parcelable { && extrasEquals(that.mExtras) && java.util.Objects.equals(mHostInputToken, that.mHostInputToken) && mHostDisplayId == that.mHostDisplayId - && java.util.Objects.equals(mInlineTooltipPresentationSpec, that.mInlineTooltipPresentationSpec) - && mServiceSupported == that.mServiceSupported - && mClientSupported == that.mClientSupported; + && java.util.Objects.equals(mInlineTooltipPresentationSpec, that.mInlineTooltipPresentationSpec); } @Override @@ -448,8 +405,6 @@ public final class InlineSuggestionsRequest implements Parcelable { _hash = 31 * _hash + java.util.Objects.hashCode(mHostInputToken); _hash = 31 * _hash + mHostDisplayId; _hash = 31 * _hash + java.util.Objects.hashCode(mInlineTooltipPresentationSpec); - _hash = 31 * _hash + Boolean.hashCode(mServiceSupported); - _hash = 31 * _hash + Boolean.hashCode(mClientSupported); return _hash; } @@ -460,8 +415,6 @@ public final class InlineSuggestionsRequest implements Parcelable { // void parcelFieldName(Parcel dest, int flags) { ... } int flg = 0; - if (mServiceSupported) flg |= 0x100; - if (mClientSupported) flg |= 0x200; if (mHostInputToken != null) flg |= 0x20; if (mInlineTooltipPresentationSpec != null) flg |= 0x80; dest.writeInt(flg); @@ -487,11 +440,9 @@ public final class InlineSuggestionsRequest implements Parcelable { // static FieldType unparcelFieldName(Parcel in) { ... } int flg = in.readInt(); - boolean serviceSupported = (flg & 0x100) != 0; - boolean clientSupported = (flg & 0x200) != 0; int maxSuggestionCount = in.readInt(); List<InlinePresentationSpec> inlinePresentationSpecs = new ArrayList<>(); - in.readParcelableList(inlinePresentationSpecs, InlinePresentationSpec.class.getClassLoader(), android.widget.inline.InlinePresentationSpec.class); + in.readParcelableList(inlinePresentationSpecs, InlinePresentationSpec.class.getClassLoader()); String hostPackageName = in.readString(); LocaleList supportedLocales = (LocaleList) in.readTypedObject(LocaleList.CREATOR); Bundle extras = in.readBundle(); @@ -515,8 +466,6 @@ public final class InlineSuggestionsRequest implements Parcelable { this.mHostInputToken = hostInputToken; this.mHostDisplayId = hostDisplayId; this.mInlineTooltipPresentationSpec = inlineTooltipPresentationSpec; - this.mServiceSupported = serviceSupported; - this.mClientSupported = clientSupported; onConstructed(); } @@ -550,8 +499,6 @@ public final class InlineSuggestionsRequest implements Parcelable { private @Nullable IBinder mHostInputToken; private int mHostDisplayId; private @Nullable InlinePresentationSpec mInlineTooltipPresentationSpec; - private boolean mServiceSupported; - private boolean mClientSupported; private long mBuilderFieldsSet = 0L; @@ -684,9 +631,7 @@ public final class InlineSuggestionsRequest implements Parcelable { } /** - * The {@link InlinePresentationSpec} for the inline suggestion tooltip in the response. - * - * @see android.service.autofill.InlinePresentation#createTooltipPresentation(Slice, InlinePresentationSpec)s + * Specifies the UI specification for the inline suggestion tooltip in the response. */ @DataClass.Generated.Member public @NonNull Builder setInlineTooltipPresentationSpec(@NonNull InlinePresentationSpec value) { @@ -696,44 +641,10 @@ public final class InlineSuggestionsRequest implements Parcelable { return this; } - /** - * Whether the IME supports inline suggestions from the default Autofill service that - * provides the input view. - * - * Note: The default value is {@code true}. - * - * @hide - */ - @TestApi - @DataClass.Generated.Member - public @NonNull Builder setServiceSupported(boolean value) { - checkNotUsed(); - mBuilderFieldsSet |= 0x100; - mServiceSupported = value; - return this; - } - - /** - * Whether the IME supports inline suggestions from the application that provides the - * input view. - * - * Note: The default value is {@code true}. - * - * @hide - */ - @TestApi - @DataClass.Generated.Member - public @NonNull Builder setClientSupported(boolean value) { - checkNotUsed(); - mBuilderFieldsSet |= 0x200; - mClientSupported = value; - return this; - } - /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull InlineSuggestionsRequest build() { checkNotUsed(); - mBuilderFieldsSet |= 0x400; // Mark builder used + mBuilderFieldsSet |= 0x100; // Mark builder used if ((mBuilderFieldsSet & 0x1) == 0) { mMaxSuggestionCount = defaultMaxSuggestionCount(); @@ -756,12 +667,6 @@ public final class InlineSuggestionsRequest implements Parcelable { if ((mBuilderFieldsSet & 0x80) == 0) { mInlineTooltipPresentationSpec = defaultInlineTooltipPresentationSpec(); } - if ((mBuilderFieldsSet & 0x100) == 0) { - mServiceSupported = defaultServiceSupported(); - } - if ((mBuilderFieldsSet & 0x200) == 0) { - mClientSupported = defaultClientSupported(); - } InlineSuggestionsRequest o = new InlineSuggestionsRequest( mMaxSuggestionCount, mInlinePresentationSpecs, @@ -770,14 +675,12 @@ public final class InlineSuggestionsRequest implements Parcelable { mExtras, mHostInputToken, mHostDisplayId, - mInlineTooltipPresentationSpec, - mServiceSupported, - mClientSupported); + mInlineTooltipPresentationSpec); return o; } private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x400) != 0) { + if ((mBuilderFieldsSet & 0x100) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } @@ -785,10 +688,10 @@ public final class InlineSuggestionsRequest implements Parcelable { } @DataClass.Generated( - time = 1615798784918L, - codegenVersion = "1.0.22", + time = 1682382296877L, + codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java", - inputSignatures = "public static final int SUGGESTION_COUNT_UNLIMITED\nprivate final int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.widget.inline.InlinePresentationSpec> mInlinePresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.NonNull android.os.Bundle mExtras\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate int mHostDisplayId\nprivate @android.annotation.Nullable android.widget.inline.InlinePresentationSpec mInlineTooltipPresentationSpec\nprivate boolean mServiceSupported\nprivate boolean mClientSupported\nprivate static final @android.compat.annotation.ChangeId @android.compat.annotation.EnabledSince long IME_AUTOFILL_DEFAULT_SUPPORTED_LOCALES_IS_EMPTY\npublic void setHostInputToken(android.os.IBinder)\nprivate boolean extrasEquals(android.os.Bundle)\nprivate void parcelHostInputToken(android.os.Parcel,int)\nprivate @android.annotation.Nullable android.os.IBinder unparcelHostInputToken(android.os.Parcel)\npublic void setHostDisplayId(int)\nprivate void onConstructed()\npublic void filterContentTypes()\nprivate static int defaultMaxSuggestionCount()\nprivate static java.lang.String defaultHostPackageName()\nprivate static android.widget.inline.InlinePresentationSpec defaultInlineTooltipPresentationSpec()\nprivate static android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable int defaultHostDisplayId()\nprivate static @android.annotation.NonNull android.os.Bundle defaultExtras()\nprivate static boolean defaultServiceSupported()\nprivate static boolean defaultClientSupported()\npublic boolean isServiceSupported()\npublic boolean isClientSupported()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List<android.widget.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List<android.widget.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []") + inputSignatures = "public static final int SUGGESTION_COUNT_UNLIMITED\nprivate final int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.widget.inline.InlinePresentationSpec> mInlinePresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.NonNull android.os.Bundle mExtras\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate int mHostDisplayId\nprivate @android.annotation.Nullable android.widget.inline.InlinePresentationSpec mInlineTooltipPresentationSpec\nprivate static final @android.compat.annotation.ChangeId @android.compat.annotation.EnabledSince long IME_AUTOFILL_DEFAULT_SUPPORTED_LOCALES_IS_EMPTY\npublic void setHostInputToken(android.os.IBinder)\nprivate boolean extrasEquals(android.os.Bundle)\nprivate void parcelHostInputToken(android.os.Parcel,int)\nprivate @android.annotation.Nullable android.os.IBinder unparcelHostInputToken(android.os.Parcel)\npublic void setHostDisplayId(int)\nprivate void onConstructed()\npublic void filterContentTypes()\nprivate static int defaultMaxSuggestionCount()\nprivate static java.lang.String defaultHostPackageName()\nprivate static android.widget.inline.InlinePresentationSpec defaultInlineTooltipPresentationSpec()\nprivate static android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable int defaultHostDisplayId()\nprivate static @android.annotation.NonNull android.os.Bundle defaultExtras()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List<android.widget.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List<android.widget.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 8f270f5c5eeb..62f3c909dd4f 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -412,4 +412,32 @@ public class InputConnectionWrapper implements InputConnection { public boolean setImeConsumesInput(boolean imeConsumesInput) { return mTarget.setImeConsumesInput(imeConsumesInput); } + + /** + * Called by the system when it needs to take a snapshot of multiple text-related data in an + * atomic manner. + * + * <p><strong>Editor authors</strong>: Supporting this method is strongly encouraged. Atomically + * taken {@link TextSnapshot} is going to be really helpful for the system when optimizing IPCs + * in a safe and deterministic manner. Return {@code null} if an atomically taken + * {@link TextSnapshot} is unavailable. The system continues supporting such a scenario + * gracefully.</p> + * + * <p><strong>IME authors</strong>: Currently IMEs cannot call this method directly and always + * receive {@code null} as the result.</p> + * + * <p>Beware that there is a bug that this method was not overridden in + * {@link InputConnectionWrapper}, which ended up always returning {@code null} when gets + * called even if the wrapped {@link InputConnection} implements this method. The bug was + * fixed in {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}.</p> + * + * @return {@code null} if {@link TextSnapshot} is unavailable and/or this API is called from + * IMEs. Beware the bug in older devices mentioned above. + * @throws NullPointerException if the target is {@code null}. + */ + @Nullable + @Override + public TextSnapshot takeSnapshot() { + return mTarget.takeSnapshot(); + } } diff --git a/core/java/android/window/ScreenCapture.java b/core/java/android/window/ScreenCapture.java index 95451a966055..fa7f577dadb8 100644 --- a/core/java/android/window/ScreenCapture.java +++ b/core/java/android/window/ScreenCapture.java @@ -198,17 +198,21 @@ public class ScreenCapture { * Create ScreenshotHardwareBuffer from an existing HardwareBuffer object. * * @param hardwareBuffer The existing HardwareBuffer object - * @param namedColorSpace Integer value of a named color space {@link ColorSpace.Named} + * @param dataspace Dataspace describing the content. + * {@see android.hardware.DataSpace} * @param containsSecureLayers Indicates whether this graphic buffer contains captured * contents of secure layers, in which case the screenshot * should not be persisted. * @param containsHdrLayers Indicates whether this graphic buffer contains HDR content. */ private static ScreenshotHardwareBuffer createFromNative(HardwareBuffer hardwareBuffer, - int namedColorSpace, boolean containsSecureLayers, boolean containsHdrLayers) { - ColorSpace colorSpace = ColorSpace.get(ColorSpace.Named.values()[namedColorSpace]); + int dataspace, boolean containsSecureLayers, boolean containsHdrLayers) { + ColorSpace colorSpace = ColorSpace.getFromDataSpace(dataspace); return new ScreenshotHardwareBuffer( - hardwareBuffer, colorSpace, containsSecureLayers, containsHdrLayers); + hardwareBuffer, + colorSpace != null ? colorSpace : ColorSpace.get(ColorSpace.Named.SRGB), + containsSecureLayers, + containsHdrLayers); } public ColorSpace getColorSpace() { @@ -271,8 +275,8 @@ public class ScreenCapture { public final boolean mAllowProtected; public final long mUid; public final boolean mGrayscale; - final SurfaceControl[] mExcludeLayers; + public final boolean mHintForSeamlessTransition; private CaptureArgs(CaptureArgs.Builder<? extends CaptureArgs.Builder<?>> builder) { mPixelFormat = builder.mPixelFormat; @@ -284,6 +288,7 @@ public class ScreenCapture { mUid = builder.mUid; mGrayscale = builder.mGrayscale; mExcludeLayers = builder.mExcludeLayers; + mHintForSeamlessTransition = builder.mHintForSeamlessTransition; } private CaptureArgs(Parcel in) { @@ -305,6 +310,7 @@ public class ScreenCapture { } else { mExcludeLayers = null; } + mHintForSeamlessTransition = in.readBoolean(); } /** Release any layers if set using {@link Builder#setExcludeLayers(SurfaceControl[])}. */ @@ -352,6 +358,7 @@ public class ScreenCapture { private long mUid = -1; private boolean mGrayscale; private SurfaceControl[] mExcludeLayers; + private boolean mHintForSeamlessTransition; /** * Construct a new {@link CaptureArgs} with the set parameters. The builder remains @@ -449,6 +456,21 @@ public class ScreenCapture { } /** + * Set whether the screenshot will be used in a system animation. + * This hint is used for picking the "best" colorspace for the screenshot, in particular + * for mixing HDR and SDR content. + * E.g., hintForSeamlessTransition is false, then a colorspace suitable for file + * encoding, such as BT2100, may be chosen. Otherwise, then the display's color space + * would be chosen, with the possibility of having an extended brightness range. This + * is important for screenshots that are directly re-routed to a SurfaceControl in + * order to preserve accurate colors. + */ + public T setHintForSeamlessTransition(boolean hintForSeamlessTransition) { + mHintForSeamlessTransition = hintForSeamlessTransition; + return getThis(); + } + + /** * Each sub class should return itself to allow the builder to chain properly */ T getThis() { @@ -471,7 +493,6 @@ public class ScreenCapture { dest.writeBoolean(mAllowProtected); dest.writeLong(mUid); dest.writeBoolean(mGrayscale); - if (mExcludeLayers != null) { dest.writeInt(mExcludeLayers.length); for (SurfaceControl excludeLayer : mExcludeLayers) { @@ -480,6 +501,7 @@ public class ScreenCapture { } else { dest.writeInt(0); } + dest.writeBoolean(mHintForSeamlessTransition); } public static final Parcelable.Creator<CaptureArgs> CREATOR = @@ -627,6 +649,7 @@ public class ScreenCapture { setUid(args.mUid); setGrayscale(args.mGrayscale); setExcludeLayers(args.mExcludeLayers); + setHintForSeamlessTransition(args.mHintForSeamlessTransition); } public Builder(SurfaceControl layer) { diff --git a/core/java/android/window/StartingWindowRemovalInfo.java b/core/java/android/window/StartingWindowRemovalInfo.java index 518123600b9a..6999e5bdc527 100644 --- a/core/java/android/window/StartingWindowRemovalInfo.java +++ b/core/java/android/window/StartingWindowRemovalInfo.java @@ -16,6 +16,7 @@ package android.window; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; @@ -23,6 +24,9 @@ import android.os.Parcel; import android.os.Parcelable; import android.view.SurfaceControl; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Information when removing a starting window of a particular task. * @hide @@ -55,11 +59,28 @@ public final class StartingWindowRemovalInfo implements Parcelable { */ public boolean playRevealAnimation; + /** The mode is no need to defer removing the starting window for IME */ + public static final int DEFER_MODE_NONE = 0; + + /** The mode to defer removing the starting window until IME has drawn */ + public static final int DEFER_MODE_NORMAL = 1; + + /** The mode to defer the starting window removal until IME drawn and finished the rotation */ + public static final int DEFER_MODE_ROTATION = 2; + + @IntDef(prefix = { "DEFER_MODE_" }, value = { + DEFER_MODE_NONE, + DEFER_MODE_NORMAL, + DEFER_MODE_ROTATION, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DeferMode {} + /** * Whether need to defer removing the starting window for IME. * @hide */ - public boolean deferRemoveForIme; + public @DeferMode int deferRemoveForImeMode; /** * The rounded corner radius @@ -95,7 +116,7 @@ public final class StartingWindowRemovalInfo implements Parcelable { windowAnimationLeash = source.readTypedObject(SurfaceControl.CREATOR); mainFrame = source.readTypedObject(Rect.CREATOR); playRevealAnimation = source.readBoolean(); - deferRemoveForIme = source.readBoolean(); + deferRemoveForImeMode = source.readInt(); roundedCornerRadius = source.readFloat(); windowlessSurface = source.readBoolean(); removeImmediately = source.readBoolean(); @@ -107,7 +128,7 @@ public final class StartingWindowRemovalInfo implements Parcelable { dest.writeTypedObject(windowAnimationLeash, flags); dest.writeTypedObject(mainFrame, flags); dest.writeBoolean(playRevealAnimation); - dest.writeBoolean(deferRemoveForIme); + dest.writeInt(deferRemoveForImeMode); dest.writeFloat(roundedCornerRadius); dest.writeBoolean(windowlessSurface); dest.writeBoolean(removeImmediately); @@ -119,7 +140,7 @@ public final class StartingWindowRemovalInfo implements Parcelable { + " frame=" + mainFrame + " playRevealAnimation=" + playRevealAnimation + " roundedCornerRadius=" + roundedCornerRadius - + " deferRemoveForIme=" + deferRemoveForIme + + " deferRemoveForImeMode=" + deferRemoveForImeMode + " windowlessSurface=" + windowlessSurface + " removeImmediately=" + removeImmediately + "}"; } diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 628fc3140ee8..c0370cc5517d 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -21,6 +21,7 @@ import static android.app.ActivityOptions.ANIM_CUSTOM; import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; +import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; @@ -1067,6 +1068,11 @@ public final class TransitionInfo implements Parcelable { return options; } + public static AnimationOptions makeSceneTransitionAnimOptions() { + AnimationOptions options = new AnimationOptions(ANIM_SCENE_TRANSITION); + return options; + } + public int getType() { return mType; } diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java index 853fe2f114f7..86c2893c9fab 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -76,7 +76,7 @@ public class SystemUiSystemPropertiesFlags { /** Gating the removal of sorting-notifications-by-interruptiveness. */ public static final Flag NO_SORT_BY_INTERRUPTIVENESS = - devFlag("persist.sysui.notification.no_sort_by_interruptiveness"); + releasedFlag("persist.sysui.notification.no_sort_by_interruptiveness"); /** Gating the logging of DND state change events. */ public static final Flag LOG_DND_STATE_EVENTS = @@ -115,7 +115,7 @@ public class SystemUiSystemPropertiesFlags { } /** - * Creates a flag that is enabled by default in debuggable builds. + * Creates a flag that is disabled by default in debuggable builds. * It can be enabled by setting this flag's SystemProperty to 1. * * This flag is ALWAYS disabled in release builds. diff --git a/core/java/com/android/internal/display/RefreshRateSettingsUtils.java b/core/java/com/android/internal/display/RefreshRateSettingsUtils.java deleted file mode 100644 index 39d8380c7e95..000000000000 --- a/core/java/com/android/internal/display/RefreshRateSettingsUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.display; - -import android.content.ContentResolver; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.provider.Settings; -import android.util.Log; -import android.view.Display; - -/** - * Constants and utility methods for refresh rate settings. - */ -public class RefreshRateSettingsUtils { - - private static final String TAG = "RefreshRateSettingsUtils"; - - public static final float DEFAULT_REFRESH_RATE = 60f; - - /** - * Find the highest refresh rate among all the modes of the default display. - * @param context The context - * @return The highest refresh rate - */ - public static float findHighestRefreshRateForDefaultDisplay(Context context) { - final DisplayManager dm = context.getSystemService(DisplayManager.class); - final Display display = dm.getDisplay(Display.DEFAULT_DISPLAY); - - if (display == null) { - Log.w(TAG, "No valid default display device"); - return DEFAULT_REFRESH_RATE; - } - - float maxRefreshRate = DEFAULT_REFRESH_RATE; - for (Display.Mode mode : display.getSupportedModes()) { - if (Math.round(mode.getRefreshRate()) > maxRefreshRate) { - maxRefreshRate = mode.getRefreshRate(); - } - } - return maxRefreshRate; - } - - /** - * Get the min refresh rate which is determined by - * {@link Settings.System.FORCE_PEAK_REFRESH_RATE}. - * @param context The context - * @return The min refresh rate - */ - public static float getMinRefreshRate(Context context) { - final ContentResolver cr = context.getContentResolver(); - int forcePeakRefreshRateSetting = Settings.System.getIntForUser(cr, - Settings.System.FORCE_PEAK_REFRESH_RATE, -1, cr.getUserId()); - return forcePeakRefreshRateSetting == 1 - ? findHighestRefreshRateForDefaultDisplay(context) - : 0; - } - - /** - * Get the peak refresh rate which is determined by {@link Settings.System.SMOOTH_DISPLAY}. - * @param context The context - * @param defaultPeakRefreshRate The refresh rate to return if the setting doesn't have a value - * @return The peak refresh rate - */ - public static float getPeakRefreshRate(Context context, float defaultPeakRefreshRate) { - final ContentResolver cr = context.getContentResolver(); - int smoothDisplaySetting = Settings.System.getIntForUser(cr, - Settings.System.SMOOTH_DISPLAY, -1, cr.getUserId()); - switch (smoothDisplaySetting) { - case 0: - return DEFAULT_REFRESH_RATE; - case 1: - return findHighestRefreshRateForDefaultDisplay(context); - default: - return defaultPeakRefreshRate; - } - } -} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index fbad4b9fd377..a554d0e77410 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -731,7 +731,7 @@ public class LockPatternUtils { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION, FLAG_ENABLE_AUTO_PIN_CONFIRMATION, - /* defaultValue= */ false); + /* defaultValue= */ true); } /** Returns if the given quality maps to an alphabetic password */ diff --git a/core/jni/android_window_ScreenCapture.cpp b/core/jni/android_window_ScreenCapture.cpp index 1b67a0da57e1..986dbe9a204c 100644 --- a/core/jni/android_window_ScreenCapture.cpp +++ b/core/jni/android_window_ScreenCapture.cpp @@ -46,6 +46,7 @@ static struct { jfieldID uid; jfieldID grayscale; jmethodID getNativeExcludeLayers; + jfieldID hintForSeamlessTransition; } gCaptureArgsClassInfo; static struct { @@ -69,23 +70,6 @@ static struct { jmethodID builder; } gScreenshotHardwareBufferClassInfo; -enum JNamedColorSpace : jint { - // ColorSpace.Named.SRGB.ordinal() = 0; - SRGB = 0, - - // ColorSpace.Named.DISPLAY_P3.ordinal() = 7; - DISPLAY_P3 = 7, -}; - -constexpr jint fromDataspaceToNamedColorSpaceValue(const ui::Dataspace dataspace) { - switch (dataspace) { - case ui::Dataspace::DISPLAY_P3: - return JNamedColorSpace::DISPLAY_P3; - default: - return JNamedColorSpace::SRGB; - } -} - static void checkAndClearException(JNIEnv* env, const char* methodName) { if (env->ExceptionCheck()) { ALOGE("An exception was thrown by callback '%s'.", methodName); @@ -119,12 +103,11 @@ public: captureResults.fenceResult.value()->waitForever(LOG_TAG); jobject jhardwareBuffer = android_hardware_HardwareBuffer_createFromAHardwareBuffer( env, captureResults.buffer->toAHardwareBuffer()); - const jint namedColorSpace = - fromDataspaceToNamedColorSpaceValue(captureResults.capturedDataspace); jobject screenshotHardwareBuffer = env->CallStaticObjectMethod(gScreenshotHardwareBufferClassInfo.clazz, gScreenshotHardwareBufferClassInfo.builder, - jhardwareBuffer, namedColorSpace, + jhardwareBuffer, + static_cast<jint>(captureResults.capturedDataspace), captureResults.capturedSecureLayers, captureResults.capturedHdrLayers); checkAndClearException(env, "builder"); @@ -185,6 +168,9 @@ static void getCaptureArgs(JNIEnv* env, jobject captureArgsObject, CaptureArgs& captureArgs.excludeHandles.emplace(excludeObject->getHandle()); } } + captureArgs.hintForSeamlessTransition = + env->GetBooleanField(captureArgsObject, + gCaptureArgsClassInfo.hintForSeamlessTransition); } static DisplayCaptureArgs displayCaptureArgsFromObject(JNIEnv* env, @@ -318,9 +304,10 @@ int register_android_window_ScreenCapture(JNIEnv* env) { GetFieldIDOrDie(env, captureArgsClazz, "mAllowProtected", "Z"); gCaptureArgsClassInfo.uid = GetFieldIDOrDie(env, captureArgsClazz, "mUid", "J"); gCaptureArgsClassInfo.grayscale = GetFieldIDOrDie(env, captureArgsClazz, "mGrayscale", "Z"); - gCaptureArgsClassInfo.getNativeExcludeLayers = GetMethodIDOrDie(env, captureArgsClazz, "getNativeExcludeLayers", "()[J"); + gCaptureArgsClassInfo.hintForSeamlessTransition = + GetFieldIDOrDie(env, captureArgsClazz, "mHintForSeamlessTransition", "Z"); jclass displayCaptureArgsClazz = FindClassOrDie(env, "android/window/ScreenCapture$DisplayCaptureArgs"); diff --git a/core/proto/android/companion/telecom.proto b/core/proto/android/companion/telecom.proto index 9ccadbf6eb2d..3a9e5eeb4877 100644 --- a/core/proto/android/companion/telecom.proto +++ b/core/proto/android/companion/telecom.proto @@ -20,9 +20,9 @@ package android.companion; option java_multiple_files = true; -// Next index: 2 +// Next index: 4 message Telecom { - // Next index: 5 + // Next index: 6 message Call { // UUID representing this call int64 id = 1; @@ -34,6 +34,8 @@ message Telecom { // Human-readable name of the app processing this call string app_name = 2; bytes app_icon = 3; + // Unique identifier for this app, such as a package name. + string app_identifier = 4; } Origin origin = 2; @@ -59,9 +61,11 @@ message Telecom { REJECT_AND_BLOCK = 9; IGNORE = 10; } - repeated Control controls_available = 4; + repeated Control controls = 4; } // The list of active calls. repeated Call calls = 1; + // The list of requested calls or call changes. + repeated Call requests = 2; } diff --git a/core/res/res/drawable-hdpi/pointer_all_scroll.png b/core/res/res/drawable-hdpi/pointer_all_scroll.png Binary files differindex 095aadce579d..4af84c3e9202 100644 --- a/core/res/res/drawable-hdpi/pointer_all_scroll.png +++ b/core/res/res/drawable-hdpi/pointer_all_scroll.png diff --git a/core/res/res/drawable-hdpi/pointer_horizontal_double_arrow.png b/core/res/res/drawable-hdpi/pointer_horizontal_double_arrow.png Binary files differindex 9388f162b17e..c4018c83da86 100644 --- a/core/res/res/drawable-hdpi/pointer_horizontal_double_arrow.png +++ b/core/res/res/drawable-hdpi/pointer_horizontal_double_arrow.png diff --git a/core/res/res/drawable-hdpi/pointer_top_left_diagonal_double_arrow.png b/core/res/res/drawable-hdpi/pointer_top_left_diagonal_double_arrow.png Binary files differindex ab52bffd9de5..58bb0d4cb2c9 100644 --- a/core/res/res/drawable-hdpi/pointer_top_left_diagonal_double_arrow.png +++ b/core/res/res/drawable-hdpi/pointer_top_left_diagonal_double_arrow.png diff --git a/core/res/res/drawable-hdpi/pointer_top_right_diagonal_double_arrow.png b/core/res/res/drawable-hdpi/pointer_top_right_diagonal_double_arrow.png Binary files differindex 1250d35df469..1981d41e2c34 100644 --- a/core/res/res/drawable-hdpi/pointer_top_right_diagonal_double_arrow.png +++ b/core/res/res/drawable-hdpi/pointer_top_right_diagonal_double_arrow.png diff --git a/core/res/res/drawable-hdpi/pointer_vertical_double_arrow.png b/core/res/res/drawable-hdpi/pointer_vertical_double_arrow.png Binary files differindex 6730c7b4a365..d4ba79ad32b2 100644 --- a/core/res/res/drawable-hdpi/pointer_vertical_double_arrow.png +++ b/core/res/res/drawable-hdpi/pointer_vertical_double_arrow.png diff --git a/core/res/res/drawable-mdpi/pointer_all_scroll.png b/core/res/res/drawable-mdpi/pointer_all_scroll.png Binary files differindex 3db456e885d2..1b81d0aaa1ab 100644 --- a/core/res/res/drawable-mdpi/pointer_all_scroll.png +++ b/core/res/res/drawable-mdpi/pointer_all_scroll.png diff --git a/core/res/res/drawable-mdpi/pointer_all_scroll_large.png b/core/res/res/drawable-mdpi/pointer_all_scroll_large.png Binary files differindex 120e1d72d233..9e1f5c915544 100644 --- a/core/res/res/drawable-mdpi/pointer_all_scroll_large.png +++ b/core/res/res/drawable-mdpi/pointer_all_scroll_large.png diff --git a/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow.png b/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow.png Binary files differindex 20f319ac5cc4..d1b3441d9117 100644 --- a/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow.png +++ b/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow.png diff --git a/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow_large.png b/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow_large.png Binary files differindex 33ef5c96ac8a..4e26371e7c7f 100644 --- a/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow_large.png +++ b/core/res/res/drawable-mdpi/pointer_horizontal_double_arrow_large.png diff --git a/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow.png b/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow.png Binary files differindex fe7d49602aa5..34c0c6a7a804 100644 --- a/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow.png +++ b/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow.png diff --git a/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow_large.png b/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow_large.png Binary files differindex 7b2e20c9d19c..87ec1847d524 100644 --- a/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow_large.png +++ b/core/res/res/drawable-mdpi/pointer_top_left_diagonal_double_arrow_large.png diff --git a/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow.png b/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow.png Binary files differindex 95a662017927..40b9c7e24cde 100644 --- a/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow.png +++ b/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow.png diff --git a/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow_large.png b/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow_large.png Binary files differindex 2e2904b6562d..6a85b493499c 100644 --- a/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow_large.png +++ b/core/res/res/drawable-mdpi/pointer_top_right_diagonal_double_arrow_large.png diff --git a/core/res/res/drawable-mdpi/pointer_vertical_double_arrow.png b/core/res/res/drawable-mdpi/pointer_vertical_double_arrow.png Binary files differindex ae6bfed37812..9bd89bf3013b 100644 --- a/core/res/res/drawable-mdpi/pointer_vertical_double_arrow.png +++ b/core/res/res/drawable-mdpi/pointer_vertical_double_arrow.png diff --git a/core/res/res/drawable-mdpi/pointer_vertical_double_arrow_large.png b/core/res/res/drawable-mdpi/pointer_vertical_double_arrow_large.png Binary files differindex 3beb1d1e9c8c..5a69bbc4713b 100644 --- a/core/res/res/drawable-mdpi/pointer_vertical_double_arrow_large.png +++ b/core/res/res/drawable-mdpi/pointer_vertical_double_arrow_large.png diff --git a/core/res/res/drawable-xhdpi/pointer_all_scroll.png b/core/res/res/drawable-xhdpi/pointer_all_scroll.png Binary files differindex e9d05d5079be..85aa0229dc9b 100644 --- a/core/res/res/drawable-xhdpi/pointer_all_scroll.png +++ b/core/res/res/drawable-xhdpi/pointer_all_scroll.png diff --git a/core/res/res/drawable-xhdpi/pointer_all_scroll_large.png b/core/res/res/drawable-xhdpi/pointer_all_scroll_large.png Binary files differindex 1fd54fb3cc9b..74483394ab71 100644 --- a/core/res/res/drawable-xhdpi/pointer_all_scroll_large.png +++ b/core/res/res/drawable-xhdpi/pointer_all_scroll_large.png diff --git a/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow.png b/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow.png Binary files differindex caf2a97bb7be..dd37f926edd6 100644 --- a/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow.png +++ b/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow.png diff --git a/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow_large.png b/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow_large.png Binary files differindex 2f22640f99e6..9e031e85fadc 100644 --- a/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow_large.png +++ b/core/res/res/drawable-xhdpi/pointer_horizontal_double_arrow_large.png diff --git a/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow.png b/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow.png Binary files differindex a36deb3f4995..150d80d91a40 100644 --- a/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow.png +++ b/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow.png diff --git a/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow_large.png b/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow_large.png Binary files differindex 6870e23ae817..bae907aca601 100644 --- a/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow_large.png +++ b/core/res/res/drawable-xhdpi/pointer_top_left_diagonal_double_arrow_large.png diff --git a/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow.png b/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow.png Binary files differindex c8d6d1f14a8a..3b23143cd44a 100644 --- a/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow.png +++ b/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow.png diff --git a/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow_large.png b/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow_large.png Binary files differindex 5bfb7712f59d..a90b28628a6b 100644 --- a/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow_large.png +++ b/core/res/res/drawable-xhdpi/pointer_top_right_diagonal_double_arrow_large.png diff --git a/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow.png b/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow.png Binary files differindex 720df913f9dc..3e7f850fbe70 100644 --- a/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow.png +++ b/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow.png diff --git a/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow_large.png b/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow_large.png Binary files differindex 82b30d1fedc2..090e3cac9cac 100644 --- a/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow_large.png +++ b/core/res/res/drawable-xhdpi/pointer_vertical_double_arrow_large.png diff --git a/core/res/res/drawable-xxhdpi/pointer_all_scroll.png b/core/res/res/drawable-xxhdpi/pointer_all_scroll.png Binary files differindex 808143aeff08..92aae724fdb3 100644 --- a/core/res/res/drawable-xxhdpi/pointer_all_scroll.png +++ b/core/res/res/drawable-xxhdpi/pointer_all_scroll.png diff --git a/core/res/res/drawable-xxhdpi/pointer_horizontal_double_arrow.png b/core/res/res/drawable-xxhdpi/pointer_horizontal_double_arrow.png Binary files differindex 677ccadbb26a..b1e2509a1e23 100644 --- a/core/res/res/drawable-xxhdpi/pointer_horizontal_double_arrow.png +++ b/core/res/res/drawable-xxhdpi/pointer_horizontal_double_arrow.png diff --git a/core/res/res/drawable-xxhdpi/pointer_top_left_diagonal_double_arrow.png b/core/res/res/drawable-xxhdpi/pointer_top_left_diagonal_double_arrow.png Binary files differindex e01aa649780c..2d1217c58684 100644 --- a/core/res/res/drawable-xxhdpi/pointer_top_left_diagonal_double_arrow.png +++ b/core/res/res/drawable-xxhdpi/pointer_top_left_diagonal_double_arrow.png diff --git a/core/res/res/drawable-xxhdpi/pointer_top_right_diagonal_double_arrow.png b/core/res/res/drawable-xxhdpi/pointer_top_right_diagonal_double_arrow.png Binary files differindex e947e0ea6f14..a99fb24cb849 100644 --- a/core/res/res/drawable-xxhdpi/pointer_top_right_diagonal_double_arrow.png +++ b/core/res/res/drawable-xxhdpi/pointer_top_right_diagonal_double_arrow.png diff --git a/core/res/res/drawable-xxhdpi/pointer_vertical_double_arrow.png b/core/res/res/drawable-xxhdpi/pointer_vertical_double_arrow.png Binary files differindex c867247c08f4..1f065fa8f632 100644 --- a/core/res/res/drawable-xxhdpi/pointer_vertical_double_arrow.png +++ b/core/res/res/drawable-xxhdpi/pointer_vertical_double_arrow.png diff --git a/core/res/res/layout-television/user_switching_dialog.xml b/core/res/res/layout-television/user_switching_dialog.xml deleted file mode 100644 index 72150e32c0a0..000000000000 --- a/core/res/res/layout-television/user_switching_dialog.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2015 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. ---> - -<TextView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/message" - style="?attr/textAppearanceListItem" - android:layout_width="match_parent" - android:background="@color/background_leanback_dark" - android:textColor="@color/primary_text_leanback_dark" - android:layout_height="match_parent" - android:gravity="center" - android:paddingStart="?attr/dialogPreferredPadding" - android:paddingEnd="?attr/dialogPreferredPadding" - android:paddingTop="24dp" - android:paddingBottom="24dp" /> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index bc0af12e9569..ad864b13b16d 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -96,12 +96,12 @@ <!-- Width of the navigation bar when it is placed vertically on the screen in car mode --> <dimen name="navigation_bar_width_car_mode">96dp</dimen> <!-- Height of notification icons in the status bar --> - <dimen name="status_bar_icon_size">22dip</dimen> + <dimen name="status_bar_icon_size">18sp</dimen> <!-- Desired size of system icons in status bar. --> - <dimen name="status_bar_system_icon_size">15dp</dimen> + <dimen name="status_bar_system_icon_size">15sp</dimen> <!-- Intrinsic size of most system icons in status bar. This is the default value that is used if a Drawable reports an intrinsic size of 0. --> - <dimen name="status_bar_system_icon_intrinsic_size">17dp</dimen> + <dimen name="status_bar_system_icon_intrinsic_size">17sp</dimen> <!-- Size of the giant number (unread count) in the notifications --> <dimen name="status_bar_content_number_size">48sp</dimen> <!-- Margin at the edge of the screen to ignore touch events for in the windowshade. --> @@ -330,7 +330,7 @@ <dimen name="notification_icon_circle_start">16dp</dimen> <!-- size (width and height) of the icon in the notification header --> - <dimen name="notification_header_icon_size_ambient">18dp</dimen> + <dimen name="notification_header_icon_size_ambient">18sp</dimen> <!-- The margin before the start of the app name in the header. --> <dimen name="notification_header_app_name_margin_start">3dp</dimen> diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 922dbb5fef38..43683ffad432 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -31,6 +31,7 @@ <permission name="android.permission.DUMP"/> <permission name="android.permission.GET_APP_OPS_STATS"/> <permission name="android.permission.INTERACT_ACROSS_USERS"/> + <permission name="android.permission.LOCATION_HARDWARE"/> <permission name="android.permission.MANAGE_DEBUGGING"/> <permission name="android.permission.MANAGE_GAME_MODE" /> <permission name="android.permission.MANAGE_SENSOR_PRIVACY"/> diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 302c72ead52e..dd4b58eb83dc 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -40,6 +40,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.media.MediaCodecInfo; import android.media.MediaCodecList; +import android.media.MediaFormat; import android.net.Uri; import android.os.Build; import android.os.Trace; @@ -914,8 +915,6 @@ public final class ImageDecoder implements AutoCloseable { case "image/jpeg": case "image/webp": case "image/gif": - case "image/heif": - case "image/heic": case "image/bmp": case "image/x-ico": case "image/vnd.wap.wbmp": @@ -930,6 +929,9 @@ public final class ImageDecoder implements AutoCloseable { case "image/x-pentax-pef": case "image/x-samsung-srw": return true; + case "image/heif": + case "image/heic": + return isHevcDecoderSupported(); case "image/avif": return isP010SupportedForAV1(); default: @@ -2067,6 +2069,28 @@ public final class ImageDecoder implements AutoCloseable { return decodeBitmapImpl(src, null); } + private static boolean sIsHevcDecoderSupported = false; + private static boolean sIsHevcDecoderSupportedInitialized = false; + private static final Object sIsHevcDecoderSupportedLock = new Object(); + + /* + * Check if HEVC decoder is supported by the device. + */ + @SuppressWarnings("AndroidFrameworkCompatChange") + private static boolean isHevcDecoderSupported() { + synchronized (sIsHevcDecoderSupportedLock) { + if (sIsHevcDecoderSupportedInitialized) { + return sIsHevcDecoderSupported; + } + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC); + MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + sIsHevcDecoderSupported = mcl.findDecoderForFormat(format) != null; + sIsHevcDecoderSupportedInitialized = true; + return sIsHevcDecoderSupported; + } + } + private static boolean sIsP010SupportedForAV1 = false; private static boolean sIsP010SupportedForHEVC = false; private static boolean sIsP010SupportedFlagsInitialized = false; @@ -2105,20 +2129,20 @@ public final class ImageDecoder implements AutoCloseable { * Checks if the device supports decoding 10-bit for the given mime type. */ private static void checkP010SupportforAV1HEVC() { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); for (MediaCodecInfo mediaCodecInfo : codecList.getCodecInfos()) { if (mediaCodecInfo.isEncoder()) { continue; } for (String mediaType : mediaCodecInfo.getSupportedTypes()) { - if (mediaType.equalsIgnoreCase("video/av01") - || mediaType.equalsIgnoreCase("video/hevc")) { + if (mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1) + || mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { MediaCodecInfo.CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(mediaType); for (int i = 0; i < codecCapabilities.colorFormats.length; ++i) { if (codecCapabilities.colorFormats[i] == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010) { - if (mediaType.equalsIgnoreCase("video/av01")) { + if (mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { sIsP010SupportedForAV1 = true; } else { sIsP010SupportedForHEVC = true; diff --git a/graphics/java/android/graphics/MeshSpecification.java b/graphics/java/android/graphics/MeshSpecification.java index 70311fdb2d1f..b1aae7f37c31 100644 --- a/graphics/java/android/graphics/MeshSpecification.java +++ b/graphics/java/android/graphics/MeshSpecification.java @@ -28,11 +28,40 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Class responsible for holding specifications for {@link Mesh} creations. This class - * generates a {@link MeshSpecification} via the Make method, where multiple parameters to set up - * the mesh are supplied, including attributes, vertex stride, varyings, and - * vertex/fragment shaders. There are also additional methods to provide an optional - * {@link ColorSpace} as well as an alpha type. + * Class responsible for holding specifications for {@link Mesh} creations. This class generates a + * {@link MeshSpecification} via the + * {@link MeshSpecification#make(Attribute[], int, Varying[], String, String)} method, + * where multiple parameters to set up the mesh are supplied, including attributes, vertex stride, + * {@link Varying}, and vertex/fragment shaders. There are also additional methods to provide an + * optional {@link ColorSpace} as well as an alpha type. + * + * For example a vertex shader that leverages a {@link Varying} may look like the following: + * + * <pre> + * Varyings main(const Attributes attributes) { + * Varyings varyings; + * varyings.position = attributes.position; + * return varyings; + * } + * </pre> + * + * The corresponding fragment shader that may consume the varying look like the following: + * + * <pre> + * float2 main(const Varyings varyings, out float4 color) { + * color = vec4(1.0, 0.0, 0.0, 1.0); + * return varyings.position; + * } + * </pre> + * + * The color returned from this fragment shader is blended with the other parameters that are + * configured on the Paint object (ex. {@link Paint#setBlendMode(BlendMode)} used to draw the mesh. + * + * The position returned in the fragment shader can be consumed by any following fragment shaders in + * the shader chain. + * + * See https://developer.android.com/develop/ui/views/graphics/agsl for more information + * regarding Android Graphics Shader Language. * * Note that there are several limitations on various mesh specifications: * 1. The max amount of attributes allowed is 8. @@ -118,7 +147,11 @@ public class MeshSpecification { public static final int TYPE_UBYTE4 = 4; /** - * Data class to represent a single attribute in a shader. + * Data class to represent a single attribute in a shader. An attribute is a variable that + * accompanies a vertex, this can be a color or texture coordinates. + * + * See https://developer.android.com/develop/ui/views/graphics/agsl for more information + * regarding Android Graphics Shader Language. * * Note that offset is the offset in number of bytes. For example, if we had two attributes * @@ -128,6 +161,10 @@ public class MeshSpecification { * </pre> * * att1 would have an offset of 0, while att2 would have an offset of 12 bytes. + * + * This is consumed as part of + * {@link MeshSpecification#make(Attribute[], int, Varying[], String, String, ColorSpace, int)} + * to create a {@link MeshSpecification} instance. */ public static class Attribute { @Type @@ -175,7 +212,15 @@ public class MeshSpecification { } /** - * Data class to represent a single varying variable. + * Data class to represent a single varying variable. A Varying variable can be altered by the + * vertex shader defined on the mesh but not by the fragment shader defined by AGSL. + * + * See https://developer.android.com/develop/ui/views/graphics/agsl for more information + * regarding Android Graphics Shader Language. + * + * This is consumed as part of + * {@link MeshSpecification#make(Attribute[], int, Varying[], String, String, ColorSpace, int)} + * to create a {@link MeshSpecification} instance. */ public static class Varying { @Type @@ -220,7 +265,7 @@ public class MeshSpecification { /** * Creates a {@link MeshSpecification} object for use within {@link Mesh}. This uses a default - * color space of {@link ColorSpace.Named#SRGB} and {@link AlphaType} of + * color space of {@link ColorSpace.Named#SRGB} and alphaType of * {@link #ALPHA_TYPE_PREMULTIPLIED}. * * @param attributes list of attributes represented by {@link Attribute}. Can hold a max of @@ -233,7 +278,11 @@ public class MeshSpecification { * the 6 varyings allowed. * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position * varying is set within the shader to get proper results. + * See {@link MeshSpecification} for an example vertex shader + * implementation * @param fragmentShader fragment shader to be supplied to the mesh. + * See {@link MeshSpecification} for an example fragment shader + * implementation * @return {@link MeshSpecification} object for use when creating {@link Mesh} */ @NonNull @@ -253,7 +302,7 @@ public class MeshSpecification { } /** - * Creates a {@link MeshSpecification} object. This uses a default {@link AlphaType} of + * Creates a {@link MeshSpecification} object. This uses a default alphaType of * {@link #ALPHA_TYPE_PREMULTIPLIED}. * * @param attributes list of attributes represented by {@link Attribute}. Can hold a max of @@ -266,7 +315,11 @@ public class MeshSpecification { * the 6 varyings allowed. * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position * varying is set within the shader to get proper results. + * See {@link MeshSpecification} for an example vertex shader + * implementation * @param fragmentShader fragment shader to be supplied to the mesh. + * See {@link MeshSpecification} for an example fragment shader + * implementation * @param colorSpace {@link ColorSpace} to tell what color space to work in. * @return {@link MeshSpecification} object for use when creating {@link Mesh} */ @@ -301,7 +354,11 @@ public class MeshSpecification { * the 6 varyings allowed. * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position * varying is set within the shader to get proper results. + * See {@link MeshSpecification} for an example vertex shader + * implementation * @param fragmentShader fragment shader to be supplied to the mesh. + * See {@link MeshSpecification} for an example fragment shader + * implementation * @param colorSpace {@link ColorSpace} to tell what color space to work in. * @param alphaType Describes how to interpret the alpha component for a pixel. Must be * one of diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index b6fd0bbafc71..9aac694e41bf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -421,7 +421,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements /** * Removes listener. */ - public void removeLocusIdListener(FocusListener listener) { + public void removeFocusListener(FocusListener listener) { synchronized (mLock) { mFocusListeners.remove(listener); } 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 b9ff5f0e820a..f3efaade945f 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 @@ -2884,6 +2884,7 @@ public class BubbleStackView extends FrameLayout /** Hide or show the manage menu for the currently expanded bubble. */ @VisibleForTesting public void showManageMenu(boolean show) { + if ((mManageMenu.getVisibility() == VISIBLE) == show) return; mShowingManage = show; // This should not happen, since the manage menu is only visible when there's an expanded diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java index b8204d013105..0bcafe513b4f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -59,4 +59,17 @@ public class SplitScreenConstants { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; + + public static final String splitPositionToString(@SplitPosition int pos) { + switch (pos) { + case SPLIT_POSITION_UNDEFINED: + return "SPLIT_POSITION_UNDEFINED"; + case SPLIT_POSITION_TOP_OR_LEFT: + return "SPLIT_POSITION_TOP_OR_LEFT"; + case SPLIT_POSITION_BOTTOM_OR_RIGHT: + return "SPLIT_POSITION_BOTTOM_OR_RIGHT"; + default: + return "UNKNOWN"; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index e1a56a1a5a7a..6b6a7bc42046 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -153,6 +153,8 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { + mWindowDecorViewModel.onTransitionMerged(merged, playing); + final List<ActivityManager.RunningTaskInfo> infoOfMerged = mTransitionToTaskInfo.get(merged); if (infoOfMerged == null) { @@ -169,8 +171,6 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs } else { mTransitionToTaskInfo.put(playing, infoOfMerged); } - - mWindowDecorViewModel.onTransitionMerged(merged, playing); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 566c130c7573..d3e7f9ca4227 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -72,7 +72,6 @@ import android.window.TaskOrganizer; import android.window.TaskSnapshot; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import android.window.WindowContainerTransactionCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -139,17 +138,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // the runnable to execute after WindowContainerTransactions is applied to finish resizing pip private Runnable mPipFinishResizeWCTRunnable; - private final WindowContainerTransactionCallback mPipFinishResizeWCTCallback = - new WindowContainerTransactionCallback() { - @Override - public void onTransactionReady(int id, SurfaceControl.Transaction t) { - t.apply(); - - // execute the runnable if non-null after WCT is applied to finish resizing pip - maybePerformFinishResizeCallback(); - } - }; - private void maybePerformFinishResizeCallback() { if (mPipFinishResizeWCTRunnable != null) { mPipFinishResizeWCTRunnable.run(); @@ -175,6 +163,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final int direction = animator.getTransitionDirection(); if (mIsCancelled) { sendOnPipTransitionFinished(direction); + maybePerformFinishResizeCallback(); return; } final int animationType = animator.getAnimationType(); @@ -199,6 +188,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, || isRemovePipDirection(direction); if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP || isExitPipDirection) { + // execute the finish resize callback if needed after the transaction is committed + tx.addTransactionCommittedListener(mMainExecutor, + PipTaskOrganizer.this::maybePerformFinishResizeCallback); + // Finish resize as long as we're not exiting PIP, or, if we are, only if this is // the end of an exit PIP animation. // This is necessary in case there was a resize animation ongoing when exit PIP @@ -1614,12 +1607,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { mSplitScreenOptional.ifPresent(splitScreenController -> splitScreenController.enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct)); - } else if (direction == TRANSITION_DIRECTION_LEAVE_PIP) { - // when leaving PiP we can call the callback without sync - maybePerformFinishResizeCallback(); - mTaskOrganizer.applyTransaction(wct); } else { - mTaskOrganizer.applySyncTransaction(wct, mPipFinishResizeWCTCallback); + mTaskOrganizer.applyTransaction(wct); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index b0bb14b49db6..f8e143575583 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -1060,13 +1060,22 @@ public class PipController implements PipTransitionController.PipTransitionCallb /** Save the state to restore to on re-entry. */ public void saveReentryState(Rect pipBounds) { float snapFraction = mPipBoundsAlgorithm.getSnapFraction(pipBounds); - if (mPipBoundsState.hasUserResizedPip()) { - final Rect reentryBounds = mTouchHandler.getUserResizeBounds(); - final Size reentrySize = new Size(reentryBounds.width(), reentryBounds.height()); - mPipBoundsState.saveReentryState(reentrySize, snapFraction); - } else { + + if (!mPipBoundsState.hasUserResizedPip()) { mPipBoundsState.saveReentryState(null /* bounds */, snapFraction); + return; } + + Size reentrySize = new Size(pipBounds.width(), pipBounds.height()); + + // TODO: b/279937014 Investigate why userResizeBounds are empty with shell transitions on + // fallback to using the userResizeBounds if userResizeBounds are not empty + if (!mTouchHandler.getUserResizeBounds().isEmpty()) { + Rect userResizeBounds = mTouchHandler.getUserResizeBounds(); + reentrySize = new Size(userResizeBounds.width(), userResizeBounds.height()); + } + + mPipBoundsState.saveReentryState(reentrySize, snapFraction); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index 956af709f156..281cae5e4ffa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -104,6 +104,7 @@ public class PipResizeGestureHandler { private boolean mAllowGesture; private boolean mIsAttached; private boolean mIsEnabled; + private boolean mEnableTouch; private boolean mEnablePinchResize; private boolean mEnableDragCornerResize; private boolean mIsSysUiStateValid; @@ -138,6 +139,7 @@ public class PipResizeGestureHandler { mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + mEnableTouch = true; mUpdateResizeBoundsCallback = (rect) -> { mUserResizeBounds.set(rect); @@ -248,6 +250,11 @@ public class PipResizeGestureHandler { return; } + if (!mEnableTouch) { + // No need to handle anything if touches are not enabled for resizing. + return; + } + // Don't allow resize when PiP is stashed. if (mPipBoundsState.isStashed()) { return; @@ -581,14 +588,13 @@ public class PipResizeGestureHandler { mLastResizeBounds, movementBounds); mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); - // disable the pinch resizing until the final bounds are updated - final boolean prevEnablePinchResize = mEnablePinchResize; - mEnablePinchResize = false; + // disable the resizing until the final bounds are updated + mEnableTouch = false; mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> { // reset the pinch resizing to its default state - mEnablePinchResize = prevEnablePinchResize; + mEnableTouch = true; }); } else { mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index bffc51c6b22f..3e568e9cb996 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -48,6 +48,7 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -97,7 +98,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mMixers.remove(mixer); } - void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, + @VisibleForTesting + public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsTransitionHandler.startRecentsTransition"); @@ -121,12 +123,13 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { if (mixer != null) { mixer.setRecentsTransition(transition); } - if (transition == null) { + if (transition != null) { + controller.setTransition(transition); + mControllers.add(controller); + } else { controller.cancel("startRecentsTransition"); - return; } - controller.setTransition(transition); - mControllers.add(controller); + return transition; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 0ef26fcff284..e5ae10c097a5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -39,6 +39,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIV import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; @@ -378,40 +379,18 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean moveToStage(ActivityManager.RunningTaskInfo task, @SplitPosition int stagePosition, WindowContainerTransaction wct) { - StageTaskListener targetStage; - int sideStagePosition; - if (isSplitScreenVisible()) { - // If the split screen is foreground, retrieves target stage based on position. - targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage; - sideStagePosition = mSideStagePosition; + prepareEnterSplitScreen(wct, task, stagePosition); + if (ENABLE_SHELL_TRANSITIONS) { + mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, + null, this, null /* consumedCallback */, null /* finishedCallback */, + isSplitScreenVisible() + ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN); } else { - targetStage = mSideStage; - sideStagePosition = stagePosition; - } - - if (!isSplitActive()) { - mSplitLayout.init(); - prepareEnterSplitScreen(wct, task, stagePosition); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); - setDividerVisibility(true, t); }); - } else { - setSideStagePosition(sideStagePosition, wct); - targetStage.addTask(task, wct); - targetStage.evictAllChildren(wct); - if (!isSplitScreenVisible()) { - final StageTaskListener anotherStage = targetStage == mMainStage - ? mSideStage : mMainStage; - anotherStage.reparentTopTask(wct); - anotherStage.evictAllChildren(wct); - wct.reorder(mRootTaskInfo.token, true); - } - setRootForceTranslucent(false, wct); - mSyncQueue.queue(wct); } - // Due to drag already pip task entering split by this method so need to reset flag here. mIsDropEntering = false; return true; @@ -1509,9 +1488,51 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ void prepareEnterSplitScreen(WindowContainerTransaction wct, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) { - if (mMainStage.isActive()) return; - onSplitScreenEnter(); + if (isSplitActive()) { + prepareBringSplit(wct, taskInfo, startPosition); + } else { + prepareActiveSplit(wct, taskInfo, startPosition); + } + } + + private void prepareBringSplit(WindowContainerTransaction wct, + @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) { + StageTaskListener targetStage; + if (isSplitScreenVisible()) { + // If the split screen is foreground, retrieves target stage based on position. + targetStage = startPosition == mSideStagePosition ? mSideStage : mMainStage; + } else { + targetStage = mSideStage; + } + + if (taskInfo != null) { + wct.startTask(taskInfo.taskId, + resolveStartStage(STAGE_TYPE_UNDEFINED, startPosition, null, wct)); + targetStage.evictAllChildren(wct); + } + // If running background, we need to reparent current top visible task to another stage + // and evict all tasks current under its. + if (!isSplitScreenVisible()) { + // Recreate so we need to reset position rather than keep position of background split. + mSplitLayout.resetDividerPosition(); + updateWindowBounds(mSplitLayout, wct); + final StageTaskListener anotherStage = targetStage == mMainStage + ? mSideStage : mMainStage; + anotherStage.evictAllChildren(wct); + anotherStage.reparentTopTask(wct); + wct.reorder(mRootTaskInfo.token, true); + setRootForceTranslucent(false, wct); + } + } + + private void prepareActiveSplit(WindowContainerTransaction wct, + @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) { + if (!ENABLE_SHELL_TRANSITIONS) { + // Legacy transition we need to create divider here, shell transition case we will + // create it on #finishEnterSplitScreen + mSplitLayout.init(); + } if (taskInfo != null) { setSideStagePosition(startPosition, wct); mSideStage.addTask(taskInfo, wct); @@ -2383,7 +2404,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo == null || !taskInfo.hasParentTask()) continue; + if (taskInfo == null) continue; + if (taskInfo.token.equals(mRootTaskInfo.token)) { + if (isOpeningType(change.getMode())) { + // Split is opened by someone so set it as visible. + setSplitsVisible(true); + } else if (isClosingType(change.getMode())) { + // Split is closed by someone so set it as invisible. + setSplitsVisible(false); + } + continue; + } final StageTaskListener stage = getStageOfTask(taskInfo); if (stage == null) continue; if (isOpeningType(change.getMode())) { @@ -2823,12 +2854,18 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final String childPrefix = innerPrefix + " "; pw.println(prefix + TAG + " mDisplayId=" + mDisplayId); pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); + pw.println(innerPrefix + "isSplitActive=" + isSplitActive()); + pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible()); pw.println(innerPrefix + "MainStage"); - pw.println(childPrefix + "stagePosition=" + getMainStagePosition()); + pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition())); pw.println(childPrefix + "isActive=" + mMainStage.isActive()); + mMainStage.dump(pw, childPrefix); + pw.println(innerPrefix + "MainStageListener"); mMainStageListener.dump(pw, childPrefix); pw.println(innerPrefix + "SideStage"); - pw.println(childPrefix + "stagePosition=" + getSideStagePosition()); + pw.println(childPrefix + "stagePosition=" + splitPositionToString(getSideStagePosition())); + mSideStage.dump(pw, childPrefix); + pw.println(innerPrefix + "SideStageListener"); mSideStageListener.dump(pw, childPrefix); if (mMainStage.isActive()) { pw.println(innerPrefix + "SplitLayout"); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index a841b7f96d3c..18b09b090794 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -421,6 +421,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; - pw.println(prefix + this); + if (mChildrenTaskInfo.size() > 0) { + pw.println(prefix + "Children list:"); + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); + pw.println(childPrefix + "Task#" + i + " taskID=" + taskInfo.taskId + + " baseActivity=" + taskInfo.baseActivity); + } + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java index 8a4d4c21194a..ae722208782e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java @@ -477,15 +477,15 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator { } @Override - public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { if (mRootView == null) { - return; + return true; } if (mSplashView == null) { // shouldn't happen, the app window may be drawn earlier than starting window? Slog.e(TAG, "Found empty splash screen, remove!"); removeWindowInner(mRootView, false); - return; + return true; } clearSystemBarColor(); if (immediately @@ -503,6 +503,7 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator { removeWindowInner(mRootView, true); } } + return true; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index ff06db370d1a..7cbf263f7cb1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -18,6 +18,8 @@ package com.android.wm.shell.startingsurface; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL; +import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION; import android.annotation.CallSuper; import android.app.TaskInfo; @@ -216,7 +218,17 @@ public class StartingSurfaceDrawer { } abstract static class StartingWindowRecord { protected int mBGColor; - abstract void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately); + + /** + * Remove the starting window with the given {@link StartingWindowRemovalInfo} if possible. + * @param info The removal info sent from the task organizer controller in the WM core. + * @param immediately {@code true} means removing the starting window immediately, + * {@code false} otherwise. + * @return {@code true} means {@link StartingWindowRecordManager} can safely remove the + * record itself. {@code false} means {@link StartingWindowRecordManager} requires + * to manage the record reference and remove it later. + */ + abstract boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately); int getBGColor() { return mBGColor; } @@ -231,6 +243,15 @@ public class StartingSurfaceDrawer { * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}. */ private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600; + + /** + * The max delay time in milliseconds for removing the task snapshot window with IME + * visible after the fixed rotation finished. + * Ideally the delay time will be shorter when receiving + * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}. + */ + private static final long MAX_DELAY_REMOVAL_TIME_FIXED_ROTATION = 3000; + private final Runnable mScheduledRunnable = this::removeImmediately; @WindowConfiguration.ActivityType protected final int mActivityType; @@ -242,24 +263,34 @@ public class StartingSurfaceDrawer { } @Override - public final void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + public final boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { if (immediately) { removeImmediately(); } else { - scheduleRemove(info.deferRemoveForIme); + scheduleRemove(info.deferRemoveForImeMode); + return false; } + return true; } - void scheduleRemove(boolean deferRemoveForIme) { + void scheduleRemove(@StartingWindowRemovalInfo.DeferMode int deferRemoveForImeMode) { // Show the latest content as soon as possible for unlocking to home. if (mActivityType == ACTIVITY_TYPE_HOME) { removeImmediately(); return; } mRemoveExecutor.removeCallbacks(mScheduledRunnable); - final long delayRemovalTime = hasImeSurface() && deferRemoveForIme - ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE - : DELAY_REMOVAL_TIME_GENERAL; + final long delayRemovalTime; + switch (deferRemoveForImeMode) { + case DEFER_MODE_ROTATION: + delayRemovalTime = MAX_DELAY_REMOVAL_TIME_FIXED_ROTATION; + break; + case DEFER_MODE_NORMAL: + delayRemovalTime = MAX_DELAY_REMOVAL_TIME_IME_VISIBLE; + break; + default: + delayRemovalTime = DELAY_REMOVAL_TIME_GENERAL; + } mRemoveExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "Defer removing snapshot surface in %d", delayRemovalTime); @@ -297,8 +328,10 @@ public class StartingSurfaceDrawer { final int taskId = removeInfo.taskId; final StartingWindowRecord record = mStartingWindowRecords.get(taskId); if (record != null) { - record.removeIfPossible(removeInfo, immediately); - mStartingWindowRecords.remove(taskId); + final boolean canRemoveRecord = record.removeIfPossible(removeInfo, immediately); + if (canRemoveRecord) { + mStartingWindowRecords.remove(taskId); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java index 12a0d4054b4d..98a803128587 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java @@ -124,7 +124,7 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { } @Override - public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { if (!immediately) { mSplashscreenContentDrawer.applyExitAnimation(mSplashView, info.windowAnimationLeash, info.mainFrame, @@ -132,6 +132,7 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { } else { release(); } + return true; } void release() { 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 7c729a46b679..49429327572e 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 @@ -73,7 +73,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, /** Pip was entered while handling an intent with its own remoteTransition. */ static final int TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE = 3; - /** Recents transition while split-screen active. */ + /** Recents transition while split-screen foreground. */ static final int TYPE_RECENTS_DURING_SPLIT = 4; /** The default animation for this mixed transition. */ @@ -152,7 +152,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, @NonNull TransitionRequestInfo request) { if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitScreenVisible()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while " - + "Split-Screen is active, so treat it as Mixed."); + + "Split-Screen is foreground, so treat it as Mixed."); if (request.getRemoteTransition() != null) { throw new IllegalStateException("Unexpected remote transition in" + "pip-enter-from-split request"); @@ -183,13 +183,13 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, mixed.mLeftoversHandler = handler.first; mActiveTransitions.add(mixed); return handler.second; - } else if (mSplitHandler.isSplitActive() + } else if (mSplitHandler.isSplitScreenVisible() && isOpeningType(request.getType()) && request.getTriggerTask() != null && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_FULLSCREEN && request.getTriggerTask().getActivityType() == ACTIVITY_TYPE_HOME) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a going-home request while " - + "Split-Screen is active, so treat it as Mixed."); + + "Split-Screen is foreground, so treat it as Mixed."); Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler = mPlayer.dispatchRequest(transition, request, this); if (handler == null) { @@ -211,7 +211,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, @Override public Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT) { - if (mRecentsHandler != null && mSplitHandler.isSplitActive()) { + if (mRecentsHandler != null && mSplitHandler.isSplitScreenVisible()) { return this; } return null; @@ -219,9 +219,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, @Override public void setRecentsTransition(IBinder transition) { - if (mSplitHandler.isSplitActive()) { + if (mSplitHandler.isSplitScreenVisible()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while " - + "Split-Screen is active, so treat it as Mixed."); + + "Split-Screen is foreground, so treat it as Mixed."); final MixedTransition mixed = new MixedTransition( MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition); mixed.mLeftoversHandler = mRecentsHandler; @@ -351,7 +351,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " - + "entering PIP while Split-Screen is active."); + + "entering PIP while Split-Screen is foreground."); TransitionInfo.Change pipChange = null; TransitionInfo.Change wallpaper = null; final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */); 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 6e9ecdaf55e7..1ee52ae00645 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 @@ -21,6 +21,7 @@ import static android.app.ActivityOptions.ANIM_CUSTOM; import static android.app.ActivityOptions.ANIM_NONE; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; +import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; @@ -625,6 +626,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } else if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0 && isOpeningType) { // This received a transferred starting window, so don't animate return null; + } else if (overrideType == ANIM_SCENE_TRANSITION) { + // If there's a scene-transition, then jump-cut. + return null; } else { a = loadAttributeAnimation(info, change, wallpaperTransit, mTransitionAnimation); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index d25318df6b6a..9ce22094d56b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -144,6 +144,7 @@ class ScreenRotationAnimation { .setCaptureSecureLayers(true) .setAllowProtected(true) .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight)) + .setHintForSeamlessTransition(true) .build(); ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = ScreenCapture.captureLayers(args); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java index 9d7c39f1c90e..ba364f8a6e59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java @@ -73,7 +73,8 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { if (mHandlerIds.containsKey(handler)) { handlerId = mHandlerIds.get(handler); } else { - handlerId = mHandlerIds.size(); + // + 1 to avoid 0 ids which can be confused with missing value when dumped to proto + handlerId = mHandlerIds.size() + 1; mHandlerIds.put(handler, handlerId); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index ff60130a0748..12edc3548642 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -113,11 +113,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private boolean mDragToDesktopAnimationStarted; private float mCaptionDragStartX; - // These values keep track of any transitions to freeform to stop relayout from running on - // changing task so that shellTransitions has a chance to animate the transition - private int mPauseRelayoutForTask = -1; - private IBinder mTransitionPausingRelayout; - public DesktopModeWindowDecorViewModel( Context context, Handler mainHandler, @@ -195,22 +190,25 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change) { if (change.getMode() == WindowManager.TRANSIT_CHANGE - && info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE) { - mTransitionPausingRelayout = transition; + && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE)) { + mWindowDecorByTaskId.get(change.getTaskInfo().taskId) + .addTransitionPausingRelayout(transition); } } @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { - if (merged.equals(mTransitionPausingRelayout)) { - mTransitionPausingRelayout = playing; + for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + decor.mergeTransitionPausingRelayout(merged, playing); } } @Override public void onTransitionFinished(@NonNull IBinder transition) { - if (transition.equals(mTransitionPausingRelayout)) { - mPauseRelayoutForTask = -1; + for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + decor.removeTransitionPausingRelayout(transition); } } @@ -225,12 +223,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { incrementEventReceiverTasks(taskInfo.displayId); } - // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell - // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl - // and interferes with the transition animation that is playing at the same time. - if (taskInfo.taskId != mPauseRelayoutForTask) { - decoration.relayout(taskInfo); - } + decoration.relayout(taskInfo); } @Override @@ -562,8 +555,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { relevantDecor.mTaskInfo.displayId); if (ev.getY() > 2 * statusBarHeight) { if (DesktopModeStatus.isProto2Enabled()) { - mPauseRelayoutForTask = relevantDecor.mTaskInfo.taskId; - centerAndMoveToDesktopWithAnimation(relevantDecor, ev); + animateToDesktop(relevantDecor, ev); } else if (DesktopModeStatus.isProto1Enabled()) { mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true)); } @@ -639,6 +631,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } /** + * Blocks relayout until transition is finished and transitions to Desktop + */ + private void animateToDesktop(DesktopModeWindowDecoration relevantDecor, + MotionEvent ev) { + relevantDecor.incrementRelayoutBlock(); + centerAndMoveToDesktopWithAnimation(relevantDecor, ev); + } + + /** * Animates a window to the center, grows to freeform size, and transitions to Desktop Mode. * @param relevantDecor the window decor of the task to be animated * @param ev the motion event that triggers the animation @@ -665,10 +666,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mDesktopTasksController.ifPresent( - c -> c.onDragPositioningEndThroughStatusBar( - relevantDecor.mTaskInfo, - calculateFreeformBounds(FINAL_FREEFORM_SCALE))); + mDesktopTasksController.ifPresent(c -> + c.onDragPositioningEndThroughStatusBar(relevantDecor.mTaskInfo, + calculateFreeformBounds(FINAL_FREEFORM_SCALE))); } }); animator.start(); 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 b1c3791ad15a..95ed42a222b8 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 @@ -30,6 +30,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.os.IBinder; import android.util.Log; import android.view.Choreographer; import android.view.MotionEvent; @@ -49,6 +50,9 @@ import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowD import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder; import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder; +import java.util.HashSet; +import java.util.Set; + /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with * {@link DesktopModeWindowDecorViewModel}. @@ -83,6 +87,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private TaskCornersListener mCornersListener; + private final Set<IBinder> mTransitionsPausingRelayout = new HashSet<>(); + private int mRelayoutBlock; + DesktopModeWindowDecoration( Context context, DisplayController displayController, @@ -134,6 +141,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { + // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell + // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl + // and interferes with the transition animation that is playing at the same time. + if (mRelayoutBlock > 0) { + return; + } + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is // synced with the buffer transaction (that draws the View). Both will be shown on screen @@ -455,6 +469,40 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return cornersRegion; } + /** + * If transition exists in mTransitionsPausingRelayout, remove the transition and decrement + * mRelayoutBlock + */ + void removeTransitionPausingRelayout(IBinder transition) { + if (mTransitionsPausingRelayout.remove(transition)) { + mRelayoutBlock--; + } + } + + /** + * Add transition to mTransitionsPausingRelayout + */ + void addTransitionPausingRelayout(IBinder transition) { + mTransitionsPausingRelayout.add(transition); + } + + /** + * If two transitions merge and the merged transition is in mTransitionsPausingRelayout, + * remove the merged transition from the set and add the transition it was merged into. + */ + public void mergeTransitionPausingRelayout(IBinder merged, IBinder playing) { + if (mTransitionsPausingRelayout.remove(merged)) { + mTransitionsPausingRelayout.add(playing); + } + } + + /** + * Increase mRelayoutBlock, stopping relayout if mRelayoutBlock is now greater than 0. + */ + public void incrementRelayoutBlock() { + mRelayoutBlock++; + } + static class Factory { DesktopModeWindowDecoration create( diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt new file mode 100644 index 000000000000..e06e074ee98a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.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.wm.shell.flicker + +import android.app.Instrumentation +import android.tools.device.flicker.junit.FlickerBuilderProvider +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation + +abstract class BaseBenchmarkTest +@JvmOverloads +constructor( + protected open val flicker: FlickerTest, + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), + protected val tapl: LauncherInstrumentation = LauncherInstrumentation() +) { + /** Specification of the test transition to execute */ + abstract val transition: FlickerBuilder.() -> Unit + + /** + * Entry point for the test runner. It will use this method to initialize and cache flicker + * executions + */ + @FlickerBuilderProvider + fun buildFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + setup { flicker.scenario.setIsTablet(tapl.isTablet) } + transition() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt index 86edc25b64d3..c98c5a0ad1a6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt @@ -17,24 +17,10 @@ package com.android.wm.shell.flicker import android.app.Instrumentation -import android.platform.test.annotations.Presubmit import android.tools.common.traces.component.ComponentNameMatcher -import android.tools.device.flicker.junit.FlickerBuilderProvider -import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.server.wm.flicker.entireScreenCovered -import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd -import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarLayerIsVisibleAtStartAndEnd -import com.android.server.wm.flicker.statusBarLayerPositionAtStartAndEnd -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd -import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible -import org.junit.Assume -import org.junit.Test /** * Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR], @@ -44,124 +30,7 @@ import org.junit.Test abstract class BaseTest @JvmOverloads constructor( - protected val flicker: FlickerTest, - protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), - protected val tapl: LauncherInstrumentation = LauncherInstrumentation() -) { - /** Specification of the test transition to execute */ - abstract val transition: FlickerBuilder.() -> Unit - - /** - * Entry point for the test runner. It will use this method to initialize and cache flicker - * executions - */ - @FlickerBuilderProvider - fun buildFlicker(): FlickerBuilder { - return FlickerBuilder(instrumentation).apply { - setup { flicker.scenario.setIsTablet(tapl.isTablet) } - transition() - } - } - - /** Checks that all parts of the screen are covered during the transition */ - @Presubmit @Test open fun entireScreenCovered() = flicker.entireScreenCovered() - - /** - * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition - */ - @Presubmit - @Test - open fun navBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) - flicker.navBarLayerIsVisibleAtStartAndEnd() - } - - /** - * Checks the position of the [ComponentNameMatcher.NAV_BAR] at the start and end of the - * transition - */ - @Presubmit - @Test - open fun navBarLayerPositionAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) - flicker.navBarLayerPositionAtStartAndEnd() - } - - /** - * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible during the whole transition - * - * Note: Phones only - */ - @Presubmit - @Test - open fun navBarWindowIsAlwaysVisible() { - Assume.assumeFalse(flicker.scenario.isTablet) - flicker.navBarWindowIsAlwaysVisible() - } - - /** - * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible during the whole transition - */ - @Presubmit - @Test - open fun taskBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) - flicker.taskBarLayerIsVisibleAtStartAndEnd() - } - - /** - * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole transition - * - * Note: Large screen only - */ - @Presubmit - @Test - open fun taskBarWindowIsAlwaysVisible() { - Assume.assumeTrue(flicker.scenario.isTablet) - flicker.taskBarWindowIsAlwaysVisible() - } - - /** - * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible during the whole - * transition - */ - @Presubmit - @Test - open fun statusBarLayerIsVisibleAtStartAndEnd() = flicker.statusBarLayerIsVisibleAtStartAndEnd() - - /** - * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the - * transition - */ - @Presubmit - @Test - open fun statusBarLayerPositionAtStartAndEnd() = flicker.statusBarLayerPositionAtStartAndEnd() - - /** - * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole - * transition - */ - @Presubmit - @Test - open fun statusBarWindowIsAlwaysVisible() = flicker.statusBarWindowIsAlwaysVisible() - - /** - * Checks that all layers that are visible on the trace, are visible for at least 2 consecutive - * entries. - */ - @Presubmit - @Test - open fun visibleLayersShownMoreThanOneConsecutiveEntry() { - flicker.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() } - } - - /** - * Checks that all windows that are visible on the trace, are visible for at least 2 consecutive - * entries. - */ - @Presubmit - @Test - open fun visibleWindowsShownMoreThanOneConsecutiveEntry() { - flicker.assertWm { this.visibleWindowsShownMoreThanOneConsecutiveEntry() } - } -} + override val flicker: FlickerTest, + instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), + tapl: LauncherInstrumentation = LauncherInstrumentation() +) : BaseBenchmarkTest(flicker, instrumentation, tapl), ICommonAssertions diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt new file mode 100644 index 000000000000..02d9a056afbf --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.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.wm.shell.flicker + +import android.platform.test.annotations.Presubmit +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.legacy.FlickerTest +import com.android.server.wm.flicker.entireScreenCovered +import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd +import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisibleAtStartAndEnd +import com.android.server.wm.flicker.statusBarLayerPositionAtStartAndEnd +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd +import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible +import org.junit.Assume +import org.junit.Test + +interface ICommonAssertions { + val flicker: FlickerTest + + /** Checks that all parts of the screen are covered during the transition */ + @Presubmit @Test fun entireScreenCovered() = flicker.entireScreenCovered() + + /** + * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition + */ + @Presubmit + @Test + fun navBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerIsVisibleAtStartAndEnd() + } + + /** + * Checks the position of the [ComponentNameMatcher.NAV_BAR] at the start and end of the + * transition + */ + @Presubmit + @Test + fun navBarLayerPositionAtStartAndEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerPositionAtStartAndEnd() + } + + /** + * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible during the whole transition + * + * Note: Phones only + */ + @Presubmit + @Test + fun navBarWindowIsAlwaysVisible() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarWindowIsAlwaysVisible() + } + + /** + * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible during the whole transition + */ + @Presubmit + @Test + fun taskBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeTrue(flicker.scenario.isTablet) + flicker.taskBarLayerIsVisibleAtStartAndEnd() + } + + /** + * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole transition + * + * Note: Large screen only + */ + @Presubmit + @Test + fun taskBarWindowIsAlwaysVisible() { + Assume.assumeTrue(flicker.scenario.isTablet) + flicker.taskBarWindowIsAlwaysVisible() + } + + /** + * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible during the whole + * transition + */ + @Presubmit + @Test + fun statusBarLayerIsVisibleAtStartAndEnd() = flicker.statusBarLayerIsVisibleAtStartAndEnd() + + /** + * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the + * transition + */ + @Presubmit + @Test + fun statusBarLayerPositionAtStartAndEnd() = flicker.statusBarLayerPositionAtStartAndEnd() + + /** + * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole + * transition + */ + @Presubmit @Test fun statusBarWindowIsAlwaysVisible() = flicker.statusBarWindowIsAlwaysVisible() + + /** + * Checks that all layers that are visible on the trace, are visible for at least 2 consecutive + * entries. + */ + @Presubmit + @Test + fun visibleLayersShownMoreThanOneConsecutiveEntry() { + flicker.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() } + } + + /** + * Checks that all windows that are visible on the trace, are visible for at least 2 consecutive + * entries. + */ + @Presubmit + @Test + fun visibleWindowsShownMoreThanOneConsecutiveEntry() { + flicker.assertWm { this.visibleWindowsShownMoreThanOneConsecutiveEntry() } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt index 93ee6992a98f..a4ac261d1946 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -54,7 +54,6 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 238367575) class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) { /** Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit @@ -71,7 +70,7 @@ class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonT transitions { tapl.goHome() } } - @FlakyTest(bugId = 256863309) + @Presubmit @Test override fun pipLayerReduces() { flicker.assertLayers { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index d53eac073e6b..b7e73ad11c9f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -26,6 +26,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.appWindowIsVisibleAtStart @@ -34,6 +35,7 @@ import com.android.wm.shell.flicker.layerKeepVisible import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -49,29 +51,19 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class CopyContentInSplit(flicker: FlickerTest) : SplitScreenBase(flicker) { - private val textEditApp = SplitScreenUtils.getIme(instrumentation) - private val MagnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#") - private val PopupWindowLayer = ComponentNameMatcher("", "PopupWindow:") - +class CopyContentInSplit(override val flicker: FlickerTest) : + CopyContentInSplitBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, textEditApp) } - transitions { - SplitScreenUtils.copyContentInSplit( - instrumentation, - device, - primaryApp, - textEditApp - ) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } @IwTest(focusArea = "sysui") @Presubmit @Test - fun cujCompleted() { + override fun cujCompleted() { flicker.appWindowIsVisibleAtStart(primaryApp) flicker.appWindowIsVisibleAtStart(textEditApp) flicker.splitScreenDividerIsVisibleAtStart() @@ -128,8 +120,8 @@ class CopyContentInSplit(flicker: FlickerTest) : SplitScreenBase(flicker) { ComponentNameMatcher.SNAPSHOT, ComponentNameMatcher.IME_SNAPSHOT, EdgeExtensionComponentMatcher(), - MagnifierLayer, - PopupWindowLayer + magnifierLayer, + popupWindowLayer ) ) } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt index 1b55f3975e1c..3fd6d17f27cb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -17,22 +17,21 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest -import android.tools.device.flicker.legacy.FlickerTestFactory import android.tools.device.helpers.WindowUtils import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.appWindowBecomesInvisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.layerBecomesInvisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible -import com.android.wm.shell.flicker.splitScreenDismissed import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible +import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByDividerBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -48,37 +47,16 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class DismissSplitScreenByDivider(flicker: FlickerTest) : SplitScreenBase(flicker) { +class DismissSplitScreenByDivider(override val flicker: FlickerTest) : + DismissSplitScreenByDividerBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } - transitions { - if (tapl.isTablet) { - SplitScreenUtils.dragDividerToDismissSplit( - device, - wmHelper, - dragToRight = false, - dragToBottom = true - ) - } else { - SplitScreenUtils.dragDividerToDismissSplit( - device, - wmHelper, - dragToRight = true, - dragToBottom = true - ) - } - wmHelper.StateSyncBuilder().withFullScreenApp(secondaryApp).waitForAndVerify() - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = false) - @Presubmit @Test fun splitScreenDividerBecomesInvisible() = flicker.splitScreenDividerBecomesInvisible() @@ -176,12 +154,4 @@ class DismissSplitScreenByDivider(flicker: FlickerTest) : SplitScreenBase(flicke @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTest> { - return FlickerTestFactory.nonRotationTests() - } - } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt index 2e81b30d2e9a..e05b22141e4d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt @@ -17,18 +17,18 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.appWindowBecomesInvisible import com.android.wm.shell.flicker.layerBecomesInvisible import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible -import com.android.wm.shell.flicker.splitScreenDismissed import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible +import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByGoHomeBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -44,21 +44,14 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class DismissSplitScreenByGoHome(flicker: FlickerTest) : SplitScreenBase(flicker) { - +class DismissSplitScreenByGoHome(override val flicker: FlickerTest) : + DismissSplitScreenByGoHomeBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } - transitions { - tapl.goHome() - wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = true) @Presubmit @Test diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt index 5180791276a2..0b0a3dad320b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt @@ -24,6 +24,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.appWindowIsVisibleAtStart @@ -32,8 +33,7 @@ import com.android.wm.shell.flicker.layerKeepVisible import com.android.wm.shell.flicker.splitAppLayerBoundsChanges import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart -import org.junit.Assume -import org.junit.Before +import com.android.wm.shell.flicker.splitscreen.benchmark.DragDividerToResizeBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -49,24 +49,19 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class DragDividerToResize(flicker: FlickerTest) : SplitScreenBase(flicker) { - +class DragDividerToResize(override val flicker: FlickerTest) : + DragDividerToResizeBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } - transitions { SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @Before - fun before() { - Assume.assumeTrue(tapl.isTablet || !flicker.scenario.isLandscapeOrSeascapeAtStart) - } - @IwTest(focusArea = "sysui") @Presubmit @Test - fun cujCompleted() { + override fun cujCompleted() { flicker.appWindowIsVisibleAtStart(primaryApp) flicker.appWindowIsVisibleAtStart(secondaryApp) flicker.splitScreenDividerIsVisibleAtStart() @@ -74,9 +69,6 @@ class DragDividerToResize(flicker: FlickerTest) : SplitScreenBase(flicker) { flicker.appWindowIsVisibleAtEnd(primaryApp) flicker.appWindowIsVisibleAtEnd(secondaryApp) flicker.splitScreenDividerIsVisibleAtEnd() - - // TODO(b/246490534): Add validation for resized app after withAppTransitionIdle is - // robust enough to get the correct end state. } @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt index 69da1e29a19c..e55868675da7 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.common.NavBar @@ -26,6 +25,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd @@ -34,9 +34,7 @@ import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered -import org.junit.Assume -import org.junit.Before +import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromAllAppsBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -53,40 +51,15 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterSplitScreenByDragFromAllApps(flicker: FlickerTest) : SplitScreenBase(flicker) { - - @Before - fun before() { - Assume.assumeTrue(tapl.isTablet) - } - +class EnterSplitScreenByDragFromAllApps(override val flicker: FlickerTest) : + EnterSplitScreenByDragFromAllAppsBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - tapl.goHome() - primaryApp.launchViaIntent(wmHelper) - } - transitions { - tapl.launchedAppState.taskbar - .openAllApps() - .getAppIcon(secondaryApp.appName) - .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = - flicker.splitScreenEntered( - primaryApp, - secondaryApp, - fromOtherApp = false, - appExistAtStart = false - ) - @FlakyTest(bugId = 245472831) @Test fun splitScreenDividerBecomesVisible() { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt index 1773846c18e9..ab8ecc54e71c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.common.NavBar @@ -26,6 +25,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.layerBecomesVisible @@ -33,9 +33,7 @@ import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered -import org.junit.Assume -import org.junit.Before +import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromNotificationBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -52,39 +50,16 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterSplitScreenByDragFromNotification(flicker: FlickerTest) : SplitScreenBase(flicker) { - - private val sendNotificationApp = SplitScreenUtils.getSendNotification(instrumentation) - - @Before - fun before() { - Assume.assumeTrue(tapl.isTablet) - } - +class EnterSplitScreenByDragFromNotification(override val flicker: FlickerTest) : + EnterSplitScreenByDragFromNotificationBenchmark(flicker), ICommonAssertions { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - // Send a notification - sendNotificationApp.launchViaIntent(wmHelper) - sendNotificationApp.postNotification(wmHelper) - tapl.goHome() - primaryApp.launchViaIntent(wmHelper) - } - transitions { - SplitScreenUtils.dragFromNotificationToSplit(instrumentation, device, wmHelper) - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp) - } - teardown { sendNotificationApp.exit(wmHelper) } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = - flicker.splitScreenEntered(primaryApp, sendNotificationApp, fromOtherApp = false) - @FlakyTest(bugId = 245472831) @Test fun splitScreenDividerBecomesVisible() { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt index c1977e9e82f7..516ca97bc531 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.device.flicker.junit.FlickerParametersRunnerFactory @@ -25,15 +24,14 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered -import org.junit.Assume -import org.junit.Before +import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromShortcutBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -49,42 +47,16 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterSplitScreenByDragFromShortcut(flicker: FlickerTest) : SplitScreenBase(flicker) { - - @Before - fun before() { - Assume.assumeTrue(tapl.isTablet) - } +class EnterSplitScreenByDragFromShortcut(override val flicker: FlickerTest) : + EnterSplitScreenByDragFromShortcutBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - tapl.goHome() - SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) - primaryApp.launchViaIntent(wmHelper) - } - transitions { - tapl.launchedAppState.taskbar - .getAppIcon(secondaryApp.appName) - .openDeepShortcutMenu() - .getMenuItem("Split Screen Secondary Activity") - .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = - flicker.splitScreenEntered( - primaryApp, - secondaryApp, - fromOtherApp = false, - appExistAtStart = false - ) - @Presubmit @Test fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt index 3bea66ef0a27..4af7e248b660 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.common.NavBar @@ -26,6 +25,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd @@ -34,9 +34,7 @@ import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered -import org.junit.Assume -import org.junit.Before +import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromTaskbarBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -53,41 +51,16 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterSplitScreenByDragFromTaskbar(flicker: FlickerTest) : SplitScreenBase(flicker) { - - @Before - fun before() { - Assume.assumeTrue(tapl.isTablet) - } - +class EnterSplitScreenByDragFromTaskbar(override val flicker: FlickerTest) : + EnterSplitScreenByDragFromTaskbarBenchmark(flicker), ICommonAssertions { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - tapl.goHome() - SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) - primaryApp.launchViaIntent(wmHelper) - } - transitions { - tapl.launchedAppState.taskbar - .getAppIcon(secondaryApp.appName) - .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = - flicker.splitScreenEntered( - primaryApp, - secondaryApp, - fromOtherApp = false, - appExistAtStart = false - ) - @FlakyTest(bugId = 245472831) @Test fun splitScreenDividerBecomesVisible() { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt index c45387722a49..faad9e82ffef 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt @@ -17,20 +17,20 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenFromOverviewBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -46,31 +46,15 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterSplitScreenFromOverview(flicker: FlickerTest) : SplitScreenBase(flicker) { +class EnterSplitScreenFromOverview(override val flicker: FlickerTest) : + EnterSplitScreenFromOverviewBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - primaryApp.launchViaIntent(wmHelper) - secondaryApp.launchViaIntent(wmHelper) - tapl.goHome() - wmHelper - .StateSyncBuilder() - .withAppTransitionIdle() - .withHomeActivityVisible() - .waitForAndVerify() - } - transitions { - SplitScreenUtils.splitFromOverview(tapl, device) - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) - @Presubmit @Test fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt index 7abdc06820d6..195b73a14a72 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt @@ -20,25 +20,33 @@ import android.content.Context import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import com.android.server.wm.flicker.helpers.setRotation -import com.android.wm.shell.flicker.BaseTest +import com.android.wm.shell.flicker.BaseBenchmarkTest -abstract class SplitScreenBase(flicker: FlickerTest) : BaseTest(flicker) { +abstract class SplitScreenBase(flicker: FlickerTest) : BaseBenchmarkTest(flicker) { protected val context: Context = instrumentation.context protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation) protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation) - /** {@inheritDoc} */ - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - tapl.setEnableRotation(true) - setRotation(flicker.scenario.startRotation) - tapl.setExpectedRotation(flicker.scenario.startRotation.value) - tapl.workspace.switchToOverview().dismissAllTasks() - } - teardown { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) - } + protected open val defaultSetup: FlickerBuilder.() -> Unit = { + setup { + tapl.setEnableRotation(true) + setRotation(flicker.scenario.startRotation) + tapl.setExpectedRotation(flicker.scenario.startRotation.value) + tapl.workspace.switchToOverview().dismissAllTasks() } + } + + protected open val defaultTeardown: FlickerBuilder.() -> Unit = { + teardown { + primaryApp.exit(wmHelper) + secondaryApp.exit(wmHelper) + } + } + + protected open val withoutTracing: FlickerBuilder.() -> Unit = { + withoutLayerTracing() + withoutWindowManagerTracing() + withoutTransitionTracing() + withoutTransactionsTracing() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt index fbb7c7159234..8cf871f88314 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt @@ -20,14 +20,12 @@ import android.platform.test.annotations.IwTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.common.NavBar -import android.tools.common.Rotation import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory -import android.tools.device.helpers.WindowUtils -import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.appWindowIsVisibleAtStart @@ -36,6 +34,7 @@ import com.android.wm.shell.flicker.layerKeepVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchAppByDoubleTapDividerBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -51,98 +50,19 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class SwitchAppByDoubleTapDivider(flicker: FlickerTest) : SplitScreenBase(flicker) { - +class SwitchAppByDoubleTapDivider(override val flicker: FlickerTest) : + SwitchAppByDoubleTapDividerBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } - transitions { - SplitScreenUtils.doubleTapDividerToSwitch(device) - wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() - - waitForLayersToSwitch(wmHelper) - waitForWindowsToSwitch(wmHelper) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - private fun waitForWindowsToSwitch(wmHelper: WindowManagerStateHelper) { - wmHelper - .StateSyncBuilder() - .add("appWindowsSwitched") { - val primaryAppWindow = - it.wmState.visibleWindows.firstOrNull { window -> - primaryApp.windowMatchesAnyOf(window) - } - ?: return@add false - val secondaryAppWindow = - it.wmState.visibleWindows.firstOrNull { window -> - secondaryApp.windowMatchesAnyOf(window) - } - ?: return@add false - - if (isLandscape(flicker.scenario.endRotation)) { - return@add if (flicker.scenario.isTablet) { - secondaryAppWindow.frame.right <= primaryAppWindow.frame.left - } else { - primaryAppWindow.frame.right <= secondaryAppWindow.frame.left - } - } else { - return@add if (flicker.scenario.isTablet) { - primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top - } else { - primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top - } - } - } - .waitForAndVerify() - } - - private fun waitForLayersToSwitch(wmHelper: WindowManagerStateHelper) { - wmHelper - .StateSyncBuilder() - .add("appLayersSwitched") { - val primaryAppLayer = - it.layerState.visibleLayers.firstOrNull { window -> - primaryApp.layerMatchesAnyOf(window) - } - ?: return@add false - val secondaryAppLayer = - it.layerState.visibleLayers.firstOrNull { window -> - secondaryApp.layerMatchesAnyOf(window) - } - ?: return@add false - - val primaryVisibleRegion = primaryAppLayer.visibleRegion?.bounds ?: return@add false - val secondaryVisibleRegion = - secondaryAppLayer.visibleRegion?.bounds ?: return@add false - - if (isLandscape(flicker.scenario.endRotation)) { - return@add if (flicker.scenario.isTablet) { - secondaryVisibleRegion.right <= primaryVisibleRegion.left - } else { - primaryVisibleRegion.right <= secondaryVisibleRegion.left - } - } else { - return@add if (flicker.scenario.isTablet) { - primaryVisibleRegion.bottom <= secondaryVisibleRegion.top - } else { - primaryVisibleRegion.bottom <= secondaryVisibleRegion.top - } - } - } - .waitForAndVerify() - } - - private fun isLandscape(rotation: Rotation): Boolean { - val displayBounds = WindowUtils.getDisplayBounds(rotation) - return displayBounds.width > displayBounds.height - } - @IwTest(focusArea = "sysui") @Presubmit @Test - fun cujCompleted() { + override fun cujCompleted() { flicker.appWindowIsVisibleAtStart(primaryApp) flicker.appWindowIsVisibleAtStart(secondaryApp) flicker.splitScreenDividerIsVisibleAtStart() @@ -150,9 +70,6 @@ class SwitchAppByDoubleTapDivider(flicker: FlickerTest) : SplitScreenBase(flicke flicker.appWindowIsVisibleAtEnd(primaryApp) flicker.appWindowIsVisibleAtEnd(secondaryApp) flicker.splitScreenDividerIsVisibleAtEnd() - - // TODO(b/246490534): Add validation for switched app after withAppTransitionIdle is - // robust enough to get the correct end state. } @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt index d675bfb0119d..078d95de1dd0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.device.flicker.junit.FlickerParametersRunnerFactory @@ -25,11 +24,12 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromAnotherAppBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -45,29 +45,15 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class SwitchBackToSplitFromAnotherApp(flicker: FlickerTest) : SplitScreenBase(flicker) { - val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation) - +class SwitchBackToSplitFromAnotherApp(override val flicker: FlickerTest) : + SwitchBackToSplitFromAnotherAppBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) - - thirdApp.launchViaIntent(wmHelper) - wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(thirdApp).waitForAndVerify() - } - transitions { - tapl.launchedAppState.quickSwitchToPreviousApp() - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) - @Presubmit @Test fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt index 9f4cb8c381fc..7c84243e00d7 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.device.flicker.junit.FlickerParametersRunnerFactory @@ -25,11 +24,12 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromHomeBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -45,28 +45,15 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class SwitchBackToSplitFromHome(flicker: FlickerTest) : SplitScreenBase(flicker) { - +class SwitchBackToSplitFromHome(override val flicker: FlickerTest) : + SwitchBackToSplitFromHomeBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) - - tapl.goHome() - wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() - } - transitions { - tapl.workspace.quickSwitchToPreviousApp() - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) - @Presubmit @Test fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt index a33d8cab9fbd..7c46d3e099a2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.device.flicker.junit.FlickerParametersRunnerFactory @@ -25,11 +24,12 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible -import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromRecentBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -45,28 +45,15 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class SwitchBackToSplitFromRecent(flicker: FlickerTest) : SplitScreenBase(flicker) { - +class SwitchBackToSplitFromRecent(override val flicker: FlickerTest) : + SwitchBackToSplitFromRecentBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) - - tapl.goHome() - wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() - } - transitions { - tapl.workspace.switchToOverview().currentTask.open() - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } - @IwTest(focusArea = "sysui") - @Presubmit - @Test - fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) - @Presubmit @Test fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt index 4c96b3a319d5..3b2da8dbcf9f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt @@ -24,6 +24,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.ICommonAssertions import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesInvisible import com.android.wm.shell.flicker.appWindowBecomesVisible @@ -36,6 +37,7 @@ import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBetweenSplitPairsBenchmark import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -51,32 +53,19 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class SwitchBetweenSplitPairs(flicker: FlickerTest) : SplitScreenBase(flicker) { - private val thirdApp = SplitScreenUtils.getIme(instrumentation) - private val fourthApp = SplitScreenUtils.getSendNotification(instrumentation) - +class SwitchBetweenSplitPairs(override val flicker: FlickerTest) : + SwitchBetweenSplitPairsBenchmark(flicker), ICommonAssertions { override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this) - setup { - SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) - SplitScreenUtils.enterSplit(wmHelper, tapl, device, thirdApp, fourthApp) - SplitScreenUtils.waitForSplitComplete(wmHelper, thirdApp, fourthApp) - } - transitions { - tapl.launchedAppState.quickSwitchToPreviousApp() - SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } - teardown { - thirdApp.exit(wmHelper) - fourthApp.exit(wmHelper) - } + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) } @IwTest(focusArea = "sysui") @Presubmit @Test - fun cujCompleted() { + override fun cujCompleted() { flicker.appWindowIsVisibleAtStart(thirdApp) flicker.appWindowIsVisibleAtStart(fourthApp) flicker.splitScreenDividerIsVisibleAtStart() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt new file mode 100644 index 000000000000..a189a3f67eca --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt @@ -0,0 +1,79 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class CopyContentInSplitBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val textEditApp = SplitScreenUtils.getIme(instrumentation) + protected val magnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#") + protected val popupWindowLayer = ComponentNameMatcher("", "PopupWindow:") + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, textEditApp) } + transitions { + SplitScreenUtils.copyContentInSplit( + instrumentation, + device, + primaryApp, + textEditApp + ) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + open fun cujCompleted() { + // The validation of copied text is already done in SplitScreenUtils.copyContentInSplit() + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt new file mode 100644 index 000000000000..55ab7b3a30e4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt @@ -0,0 +1,83 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenDismissed +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class DismissSplitScreenByDividerBenchmark(flicker: FlickerTest) : SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { + if (tapl.isTablet) { + SplitScreenUtils.dragDividerToDismissSplit( + device, + wmHelper, + dragToRight = false, + dragToBottom = true + ) + } else { + SplitScreenUtils.dragDividerToDismissSplit( + device, + wmHelper, + dragToRight = true, + dragToBottom = true + ) + } + wmHelper.StateSyncBuilder().withFullScreenApp(secondaryApp).waitForAndVerify() + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = false) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt new file mode 100644 index 000000000000..c4cfd1add25c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt @@ -0,0 +1,70 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenDismissed +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class DismissSplitScreenByGoHomeBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { + tapl.goHome() + wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = true) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt new file mode 100644 index 000000000000..146287c21c75 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt @@ -0,0 +1,76 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class DragDividerToResizeBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet || !flicker.scenario.isLandscapeOrSeascapeAtStart) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + open fun cujCompleted() { + // TODO(b/246490534): Add validation for resized app after withAppTransitionIdle is + // robust enough to get the correct end state. + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt new file mode 100644 index 000000000000..cc715021adf4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.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.wm.shell.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + tapl.goHome() + primaryApp.launchViaIntent(wmHelper) + } + transitions { + tapl.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered( + primaryApp, + secondaryApp, + fromOtherApp = false, + appExistAtStart = false + ) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt new file mode 100644 index 000000000000..de78f09d3ef4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt @@ -0,0 +1,91 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterSplitScreenByDragFromNotificationBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val sendNotificationApp = SplitScreenUtils.getSendNotification(instrumentation) + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + // Send a notification + sendNotificationApp.launchViaIntent(wmHelper) + sendNotificationApp.postNotification(wmHelper) + tapl.goHome() + primaryApp.launchViaIntent(wmHelper) + } + transitions { + SplitScreenUtils.dragFromNotificationToSplit(instrumentation, device, wmHelper) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp) + } + teardown { sendNotificationApp.exit(wmHelper) } + } + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered(primaryApp, sendNotificationApp, fromOtherApp = false) + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt new file mode 100644 index 000000000000..a29eb4085e54 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.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.wm.shell.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterSplitScreenByDragFromShortcutBenchmark(flicker: FlickerTest) : + SplitScreenBase(flicker) { + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + protected val thisTransition: FlickerBuilder.() -> Unit = { + setup { + tapl.goHome() + SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) + primaryApp.launchViaIntent(wmHelper) + } + transitions { + tapl.launchedAppState.taskbar + .getAppIcon(secondaryApp.appName) + .openDeepShortcutMenu() + .getMenuItem("Split Screen Secondary Activity") + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered( + primaryApp, + secondaryApp, + fromOtherApp = false, + appExistAtStart = false + ) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt new file mode 100644 index 000000000000..b2395cafafc1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt @@ -0,0 +1,93 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + tapl.goHome() + SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) + primaryApp.launchViaIntent(wmHelper) + } + transitions { + tapl.launchedAppState.taskbar + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered( + primaryApp, + secondaryApp, + fromOtherApp = false, + appExistAtStart = false + ) + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt new file mode 100644 index 000000000000..e1d85d0a4371 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt @@ -0,0 +1,79 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterSplitScreenFromOverviewBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + tapl.goHome() + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + transitions { + SplitScreenUtils.splitFromOverview(tapl, device) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt new file mode 100644 index 000000000000..ba8c46091808 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt @@ -0,0 +1,155 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import android.tools.device.traces.parsers.WindowManagerStateHelper +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class SwitchAppByDoubleTapDividerBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { + SplitScreenUtils.doubleTapDividerToSwitch(device) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + waitForLayersToSwitch(wmHelper) + waitForWindowsToSwitch(wmHelper) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + private fun waitForWindowsToSwitch(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .add("appWindowsSwitched") { + val primaryAppWindow = + it.wmState.visibleWindows.firstOrNull { window -> + primaryApp.windowMatchesAnyOf(window) + } + ?: return@add false + val secondaryAppWindow = + it.wmState.visibleWindows.firstOrNull { window -> + secondaryApp.windowMatchesAnyOf(window) + } + ?: return@add false + + if (isLandscape(flicker.scenario.endRotation)) { + return@add if (flicker.scenario.isTablet) { + secondaryAppWindow.frame.right <= primaryAppWindow.frame.left + } else { + primaryAppWindow.frame.right <= secondaryAppWindow.frame.left + } + } else { + return@add if (flicker.scenario.isTablet) { + primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top + } else { + primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top + } + } + } + .waitForAndVerify() + } + + private fun waitForLayersToSwitch(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .add("appLayersSwitched") { + val primaryAppLayer = + it.layerState.visibleLayers.firstOrNull { window -> + primaryApp.layerMatchesAnyOf(window) + } + ?: return@add false + val secondaryAppLayer = + it.layerState.visibleLayers.firstOrNull { window -> + secondaryApp.layerMatchesAnyOf(window) + } + ?: return@add false + + val primaryVisibleRegion = primaryAppLayer.visibleRegion?.bounds ?: return@add false + val secondaryVisibleRegion = + secondaryAppLayer.visibleRegion?.bounds ?: return@add false + + if (isLandscape(flicker.scenario.endRotation)) { + return@add if (flicker.scenario.isTablet) { + secondaryVisibleRegion.right <= primaryVisibleRegion.left + } else { + primaryVisibleRegion.right <= secondaryVisibleRegion.left + } + } else { + return@add if (flicker.scenario.isTablet) { + primaryVisibleRegion.bottom <= secondaryVisibleRegion.top + } else { + primaryVisibleRegion.bottom <= secondaryVisibleRegion.top + } + } + } + .waitForAndVerify() + } + + private fun isLandscape(rotation: Rotation): Boolean { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return displayBounds.width > displayBounds.height + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + open fun cujCompleted() { + // TODO(b/246490534): Add validation for switched app after withAppTransitionIdle is + // robust enough to get the correct end state. + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt new file mode 100644 index 000000000000..bbb2edc621e2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt @@ -0,0 +1,81 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class SwitchBackToSplitFromAnotherAppBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + private val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation) + + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + + thirdApp.launchViaIntent(wmHelper) + wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(thirdApp).waitForAndVerify() + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt new file mode 100644 index 000000000000..fa382932eb88 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt @@ -0,0 +1,79 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class SwitchBackToSplitFromHomeBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + } + transitions { + tapl.workspace.quickSwitchToPreviousApp() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt new file mode 100644 index 000000000000..1064bd93e3c1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt @@ -0,0 +1,79 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitScreenEntered +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class SwitchBackToSplitFromRecentBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + } + transitions { + tapl.workspace.switchToOverview().currentTask.open() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + withoutTracing(this) + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt new file mode 100644 index 000000000000..8f4393f8d20d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt @@ -0,0 +1,76 @@ +/* + * 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.flicker.splitscreen.benchmark + +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.splitscreen.SplitScreenBase +import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class SwitchBetweenSplitPairsBenchmark(override val flicker: FlickerTest) : + SplitScreenBase(flicker) { + protected val thirdApp = SplitScreenUtils.getIme(instrumentation) + protected val fourthApp = SplitScreenUtils.getSendNotification(instrumentation) + + protected val thisTransition: FlickerBuilder.() -> Unit + get() = { + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, thirdApp, fourthApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, thirdApp, fourthApp) + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + teardown { + thirdApp.exit(wmHelper) + fourthApp.exit(wmHelper) + } + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + defaultSetup(this) + defaultTeardown(this) + thisTransition(this) + } + + @IwTest(focusArea = "sysui") @Presubmit @Test open fun cujCompleted() {} + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 57a698128d77..ad4d97f6fe40 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -47,7 +47,7 @@ android_test { "truth-prebuilt", "testables", "platform-test-annotations", - "frameworks-base-testutils", + "servicestests-utils", ], libs: [ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 6995d10dd78d..04f2c99783da 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -268,7 +268,7 @@ public class PipControllerTest extends ShellTestCase { } @Test - public void saveReentryState_userHasResized_savesSize() { + public void saveReentryState_nonEmptyUserResizeBounds_savesSize() { final Rect bounds = new Rect(0, 0, 10, 10); final Rect resizedBounds = new Rect(0, 0, 30, 30); when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); @@ -281,6 +281,19 @@ public class PipControllerTest extends ShellTestCase { } @Test + public void saveReentryState_emptyUserResizeBounds_savesSize() { + final Rect bounds = new Rect(0, 0, 10, 10); + final Rect resizedBounds = new Rect(0, 0, 0, 0); + when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); + when(mMockPipTouchHandler.getUserResizeBounds()).thenReturn(resizedBounds); + when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(true); + + mPipController.saveReentryState(bounds); + + verify(mMockPipBoundsState).saveReentryState(new Size(10, 10), 1.0f); + } + + @Test public void onDisplayConfigurationChanged_inPip_movePip() { final int displayId = 1; final Rect bounds = new Rect(0, 0, 10, 10); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index e6219d1aa792..d27064d1b4da 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -152,10 +152,15 @@ public class StageCoordinatorTests extends ShellTestCase { when(mStageCoordinator.isSplitActive()).thenReturn(true); final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final WindowContainerTransaction wct = spy(new WindowContainerTransaction()); mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); - verify(mSideStage).addTask(eq(task), eq(wct)); + verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT)); + verify(mMainStage).reparentTopTask(eq(wct)); + verify(mMainStage).evictAllChildren(eq(wct)); + verify(mSideStage).evictAllChildren(eq(wct)); + verify(mSplitLayout).resetDividerPosition(); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); } @@ -171,14 +176,11 @@ public class StageCoordinatorTests extends ShellTestCase { final WindowContainerTransaction wct = new WindowContainerTransaction(); mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); - verify(mMainStage).addTask(eq(task), eq(wct)); + verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT)); + verify(mMainStage).evictAllChildren(eq(wct)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition()); - - mStageCoordinator.moveToStage(task, SPLIT_POSITION_TOP_OR_LEFT, wct); - verify(mSideStage).addTask(eq(task), eq(wct)); - assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition()); - assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index bf62acfc47a1..8115a5d4e89c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -265,17 +265,17 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { mStartingSurfaceDrawer.mWindowRecords.addRecord(taskId, new StartingSurfaceDrawer.StartingWindowRecord() { @Override - public void removeIfPossible(StartingWindowRemovalInfo info, + public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { - + return true; } }); mStartingSurfaceDrawer.mWindowlessRecords.addRecord(taskId, new StartingSurfaceDrawer.StartingWindowRecord() { @Override - public void removeIfPossible(StartingWindowRemovalInfo info, + public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { - + return true; } }); mStartingSurfaceDrawer.clearAllWindows(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 8eb5c6a08d88..963632b1f8f6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -17,6 +17,7 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -50,6 +51,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.after; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; @@ -58,14 +60,19 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; +import android.app.IApplicationThread; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.os.Binder; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.util.ArraySet; import android.util.Pair; +import android.view.IRecentsAnimationRunner; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; @@ -86,6 +93,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.server.testutils.StubTransaction; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.TransitionInfoBuilder; @@ -93,6 +101,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.sysui.ShellSharedConstants; @@ -100,6 +109,7 @@ import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.InOrder; import java.util.ArrayList; @@ -162,8 +172,8 @@ public class ShellTransitionTests extends ShellTestCase { verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); assertEquals(1, mDefaultHandler.activeCount()); mDefaultHandler.finishAll(); mMainExecutor.flushAll(); @@ -212,8 +222,8 @@ public class ShellTransitionTests extends ShellTestCase { transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); verify(mOrganizer, times(1)).startTransition(eq(transitToken), isNull()); - transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, open, new StubTransaction(), + new StubTransaction()); assertEquals(1, mDefaultHandler.activeCount()); assertEquals(0, testHandler.activeCount()); mDefaultHandler.finishAll(); @@ -228,8 +238,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */)); verify(mOrganizer, times(1)).startTransition( eq(transitToken), eq(handlerWCT)); - transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, open, new StubTransaction(), + new StubTransaction()); assertEquals(1, mDefaultHandler.activeCount()); assertEquals(0, testHandler.activeCount()); mDefaultHandler.finishAll(); @@ -246,8 +256,8 @@ public class ShellTransitionTests extends ShellTestCase { eq(transitToken), eq(handlerWCT)); TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) .addChange(TRANSIT_CHANGE).build(); - transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, change, new StubTransaction(), + new StubTransaction()); assertEquals(0, mDefaultHandler.activeCount()); assertEquals(1, testHandler.activeCount()); assertEquals(0, topHandler.activeCount()); @@ -284,8 +294,8 @@ public class ShellTransitionTests extends ShellTestCase { verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); assertEquals(0, mDefaultHandler.activeCount()); assertTrue(remoteCalled[0]); mDefaultHandler.finishAll(); @@ -434,8 +444,8 @@ public class ShellTransitionTests extends ShellTestCase { verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); assertEquals(0, mDefaultHandler.activeCount()); assertTrue(remoteCalled[0]); mDefaultHandler.finishAll(); @@ -484,10 +494,10 @@ public class ShellTransitionTests extends ShellTestCase { oneShot.setTransition(transitToken); IBinder anotherToken = new Binder(); assertFalse(oneShot.startAnimation(anotherToken, new TransitionInfo(transitType, 0), - mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + new StubTransaction(), new StubTransaction(), testFinish)); assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0), - mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + new StubTransaction(), new StubTransaction(), testFinish)); } @@ -501,8 +511,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken1, info1, new StubTransaction(), + new StubTransaction()); assertEquals(1, mDefaultHandler.activeCount()); IBinder transitToken2 = new Binder(); @@ -510,8 +520,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken2, info2, new StubTransaction(), + new StubTransaction()); // default handler doesn't merge by default, so it shouldn't increment active count. assertEquals(1, mDefaultHandler.activeCount()); assertEquals(0, mDefaultHandler.mergeCount()); @@ -542,8 +552,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken1, info1, new StubTransaction(), + new StubTransaction()); assertEquals(1, mDefaultHandler.activeCount()); IBinder transitToken2 = new Binder(); @@ -551,8 +561,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken2, info2, new StubTransaction(), + new StubTransaction()); // it should still only have 1 active, but then show 1 merged assertEquals(1, mDefaultHandler.activeCount()); assertEquals(1, mDefaultHandler.mergeCount()); @@ -611,8 +621,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(token, info, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(token, info, new StubTransaction(), + new StubTransaction()); return token; }; @@ -678,8 +688,8 @@ public class ShellTransitionTests extends ShellTestCase { // queued), so continue the transition lifecycle for that. TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); // At this point, if things are not working, we'd get an NPE due to attempting to merge // into the shellInit transition which hasn't started yet. assertEquals(1, mDefaultHandler.activeCount()); @@ -791,8 +801,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken1, info1, new StubTransaction(), + new StubTransaction()); assertEquals(1, mDefaultHandler.activeCount()); transitions.runOnIdle(runnable2); @@ -806,8 +816,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken2, info2, new StubTransaction(), + new StubTransaction()); assertEquals(1, mDefaultHandler.activeCount()); mDefaultHandler.finishAll(); @@ -858,8 +868,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT = new StubTransaction(); + SurfaceControl.Transaction finishT = new StubTransaction(); transitions.onTransitionReady(transitToken, info, startT, finishT); InOrder observerOrder = inOrder(observer); @@ -883,8 +893,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT1 = new StubTransaction(); + SurfaceControl.Transaction finishT1 = new StubTransaction(); transitions.onTransitionReady(transitToken1, info1, startT1, finishT1); verify(observer).onTransitionReady(transitToken1, info1, startT1, finishT1); @@ -893,8 +903,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT2 = new StubTransaction(); + SurfaceControl.Transaction finishT2 = new StubTransaction(); transitions.onTransitionReady(transitToken2, info2, startT2, finishT2); verify(observer, times(1)).onTransitionReady(transitToken2, info2, startT2, finishT2); verify(observer, times(0)).onTransitionStarting(transitToken2); @@ -927,8 +937,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT1 = new StubTransaction(); + SurfaceControl.Transaction finishT1 = new StubTransaction(); transitions.onTransitionReady(transitToken1, info1, startT1, finishT1); IBinder transitToken2 = new Binder(); @@ -936,8 +946,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); - SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT2 = new StubTransaction(); + SurfaceControl.Transaction finishT2 = new StubTransaction(); transitions.onTransitionReady(transitToken2, info2, startT2, finishT2); InOrder observerOrder = inOrder(observer); @@ -999,8 +1009,8 @@ public class ShellTransitionTests extends ShellTestCase { new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */)); TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) .addChange(TRANSIT_CHANGE).build(); - SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT1 = new StubTransaction(); + SurfaceControl.Transaction finishT1 = new StubTransaction(); transitions.onTransitionReady(transitToken1, change, startT1, finishT1); // Request the second transition that should be handled by the default handler @@ -1009,8 +1019,8 @@ public class ShellTransitionTests extends ShellTestCase { .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.requestStartTransition(transitToken2, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT2 = new StubTransaction(); + SurfaceControl.Transaction finishT2 = new StubTransaction(); transitions.onTransitionReady(transitToken2, open, startT2, finishT2); verify(observer).onTransitionReady(transitToken2, open, startT2, finishT2); verify(observer, times(0)).onTransitionStarting(transitToken2); @@ -1019,8 +1029,8 @@ public class ShellTransitionTests extends ShellTestCase { IBinder transitToken3 = new Binder(); transitions.requestStartTransition(transitToken3, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - SurfaceControl.Transaction startT3 = mock(SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishT3 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction startT3 = new StubTransaction(); + SurfaceControl.Transaction finishT3 = new StubTransaction(); transitions.onTransitionReady(transitToken3, open, startT3, finishT3); verify(observer, times(0)).onTransitionStarting(transitToken2); verify(observer).onTransitionReady(transitToken3, open, startT3, finishT3); @@ -1045,6 +1055,104 @@ public class ShellTransitionTests extends ShellTestCase { } @Test + public void testTransitSleep_squashesRecents() { + ShellInit shellInit = new ShellInit(mMainExecutor); + final Transitions transitions = + new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, + mTransactionPool, createTestDisplayController(), mMainExecutor, + mMainHandler, mAnimExecutor); + final RecentsTransitionHandler recentsHandler = + new RecentsTransitionHandler(shellInit, transitions, null); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + shellInit.init(); + + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_RECENTS); + RunningTaskInfo task2 = createTaskInfo(2); + + // Start an open transition for the purpose of occupying the ready queue + final IBinder transitOpen1 = new Binder("transitOpen1"); + final TransitionInfo infoOpen1 = + new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, task1) + .build(); + mMainExecutor.execute(() -> { + transitions.requestStartTransition(transitOpen1, new TransitionRequestInfo( + TRANSIT_OPEN, task1 /* trigger */, null /* remote */)); + onTransitionReady(transitions, transitOpen1, infoOpen1); + }); + + // First transition on the queue should start immediately. + mMainExecutor.flushAll(); + verify(observer).onTransitionReady(eq(transitOpen1), any(), any(), any()); + verify(observer).onTransitionStarting(eq(transitOpen1)); + + // Start recents + final IRecentsAnimationRunner recentsListener = + mock(IRecentsAnimationRunner.class, Answers.RETURNS_DEEP_STUBS); + final IBinder transitRecents = recentsHandler.startRecentsTransition( + mock(PendingIntent.class) /* intent */, + mock(Intent.class) /* fillIn */, + new Bundle() /* options */, + mock(IApplicationThread.class) /* appThread */, + recentsListener); + final TransitionInfo infoRecents = + new TransitionInfoBuilder(TRANSIT_TO_FRONT) + .addChange(TRANSIT_TO_FRONT, task1) + .addChange(TRANSIT_CLOSE, task2) + .build(); + onTransitionReady(transitions, transitRecents, infoRecents); + + // Start another open transition during recents + final IBinder transitOpen2 = new Binder("transitOpen2"); + final TransitionInfo infoOpen2 = + new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, task2) + .addChange(TRANSIT_TO_BACK, task1) + .build(); + mMainExecutor.execute(() -> { + transitions.requestStartTransition(transitOpen2, new TransitionRequestInfo( + TRANSIT_OPEN, task2 /* trigger */, null /* remote */)); + onTransitionReady(transitions, transitOpen2, infoOpen2); + }); + + // Finish testOpen1 to start processing the other transitions + mMainExecutor.execute(() -> { + mDefaultHandler.finishOne(); + }); + mMainExecutor.flushAll(); + + // Recents transition SHOULD start, and merge the open transition, which should NOT start. + verify(observer).onTransitionFinished(eq(transitOpen1), eq(false) /* aborted */); + verify(observer).onTransitionReady(eq(transitRecents), any(), any(), any()); + verify(observer).onTransitionStarting(eq(transitRecents)); + verify(observer).onTransitionReady(eq(transitOpen2), any(), any(), any()); + verify(observer).onTransitionMerged(eq(transitOpen2), eq(transitRecents)); + // verify(observer).onTransitionFinished(eq(transitOpen2), eq(true) /* aborted */); + + // Go to sleep + final IBinder transitSleep = new Binder("transitSleep"); + final TransitionInfo infoSleep = new TransitionInfoBuilder(TRANSIT_SLEEP).build(); + mMainExecutor.execute(() -> { + transitions.requestStartTransition(transitSleep, new TransitionRequestInfo( + TRANSIT_SLEEP, null /* trigger */, null /* remote */)); + onTransitionReady(transitions, transitSleep, infoSleep); + }); + mMainExecutor.flushAll(); + + // Recents transition should finish itself when it sees the sleep transition coming. + verify(observer).onTransitionFinished(eq(transitRecents), eq(false)); + verify(observer).onTransitionFinished(eq(transitSleep), eq(false)); + } + + private void onTransitionReady(Transitions transitions, IBinder token, TransitionInfo info) { + transitions.onTransitionReady(token, info, new StubTransaction(), + new StubTransaction()); + } + + @Test public void testEmptyTransitionStillReportsKeyguardGoingAway() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -1056,8 +1164,8 @@ public class ShellTransitionTests extends ShellTestCase { // Make a no-op transition TransitionInfo info = new TransitionInfoBuilder( TRANSIT_OPEN, TRANSIT_FLAG_KEYGUARD_GOING_AWAY, true /* noOp */).build(); - transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class)); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); // If keyguard-going-away flag set, then it shouldn't be aborted. assertEquals(1, mDefaultHandler.activeCount()); @@ -1397,7 +1505,7 @@ public class ShellTransitionTests extends ShellTestCase { private static void onTransitionReady(Transitions transitions, IBinder token) { transitions.onTransitionReady(token, createTransitionInfo(), - mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); + new StubTransaction(), new StubTransaction()); } private static TransitionInfo createTransitionInfo() { @@ -1414,15 +1522,15 @@ public class ShellTransitionTests extends ShellTestCase { private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, int activityType) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; + taskInfo.topActivityType = activityType; taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); taskInfo.configuration.windowConfiguration.setActivityType(activityType); + taskInfo.token = mock(WindowContainerToken.class); return taskInfo; } private static RunningTaskInfo createTaskInfo(int taskId) { - RunningTaskInfo taskInfo = new RunningTaskInfo(); - taskInfo.taskId = taskId; - return taskInfo; + return createTaskInfo(taskId, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); } private DisplayController createTestDisplayController() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt new file mode 100644 index 000000000000..348b3659e864 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -0,0 +1,232 @@ +/* + * 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.windowdecor + +import android.app.ActivityManager +import android.graphics.PointF +import android.graphics.Rect +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.view.Display +import android.window.WindowContainerToken +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.any +import org.mockito.MockitoAnnotations + +/** + * Tests for [DragPositioningCallbackUtility]. + * + * Build/Install/Run: + * atest WMShellUnitTests:DragPositioningCallbackUtilityTest + */ +@RunWith(AndroidTestingRunner::class) +class DragPositioningCallbackUtilityTest { + @Mock + private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock + private lateinit var taskToken: WindowContainerToken + @Mock + private lateinit var taskBinder: IBinder + @Mock + private lateinit var mockDisplayController: DisplayController + @Mock + private lateinit var mockDisplayLayout: DisplayLayout + @Mock + private lateinit var mockDisplay: Display + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(taskToken.asBinder()).thenReturn(taskBinder) + whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + + mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + minWidth = MIN_WIDTH + minHeight = MIN_HEIGHT + defaultMinSize = DEFAULT_MIN + displayId = DISPLAY_ID + configuration.windowConfiguration.bounds = STARTING_BOUNDS + } + mockWindowDecoration.mDisplay = mockDisplay + whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } + } + + @Test + fun testChangeBoundsDoesNotChangeHeightWhenLessThanMin() { + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect() + + // Resize to width of 95px and height of 5px with min width of 10px + val newX = STARTING_BOUNDS.right.toFloat() - 5 + val newY = STARTING_BOUNDS.top.toFloat() + 95 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + false /* hasMoved */, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 5) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + fun testChangeBoundsDoesNotChangeWidthWhenLessThanMin() { + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect() + + // Resize to height of 95px and width of 5px with min width of 10px + val newX = STARTING_BOUNDS.right.toFloat() - 95 + val newY = STARTING_BOUNDS.top.toFloat() + 5 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + false /* hasMoved */, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 5) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + fun testChangeBoundsDoesNotChangeHeightWhenNegative() { + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect() + + // Resize to width of 95px and width of -5px with minimum of 10px + val newX = STARTING_BOUNDS.right.toFloat() - 5 + val newY = STARTING_BOUNDS.top.toFloat() + 105 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + false /* hasMoved */, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 5) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + fun testChangeBoundsRunsWhenResizeBoundsValid() { + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect() + + // Shrink to height 20px and width 20px with both min height/width equal to 10px + val newX = STARTING_BOUNDS.right.toFloat() - 80 + val newY = STARTING_BOUNDS.top.toFloat() + 80 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + false /* hasMoved */, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 80) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + fun testChangeBoundsDoesNotRunWithNegativeHeightAndWidth() { + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect() + // Shrink to height -5px and width -5px with both min height/width equal to 10px + val newX = STARTING_BOUNDS.right.toFloat() - 105 + val newY = STARTING_BOUNDS.top.toFloat() + 105 + + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + false /* hasMoved */, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + fun testChangeBounds_toDisallowedBounds_freezesAtLimit() { + var hasMoved = false + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect() + // Initial resize to width and height 110px. + var newX = STARTING_BOUNDS.right.toFloat() + 10 + var newY = STARTING_BOUNDS.bottom.toFloat() + 10 + var delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + hasMoved, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration)) + hasMoved = true + // Resize width to 120px, height to disallowed area which should not result in a change. + newX += 10 + newY = DISALLOWED_RESIZE_AREA.top.toFloat() + delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + hasMoved, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration)) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right + 20) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom + 10) + } + + companion object { + private const val TASK_ID = 5 + private const val MIN_WIDTH = 10 + private const val MIN_HEIGHT = 10 + private const val DENSITY_DPI = 20 + private const val DEFAULT_MIN = 40 + private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) + private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val DISALLOWED_RESIZE_AREA = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom) + private val STABLE_BOUNDS = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT + ) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index 84ccddeb6a76..5bea8f2d1c45 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -14,7 +14,6 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED @@ -22,7 +21,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` +import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.never @@ -72,10 +71,10 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { mockDragStartListener ) - `when`(taskToken.asBinder()).thenReturn(taskBinder) - `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) - `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) - `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + whenever(taskToken.asBinder()).thenReturn(taskBinder) + whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } @@ -89,7 +88,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { configuration.windowConfiguration.bounds = STARTING_BOUNDS } mockWindowDecoration.mDisplay = mockDisplay - `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID } + whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } @Test @@ -237,293 +236,6 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { }) } - @Test - fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenLessThanMin() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Resize to width of 95px and height of 5px with min width of 10px - val newX = STARTING_BOUNDS.right.toFloat() - 5 - val newY = STARTING_BOUNDS.top.toFloat() + 95 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) - != 0) && change.configuration.windowConfiguration.bounds.top == - STARTING_BOUNDS.top && - change.configuration.windowConfiguration.bounds.bottom == - STARTING_BOUNDS.bottom - } - }) - } - - @Test - fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenLessThanMin() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Resize to height of 95px and width of 5px with min width of 10px - val newX = STARTING_BOUNDS.right.toFloat() - 95 - val newY = STARTING_BOUNDS.top.toFloat() + 5 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) - != 0) && change.configuration.windowConfiguration.bounds.right == - STARTING_BOUNDS.right && - change.configuration.windowConfiguration.bounds.left == - STARTING_BOUNDS.left - } - }) - } - - @Test - fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenNegative() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Resize to height of -5px and width of 95px - val newX = STARTING_BOUNDS.right.toFloat() - 5 - val newY = STARTING_BOUNDS.top.toFloat() + 105 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) - != 0) && change.configuration.windowConfiguration.bounds.top == - STARTING_BOUNDS.top && - change.configuration.windowConfiguration.bounds.bottom == - STARTING_BOUNDS.bottom - } - }) - } - - @Test - fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenNegative() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Resize to width of -5px and height of 95px - val newX = STARTING_BOUNDS.right.toFloat() - 105 - val newY = STARTING_BOUNDS.top.toFloat() + 5 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) - != 0) && change.configuration.windowConfiguration.bounds.right == - STARTING_BOUNDS.right && - change.configuration.windowConfiguration.bounds.left == - STARTING_BOUNDS.left - } - }) - } - - @Test - fun testDragResize_resize_setBoundsRunsWhenResizeBoundsValid() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Shrink to height 20px and width 20px with both min height/width equal to 10px - val newX = STARTING_BOUNDS.right.toFloat() - 80 - val newY = STARTING_BOUNDS.top.toFloat() + 80 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) - } - }) - } - - @Test - fun testDragResize_resize_setBoundsDoesNotRunWithNegativeHeightAndWidth() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Shrink to height 5px and width 5px with both min height/width equal to 10px - val newX = STARTING_BOUNDS.right.toFloat() - 95 - val newY = STARTING_BOUNDS.top.toFloat() + 95 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) - } - }) - } - - @Test - fun testDragResize_resize_useDefaultMinWhenMinWidthInvalid() { - mockWindowDecoration.mTaskInfo.minWidth = -1 - - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Shrink to width and height of 3px with invalid minWidth = -1 and defaultMinSize = 5px - val newX = STARTING_BOUNDS.right.toFloat() - 97 - val newY = STARTING_BOUNDS.top.toFloat() + 97 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) - } - }) - } - - @Test - fun testDragResize_resize_useMinWidthWhenValid() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - // Shrink to width and height of 7px with valid minWidth = 10px and defaultMinSize = 5px - val newX = STARTING_BOUNDS.right.toFloat() - 93 - val newY = STARTING_BOUNDS.top.toFloat() + 93 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) - } - }) - } - - fun testDragResize_toDisallowedBounds_freezesAtLimit() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, // Resize right-bottom corner - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.bottom.toFloat() - ) - - // Resize the task by 10px to the right and bottom, a valid destination - val newBounds = Rect( - STARTING_BOUNDS.left, - STARTING_BOUNDS.top, - STARTING_BOUNDS.right + 10, - STARTING_BOUNDS.bottom + 10) - taskPositioner.onDragPositioningMove( - newBounds.right.toFloat(), - newBounds.bottom.toFloat() - ) - - // Resize the task by another 10px to the right (allowed) and to just in the disallowed - // area of the Y coordinate. - val newBounds2 = Rect( - newBounds.left, - newBounds.top, - newBounds.right + 10, - DISALLOWED_RESIZE_AREA.top - ) - taskPositioner.onDragPositioningMove( - newBounds2.right.toFloat(), - newBounds2.bottom.toFloat() - ) - - taskPositioner.onDragPositioningEnd(newBounds2.right.toFloat(), newBounds2.bottom.toFloat()) - - // The first resize falls in the allowed area, verify there's a change for it. - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && change.ofBounds(newBounds) - } - }) - // The second resize falls in the disallowed area, verify there's no change for it. - verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && change.ofBounds(newBounds2) - } - }) - // Instead, there should be a change for its allowed portion (the X movement) with the Y - // staying frozen in the last valid resize position. - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && change.ofBounds( - Rect( - newBounds2.left, - newBounds2.top, - newBounds2.right, - newBounds.bottom // Stayed at the first resize destination. - ) - ) - } - }) - } - private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && bounds == configuration.windowConfiguration.bounds diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index bf365ca782ee..498082bd53e5 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 @@ -34,7 +34,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` +import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.never @@ -85,10 +85,10 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDragStartListener ) - `when`(taskToken.asBinder()).thenReturn(taskBinder) - `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) - `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) - `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + whenever(taskToken.asBinder()).thenReturn(taskBinder) + whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } @@ -102,7 +102,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { configuration.windowConfiguration.bounds = STARTING_BOUNDS } mockDesktopWindowDecoration.mDisplay = mockDisplay - `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID } + whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } @Test diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp index bfe4eaf39e21..613f52b32bea 100644 --- a/libs/hwui/effects/GainmapRenderer.cpp +++ b/libs/hwui/effects/GainmapRenderer.cpp @@ -38,7 +38,7 @@ namespace android::uirenderer { using namespace renderthread; -static float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) { +float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) { // We should always have a known destination colorspace. If we don't we must be in some // legacy mode where we're lost and also definitely not going to HDR if (destColorspace == nullptr) { diff --git a/libs/hwui/effects/GainmapRenderer.h b/libs/hwui/effects/GainmapRenderer.h index 4ed2445da17e..0ab03f0b571a 100644 --- a/libs/hwui/effects/GainmapRenderer.h +++ b/libs/hwui/effects/GainmapRenderer.h @@ -25,6 +25,8 @@ namespace android::uirenderer { +float getTargetHdrSdrRatio(const SkColorSpace* destColorspace); + void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src, const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint, SkCanvas::SrcRectConstraint constraint, diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index a4960ea17c79..c58ba6868eb5 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -26,6 +26,7 @@ #include "SkM44.h" #include "include/gpu/GpuTypes.h" // from Skia #include "utils/GLUtils.h" +#include <effects/GainmapRenderer.h> namespace android { namespace uirenderer { @@ -129,6 +130,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { info.height = fboSize.height(); mat4.getColMajor(&info.transform[0]); info.color_space_ptr = canvas->imageInfo().colorSpace(); + info.currentHdrSdrRatio = getTargetHdrSdrRatio(info.color_space_ptr); // 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 e6ef95b9cf91..e299d12b1d67 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -30,6 +30,7 @@ #include "renderthread/VulkanManager.h" #include "thread/ThreadBase.h" #include "utils/TimeUtils.h" +#include "effects/GainmapRenderer.h" namespace android { namespace uirenderer { @@ -73,6 +74,7 @@ void VkFunctorDrawHandler::draw(const GrBackendDrawableInfo& info) { .clip_right = mClip.fRight, .clip_bottom = mClip.fBottom, .is_layer = !vulkan_info.fFromSwapchainOrAndroidWindow, + .currentHdrSdrRatio = getTargetHdrSdrRatio(mImageInfo.colorSpace()), }; mat4.getColMajor(¶ms.transform[0]); params.secondary_command_buffer = vulkan_info.fSecondaryCommandBuffer; diff --git a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp index e168a7b9459a..adf3c06b8624 100644 --- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp @@ -32,6 +32,7 @@ #include "renderthread/EglManager.h" #include "thread/ThreadBase.h" #include "utils/TimeUtils.h" +#include "effects/GainmapRenderer.h" #include <SkBlendMode.h> @@ -139,6 +140,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { info.height = mFBInfo.height(); mat4.getColMajor(&info.transform[0]); info.color_space_ptr = canvas->imageInfo().colorSpace(); + info.currentHdrSdrRatio = getTargetHdrSdrRatio(info.color_space_ptr); glViewport(0, 0, info.width, info.height); diff --git a/libs/hwui/private/hwui/DrawGlInfo.h b/libs/hwui/private/hwui/DrawGlInfo.h index 501b8df9bc36..7888c8719e88 100644 --- a/libs/hwui/private/hwui/DrawGlInfo.h +++ b/libs/hwui/private/hwui/DrawGlInfo.h @@ -86,6 +86,11 @@ struct DrawGlInfo { // commands are issued. kStatusDrew = 0x4 }; + + // The current HDR/SDR ratio that we are rendering to. The transform to SDR will already + // be baked into the color_space_ptr, so this is just to indicate the amount of extended + // range is available if desired + float currentHdrSdrRatio; }; // struct DrawGlInfo } // namespace uirenderer diff --git a/libs/hwui/private/hwui/DrawVkInfo.h b/libs/hwui/private/hwui/DrawVkInfo.h index 5c596576df4e..8f7063d72314 100644 --- a/libs/hwui/private/hwui/DrawVkInfo.h +++ b/libs/hwui/private/hwui/DrawVkInfo.h @@ -71,6 +71,11 @@ struct VkFunctorDrawParams { // Input: Whether destination surface is offscreen surface. bool is_layer; + + // The current HDR/SDR ratio that we are rendering to. The transform to SDR will already + // be baked into the color_space_ptr, so this is just to indicate the amount of extended + // range is available if desired + float currentHdrSdrRatio; }; } // namespace uirenderer diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 96bfc1061d4e..f198bca9060c 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -42,7 +42,7 @@ namespace android { namespace uirenderer { namespace renderthread { -static std::array<std::string_view, 18> sEnableExtensions{ +static std::array<std::string_view, 19> sEnableExtensions{ VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME, @@ -56,6 +56,7 @@ static std::array<std::string_view, 18> sEnableExtensions{ VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_EXT_BLEND_OPERATION_ADVANCED_EXTENSION_NAME, + VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME, VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME, VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, diff --git a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java index 80bc5c07dd66..3dfc58788e8a 100644 --- a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java +++ b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java @@ -231,9 +231,17 @@ public final class SoundTriggerInstrumentation { */ public void setModelCallback(@NonNull @CallbackExecutor Executor executor, @NonNull ModelCallback callback) { + Objects.requireNonNull(callback); + Objects.requireNonNull(executor); synchronized (SoundTriggerInstrumentation.this.mLock) { - mModelCallback = Objects.requireNonNull(callback); - mModelExecutor = Objects.requireNonNull(executor); + if (mModelCallback == null) { + for (var droppedConsumer : mDroppedConsumerList) { + executor.execute(() -> droppedConsumer.accept(callback)); + } + mDroppedConsumerList.clear(); + } + mModelCallback = callback; + mModelExecutor = executor; } } @@ -267,9 +275,11 @@ public final class SoundTriggerInstrumentation { private void wrap(Consumer<ModelCallback> consumer) { synchronized (SoundTriggerInstrumentation.this.mLock) { - if (mModelCallback != null && mModelExecutor != null) { + if (mModelCallback != null) { final ModelCallback callback = mModelCallback; mModelExecutor.execute(() -> consumer.accept(callback)); + } else { + mDroppedConsumerList.add(consumer); } } } @@ -282,6 +292,8 @@ public final class SoundTriggerInstrumentation { private ModelCallback mModelCallback = null; @GuardedBy("SoundTriggerInstrumentation.this.mLock") private Executor mModelExecutor = null; + @GuardedBy("SoundTriggerInstrumentation.this.mLock") + private final List<Consumer<ModelCallback>> mDroppedConsumerList = new ArrayList<>(); } /** @@ -374,9 +386,18 @@ public final class SoundTriggerInstrumentation { */ public void setRecognitionCallback(@NonNull @CallbackExecutor Executor executor, @NonNull RecognitionCallback callback) { + Objects.requireNonNull(callback); + Objects.requireNonNull(executor); synchronized (SoundTriggerInstrumentation.this.mLock) { + if (mRecognitionCallback == null) { + for (var droppedConsumer : mDroppedConsumerList) { + executor.execute(() -> droppedConsumer.accept(callback)); + } + mDroppedConsumerList.clear(); + } mRecognitionCallback = callback; mRecognitionExecutor = executor; + } } @@ -401,9 +422,11 @@ public final class SoundTriggerInstrumentation { private void wrap(Consumer<RecognitionCallback> consumer) { synchronized (SoundTriggerInstrumentation.this.mLock) { - if (mRecognitionCallback != null && mRecognitionExecutor != null) { + if (mRecognitionCallback != null) { final RecognitionCallback callback = mRecognitionCallback; mRecognitionExecutor.execute(() -> consumer.accept(callback)); + } else { + mDroppedConsumerList.add(consumer); } } } @@ -416,6 +439,8 @@ public final class SoundTriggerInstrumentation { private Executor mRecognitionExecutor = null; @GuardedBy("SoundTriggerInstrumentation.this.mLock") private RecognitionCallback mRecognitionCallback = null; + @GuardedBy("SoundTriggerInstrumentation.this.mLock") + private final List<Consumer<RecognitionCallback>> mDroppedConsumerList = new ArrayList<>(); } // Implementation of injection interface passed to the HAL. diff --git a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml index 6bfcd82b0a4a..8c35da13e2c4 100644 --- a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml +++ b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml @@ -58,6 +58,7 @@ android:textSize="14sp" android:layout_marginTop="2dp" style="@style/TextAppearance" + android:focusable="true" android:textColor="?android:attr/textColorSecondary"/> </LinearLayout> diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 74072e9d4ec3..2502bbf7b40b 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -113,17 +113,11 @@ <!-- Back button for the helper consent dialog [CHAR LIMIT=30] --> <string name="consent_back">Back</string> - <!-- Action when permission list view is expanded CHAR LIMIT=30] --> - <string name="permission_expanded">Expanded</string> + <!-- Expand permission in the list CHAR LIMIT=30] --> + <string name="permission_expand">Expand <xliff:g id="permission_type" example="Notification">%1$s</xliff:g></string> - <!-- Expand action permission list CHAR LIMIT=30] --> - <string name="permission_expand">Expand</string> - - <!-- Action when permission list view is collapsed CHAR LIMIT=30] --> - <string name="permission_collapsed">Collapsed</string> - - <!-- Collapse action permission list CHAR LIMIT=30] --> - <string name="permission_collapse">Collapse</string> + <!-- Collapse permission int the list CHAR LIMIT=30] --> + <string name="permission_collapse">Collapse <xliff:g id="permission_type" example="Notification">%1$s</xliff:g></string> <!-- ================== System data transfer ==================== --> <!-- Title of the permission sync confirmation dialog. [CHAR LIMIT=NONE] --> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java index b86ef649331a..f594bf270d29 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java @@ -124,7 +124,7 @@ class PermissionListAdapter extends RecyclerView.Adapter<PermissionListAdapter.V } setAccessibility(view, viewType, - AccessibilityNodeInfo.ACTION_CLICK, R.string.permission_expand); + AccessibilityNodeInfo.ACTION_CLICK, R.string.permission_expand, 0); // Add expand buttons if the permissions are more than PERMISSION_SIZE in this list also // make the summary invisible by default. @@ -137,18 +137,16 @@ class PermissionListAdapter extends RecyclerView.Adapter<PermissionListAdapter.V viewHolder.mExpandButton.setImageResource(R.drawable.btn_expand_less); viewHolder.mPermissionSummary.setVisibility(View.VISIBLE); viewHolder.mExpandButton.setTag(R.drawable.btn_expand_less); - view.setContentDescription(mContext.getString(R.string.permission_expanded)); setAccessibility(view, viewType, - AccessibilityNodeInfo.ACTION_CLICK, R.string.permission_collapse); - viewHolder.mPermissionSummary.setFocusable(true); + AccessibilityNodeInfo.ACTION_CLICK, + R.string.permission_collapse, R.string.permission_expand); } else { viewHolder.mExpandButton.setImageResource(R.drawable.btn_expand_more); viewHolder.mPermissionSummary.setVisibility(View.GONE); viewHolder.mExpandButton.setTag(R.drawable.btn_expand_more); - view.setContentDescription(mContext.getString(R.string.permission_collapsed)); setAccessibility(view, viewType, - AccessibilityNodeInfo.ACTION_CLICK, R.string.permission_expanded); - viewHolder.mPermissionSummary.setFocusable(false); + AccessibilityNodeInfo.ACTION_CLICK, + R.string.permission_expand, R.string.permission_collapse); } }); } else { @@ -200,14 +198,20 @@ class PermissionListAdapter extends RecyclerView.Adapter<PermissionListAdapter.V } } - private void setAccessibility(View view, int viewType, int action, int resourceId) { - final String actionString = mContext.getString(resourceId); + private void setAccessibility(View view, int viewType, int action, int statusResourceId, + int actionResourceId) { final String permission = mContext.getString(sTitleMap.get(viewType)); + + if (actionResourceId != 0) { + view.announceForAccessibility( + getHtmlFromResources(mContext, actionResourceId, permission)); + } + view.setAccessibilityDelegate(new View.AccessibilityDelegate() { public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); info.addAction(new AccessibilityNodeInfo.AccessibilityAction(action, - actionString + permission)); + getHtmlFromResources(mContext, statusResourceId, permission))); } }); } diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml index 8724d69258ed..4161601e2317 100644 --- a/packages/CredentialManager/AndroidManifest.xml +++ b/packages/CredentialManager/AndroidManifest.xml @@ -42,15 +42,6 @@ android:excludeFromRecents="true" android:theme="@style/Theme.CredentialSelector"> </activity> - - <receiver - android:name=".CredentialProviderReceiver" - android:exported="true" - android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR"> - <intent-filter> - <action android:name="android.credentials.ui.action.CREDMAN_ENABLED_PROVIDERS_UPDATED"/> - </intent-filter> - </receiver> </application> </manifest> diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 340285760cda..7d71bd5c91ab 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -78,8 +78,12 @@ <!-- This text is followed by a list of one or more options. [CHAR LIMIT=200] --> <string name="save_credential_to_title">Save <xliff:g id="credentialTypes" example="passkey">%1$s</xliff:g> to</string> - <!-- This appears as the title of the modal bottom sheet for users to choose to create a passkey on another device. [CHAR LIMIT=200] --> - <string name="create_passkey_in_other_device_title">Create passkey in another device?</string> + <!-- This appears as the title of the modal bottom sheet for users to confirm to create a passkey on another device. [CHAR LIMIT=200] --> + <string name="create_passkey_in_other_device_title">Create passkey on another device?</string> + <!-- This appears as the title of the modal bottom sheet for users to confirm to save a password on another device. [CHAR LIMIT=200] --> + <string name="save_password_on_other_device_title">Save password on another device?</string> + <!-- This appears as the title of the modal bottom sheet for users to confirm to save a sign-in credential on another device. [CHAR LIMIT=200] --> + <string name="save_sign_in_on_other_device_title">Save sign-in on another device?</string> <!-- This appears as the title of the modal bottom sheet for users to confirm whether they should use the selected provider as default or not. [CHAR LIMIT=200] --> <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoDisplayName" example="Google Password Manager">%1$s</xliff:g> for all your sign-ins?</string> <!-- This appears as the description body of the modal bottom sheet for users to confirm whether they should use the selected provider as default or not. [CHAR LIMIT=300] --> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index a9bee039264e..693e76731834 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -107,7 +107,6 @@ class CredentialManagerRepo( initialUiState = when (requestInfo?.type) { RequestInfo.TYPE_CREATE -> { - val defaultProviderIdSetByUser = userConfigRepo.getDefaultProviderId() val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse() val providerEnableListUiState = getCreateProviderEnableListInitialUiState() val providerDisableListUiState = getCreateProviderDisableListInitialUiState() @@ -119,7 +118,8 @@ class CredentialManagerRepo( disabledProviders = providerDisableListUiState, defaultProviderIdPreferredByApp = requestDisplayInfoUiState.appPreferredDefaultProviderId, - defaultProviderIdSetByUser = defaultProviderIdSetByUser, + defaultProviderIdsSetByUser = + requestDisplayInfoUiState.userSetDefaultProviderIds, requestDisplayInfo = requestDisplayInfoUiState, isOnPasskeyIntroStateAlready = false, isPasskeyFirstUse = isPasskeyFirstUse, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 6549b2d0187c..1fb5e3f853b2 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -50,6 +50,8 @@ class CredentialSelectorActivity : ComponentActivity() { super.onCreate(savedInstanceState) overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0) + overrideActivityTransition(Activity.OVERRIDE_TRANSITION_CLOSE, + 0, 0) Log.d(Constants.LOG_TAG, "Creating new CredentialSelectorActivity") try { val (isCancellationRequest, shouldShowCancellationUi, _) = diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 8b74d766a152..de679895eedb 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -255,7 +255,8 @@ class CredentialSelectorViewModel( disabledProviders = prevUiState.disabledProviders, defaultProviderIdPreferredByApp = prevUiState.requestDisplayInfo.appPreferredDefaultProviderId, - defaultProviderIdSetByUser = userConfigRepo.getDefaultProviderId(), + defaultProviderIdsSetByUser = + prevUiState.requestDisplayInfo.userSetDefaultProviderIds, requestDisplayInfo = prevUiState.requestDisplayInfo, isOnPasskeyIntroStateAlready = true, isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse() @@ -269,28 +270,10 @@ class CredentialSelectorViewModel( userConfigRepo.setIsPasskeyFirstUse(false) } - fun createFlowOnMoreOptionsSelectedOnProviderSelection() { - uiState = uiState.copy( - createCredentialUiState = uiState.createCredentialUiState?.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, - isFromProviderSelection = true - ) - ) - } - fun createFlowOnMoreOptionsSelectedOnCreationSelection() { uiState = uiState.copy( createCredentialUiState = uiState.createCredentialUiState?.copy( currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, - isFromProviderSelection = false - ) - ) - } - - fun createFlowOnBackProviderSelectionButtonSelected() { - uiState = uiState.copy( - createCredentialUiState = uiState.createCredentialUiState?.copy( - currentScreenState = CreateScreenState.PROVIDER_SELECTION, ) ) } @@ -315,7 +298,10 @@ class CredentialSelectorViewModel( uiState = uiState.copy( createCredentialUiState = uiState.createCredentialUiState?.copy( currentScreenState = - if (activeEntry.activeProvider.id == userConfigRepo.getDefaultProviderId() || + if (uiState.createCredentialUiState?.requestDisplayInfo?.userSetDefaultProviderIds + ?.contains(activeEntry.activeProvider.id) ?: true || + !(uiState.createCredentialUiState?.foundCandidateFromUserDefaultProvider + ?: false) || !TextUtils.isEmpty(uiState.createCredentialUiState?.requestDisplayInfo ?.appPreferredDefaultProviderId)) CreateScreenState.CREATION_OPTION_SELECTION @@ -325,18 +311,7 @@ class CredentialSelectorViewModel( ) } - fun createFlowOnEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) { - val providerId = activeEntry.activeProvider.id - createFlowOnDefaultChanged(providerId) - uiState = uiState.copy( - createCredentialUiState = uiState.createCredentialUiState?.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - activeEntry = activeEntry - ) - ) - } - - fun createFlowOnDisabledProvidersSelected() { + fun createFlowOnLaunchSettings() { credManRepo.onSettingLaunchCancel() uiState = uiState.copy(dialogState = DialogState.CANCELED_FOR_SETTINGS) } @@ -349,16 +324,6 @@ class CredentialSelectorViewModel( ) } - fun createFlowOnChangeDefaultSelected() { - uiState = uiState.copy( - createCredentialUiState = uiState.createCredentialUiState?.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - ) - ) - val providerId = uiState.createCredentialUiState?.activeEntry?.activeProvider?.id - createFlowOnDefaultChanged(providerId) - } - fun createFlowOnUseOnceSelected() { uiState = uiState.copy( createCredentialUiState = uiState.createCredentialUiState?.copy( @@ -367,17 +332,6 @@ class CredentialSelectorViewModel( ) } - fun createFlowOnDefaultChanged(providerId: String?) { - if (providerId != null) { - Log.d( - Constants.LOG_TAG, "Default provider changed to: " + - " {provider=$providerId") - userConfigRepo.setDefaultProvider(providerId) - } else { - Log.w(Constants.LOG_TAG, "Null provider is being changed") - } - } - fun createFlowOnEntrySelected(selectedEntry: BaseEntry) { val providerId = selectedEntry.providerId val entryKey = selectedEntry.entryKey diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 57035d426654..00c2f1a63ed4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -61,6 +61,7 @@ import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.credentials.provider.RemoteEntry import org.json.JSONObject +import java.time.Instant // TODO: remove all !! checks fun getAppLabel( @@ -420,7 +421,7 @@ class CreateFlowUtils { id = it.providerFlattenedComponentName, displayName = providerLabel, icon = providerIcon, - createOptions = toCreationOptionInfoList( + sortedCreateOptions = toSortedCreationOptionInfoList( it.providerFlattenedComponentName, it.saveEntries, context ), remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry), @@ -483,6 +484,7 @@ class CreateFlowUtils { context.getDrawable(R.drawable.ic_password_24) ?: return null, preferImmediatelyAvailableCredentials = false, appPreferredDefaultProviderId = appPreferredDefaultProviderId, + userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(), ) is CreatePublicKeyCredentialRequest -> { newRequestDisplayInfoFromPasskeyJson( @@ -492,6 +494,7 @@ class CreateFlowUtils { preferImmediatelyAvailableCredentials = createCredentialRequestJetpack.preferImmediatelyAvailableCredentials, appPreferredDefaultProviderId = appPreferredDefaultProviderId, + userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(), ) } is CreateCustomCredentialRequest -> { @@ -508,6 +511,7 @@ class CreateFlowUtils { ?: context.getDrawable(R.drawable.ic_other_sign_in_24) ?: return null, preferImmediatelyAvailableCredentials = false, appPreferredDefaultProviderId = appPreferredDefaultProviderId, + userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(), ) } else -> null @@ -518,13 +522,13 @@ class CreateFlowUtils { enabledProviders: List<EnabledProviderInfo>, disabledProviders: List<DisabledProviderInfo>?, defaultProviderIdPreferredByApp: String?, - defaultProviderIdSetByUser: String?, + defaultProviderIdsSetByUser: Set<String>, requestDisplayInfo: RequestDisplayInfo, isOnPasskeyIntroStateAlready: Boolean, isPasskeyFirstUse: Boolean, ): CreateCredentialUiState? { - var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null var remoteEntry: RemoteInfo? = null + var remoteEntryProvider: EnabledProviderInfo? = null var defaultProviderPreferredByApp: EnabledProviderInfo? = null var defaultProviderSetByUser: EnabledProviderInfo? = null var createOptionsPairs: @@ -535,14 +539,24 @@ class CreateFlowUtils { defaultProviderPreferredByApp = enabledProvider } } - if (defaultProviderIdSetByUser != null) { - if (enabledProvider.id == defaultProviderIdSetByUser) { + if (enabledProvider.sortedCreateOptions.isNotEmpty() && + defaultProviderIdsSetByUser.contains(enabledProvider.id)) { + if (defaultProviderSetByUser == null) { defaultProviderSetByUser = enabledProvider + } else { + val newLastUsedTime = enabledProvider.sortedCreateOptions.firstOrNull() + ?.lastUsedTime + val curLastUsedTime = defaultProviderSetByUser?.sortedCreateOptions + ?.firstOrNull()?.lastUsedTime ?: Instant.MIN + if (newLastUsedTime != null) { + if (curLastUsedTime == null || newLastUsedTime > curLastUsedTime) { + defaultProviderSetByUser = enabledProvider + } + } } } - if (enabledProvider.createOptions.isNotEmpty()) { - lastSeenProviderWithNonEmptyCreateOptions = enabledProvider - enabledProvider.createOptions.forEach { + if (enabledProvider.sortedCreateOptions.isNotEmpty()) { + enabledProvider.sortedCreateOptions.forEach { createOptionsPairs.add(Pair(it, enabledProvider)) } } @@ -554,6 +568,7 @@ class CreateFlowUtils { return null } remoteEntry = currRemoteEntry + remoteEntryProvider = enabledProvider } } val defaultProvider = defaultProviderPreferredByApp ?: defaultProviderSetByUser @@ -561,27 +576,26 @@ class CreateFlowUtils { createOptionSize = createOptionsPairs.size, isOnPasskeyIntroStateAlready = isOnPasskeyIntroStateAlready, requestDisplayInfo = requestDisplayInfo, - defaultProvider = defaultProvider, remoteEntry = remoteEntry, isPasskeyFirstUse = isPasskeyFirstUse ) ?: return null + val sortedCreateOptionsPairs = createOptionsPairs.sortedWith( + compareByDescending { it.first.lastUsedTime } + ) return CreateCredentialUiState( enabledProviders = enabledProviders, disabledProviders = disabledProviders, currentScreenState = initialScreenState, requestDisplayInfo = requestDisplayInfo, - sortedCreateOptionsPairs = createOptionsPairs.sortedWith( - compareByDescending { it.first.lastUsedTime } - ), - hasDefaultProvider = defaultProvider != null, + sortedCreateOptionsPairs = sortedCreateOptionsPairs, activeEntry = toActiveEntry( - /*defaultProvider=*/defaultProvider, - /*createOptionSize=*/createOptionsPairs.size, - /*lastSeenProviderWithNonEmptyCreateOptions=*/ - lastSeenProviderWithNonEmptyCreateOptions, - /*remoteEntry=*/remoteEntry + defaultProvider = defaultProvider, + sortedCreateOptionsPairs = sortedCreateOptionsPairs, + remoteEntry = remoteEntry, + remoteEntryProvider = remoteEntryProvider, ), remoteEntry = remoteEntry, + foundCandidateFromUserDefaultProvider = defaultProviderSetByUser != null, ) } @@ -589,59 +603,43 @@ class CreateFlowUtils { createOptionSize: Int, isOnPasskeyIntroStateAlready: Boolean, requestDisplayInfo: RequestDisplayInfo, - defaultProvider: EnabledProviderInfo?, remoteEntry: RemoteInfo?, isPasskeyFirstUse: Boolean, ): CreateScreenState? { - return if (isPasskeyFirstUse && requestDisplayInfo.type == - CredentialType.PASSKEY && !isOnPasskeyIntroStateAlready) { + return if (isPasskeyFirstUse && requestDisplayInfo.type == CredentialType.PASSKEY && + !isOnPasskeyIntroStateAlready) { CreateScreenState.PASSKEY_INTRO - } else if ((defaultProvider == null || defaultProvider.createOptions.isEmpty()) && - createOptionSize > 1) { - CreateScreenState.PROVIDER_SELECTION - } else if (((defaultProvider == null || defaultProvider.createOptions.isEmpty()) && - createOptionSize == 1) || (defaultProvider != null && - defaultProvider.createOptions.isNotEmpty())) { - CreateScreenState.CREATION_OPTION_SELECTION } else if (createOptionSize == 0 && remoteEntry != null) { CreateScreenState.EXTERNAL_ONLY_SELECTION } else { - Log.d( - Constants.LOG_TAG, - "Unexpected failure: the screen state failed to instantiate" + - " because the provider list is empty." - ) - null + CreateScreenState.CREATION_OPTION_SELECTION } } private fun toActiveEntry( defaultProvider: EnabledProviderInfo?, - createOptionSize: Int, - lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo?, + sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, remoteEntry: RemoteInfo?, + remoteEntryProvider: EnabledProviderInfo?, ): ActiveEntry? { return if ( - defaultProvider != null && defaultProvider.createOptions.isEmpty() && - remoteEntry != null - ) { - ActiveEntry(defaultProvider, remoteEntry) - } else if ( - defaultProvider != null && defaultProvider.createOptions.isNotEmpty() + sortedCreateOptionsPairs.isEmpty() && remoteEntry != null && + remoteEntryProvider != null ) { - ActiveEntry(defaultProvider, defaultProvider.createOptions.first()) - } else if (createOptionSize == 1) { - ActiveEntry( - lastSeenProviderWithNonEmptyCreateOptions!!, - lastSeenProviderWithNonEmptyCreateOptions.createOptions.first() - ) + ActiveEntry(remoteEntryProvider, remoteEntry) + } else if (defaultProvider != null && + defaultProvider.sortedCreateOptions.isNotEmpty()) { + ActiveEntry(defaultProvider, defaultProvider.sortedCreateOptions.first()) + } else if (sortedCreateOptionsPairs.isNotEmpty()) { + val (topEntry, topEntryProvider) = sortedCreateOptionsPairs.first() + ActiveEntry(topEntryProvider, topEntry) } else null } /** * Note: caller required handle empty list due to parsing error. */ - private fun toCreationOptionInfoList( + private fun toSortedCreationOptionInfoList( providerId: String, creationEntries: List<Entry>, context: Context, @@ -664,7 +662,9 @@ class CreateFlowUtils { footerDescription = createEntry.description?.toString() )) } - return result + return result.sortedWith( + compareByDescending { it.lastUsedTime } + ) } private fun toRemoteInfo( @@ -690,6 +690,7 @@ class CreateFlowUtils { context: Context, preferImmediatelyAvailableCredentials: Boolean, appPreferredDefaultProviderId: String?, + userSetDefaultProviderIds: Set<String>, ): RequestDisplayInfo? { val json = JSONObject(requestJson) var passkeyUsername = "" @@ -711,6 +712,7 @@ class CreateFlowUtils { context.getDrawable(R.drawable.ic_passkey_24) ?: return null, preferImmediatelyAvailableCredentials, appPreferredDefaultProviderId, + userSetDefaultProviderIds, ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt index a17f2c88abcd..bfcca4970a71 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt @@ -23,15 +23,6 @@ class UserConfigRepo(context: Context) { val sharedPreferences: SharedPreferences = context.getSharedPreferences( context.packageName, Context.MODE_PRIVATE) - fun setDefaultProvider( - providerId: String - ) { - sharedPreferences.edit().apply { - putString(DEFAULT_PROVIDER, providerId) - apply() - } - } - fun setIsPasskeyFirstUse( isFirstUse: Boolean ) { @@ -41,16 +32,11 @@ class UserConfigRepo(context: Context) { } } - fun getDefaultProviderId(): String? { - return sharedPreferences.getString(DEFAULT_PROVIDER, null) - } - fun getIsPasskeyFirstUse(): Boolean { return sharedPreferences.getBoolean(IS_PASSKEY_FIRST_USE, true) } companion object { - const val DEFAULT_PROVIDER = "default_provider" // This first use value only applies to passkeys, not related with if generally // credential manager is first use or not const val IS_PASSKEY_FIRST_USE = "is_passkey_first_use" diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 9d871ed75494..d16120fee452 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -88,27 +88,11 @@ fun CreateCredentialScreen( onLearnMore = viewModel::createFlowOnLearnMore, onLog = { viewModel.logUiEvent(it) }, ) - CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard( - requestDisplayInfo = createCredentialUiState.requestDisplayInfo, - disabledProviderList = createCredentialUiState - .disabledProviders, - sortedCreateOptionsPairs = - createCredentialUiState.sortedCreateOptionsPairs, - hasRemoteEntry = createCredentialUiState.remoteEntry != null, - onOptionSelected = - viewModel::createFlowOnEntrySelectedFromFirstUseScreen, - onDisabledProvidersSelected = - viewModel::createFlowOnDisabledProvidersSelected, - onMoreOptionsSelected = - viewModel::createFlowOnMoreOptionsSelectedOnProviderSelection, - onLog = { viewModel.logUiEvent(it) }, - ) CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( requestDisplayInfo = createCredentialUiState.requestDisplayInfo, enabledProviderList = createCredentialUiState.enabledProviders, providerInfo = createCredentialUiState .activeEntry?.activeProvider!!, - hasDefaultProvider = createCredentialUiState.hasDefaultProvider, createOptionInfo = createCredentialUiState.activeEntry.activeEntryInfo as CreateOptionInfo, @@ -121,21 +105,15 @@ fun CreateCredentialScreen( CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( requestDisplayInfo = createCredentialUiState.requestDisplayInfo, enabledProviderList = createCredentialUiState.enabledProviders, - disabledProviderList = createCredentialUiState - .disabledProviders, + disabledProviderList = createCredentialUiState.disabledProviders, sortedCreateOptionsPairs = createCredentialUiState.sortedCreateOptionsPairs, - hasDefaultProvider = createCredentialUiState.hasDefaultProvider, - isFromProviderSelection = - createCredentialUiState.isFromProviderSelection!!, - onBackProviderSelectionButtonSelected = - viewModel::createFlowOnBackProviderSelectionButtonSelected, onBackCreationSelectionButtonSelected = viewModel::createFlowOnBackCreationSelectionButtonSelected, onOptionSelected = viewModel::createFlowOnEntrySelectedFromMoreOptionScreen, onDisabledProvidersSelected = - viewModel::createFlowOnDisabledProvidersSelected, + viewModel::createFlowOnLaunchSettings, onRemoteEntrySelected = viewModel::createFlowOnEntrySelected, onLog = { viewModel.logUiEvent(it) }, ) @@ -144,11 +122,11 @@ fun CreateCredentialScreen( viewModel.onIllegalUiState("Expect active entry to be non-null" + " upon default provider dialog.") } else { - DefaultProviderConfirmationCard( + NonDefaultUsageConfirmationCard( selectedEntry = createCredentialUiState.activeEntry, onIllegalScreenState = viewModel::onIllegalUiState, - onChangeDefaultSelected = - viewModel::createFlowOnChangeDefaultSelected, + onLaunchSettings = + viewModel::createFlowOnLaunchSettings, onUseOnceSelected = viewModel::createFlowOnUseOnceSelected, onLog = { viewModel.logUiEvent(it) }, ) @@ -263,94 +241,11 @@ fun PasskeyIntroCard( } @Composable -fun ProviderSelectionCard( - requestDisplayInfo: RequestDisplayInfo, - disabledProviderList: List<DisabledProviderInfo>?, - sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, - hasRemoteEntry: Boolean, - onOptionSelected: (ActiveEntry) -> Unit, - onDisabledProvidersSelected: () -> Unit, - onMoreOptionsSelected: () -> Unit, - onLog: @Composable (UiEventEnum) -> Unit, -) { - SheetContainerCard { - item { HeadlineIcon(bitmap = requestDisplayInfo.typeIcon.toBitmap().asImageBitmap()) } - item { Divider(thickness = 16.dp, color = Color.Transparent) } - item { - HeadlineText( - text = stringResource( - R.string.choose_provider_title, - when (requestDisplayInfo.type) { - CredentialType.PASSKEY -> - stringResource(R.string.passkeys) - CredentialType.PASSWORD -> - stringResource(R.string.passwords) - CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info) - } - ) - ) - } - item { Divider(thickness = 24.dp, color = Color.Transparent) } - - item { - Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) { - BodyMediumText(text = stringResource(R.string.choose_provider_body)) - } - } - item { Divider(thickness = 16.dp, color = Color.Transparent) } - item { - CredentialContainerCard { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - sortedCreateOptionsPairs.forEach { entry -> - MoreOptionsInfoRow( - requestDisplayInfo = requestDisplayInfo, - providerInfo = entry.second, - createOptionInfo = entry.first, - onOptionSelected = { - onOptionSelected( - ActiveEntry( - entry.second, - entry.first - ) - ) - } - ) - } - MoreOptionsDisabledProvidersRow( - disabledProviders = disabledProviderList, - onDisabledProvidersSelected = onDisabledProvidersSelected, - ) - } - } - } - if (hasRemoteEntry) { - item { Divider(thickness = 24.dp, color = Color.Transparent) } - item { - CtaButtonRow( - leftButton = { - ActionButton( - stringResource(R.string.string_more_options), - onMoreOptionsSelected - ) - } - ) - } - } - } - onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_PROVIDER_SELECTION) -} - -@Composable fun MoreOptionsSelectionCard( requestDisplayInfo: RequestDisplayInfo, enabledProviderList: List<EnabledProviderInfo>, disabledProviderList: List<DisabledProviderInfo>?, sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, - hasDefaultProvider: Boolean, - isFromProviderSelection: Boolean, - onBackProviderSelectionButtonSelected: () -> Unit, onBackCreationSelectionButtonSelected: () -> Unit, onOptionSelected: (ActiveEntry) -> Unit, onDisabledProvidersSelected: () -> Unit, @@ -369,9 +264,7 @@ fun MoreOptionsSelectionCard( CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info) } ), - onNavigationIconClicked = - if (isFromProviderSelection) onBackProviderSelectionButtonSelected - else onBackCreationSelectionButtonSelected, + onNavigationIconClicked = onBackCreationSelectionButtonSelected, bottomPadding = 16.dp, ) }) { @@ -379,30 +272,26 @@ fun MoreOptionsSelectionCard( item { CredentialContainerCard { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - // Only in the flows with default provider(not first time use) we can show the - // createOptions here, or they will be shown on ProviderSelectionCard - if (hasDefaultProvider) { - sortedCreateOptionsPairs.forEach { entry -> - MoreOptionsInfoRow( - requestDisplayInfo = requestDisplayInfo, - providerInfo = entry.second, - createOptionInfo = entry.first, - onOptionSelected = { - onOptionSelected( - ActiveEntry( - entry.second, - entry.first - ) + sortedCreateOptionsPairs.forEach { entry -> + MoreOptionsInfoRow( + requestDisplayInfo = requestDisplayInfo, + providerInfo = entry.second, + createOptionInfo = entry.first, + onOptionSelected = { + onOptionSelected( + ActiveEntry( + entry.second, + entry.first ) - } - ) - } - MoreOptionsDisabledProvidersRow( - disabledProviders = disabledProviderList, - onDisabledProvidersSelected = - onDisabledProvidersSelected, + ) + } ) } + MoreOptionsDisabledProvidersRow( + disabledProviders = disabledProviderList, + onDisabledProvidersSelected = + onDisabledProvidersSelected, + ) enabledProviderList.forEach { if (it.remoteEntry != null) { RemoteEntryRow( @@ -420,10 +309,10 @@ fun MoreOptionsSelectionCard( } @Composable -fun DefaultProviderConfirmationCard( +fun NonDefaultUsageConfirmationCard( selectedEntry: ActiveEntry, onIllegalScreenState: (String) -> Unit, - onChangeDefaultSelected: () -> Unit, + onLaunchSettings: () -> Unit, onUseOnceSelected: () -> Unit, onLog: @Composable (UiEventEnum) -> Unit, ) { @@ -454,14 +343,14 @@ fun DefaultProviderConfirmationCard( CtaButtonRow( leftButton = { ActionButton( - stringResource(R.string.use_once), - onClick = onUseOnceSelected + stringResource(R.string.settings), + onClick = onLaunchSettings, ) }, rightButton = { ConfirmButton( - stringResource(R.string.set_as_default), - onClick = onChangeDefaultSelected + stringResource(R.string.use_once), + onClick = onUseOnceSelected, ) }, ) @@ -479,7 +368,6 @@ fun CreationSelectionCard( onOptionSelected: (BaseEntry) -> Unit, onConfirm: () -> Unit, onMoreOptionsSelected: () -> Unit, - hasDefaultProvider: Boolean, onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard { @@ -527,16 +415,9 @@ fun CreationSelectionCard( if (enabledProvider.remoteEntry != null) { remoteEntry = enabledProvider.remoteEntry } - createOptionsSize += enabledProvider.createOptions.size - } - val shouldShowMoreOptionsButton = if (!hasDefaultProvider) { - // User has already been presented with all options on the default provider - // selection screen. Don't show them again. Therefore, only show the more option - // button if remote option is present. - remoteEntry != null - } else { - createOptionsSize > 1 || remoteEntry != null + createOptionsSize += enabledProvider.sortedCreateOptions.size } + val shouldShowMoreOptionsButton = createOptionsSize > 1 || remoteEntry != null item { CtaButtonRow( leftButton = if (shouldShowMoreOptionsButton) { @@ -584,7 +465,17 @@ fun ExternalOnlySelectionCard( SheetContainerCard { item { HeadlineIcon(imageVector = Icons.Outlined.QrCodeScanner) } item { Divider(thickness = 16.dp, color = Color.Transparent) } - item { HeadlineText(text = stringResource(R.string.create_passkey_in_other_device_title)) } + item { + HeadlineText( + text = stringResource( + when (requestDisplayInfo.type) { + CredentialType.PASSKEY -> R.string.create_passkey_in_other_device_title + CredentialType.PASSWORD -> R.string.save_password_on_other_device_title + else -> R.string.save_sign_in_on_other_device_title + } + ) + ) + } item { Divider(thickness = 24.dp, color = Color.Transparent) } item { CredentialContainerCard { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 225dbf2b744f..fe1ce1b60413 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -29,12 +29,9 @@ data class CreateCredentialUiState( val currentScreenState: CreateScreenState, val requestDisplayInfo: RequestDisplayInfo, val sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, - // Should not change with the real time update of default provider, only determine whether - // we're showing provider selection page at the beginning - val hasDefaultProvider: Boolean, val activeEntry: ActiveEntry? = null, val remoteEntry: RemoteInfo? = null, - val isFromProviderSelection: Boolean? = null, + val foundCandidateFromUserDefaultProvider: Boolean, ) internal fun hasContentToDisplay(state: CreateCredentialUiState): Boolean { @@ -50,11 +47,12 @@ open class ProviderInfo( ) class EnabledProviderInfo( - icon: Drawable, - id: String, - displayName: String, - var createOptions: List<CreateOptionInfo>, - var remoteEntry: RemoteInfo?, + icon: Drawable, + id: String, + displayName: String, + // Sorted by last used time + var sortedCreateOptions: List<CreateOptionInfo>, + var remoteEntry: RemoteInfo?, ) : ProviderInfo(icon, id, displayName) class DisabledProviderInfo( @@ -108,6 +106,7 @@ data class RequestDisplayInfo( val typeIcon: Drawable, val preferImmediatelyAvailableCredentials: Boolean, val appPreferredDefaultProviderId: String?, + val userSetDefaultProviderIds: Set<String>, ) /** @@ -123,7 +122,6 @@ data class ActiveEntry ( enum class CreateScreenState { PASSKEY_INTRO, MORE_ABOUT_PASSKEYS_INTRO, - PROVIDER_SELECTION, CREATION_OPTION_SELECTION, MORE_OPTIONS_SELECTION, DEFAULT_PROVIDER_CONFIRMATION, diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java index 4c5875b1f2d9..7b17cbdd3a1e 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java @@ -16,14 +16,14 @@ package com.android.packageinstaller; +import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID; + import android.app.Activity; import android.content.Intent; import android.os.Bundle; import androidx.annotation.Nullable; -import java.io.File; - /** * Trampoline activity. Calls PackageInstallerActivity and deletes staged install file onResult. */ @@ -52,8 +52,13 @@ public class DeleteStagedFileOnResult extends Activity { super.onDestroy(); if (isFinishing()) { - File sourceFile = new File(getIntent().getData().getPath()); - new Thread(sourceFile::delete).start(); + // While we expect PIA/InstallStaging to abandon/commit the session, still there + // might be cases when the session becomes orphan. + int sessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0); + try { + getPackageManager().getPackageInstaller().abandonSession(sessionId); + } catch (SecurityException ignored) { + } } } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java index c6217ece800d..7bea33971259 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java @@ -16,17 +16,17 @@ package com.android.packageinstaller; +import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID; + import android.app.PendingIntent; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; -import android.content.pm.PackageInstaller.InstallInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Process; import android.util.Log; import android.view.View; import android.widget.Button; @@ -34,10 +34,7 @@ import android.widget.Button; import androidx.annotation.Nullable; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; /** * Send package to the package manager and handle results from package manager. Once the @@ -77,7 +74,7 @@ public class InstallInstalling extends AlertActivity { .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); mPackageURI = getIntent().getData(); - if ("package".equals(mPackageURI.getScheme())) { + if (PackageInstallerActivity.SCHEME_PACKAGE.equals(mPackageURI.getScheme())) { try { getPackageManager().installExistingPackage(appInfo.packageName); launchSuccess(); @@ -86,6 +83,8 @@ public class InstallInstalling extends AlertActivity { PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); } } else { + // ContentResolver.SCHEME_FILE + // STAGED_SESSION_ID extra contains an ID of a previously staged install session. final File sourceFile = new File(mPackageURI.getPath()); PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile); @@ -122,41 +121,6 @@ public class InstallInstalling extends AlertActivity { // Does not happen } } else { - PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_FULL_INSTALL); - final Uri referrerUri = getIntent().getParcelableExtra(Intent.EXTRA_REFERRER); - params.setPackageSource( - referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE - : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE); - params.setInstallAsInstantApp(false); - params.setReferrerUri(referrerUri); - params.setOriginatingUri(getIntent() - .getParcelableExtra(Intent.EXTRA_ORIGINATING_URI)); - params.setOriginatingUid(getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, - Process.INVALID_UID)); - params.setInstallerPackageName(getIntent().getStringExtra( - Intent.EXTRA_INSTALLER_PACKAGE_NAME)); - params.setInstallReason(PackageManager.INSTALL_REASON_USER); - - File file = new File(mPackageURI.getPath()); - try { - final InstallInfo result = getPackageManager().getPackageInstaller() - .readInstallInfo(file, 0); - params.setAppPackageName(result.getPackageName()); - params.setInstallLocation(result.getInstallLocation()); - try { - params.setSize(result.calculateInstalledSize(params)); - } catch (IOException e) { - e.printStackTrace(); - params.setSize(file.length()); - } - } catch (PackageInstaller.PackageParsingException e) { - - Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults.", e); - Log.e(LOG_TAG, - "Cannot calculate installed size " + file + ". Try only apk size."); - params.setSize(file.length()); - } try { mInstallId = InstallEventReceiver .addObserver(this, EventResultPersister.GENERATE_NEW_ID, @@ -166,9 +130,14 @@ public class InstallInstalling extends AlertActivity { PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); } - try { - mSessionId = getPackageManager().getPackageInstaller().createSession(params); - } catch (IOException e) { + mSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0); + // Try to open session previously staged in InstallStaging. + try (PackageInstaller.Session ignored = + getPackageManager().getPackageInstaller().openSession( + mSessionId)) { + Log.d(LOG_TAG, "Staged session is valid, proceeding with the install"); + } catch (IOException | SecurityException e) { + Log.e(LOG_TAG, "Invalid session id passed", e); launchFailure(PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); } @@ -293,57 +262,9 @@ public class InstallInstalling extends AlertActivity { @Override protected PackageInstaller.Session doInBackground(Void... params) { - PackageInstaller.Session session; try { - session = getPackageManager().getPackageInstaller().openSession(mSessionId); + return getPackageManager().getPackageInstaller().openSession(mSessionId); } catch (IOException e) { - synchronized (this) { - isDone = true; - notifyAll(); - } - return null; - } - - session.setStagingProgress(0); - - try { - File file = new File(mPackageURI.getPath()); - - try (InputStream in = new FileInputStream(file)) { - long sizeBytes = file.length(); - long totalRead = 0; - try (OutputStream out = session - .openWrite("PackageInstaller", 0, sizeBytes)) { - byte[] buffer = new byte[1024 * 1024]; - while (true) { - int numRead = in.read(buffer); - - if (numRead == -1) { - session.fsync(out); - break; - } - - if (isCancelled()) { - session.close(); - break; - } - - out.write(buffer, 0, numRead); - if (sizeBytes > 0) { - totalRead += numRead; - float fraction = ((float) totalRead / (float) sizeBytes); - session.setStagingProgress(fraction); - } - } - } - } - - return session; - } catch (IOException | SecurityException e) { - Log.e(LOG_TAG, "Could not write package", e); - - session.close(); - return null; } finally { synchronized (this) { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java index 68de2f67bd94..097e47f58db1 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java @@ -16,6 +16,10 @@ package com.android.packageinstaller; +import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH; + +import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID; + import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -23,40 +27,49 @@ import android.app.DialogFragment; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.Process; import android.util.Log; import android.view.View; +import android.widget.ProgressBar; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** - * If a package gets installed from an content URI this step loads the package and turns it into - * and installation from a file. Then it re-starts the installation as usual. + * If a package gets installed from a content URI this step stages the installation session + * reading bytes from the URI. */ public class InstallStaging extends AlertActivity { private static final String LOG_TAG = InstallStaging.class.getSimpleName(); - private static final String STAGED_FILE = "STAGED_FILE"; + private static final String STAGED_SESSION_ID = "STAGED_SESSION_ID"; + + private @Nullable PackageInstaller mInstaller; /** Currently running task that loads the file from the content URI into a file */ private @Nullable StagingAsyncTask mStagingTask; - /** The file the package is in */ - private @Nullable File mStagedFile; + /** The session the package is in */ + private int mStagedSessionId; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mInstaller = getPackageManager().getPackageInstaller(); + setFinishOnTouchOutside(true); mAlert.setIcon(R.drawable.ic_file_download); mAlert.setTitle(getString(R.string.app_name_unknown)); @@ -66,6 +79,9 @@ public class InstallStaging extends AlertActivity { if (mStagingTask != null) { mStagingTask.cancel(true); } + + cleanupStagingSession(); + setResult(RESULT_CANCELED); finish(); }, null); @@ -73,11 +89,7 @@ public class InstallStaging extends AlertActivity { requireViewById(R.id.staging).setVisibility(View.VISIBLE); if (savedInstanceState != null) { - mStagedFile = new File(savedInstanceState.getString(STAGED_FILE)); - - if (!mStagedFile.exists()) { - mStagedFile = null; - } + mStagedSessionId = savedInstanceState.getInt(STAGED_SESSION_ID, 0); } } @@ -85,21 +97,41 @@ public class InstallStaging extends AlertActivity { protected void onResume() { super.onResume(); - // This is the first onResume in a single life of the activity + // This is the first onResume in a single life of the activity. if (mStagingTask == null) { - // File does not exist, or became invalid - if (mStagedFile == null) { - // Create file delayed to be able to show error + if (mStagedSessionId > 0) { + final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo( + mStagedSessionId); + if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) { + Log.w(LOG_TAG, "Session " + mStagedSessionId + " in funky state; ignoring"); + if (info != null) { + cleanupStagingSession(); + } + mStagedSessionId = 0; + } + } + + // Session does not exist, or became invalid. + if (mStagedSessionId <= 0) { + // Create session here to be able to show error. + final Uri packageUri = getIntent().getData(); + final AssetFileDescriptor afd = openAssetFileDescriptor(packageUri); try { - mStagedFile = TemporaryFileManager.getStagedFile(this); + ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null; + PackageInstaller.SessionParams params = createSessionParams( + mInstaller, getIntent(), pfd, packageUri.toString()); + mStagedSessionId = mInstaller.createSession(params); } catch (IOException e) { + Log.w(LOG_TAG, "Failed to create a staging session", e); showError(); return; + } finally { + PackageUtil.safeClose(afd); } } mStagingTask = new StagingAsyncTask(); - mStagingTask.execute(getIntent().getData()); + mStagingTask.execute(); } } @@ -107,7 +139,7 @@ public class InstallStaging extends AlertActivity { protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putString(STAGED_FILE, mStagedFile.getPath()); + outState.putInt(STAGED_SESSION_ID, mStagedSessionId); } @Override @@ -119,6 +151,65 @@ public class InstallStaging extends AlertActivity { super.onDestroy(); } + private AssetFileDescriptor openAssetFileDescriptor(Uri uri) { + try { + return getContentResolver().openAssetFileDescriptor(uri, "r"); + } catch (Exception e) { + Log.w(LOG_TAG, "Failed to open asset file descriptor", e); + return null; + } + } + + private static PackageInstaller.SessionParams createSessionParams( + @NonNull PackageInstaller installer, @NonNull Intent intent, + @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) { + PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER); + params.setPackageSource( + referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE + : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE); + params.setInstallAsInstantApp(false); + params.setReferrerUri(referrerUri); + params.setOriginatingUri(intent + .getParcelableExtra(Intent.EXTRA_ORIGINATING_URI)); + params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, + Process.INVALID_UID)); + params.setInstallerPackageName(intent.getStringExtra( + Intent.EXTRA_INSTALLER_PACKAGE_NAME)); + params.setInstallReason(PackageManager.INSTALL_REASON_USER); + + if (pfd != null) { + try { + final PackageInstaller.InstallInfo result = installer.readInstallInfo(pfd, + debugPathName, 0); + params.setAppPackageName(result.getPackageName()); + params.setInstallLocation(result.getInstallLocation()); + params.setSize(result.calculateInstalledSize(params, pfd)); + } catch (PackageInstaller.PackageParsingException | IOException e) { + Log.e(LOG_TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e); + Log.e(LOG_TAG, + "Cannot calculate installed size " + debugPathName + + ". Try only apk size."); + params.setSize(pfd.getStatSize()); + } + } else { + Log.e(LOG_TAG, "Cannot parse package " + debugPathName + ". Assuming defaults."); + } + return params; + } + + private void cleanupStagingSession() { + if (mStagedSessionId > 0) { + try { + mInstaller.abandonSession(mStagedSessionId); + } catch (SecurityException ignored) { + + } + mStagedSessionId = 0; + } + } + /** * Show an error message and set result as error. */ @@ -165,58 +256,109 @@ public class InstallStaging extends AlertActivity { } } - private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> { + private final class StagingAsyncTask extends + AsyncTask<Void, Integer, PackageInstaller.SessionInfo> { + private ProgressBar mProgressBar = null; + + private long getContentSizeBytes() { + try (AssetFileDescriptor afd = openAssetFileDescriptor(getIntent().getData())) { + return afd != null ? afd.getLength() : UNKNOWN_LENGTH; + } catch (IOException ignored) { + return UNKNOWN_LENGTH; + } + } + @Override - protected Boolean doInBackground(Uri... params) { - if (params == null || params.length <= 0) { - return false; + protected void onPreExecute() { + final long sizeBytes = getContentSizeBytes(); + + mProgressBar = sizeBytes > 0 ? requireViewById(R.id.progress_indeterminate) : null; + if (mProgressBar != null) { + mProgressBar.setProgress(0); + mProgressBar.setMax(100); + mProgressBar.setIndeterminate(false); } - Uri packageUri = params[0]; - try (InputStream in = getContentResolver().openInputStream(packageUri)) { - // Despite the comments in ContentResolver#openInputStream the returned stream can - // be null. + } + + @Override + protected PackageInstaller.SessionInfo doInBackground(Void... params) { + Uri packageUri = getIntent().getData(); + try (PackageInstaller.Session session = mInstaller.openSession(mStagedSessionId); + InputStream in = getContentResolver().openInputStream(packageUri)) { + session.setStagingProgress(0); + if (in == null) { - return false; + return null; } - try (OutputStream out = new FileOutputStream(mStagedFile)) { + long sizeBytes = getContentSizeBytes(); + + long totalRead = 0; + try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) { byte[] buffer = new byte[1024 * 1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) >= 0) { - // Be nice and respond to a cancellation + while (true) { + int numRead = in.read(buffer); + + if (numRead == -1) { + session.fsync(out); + break; + } + if (isCancelled()) { - return false; + break; + } + + out.write(buffer, 0, numRead); + if (sizeBytes > 0) { + totalRead += numRead; + float fraction = ((float) totalRead / (float) sizeBytes); + session.setStagingProgress(fraction); + publishProgress((int) (fraction * 100.0)); } - out.write(buffer, 0, bytesRead); } } + + return mInstaller.getSessionInfo(mStagedSessionId); } catch (IOException | SecurityException | IllegalStateException - | IllegalArgumentException e) { + | IllegalArgumentException e) { Log.w(LOG_TAG, "Error staging apk from content URI", e); - return false; + return null; } - return true; } @Override - protected void onPostExecute(Boolean success) { - if (success) { - // Now start the installation again from a file - Intent installIntent = new Intent(getIntent()); - installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class); - installIntent.setData(Uri.fromFile(mStagedFile)); - - if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { - installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - } - - installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(installIntent); + protected void onProgressUpdate(Integer... progress) { + if (mProgressBar != null && progress != null && progress.length > 0) { + mProgressBar.setProgress(progress[0], true); + } + } - InstallStaging.this.finish(); - } else { + @Override + protected void onPostExecute(PackageInstaller.SessionInfo sessionInfo) { + if (sessionInfo == null || !sessionInfo.isActive() + || sessionInfo.getResolvedBaseApkPath() == null) { + Log.w(LOG_TAG, "Session info is invalid: " + sessionInfo); + cleanupStagingSession(); showError(); + return; + } + + // Pass the staged session to the installer. + Intent installIntent = new Intent(getIntent()); + installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class); + installIntent.setData(Uri.fromFile(new File(sessionInfo.getResolvedBaseApkPath()))); + + installIntent.putExtra(EXTRA_STAGED_SESSION_ID, mStagedSessionId); + + if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { + installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); } + + installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + + startActivity(installIntent); + + InstallStaging.this.finish(); } } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index ac32020e842a..3f98867cb267 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -77,7 +77,21 @@ public class InstallStart extends Activity { } final ApplicationInfo sourceInfo = getSourceInfo(callingPackage); - final int originatingUid = getOriginatingUid(sourceInfo); + // Uid of the source package, coming from ActivityManager + int callingUid = getLaunchedFromUid(); + if (callingUid == Process.INVALID_UID) { + // Cannot reach ActivityManager. Aborting install. + Log.e(LOG_TAG, "Could not determine the launching uid."); + } + // Uid of the source package, with a preference to uid from ApplicationInfo + final int originatingUid = sourceInfo != null ? sourceInfo.uid : callingUid; + + if (callingUid == Process.INVALID_UID && sourceInfo == null) { + mAbortInstall = true; + } + + boolean isDocumentsManager = checkPermission(Manifest.permission.MANAGE_DOCUMENTS, + -1, callingUid) == PackageManager.PERMISSION_GRANTED; boolean isTrustedSource = false; if (sourceInfo != null && sourceInfo.isPrivilegedApp()) { isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) || ( @@ -86,7 +100,8 @@ public class InstallStart extends Activity { == PackageManager.PERMISSION_GRANTED); } - if (!isTrustedSource && originatingUid != Process.INVALID_UID) { + if (!isTrustedSource && !isSystemDownloadsProvider(callingUid) && !isDocumentsManager + && originatingUid != Process.INVALID_UID) { final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid); if (targetSdkVersion < 0) { Log.w(LOG_TAG, "Cannot get target sdk version for uid " + originatingUid); @@ -144,14 +159,15 @@ public class InstallStart extends Activity { if (packageUri != null && packageUri.getScheme().equals(ContentResolver.SCHEME_CONTENT) - && canPackageQuery(originatingUid, packageUri)) { + && canPackageQuery(callingUid, packageUri)) { // [IMPORTANT] This path is deprecated, but should still work. Only necessary // features should be added. - // Copy file to prevent it from being changed underneath this process + // Stage a session with this file to prevent it from being changed underneath + // this process. nextActivity.setClass(this, InstallStaging.class); - } else if (packageUri != null && packageUri.getScheme().equals( - PackageInstallerActivity.SCHEME_PACKAGE)) { + } else if (packageUri != null && PackageInstallerActivity.SCHEME_PACKAGE.equals( + packageUri.getScheme())) { nextActivity.setClass(this, PackageInstallerActivity.class); } else { Intent result = new Intent(); @@ -212,41 +228,6 @@ public class InstallStart extends Activity { return null; } - /** - * Get the originating uid if possible, or {@link Process#INVALID_UID} if not available - * - * @param sourceInfo The source of this installation - * @return The UID of the installation source or INVALID_UID - */ - private int getOriginatingUid(@Nullable ApplicationInfo sourceInfo) { - // The originating uid from the intent. We only trust/use this if it comes from either - // the document manager app or the downloads provider - final int uidFromIntent = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, - Process.INVALID_UID); - - final int callingUid; - if (sourceInfo != null) { - callingUid = sourceInfo.uid; - } else { - callingUid = getLaunchedFromUid(); - if (callingUid == Process.INVALID_UID) { - // Cannot reach ActivityManager. Aborting install. - Log.e(LOG_TAG, "Could not determine the launching uid."); - mAbortInstall = true; - return Process.INVALID_UID; - } - } - if (checkPermission(Manifest.permission.MANAGE_DOCUMENTS, -1, callingUid) - == PackageManager.PERMISSION_GRANTED) { - return uidFromIntent; - } - if (isSystemDownloadsProvider(callingUid)) { - return uidFromIntent; - } - // We don't trust uid from the intent. Use the calling uid instead. - return callingUid; - } - private boolean isSystemDownloadsProvider(int uid) { final ProviderInfo downloadProviderPackage = getPackageManager().resolveContentProvider( DOWNLOADS_AUTHORITY, 0); @@ -260,8 +241,7 @@ public class InstallStart extends Activity { } @NonNull - private boolean canPackageQuery(int originatingUid, Uri packageUri) { - String callingPackage = mPackageManager.getPackagesForUid(originatingUid)[0]; + private boolean canPackageQuery(int callingUid, Uri packageUri) { ProviderInfo info = mPackageManager.resolveContentProvider(packageUri.getAuthority(), PackageManager.ComponentInfoFlags.of(0)); if (info == null) { @@ -269,11 +249,20 @@ public class InstallStart extends Activity { } String targetPackage = info.packageName; - try { - return mPackageManager.canPackageQuery(callingPackage, targetPackage); - } catch (PackageManager.NameNotFoundException e) { + String[] callingPackages = mPackageManager.getPackagesForUid(callingUid); + if (callingPackages == null) { return false; } + for (String callingPackage: callingPackages) { + try { + if (mPackageManager.canPackageQuery(callingPackage, targetPackage)) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + // no-op + } + } + return false; } private boolean isCallerSessionOwner(int originatingUid, int sessionId) { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index 3ba2acb113d3..7e294eee024f 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -82,6 +82,7 @@ public class PackageInstallerActivity extends AlertActivity { static final String EXTRA_CALLING_PACKAGE = "EXTRA_CALLING_PACKAGE"; static final String EXTRA_CALLING_ATTRIBUTION_TAG = "EXTRA_CALLING_ATTRIBUTION_TAG"; static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO"; + static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID"; private static final String ALLOW_UNKNOWN_SOURCES_KEY = PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY"; @@ -403,6 +404,10 @@ public class PackageInstallerActivity extends AlertActivity { mReferrerURI = null; mPendingUserActionReason = info.getPendingUserActionReason(); } else { + // Two possible callers: + // 1. InstallStart with "SCHEME_PACKAGE". + // 2. InstallStaging with "SCHEME_FILE" and EXTRA_STAGED_SESSION_ID with staged + // session id. mSessionId = -1; packageSource = intent.getData(); mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); @@ -721,14 +726,16 @@ public class PackageInstallerActivity extends AlertActivity { } private void startInstall() { + String installerPackageName = getIntent().getStringExtra( + Intent.EXTRA_INSTALLER_PACKAGE_NAME); + int stagedSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0); + // Start subactivity to actually install the application Intent newIntent = new Intent(); newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mPkgInfo.applicationInfo); newIntent.setData(mPackageURI); newIntent.setClass(this, InstallInstalling.class); - String installerPackageName = getIntent().getStringExtra( - Intent.EXTRA_INSTALLER_PACKAGE_NAME); if (mOriginatingURI != null) { newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI); } @@ -745,6 +752,9 @@ public class PackageInstallerActivity extends AlertActivity { if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); } + if (stagedSessionId > 0) { + newIntent.putExtra(EXTRA_STAGED_SESSION_ID, stagedSessionId); + } newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI); startActivity(newIntent); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java index 698159f24bb7..0270591acceb 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java @@ -33,7 +33,9 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.io.Closeable; import java.io.File; +import java.io.IOException; /** * This is a utility class for defining some utility methods and constants @@ -190,4 +192,19 @@ public class PackageUtil { } return targetSdkVersion; } + + + /** + * Quietly close a closeable resource (e.g. a stream or file). The input may already + * be closed and it may even be null. + */ + static void safeClose(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } catch (IOException ioe) { + // Catch and discard the error + } + } + } } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java index 01f92c4fa7b1..19b7e8546805 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java @@ -17,6 +17,7 @@ package com.android.settingslib.collapsingtoolbar; import android.app.ActionBar; +import android.content.pm.PackageManager; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -59,7 +60,8 @@ public class CollapsingToolbarBaseActivity extends FragmentActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (mCustomizeLayoutResId > 0 && !BuildCompatUtils.isAtLeastS()) { + // for backward compatibility on R devices or wearable devices due to small device size. + if (mCustomizeLayoutResId > 0 && (!BuildCompatUtils.isAtLeastS() || isWatch())) { super.setContentView(mCustomizeLayoutResId); return; } @@ -157,6 +159,14 @@ public class CollapsingToolbarBaseActivity extends FragmentActivity { return getToolbarDelegate().getAppBarLayout(); } + private boolean isWatch() { + PackageManager packageManager = getPackageManager(); + if (packageManager == null) { + return false; + } + return packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH); + } + private CollapsingToolbarDelegate getToolbarDelegate() { if (mToolbardelegate == null) { mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java b/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java index 3c451127508d..dae48dbac0a4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java @@ -17,6 +17,7 @@ package com.android.settingslib.applications; import android.app.AppGlobals; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -94,6 +95,10 @@ public class DefaultAppInfo extends CandidateInfo { } + public @Nullable String getSummary() { + return this.summary; + } + @Override public Drawable loadIcon() { final IconDrawableFactory factory = IconDrawableFactory.newInstance(mContext); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index e4cc9f15aea1..6a5535d345db 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -100,6 +100,5 @@ public class SystemSettings { Settings.System.CAMERA_FLASH_NOTIFICATION, Settings.System.SCREEN_FLASH_NOTIFICATION, Settings.System.SCREEN_FLASH_NOTIFICATION_COLOR, - Settings.System.SMOOTH_DISPLAY }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 4b720636c1d4..85623b26c589 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -226,6 +226,5 @@ public class SystemSettingsValidators { VALIDATORS.put(System.CAMERA_FLASH_NOTIFICATION, BOOLEAN_VALIDATOR); VALIDATORS.put(System.SCREEN_FLASH_NOTIFICATION, BOOLEAN_VALIDATOR); VALIDATORS.put(System.SCREEN_FLASH_NOTIFICATION_COLOR, ANY_INTEGER_VALIDATOR); - VALIDATORS.put(System.SMOOTH_DISPLAY, BOOLEAN_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index d1bd5e661072..46b45d12efc8 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -34,7 +34,6 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OV import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import static com.android.internal.accessibility.util.AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM; -import static com.android.internal.display.RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE; import static com.android.providers.settings.SettingsState.FALLBACK_FILE_SUFFIX; import static com.android.providers.settings.SettingsState.getTypeFromKey; import static com.android.providers.settings.SettingsState.getUserIdFromKey; @@ -5674,7 +5673,7 @@ public class SettingsProvider extends ContentProvider { providers.addAll(Arrays.asList(resources.getStringArray(resourceId))); } catch (Resources.NotFoundException e) { Slog.w(LOG_TAG, - "Get default array Cred Provider not found: " + e.toString()); + "Get default array Cred Provider not found: " + e.toString()); } try { final String storedValue = resources.getString(resourceId); @@ -5683,7 +5682,7 @@ public class SettingsProvider extends ContentProvider { } } catch (Resources.NotFoundException e) { Slog.w(LOG_TAG, - "Get default Cred Provider not found: " + e.toString()); + "Get default Cred Provider not found: " + e.toString()); } if (!providers.isEmpty()) { @@ -5732,8 +5731,8 @@ public class SettingsProvider extends ContentProvider { final Setting currentSetting = secureSettings .getSettingLocked(Settings.Secure.CREDENTIAL_SERVICE); if (currentSetting.isNull()) { - final int resourceId = com.android.internal.R.array - .config_defaultCredentialProviderService; + final int resourceId = + com.android.internal.R.array.config_defaultCredentialProviderService; final Resources resources = getContext().getResources(); // If the config has not be defined we might get an exception. final List<String> providers = new ArrayList<>(); @@ -5741,7 +5740,7 @@ public class SettingsProvider extends ContentProvider { providers.addAll(Arrays.asList(resources.getStringArray(resourceId))); } catch (Resources.NotFoundException e) { Slog.w(LOG_TAG, - "Get default array Cred Provider not found: " + e.toString()); + "Get default array Cred Provider not found: " + e.toString()); } if (!providers.isEmpty()) { @@ -5840,44 +5839,12 @@ public class SettingsProvider extends ContentProvider { currentVersion = 218; } - // v218: Convert Smooth Display and Force Peak Refresh Rate to a boolean if (currentVersion == 218) { - final String peakRefreshRateSettingName = "peak_refresh_rate"; - final String minRefreshRateSettingName = "min_refresh_rate"; - - final SettingsState systemSettings = getSystemSettingsLocked(userId); - final Setting peakRefreshRateSetting = - systemSettings.getSettingLocked(peakRefreshRateSettingName); - final Setting minRefreshRateSetting = - systemSettings.getSettingLocked(minRefreshRateSettingName); - - float peakRefreshRate = DEFAULT_REFRESH_RATE; - float minRefreshRate = 0; - try { - if (!peakRefreshRateSetting.isNull()) { - peakRefreshRate = Float.parseFloat(peakRefreshRateSetting.getValue()); - } - } catch (NumberFormatException e) { - // Do nothing. Overwrite with default value. - } - try { - if (!minRefreshRateSetting.isNull()) { - minRefreshRate = Float.parseFloat(minRefreshRateSetting.getValue()); - } - } catch (NumberFormatException e) { - // Do nothing. Overwrite with default value. - } - - systemSettings.deleteSettingLocked(peakRefreshRateSettingName); - systemSettings.deleteSettingLocked(minRefreshRateSettingName); - - systemSettings.insertSettingLocked(Settings.System.SMOOTH_DISPLAY, - peakRefreshRate > DEFAULT_REFRESH_RATE ? "1" : "0", /* tag= */ null, - /* makeDefault= */ false, SettingsState.SYSTEM_PACKAGE_NAME); - systemSettings.insertSettingLocked(Settings.System.FORCE_PEAK_REFRESH_RATE, - minRefreshRate > 0 ? "1" : "0", /* tag= */ null, - /* makeDefault= */ false, SettingsState.SYSTEM_PACKAGE_NAME); - + // Version 219: Removed + // TODO(b/211737588): Back up the Smooth Display setting + // Future upgrades to the `peak_refresh_rate` and `min_refresh_rate` settings + // should account for the database in a non-upgraded and upgraded (change id: + // Ib2cb2dd100f06f5452083b7606109a486e795a0e) state. currentVersion = 219; } diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 36aa2ac74406..706666cbebab 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -97,7 +97,8 @@ public class SettingsBackupTest { Settings.System.WHEN_TO_MAKE_WIFI_CALLS, // bug? Settings.System.WINDOW_ORIENTATION_LISTENER_LOG, // used for debugging only Settings.System.DESKTOP_MODE, // developer setting for internal prototyping - Settings.System.FORCE_PEAK_REFRESH_RATE, // depends on hardware capabilities + Settings.System.MIN_REFRESH_RATE, // depends on hardware capabilities + Settings.System.PEAK_REFRESH_RATE, // depends on hardware capabilities Settings.System.SCREEN_BRIGHTNESS_FLOAT, Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 6e55000dfe8a..a27f113177c7 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -82,6 +82,7 @@ <uses-permission android:name="android.permission.CONTROL_VPN" /> <uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/> <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/> + <uses-permission android:name="android.permission.LOCATION_HARDWARE" /> <!-- Physical hardware --> <uses-permission android:name="android.permission.MANAGE_USB" /> <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" /> @@ -992,6 +993,18 @@ android:excludeFromRecents="true" android:resizeableActivity="false" android:theme="@android:style/Theme.NoDisplay" /> + + <activity + android:name=".notetask.LaunchNotesRoleSettingsTrampolineActivity" + android:exported="true" + android:excludeFromRecents="true" + android:resizeableActivity="false" + android:theme="@android:style/Theme.NoDisplay" > + <intent-filter> + <action android:name="com.android.systemui.action.MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> <!-- endregion --> <!-- started from ControlsRequestReceiver --> diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 77ddc6e573b7..1ce347215954 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -55,6 +55,7 @@ lynhan@google.com madym@google.com mankoff@google.com mateuszc@google.com +mgalhardo@google.com michaelmikhil@google.com michschn@google.com mkephart@google.com diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index 9346a2f7ebb7..00108940e6ec 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -23,8 +23,8 @@ import android.animation.ValueAnimator import android.graphics.Canvas import android.graphics.Typeface import android.graphics.fonts.Font +import android.graphics.fonts.FontVariationAxis import android.text.Layout -import android.text.TextPaint import android.util.LruCache private const val DEFAULT_ANIMATION_DURATION: Long = 300 @@ -33,18 +33,39 @@ private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit interface TypefaceVariantCache { - fun getTypefaceForVariant(fvar: String, targetPaint: TextPaint): Typeface? + fun getTypefaceForVariant(fvar: String?): Typeface? + + companion object { + fun createVariantTypeface(baseTypeface: Typeface, fVar: String?): Typeface { + if (fVar.isNullOrEmpty()) { + return baseTypeface + } + + val axes = FontVariationAxis.fromFontVariationSettings(fVar).toMutableList() + axes.removeIf { !baseTypeface.isSupportedAxes(it.getOpenTypeTagValue()) } + if (axes.isEmpty()) { + return baseTypeface + } + return Typeface.createFromTypefaceWithVariation(baseTypeface, axes) + } + } } -class TypefaceVariantCacheImpl() : TypefaceVariantCache { +class TypefaceVariantCacheImpl( + var baseTypeface: Typeface, +) : TypefaceVariantCache { private val cache = LruCache<String, Typeface>(TYPEFACE_CACHE_MAX_ENTRIES) - override fun getTypefaceForVariant(fvar: String, targetPaint: TextPaint): Typeface? { + override fun getTypefaceForVariant(fvar: String?): Typeface? { + if (fvar == null) { + return baseTypeface + } cache.get(fvar)?.let { return it } - targetPaint.fontVariationSettings = fvar - return targetPaint.typeface?.also { cache.put(fvar, it) } + return TypefaceVariantCache + .createVariantTypeface(baseTypeface, fvar) + .also { cache.put(fvar, it) } } } @@ -78,7 +99,7 @@ class TextAnimator( layout: Layout, private val invalidateCallback: () -> Unit, ) { - var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl() + var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl(layout.paint.typeface) get() = field set(value) { field = value @@ -244,8 +265,7 @@ class TextAnimator( } if (!fvar.isNullOrBlank()) { - textInterpolator.targetPaint.typeface = - typefaceCache.getTypefaceForVariant(fvar, textInterpolator.targetPaint) + textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar) } if (color != null) { @@ -339,4 +359,3 @@ class TextAnimator( ) } } - diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt index 79189bc3e5c3..8ed8d8fb61fd 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt @@ -38,6 +38,8 @@ class TextInterpolator( * Once you modified the style parameters, you have to call reshapeText to recalculate base text * layout. * + * Do not bypass the cache and update the typeface or font variation directly. + * * @return a paint object */ val basePaint = TextPaint(layout.paint) @@ -48,6 +50,8 @@ class TextInterpolator( * Once you modified the style parameters, you have to call reshapeText to recalculate target * text layout. * + * Do not bypass the cache and update the typeface or font variation directly. + * * @return a paint object */ val targetPaint = TextPaint(layout.paint) @@ -217,14 +221,8 @@ class TextInterpolator( run.fontRuns.forEach { fontRun -> fontRun.baseFont = fontInterpolator.lerp(fontRun.baseFont, fontRun.targetFont, progress) - val fvar = run { - val tmpFontVariationsArray = mutableListOf<FontVariationAxis>() - fontRun.baseFont.axes.forEach { - tmpFontVariationsArray.add(FontVariationAxis(it.tag, it.styleValue)) - } - FontVariationAxis.toFontVariationSettings(tmpFontVariationsArray) - } - basePaint.typeface = typefaceCache.getTypefaceForVariant(fvar, basePaint) + val fvar = FontVariationAxis.toFontVariationSettings(fontRun.baseFont.axes) + basePaint.typeface = typefaceCache.getTypefaceForVariant(fvar) } } } diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index 1008d37b6d2f..fec093b26bcc 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -32,6 +32,8 @@ java_library { "bcsmartspace/src/**/*.kt", ], + // If you add a static lib here, you may need to also add the package to the ClassLoaderFilter + // in PluginInstance. That will ensure that loaded plugins have access to the related classes. static_libs: [ "androidx.annotation_annotation", "PluginCoreLib", diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java index 436145ec0692..3244eb43c8c4 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java @@ -131,7 +131,8 @@ public interface QS extends FragmentBase { /** * A rounded corner clipping that makes QS feel as if it were behind everything. */ - void setFancyClipping(int top, int bottom, int cornerRadius, boolean visible); + void setFancyClipping(int leftInset, int top, int rightInset, int bottom, int cornerRadius, + boolean visible, boolean fullWidth); /** * @return if quick settings is fully collapsed currently diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml index 29832a081612..934fa6f54286 100644 --- a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml +++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml @@ -30,7 +30,7 @@ <FrameLayout android:id="@+id/inout_container" - android:layout_height="17dp" + android:layout_height="@*android:dimen/status_bar_system_icon_intrinsic_size" android:layout_width="wrap_content" android:layout_gravity="center_vertical"> <ImageView @@ -39,24 +39,25 @@ android:layout_width="wrap_content" android:src="@drawable/ic_activity_down" android:visibility="gone" - android:paddingEnd="2dp" + android:paddingEnd="2sp" /> <ImageView android:id="@+id/mobile_out" android:layout_height="wrap_content" android:layout_width="wrap_content" android:src="@drawable/ic_activity_up" - android:paddingEnd="2dp" + android:paddingEnd="2sp" android:visibility="gone" /> </FrameLayout> <ImageView android:id="@+id/mobile_type" - android:layout_height="wrap_content" + android:layout_height="@dimen/status_bar_mobile_signal_size" android:layout_width="wrap_content" android:layout_gravity="center_vertical" - android:paddingStart="2.5dp" - android:paddingEnd="1dp" + android:adjustViewBounds="true" + android:paddingStart="2.5sp" + android:paddingEnd="1sp" android:visibility="gone" /> <Space android:id="@+id/mobile_roaming_space" @@ -70,14 +71,14 @@ android:layout_gravity="center_vertical"> <com.android.systemui.statusbar.AnimatedImageView android:id="@+id/mobile_signal" - android:layout_height="wrap_content" - android:layout_width="wrap_content" + android:layout_height="@dimen/status_bar_mobile_signal_size" + android:layout_width="@dimen/status_bar_mobile_signal_size" systemui:hasOverlappingRendering="false" /> <ImageView android:id="@+id/mobile_roaming" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="@dimen/status_bar_mobile_signal_size" + android:layout_height="@dimen/status_bar_mobile_signal_size" android:src="@drawable/stat_sys_roaming" android:contentDescription="@string/data_connection_roaming" android:visibility="gone" /> diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index 441f963a855a..e989372adde3 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -126,8 +126,7 @@ <com.android.systemui.battery.BatteryMeterView android:id="@+id/batteryRemainingIcon" android:layout_width="wrap_content" - android:layout_height="@dimen/large_screen_shade_header_min_height" - app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height" + android:layout_height="0dp" app:layout_constrainedWidth="true" app:textAppearance="@style/TextAppearance.QS.Status" app:layout_constraintStart_toEndOf="@id/statusIcons" diff --git a/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml index 0ea0653ab89f..473ab08a1935 100644 --- a/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml +++ b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml @@ -24,11 +24,11 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" - android:layout_marginStart="2.5dp" + android:layout_marginStart="2.5sp" > <FrameLayout android:id="@+id/inout_container" - android:layout_height="17dp" + android:layout_height="@*android:dimen/status_bar_system_icon_intrinsic_size" android:layout_width="wrap_content" android:gravity="center_vertical" > <ImageView @@ -37,14 +37,14 @@ android:layout_width="wrap_content" android:src="@drawable/ic_activity_down" android:visibility="gone" - android:paddingEnd="2dp" + android:paddingEnd="2sp" /> <ImageView android:id="@+id/wifi_out" android:layout_height="wrap_content" android:layout_width="wrap_content" android:src="@drawable/ic_activity_up" - android:paddingEnd="2dp" + android:paddingEnd="2sp" android:visibility="gone" /> </FrameLayout> @@ -62,7 +62,7 @@ <View android:id="@+id/wifi_signal_spacer" android:layout_width="@dimen/status_bar_wifi_signal_spacer_width" - android:layout_height="4dp" + android:layout_height="4sp" android:visibility="gone" /> <!-- Looks like CarStatusBar uses this... --> @@ -75,7 +75,7 @@ <View android:id="@+id/wifi_airplane_spacer" android:layout_width="@dimen/status_bar_airplane_spacer_width" - android:layout_height="4dp" + android:layout_height="4sp" android:visibility="gone" /> </com.android.keyguard.AlphaOptimizedLinearLayout> diff --git a/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml b/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml index 18386637e8f8..191158e4e8c2 100644 --- a/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml +++ b/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml @@ -20,7 +20,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/udfps_animation_view_internal" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:contentDescription="@string/accessibility_fingerprint_label"> <!-- Background protection --> <ImageView diff --git a/packages/SystemUI/res/raw/biometricprompt_rear_landscape_base.json b/packages/SystemUI/res/raw/biometricprompt_rear_landscape_base.json new file mode 100644 index 000000000000..49c1c40f7f4f --- /dev/null +++ b/packages/SystemUI/res/raw/biometricprompt_rear_landscape_base.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"BiometricPrompt_Rear_Landscape_Base_Foldable","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 18","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[169.478,169.749,0],"ix":2,"l":2},"a":{"a":0,"k":[-48.123,-30.19,0],"ix":1,"l":2},"s":{"a":0,"k":[132,132,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".grey400","cl":"grey400","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.741176486015,0.75686275959,0.776470601559,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"black circle matte","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".grey904","cl":"grey904","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,35.536,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.552,0.087],[0,0]],"o":[[0,0],[0,-3.287],[0,0],[0,0]],"v":[[-2.301,8.869],[-2.301,-3.772],[2.301,-9.806],[2.301,9.806]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"black circle matte 2","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue401","cl":"blue401","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,-27.655,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,3.286],[0,0],[-2.552,0.086],[0,0]],"o":[[0,0],[0,-3.286],[0,0],[-2.552,-0.086]],"v":[[-2.301,16.282],[-2.301,-16.281],[2.301,-22.313],[2.301,22.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"black circle matte 3","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Finger 2","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-129,"s":[-67]},{"t":-29,"s":[0]}],"ix":10},"p":{"a":0,"k":[-75.352,41.307,0],"ix":2,"l":2},"a":{"a":0,"k":[94.648,211.307,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.72,-5.642],[0,0],[-9.394,-0.562],[-0.298,-0.038]],"o":[[-5.153,4.329],[3.882,-16.05],[0.31,0.019],[-0.044,0.75]],"v":[[0.863,12.222],[-8.931,14.755],[8.005,-15.108],[8.931,-15.021]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.792156875134,0.454901963472,0.376470595598,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[81.486,130.081],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 9","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.459,6.045],[-5.153,4.329],[-0.044,0.75],[3.116,-24.664],[5.23,-22.052],[8.666,11.92],[-2.9,9.135]],"o":[[0,0],[6.72,-5.642],[12.723,1.335],[-2.369,18.762],[-13.993,-5.333],[2.255,-5.502],[1.843,-5.815]],"v":[[-9.99,-18.348],[-0.196,-20.881],[7.872,-48.124],[21.578,-9.331],[12.104,48.124],[-22.574,21.555],[-14.791,-0.206]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.713725507259,0.384313732386,0.282352954149,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[82.545,163.184],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 8","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"black circle matte 4","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".grey903","cl":"grey903","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.345,-92.442,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24.07,0],[0,0],[-8.27,0],[0,0]],"o":[[0,0],[0,8.269],[0,0],[-14.024,-17.379]],"v":[[-29.778,-14.252],[-29.778,-0.721],[-14.805,14.252],[29.778,14.252]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"black circle matte 5","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".grey902","cl":"grey902","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-15.947,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[154.053,139.81,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.3,0.367],[0,0],[-2.364,0.157],[0,0]],"o":[[0,0],[2.3,-0.367],[0,0],[-2.364,-0.157]],"v":[[-3.5,75.533],[-3.5,-75.533],[3.5,-76.312],[3.5,76.312]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[113.225,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 7","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,8.269],[0,0],[2.181,-0.187],[0,0],[-2.23,0],[0,42.252],[10.593,13.127],[0,0]],"o":[[0,0],[-2.23,0],[0,0],[2.181,0.187],[42.252,0],[0,-18.182],[0,0],[-8.27,0]],"v":[[-34.946,-62.973],[-34.946,-76.504],[-41.558,-76.201],[-41.558,76.201],[-34.946,76.504],[41.558,0],[24.61,-48],[-19.973,-48]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[156.824,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".black 2","cl":"black","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48.123,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-129,"s":[0,0,100]},{"t":-79,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".grey700","cl":"grey700","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-56.481,-59.936,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.767,0],[0,0],[0,-3.767],[0,0],[-3.767,0],[0,0],[0,3.767],[0,0]],"o":[[0,0],[-3.767,0],[0,0],[0,3.767],[0,0],[3.767,0],[0,0],[0,-3.767]],"v":[[46.055,-14.479],[-46.056,-14.479],[-52.876,-7.659],[-52.876,7.658],[-46.056,14.479],[46.055,14.479],[52.876,7.658],[52.876,-7.659]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".grey901","cl":"grey901","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[16.485,2.727,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[4.184,0],[0,0],[0,0],[0,0],[0,-4.375]],"o":[[0,4.184],[0,0],[0,0],[0,0],[4.375,0],[0,0]],"v":[[114.116,92.129],[106.54,99.705],[7.788,99.705],[7.788,-99.704],[106.161,-99.704],[114.116,-91.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[5.707,0],[0,0],[1.894,-1.05],[0.886,0.346],[0,0],[2.166,0],[0,0],[0,-5.707],[0,0],[0,-1.46],[0,0],[-1.133,-0.038],[0,0],[0,-1.459],[0,0],[-1.133,-0.038],[0,0],[-5.708,0],[0,0],[-1.894,1.05],[-0.846,-0.289],[0,0],[-2.166,0],[0,0],[0,5.706],[0,0]],"o":[[0,0],[-2.166,0],[-0.883,0.354],[0,0],[-1.895,-1.05],[0,0],[-5.708,0],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[0,5.707],[0,0],[2.165,0],[0.833,-0.334],[0,0],[1.894,1.05],[0,0],[5.707,0],[0,0],[0,-5.707]],"v":[[106.16,-102.082],[8.455,-102.082],[2.265,-100.48],[-0.488,-100.468],[-0.519,-100.48],[-6.71,-102.082],[-104.116,-102.082],[-114.45,-91.748],[-114.45,-36.119],[-116.494,-33.44],[-116.494,-18.979],[-114.45,-16.3],[-114.45,-0.877],[-116.494,1.802],[-116.494,28.704],[-114.45,31.383],[-114.45,91.749],[-104.116,102.083],[-6.495,102.083],[-0.305,100.481],[2.294,100.425],[2.395,100.481],[9.872,102.083],[106.161,102.083],[116.494,91.75],[116.494,-91.748]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.529411792755,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/biometricprompt_rear_portrait_base.json b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_base.json new file mode 100644 index 000000000000..9ea0d35e1de2 --- /dev/null +++ b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_base.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"BiometricPrompt_Rear_Portrait_Base_Foldable","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 18","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[169.478,169.749,0],"ix":2,"l":2},"a":{"a":0,"k":[-48.123,-30.19,0],"ix":1,"l":2},"s":{"a":0,"k":[132,132,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".grey400","cl":"grey400","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.741176486015,0.75686275959,0.776470601559,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"black circle matte","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".grey904","cl":"grey904","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,35.536,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.552,0.087],[0,0]],"o":[[0,0],[0,-3.287],[0,0],[0,0]],"v":[[-2.301,8.869],[-2.301,-3.772],[2.301,-9.806],[2.301,9.806]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"black circle matte 2","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue401","cl":"blue401","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,-27.655,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,3.286],[0,0],[-2.552,0.086],[0,0]],"o":[[0,0],[0,-3.286],[0,0],[-2.552,-0.086]],"v":[[-2.301,16.282],[-2.301,-16.281],[2.301,-22.313],[2.301,22.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"black circle matte 3","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Finger 3","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-2,"ix":10},"p":{"a":0,"k":[260.134,83.782,0],"ix":2,"l":2},"a":{"a":0,"k":[302.634,38.782,0],"ix":1,"l":2},"s":{"a":0,"k":[178,178,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.262,5.076],[0,0],[-0.424,-7.095],[-0.028,-0.225]],"o":[[3.269,-3.892],[-12.123,2.932],[0.015,0.234],[0.567,-0.034]],"v":[[9.232,0.652],[11.145,-6.746],[-11.412,6.046],[-11.346,6.746]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.792156875134,0.454901963472,0.376470595598,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[241.281,55.033],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.565,-1.102],[3.269,-3.892],[0.566,-0.033],[-18.63,2.353],[-16.656,3.951],[9.004,6.546],[6.9,-2.19]],"o":[[0,0],[-4.262,5.076],[1.008,9.61],[14.171,-1.79],[-4.028,-10.569],[-4.156,1.703],[-4.392,1.392]],"v":[[-13.858,-7.546],[-15.771,-0.148],[-36.349,5.946],[-7.047,16.299],[36.349,9.142],[16.281,-17.051],[-0.156,-11.172]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.713725507259,0.384313732386,0.282352954149,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[266.285,55.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 4","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"black circle matte 4","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".grey903","cl":"grey903","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.345,-92.442,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24.07,0],[0,0],[-8.27,0],[0,0]],"o":[[0,0],[0,8.269],[0,0],[-14.024,-17.379]],"v":[[-29.778,-14.252],[-29.778,-0.721],[-14.805,14.252],[29.778,14.252]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"black circle matte 5","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".grey902","cl":"grey902","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-15.947,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[154.053,139.81,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.3,0.367],[0,0],[-2.364,0.157],[0,0]],"o":[[0,0],[2.3,-0.367],[0,0],[-2.364,-0.157]],"v":[[-3.5,75.533],[-3.5,-75.533],[3.5,-76.312],[3.5,76.312]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[113.225,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 7","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,8.269],[0,0],[2.181,-0.187],[0,0],[-2.23,0],[0,42.252],[10.593,13.127],[0,0]],"o":[[0,0],[-2.23,0],[0,0],[2.181,0.187],[42.252,0],[0,-18.182],[0,0],[-8.27,0]],"v":[[-34.946,-62.973],[-34.946,-76.504],[-41.558,-76.201],[-41.558,76.201],[-34.946,76.504],[41.558,0],[24.61,-48],[-19.973,-48]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[156.824,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".black 2","cl":"black","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48.123,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-129,"s":[0,0,100]},{"t":-79,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".grey700","cl":"grey700","parent":16,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-56.481,-59.936,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.767,0],[0,0],[0,-3.767],[0,0],[-3.767,0],[0,0],[0,3.767],[0,0]],"o":[[0,0],[-3.767,0],[0,0],[0,3.767],[0,0],[3.767,0],[0,0],[0,-3.767]],"v":[[46.055,-14.479],[-46.056,-14.479],[-52.876,-7.659],[-52.876,7.658],[-46.056,14.479],[46.055,14.479],[52.876,7.658],[52.876,-7.659]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":".grey901","cl":"grey901","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[16.485,2.727,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[4.184,0],[0,0],[0,0],[0,0],[0,-4.375]],"o":[[0,4.184],[0,0],[0,0],[0,0],[4.375,0],[0,0]],"v":[[114.116,92.129],[106.54,99.705],[7.788,99.705],[7.788,-99.704],[106.161,-99.704],[114.116,-91.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[5.707,0],[0,0],[1.894,-1.05],[0.886,0.346],[0,0],[2.166,0],[0,0],[0,-5.707],[0,0],[0,-1.46],[0,0],[-1.133,-0.038],[0,0],[0,-1.459],[0,0],[-1.133,-0.038],[0,0],[-5.708,0],[0,0],[-1.894,1.05],[-0.846,-0.289],[0,0],[-2.166,0],[0,0],[0,5.706],[0,0]],"o":[[0,0],[-2.166,0],[-0.883,0.354],[0,0],[-1.895,-1.05],[0,0],[-5.708,0],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[0,5.707],[0,0],[2.165,0],[0.833,-0.334],[0,0],[1.894,1.05],[0,0],[5.707,0],[0,0],[0,-5.707]],"v":[[106.16,-102.082],[8.455,-102.082],[2.265,-100.48],[-0.488,-100.468],[-0.519,-100.48],[-6.71,-102.082],[-104.116,-102.082],[-114.45,-91.748],[-114.45,-36.119],[-116.494,-33.44],[-116.494,-18.979],[-114.45,-16.3],[-114.45,-0.877],[-116.494,1.802],[-116.494,28.704],[-114.45,31.383],[-114.45,91.749],[-104.116,102.083],[-6.495,102.083],[-0.305,100.481],[2.294,100.425],[2.395,100.481],[9.872,102.083],[106.161,102.083],[116.494,91.75],[116.494,-91.748]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.529411792755,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/biometricprompt_rear_portrait_reverse_base.json b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_reverse_base.json new file mode 100644 index 000000000000..f2b259335034 --- /dev/null +++ b/packages/SystemUI/res/raw/biometricprompt_rear_portrait_reverse_base.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"BiometricPrompt_Rear_Portrait_Reverse_Base_Foldable","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 18","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":270,"ix":10},"p":{"a":0,"k":[169.478,169.749,0],"ix":2,"l":2},"a":{"a":0,"k":[-48.123,-30.19,0],"ix":1,"l":2},"s":{"a":0,"k":[132,132,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".grey400","cl":"grey400","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.741176486015,0.75686275959,0.776470601559,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"black circle matte","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".grey904","cl":"grey904","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,35.536,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.552,0.087],[0,0]],"o":[[0,0],[0,-3.287],[0,0],[0,0]],"v":[[-2.301,8.869],[-2.301,-3.772],[2.301,-9.806],[2.301,9.806]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"black circle matte 2","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue401","cl":"blue401","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-62.577,-27.655,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,3.286],[0,0],[-2.552,0.086],[0,0]],"o":[[0,0],[0,-3.286],[0,0],[-2.552,-0.086]],"v":[[-2.301,16.282],[-2.301,-16.281],[2.301,-22.313],[2.301,22.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"black circle matte 3","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Finger 2","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-75.352,41.307,0],"ix":2,"l":2},"a":{"a":0,"k":[94.648,211.307,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.72,-5.642],[0,0],[-9.394,-0.562],[-0.298,-0.038]],"o":[[-5.153,4.329],[3.882,-16.05],[0.31,0.019],[-0.044,0.75]],"v":[[0.863,12.222],[-8.931,14.755],[8.005,-15.108],[8.931,-15.021]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.792156875134,0.454901963472,0.376470595598,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[81.486,130.081],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 9","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.459,6.045],[-5.153,4.329],[-0.044,0.75],[3.116,-24.664],[5.23,-22.052],[8.666,11.92],[-2.9,9.135]],"o":[[0,0],[6.72,-5.642],[12.723,1.335],[-2.369,18.762],[-13.993,-5.333],[2.255,-5.502],[1.843,-5.815]],"v":[[-9.99,-18.348],[-0.196,-20.881],[7.872,-48.124],[21.578,-9.331],[12.104,48.124],[-22.574,21.555],[-14.791,-0.206]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.713725507259,0.384313732386,0.282352954149,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[82.545,163.184],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 8","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"black circle matte 4","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".grey903","cl":"grey903","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18.345,-92.442,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24.07,0],[0,0],[-8.27,0],[0,0]],"o":[[0,0],[0,8.269],[0,0],[-14.024,-17.379]],"v":[[-29.778,-14.252],[-29.778,-0.721],[-14.805,14.252],[29.778,14.252]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"black circle matte 5","parent":13,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".grey902","cl":"grey902","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-15.947,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[154.053,139.81,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.3,0.367],[0,0],[-2.364,0.157],[0,0]],"o":[[0,0],[2.3,-0.367],[0,0],[-2.364,-0.157]],"v":[[-3.5,75.533],[-3.5,-75.533],[3.5,-76.312],[3.5,76.312]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[113.225,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 7","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,8.269],[0,0],[2.181,-0.187],[0,0],[-2.23,0],[0,42.252],[10.593,13.127],[0,0]],"o":[[0,0],[-2.23,0],[0,0],[2.181,0.187],[42.252,0],[0,-18.182],[0,0],[-8.27,0]],"v":[[-34.946,-62.973],[-34.946,-76.504],[-41.558,-76.201],[-41.558,76.201],[-34.946,76.504],[41.558,0],[24.61,-48],[-19.973,-48]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.525490224361,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[156.824,139.81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Layer 5","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".black 2","cl":"black","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48.123,-30.19,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-129,"s":[0,0,100]},{"t":-79,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-42.252,0],[0,42.252],[42.252,0],[0,-42.252]],"o":[[42.252,0],[0,-42.252],[-42.252,0],[0,42.252]],"v":[[0,76.504],[76.504,0],[0,-76.504],[-76.504,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".grey700","cl":"grey700","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-56.481,-59.936,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.767,0],[0,0],[0,-3.767],[0,0],[-3.767,0],[0,0],[0,3.767],[0,0]],"o":[[0,0],[-3.767,0],[0,0],[0,3.767],[0,0],[3.767,0],[0,0],[0,-3.767]],"v":[[46.055,-14.479],[-46.056,-14.479],[-52.876,-7.659],[-52.876,7.658],[-46.056,14.479],[46.055,14.479],[52.876,7.658],[52.876,-7.659]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.372549027205,0.388235300779,0.407843142748,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".grey901","cl":"grey901","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[16.485,2.727,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[4.184,0],[0,0],[0,0],[0,0],[0,-4.375]],"o":[[0,4.184],[0,0],[0,0],[0,0],[4.375,0],[0,0]],"v":[[114.116,92.129],[106.54,99.705],[7.788,99.705],[7.788,-99.704],[106.161,-99.704],[114.116,-91.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[5.707,0],[0,0],[1.894,-1.05],[0.886,0.346],[0,0],[2.166,0],[0,0],[0,-5.707],[0,0],[0,-1.46],[0,0],[-1.133,-0.038],[0,0],[0,-1.459],[0,0],[-1.133,-0.038],[0,0],[-5.708,0],[0,0],[-1.894,1.05],[-0.846,-0.289],[0,0],[-2.166,0],[0,0],[0,5.706],[0,0]],"o":[[0,0],[-2.166,0],[-0.883,0.354],[0,0],[-1.895,-1.05],[0,0],[-5.708,0],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[-1.133,0.038],[0,0],[0,1.46],[0,0],[0,5.707],[0,0],[2.165,0],[0.833,-0.334],[0,0],[1.894,1.05],[0,0],[5.707,0],[0,0],[0,-5.707]],"v":[[106.16,-102.082],[8.455,-102.082],[2.265,-100.48],[-0.488,-100.468],[-0.519,-100.48],[-6.71,-102.082],[-104.116,-102.082],[-114.45,-91.748],[-114.45,-36.119],[-116.494,-33.44],[-116.494,-18.979],[-114.45,-16.3],[-114.45,-0.877],[-116.494,1.802],[-116.494,28.704],[-114.45,31.383],[-114.45,91.749],[-104.116,102.083],[-6.495,102.083],[-0.305,100.481],[2.294,100.425],[2.395,100.481],[9.872,102.083],[106.161,102.083],[116.494,91.75],[116.494,-91.748]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.501960813999,0.529411792755,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-189,"op":711,"st":-189,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml index 20864591ae5a..f40615eb46d0 100644 --- a/packages/SystemUI/res/values-sw720dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp/dimens.xml @@ -17,7 +17,7 @@ --> <resources> <!-- gap on either side of status bar notification icons --> - <dimen name="status_bar_icon_padding">1dp</dimen> + <dimen name="status_bar_icon_padding">1sp</dimen> <dimen name="controls_header_horizontal_padding">28dp</dimen> <dimen name="controls_content_margin_horizontal">40dp</dimen> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 0aa880fe6d88..f5c4a4e4bb52 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -122,26 +122,26 @@ <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen> <!-- Default horizontal drawable padding for status bar icons. --> - <dimen name="status_bar_horizontal_padding">2.5dp</dimen> + <dimen name="status_bar_horizontal_padding">2.5sp</dimen> <!-- Height of the battery icon in the status bar. --> - <dimen name="status_bar_battery_icon_height">13.0dp</dimen> + <dimen name="status_bar_battery_icon_height">13.0sp</dimen> <!-- Width of the battery icon in the status bar. The battery drawable assumes a 12x20 canvas, - so the width of the icon should be 13.0dp * (12.0 / 20.0) --> - <dimen name="status_bar_battery_icon_width">7.8dp</dimen> + so the width of the icon should be 13.0sp * (12.0 / 20.0) --> + <dimen name="status_bar_battery_icon_width">7.8sp</dimen> - <!-- The battery icon is 13dp tall, but the other system icons are 15dp tall (see + <!-- The battery icon is 13sp tall, but the other system icons are 15sp tall (see @*android:dimen/status_bar_system_icon_size) with some top and bottom padding embedded in - the drawables themselves. So, the battery icon may need an extra 1dp of spacing so that its + the drawables themselves. So, the battery icon may need an extra 1sp of spacing so that its bottom still aligns with the bottom of all the other system icons. See b/258672854. --> - <dimen name="status_bar_battery_extra_vertical_spacing">1dp</dimen> + <dimen name="status_bar_battery_extra_vertical_spacing">1sp</dimen> <!-- The font size for the clock in the status bar. --> <dimen name="status_bar_clock_size">14sp</dimen> <!-- The starting padding for the clock in the status bar. --> - <dimen name="status_bar_clock_starting_padding">7dp</dimen> + <dimen name="status_bar_clock_starting_padding">7sp</dimen> <!-- The end padding for the clock in the status bar. --> <dimen name="status_bar_clock_end_padding">0dp</dimen> @@ -153,16 +153,19 @@ <dimen name="status_bar_left_clock_end_padding">2dp</dimen> <!-- Spacing after the wifi signals that is present if there are any icons following it. --> - <dimen name="status_bar_wifi_signal_spacer_width">2.5dp</dimen> + <dimen name="status_bar_wifi_signal_spacer_width">2.5sp</dimen> <!-- Size of the view displaying the wifi signal icon in the status bar. --> - <dimen name="status_bar_wifi_signal_size">@*android:dimen/status_bar_system_icon_size</dimen> + <dimen name="status_bar_wifi_signal_size">13sp</dimen> + + <!-- Size of the view displaying the mobile signal icon in the status bar. --> + <dimen name="status_bar_mobile_signal_size">13sp</dimen> <!-- Spacing before the airplane mode icon if there are any icons preceding it. --> - <dimen name="status_bar_airplane_spacer_width">4dp</dimen> + <dimen name="status_bar_airplane_spacer_width">4sp</dimen> <!-- Spacing between system icons. --> - <dimen name="status_bar_system_icon_spacing">0dp</dimen> + <dimen name="status_bar_system_icon_spacing">2sp</dimen> <!-- The amount to scale each of the status bar icons by. A value of 1 means no scaling. --> <item name="status_bar_icon_scale_factor" format="float" type="dimen">1.0</item> @@ -310,7 +313,7 @@ <dimen name="snooze_snackbar_min_height">56dp</dimen> <!-- size at which Notification icons will be drawn in the status bar --> - <dimen name="status_bar_icon_drawing_size">15dp</dimen> + <dimen name="status_bar_icon_drawing_size">15sp</dimen> <!-- size at which Notification icons will be drawn on Ambient Display --> <dimen name="status_bar_icon_drawing_size_dark"> @@ -321,22 +324,22 @@ <item type="dimen" name="status_bar_icon_drawing_alpha">90%</item> <!-- gap on either side of status bar notification icons --> - <dimen name="status_bar_icon_padding">0dp</dimen> + <dimen name="status_bar_icon_padding">0sp</dimen> <!-- the padding on the start of the statusbar --> - <dimen name="status_bar_padding_start">8dp</dimen> + <dimen name="status_bar_padding_start">8sp</dimen> <!-- the padding on the end of the statusbar --> - <dimen name="status_bar_padding_end">8dp</dimen> + <dimen name="status_bar_padding_end">8sp</dimen> <!-- the padding on the top of the statusbar (usually 0) --> - <dimen name="status_bar_padding_top">0dp</dimen> + <dimen name="status_bar_padding_top">0sp</dimen> <!-- the radius of the overflow dot in the status bar --> - <dimen name="overflow_dot_radius">2dp</dimen> + <dimen name="overflow_dot_radius">2sp</dimen> <!-- the padding between dots in the icon overflow --> - <dimen name="overflow_icon_dot_padding">3dp</dimen> + <dimen name="overflow_icon_dot_padding">3sp</dimen> <!-- Dimensions related to screenshots --> @@ -617,8 +620,8 @@ <dimen name="qs_footer_icon_size">20dp</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> @@ -822,7 +825,7 @@ <!-- Padding between the mobile signal indicator and the start icon when the roaming icon is displayed in the upper left corner. --> - <dimen name="roaming_icon_start_padding">2dp</dimen> + <dimen name="roaming_icon_start_padding">2sp</dimen> <!-- Extra padding between the mobile data type icon and the strength indicator when the data type icon is wide for the tile in quick settings. --> @@ -1042,13 +1045,13 @@ <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 --> <dimen name="ongoing_appops_chip_icon_margin">4dp</dimen> <!-- Icon size of Ongoing App Ops chip --> - <dimen name="ongoing_appops_chip_icon_size">16dp</dimen> + <dimen name="ongoing_appops_chip_icon_size">16sp</dimen> <!-- Radius of Ongoing App Ops chip corners --> <dimen name="ongoing_appops_chip_bg_corner_radius">28dp</dimen> <!-- One or two privacy items --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6b461eebe2e0..c57fef1c52f7 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3038,6 +3038,19 @@ --> <string name="keyguard_affordance_enablement_dialog_home_instruction_2">• At least one device is available</string> + <!--- + Requirement for the notes app to be available for the user to use. This is shown as part of a + bulleted list of requirements. When all requirements are met, the app can be accessed through a + shortcut button on the lock screen. [CHAR LIMIT=NONE] --> + <string name="keyguard_affordance_enablement_dialog_notes_app_instruction">Select a default notes app to use the notetaking shortcut</string> + + <!--- + The action to make the lock screen shortcut for the notes app to be available for the user to + use. This is shown as the action button in the dialog listing the requirements. When all + requirements are met, the app can be accessed through a shortcut button on the lock screen. + [CHAR LIMIT=NONE] --> + <string name="keyguard_affordance_enablement_dialog_notes_app_action">Select app</string> + <!-- Error message shown when a shortcut must be pressed and held to activate it, usually shown when the user tried to tap the shortcut or held it for too short a time. [CHAR LIMIT=32]. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java index 016d573930e8..4a665621b3fe 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java @@ -230,7 +230,7 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager private ClassLoader getParentClassLoader(ClassLoader baseClassLoader) { return new PluginManagerImpl.ClassLoaderFilter( - baseClassLoader, "com.android.systemui.plugin"); + baseClassLoader, "com.android.systemui.log", "com.android.systemui.plugin"); } /** Returns class loader specific for the given plugin. */ diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java index 2f9f5b2ac938..1e668b84cdc6 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java @@ -248,19 +248,23 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage // This allows plugins to include any libraries or copied code they want by only including // classes from the plugin library. static class ClassLoaderFilter extends ClassLoader { - private final String mPackage; + private final String[] mPackages; private final ClassLoader mBase; - public ClassLoaderFilter(ClassLoader base, String pkg) { + ClassLoaderFilter(ClassLoader base, String... pkgs) { super(ClassLoader.getSystemClassLoader()); mBase = base; - mPackage = pkg; + mPackages = pkgs; } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (!name.startsWith(mPackage)) super.loadClass(name, resolve); - return mBase.loadClass(name); + for (String pkg : mPackages) { + if (name.startsWith(pkg)) { + return mBase.loadClass(name); + } + } + return super.loadClass(name, resolve); } } diff --git a/packages/SystemUI/src-debug/com/android/systemui/log/DebugLogger.kt b/packages/SystemUI/src-debug/com/android/systemui/log/DebugLogger.kt new file mode 100644 index 000000000000..af29b05a3fb1 --- /dev/null +++ b/packages/SystemUI/src-debug/com/android/systemui/log/DebugLogger.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.log + +import android.os.Build +import android.util.Log +import android.util.Log.LOG_ID_MAIN + +/** + * A simplified debug logger built as a wrapper around Android's [Log]. Internal for development. + * + * The main advantages are: + * - Sensible defaults, automatically retrieving the class name from the call-site (i.e., tag); + * - The messages are purged from source on release builds (keep in mind they are visible on AOSP); + * - Lazily evaluate Strings for zero impact in production builds or when disabled; + * + * Usage example: + * ```kotlin + * // Logging a message: + * debugLog { "message" } + * + * // Logging an error: + * debugLog(error = exception) { "message" } + * + * // Logging the current stack trace, for debugging: + * debugLog(error = Throwable()) { "message" } + * ``` + */ +object DebugLogger { + + /** + * Log a debug message, with sensible defaults. + * + * For example: + * ```kotlin + * val one = 1 + * debugLog { "message#$one" } + * ``` + * + * The output will be: `D/NoteTaskController: message#1` + * + * Beware, the [debugLog] content is **REMOVED FROM SOURCE AND BINARY** in Release builds. + * + * @param enabled: whether or not the message should be logged. By default, it is + * [Build.IS_DEBUGGABLE]. + * @param priority: type of this log. By default, it is [Log.DEBUG]. + * @param tag: identifies the source of a log. By default, it is the receiver's simple name. + * @param error: a [Throwable] to log. + * @param message: a lazily evaluated message you wish to log. + */ + inline fun Any.debugLog( + enabled: Boolean = Build.IS_DEBUGGABLE, + priority: Int = Log.DEBUG, + tag: String = this::class.simpleName.orEmpty(), + error: Throwable? = null, + message: () -> String, + ) { + if (enabled) { + if (error == null) { + Log.println(priority, tag, message()) + } else { + Log.printlns(LOG_ID_MAIN, priority, tag, message(), error) + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialProviderReceiver.kt b/packages/SystemUI/src-release/com/android/systemui/log/DebugLogger.kt index ee8cffed7f7d..2764a1fdfe3d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialProviderReceiver.kt +++ b/packages/SystemUI/src-release/com/android/systemui/log/DebugLogger.kt @@ -14,22 +14,22 @@ * limitations under the License. */ -package com.android.credentialmanager +package com.android.systemui.log -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent +import android.os.Build import android.util.Log -import com.android.credentialmanager.common.Constants +/** An empty logger for release builds. */ +object DebugLogger { -class CredentialProviderReceiver : BroadcastReceiver() { - - override fun onReceive(context: Context?, intent: Intent?) { - Log.d(Constants.LOG_TAG, "Received intent in CredentialProviderReceiver") - - val sharedPreferences = context?.getSharedPreferences(context?.packageName, - Context.MODE_PRIVATE) - sharedPreferences?.edit()?.remove(UserConfigRepo.DEFAULT_PROVIDER)?.commit() + @JvmName("logcatMessage") + inline fun Any.debugLog( + enabled: Boolean = Build.IS_DEBUGGABLE, + priority: Int = Log.DEBUG, + tag: String = this::class.simpleName.orEmpty(), + error: Throwable? = null, + message: () -> String, + ) { + // no-op. } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardBouncerMessages.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardBouncerMessages.kt new file mode 100644 index 000000000000..f4145dbef80f --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardBouncerMessages.kt @@ -0,0 +1,323 @@ +/* + * 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.keyguard + +import android.annotation.IntDef +import com.android.keyguard.KeyguardSecurityModel.SecurityMode +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST +import com.android.systemui.R.string.bouncer_face_not_recognized +import com.android.systemui.R.string.keyguard_enter_password +import com.android.systemui.R.string.keyguard_enter_pattern +import com.android.systemui.R.string.keyguard_enter_pin +import com.android.systemui.R.string.kg_bio_too_many_attempts_password +import com.android.systemui.R.string.kg_bio_too_many_attempts_pattern +import com.android.systemui.R.string.kg_bio_too_many_attempts_pin +import com.android.systemui.R.string.kg_bio_try_again_or_password +import com.android.systemui.R.string.kg_bio_try_again_or_pattern +import com.android.systemui.R.string.kg_bio_try_again_or_pin +import com.android.systemui.R.string.kg_face_locked_out +import com.android.systemui.R.string.kg_fp_locked_out +import com.android.systemui.R.string.kg_fp_not_recognized +import com.android.systemui.R.string.kg_primary_auth_locked_out_password +import com.android.systemui.R.string.kg_primary_auth_locked_out_pattern +import com.android.systemui.R.string.kg_primary_auth_locked_out_pin +import com.android.systemui.R.string.kg_prompt_after_dpm_lock +import com.android.systemui.R.string.kg_prompt_after_user_lockdown_password +import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pattern +import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin +import com.android.systemui.R.string.kg_prompt_auth_timeout +import com.android.systemui.R.string.kg_prompt_password_auth_timeout +import com.android.systemui.R.string.kg_prompt_pattern_auth_timeout +import com.android.systemui.R.string.kg_prompt_pin_auth_timeout +import com.android.systemui.R.string.kg_prompt_reason_restart_password +import com.android.systemui.R.string.kg_prompt_reason_restart_pattern +import com.android.systemui.R.string.kg_prompt_reason_restart_pin +import com.android.systemui.R.string.kg_prompt_unattended_update +import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown +import com.android.systemui.R.string.kg_trust_agent_disabled +import com.android.systemui.R.string.kg_unlock_with_password_or_fp +import com.android.systemui.R.string.kg_unlock_with_pattern_or_fp +import com.android.systemui.R.string.kg_unlock_with_pin_or_fp +import com.android.systemui.R.string.kg_wrong_input_try_fp_suggestion +import com.android.systemui.R.string.kg_wrong_password_try_again +import com.android.systemui.R.string.kg_wrong_pattern_try_again +import com.android.systemui.R.string.kg_wrong_pin_try_again + +typealias BouncerMessage = Pair<Int, Int> + +fun emptyBouncerMessage(): BouncerMessage = Pair(0, 0) + +@Retention(AnnotationRetention.SOURCE) +@IntDef( + PROMPT_REASON_TIMEOUT, + PROMPT_REASON_DEVICE_ADMIN, + PROMPT_REASON_USER_REQUEST, + PROMPT_REASON_AFTER_LOCKOUT, + PROMPT_REASON_PREPARE_FOR_UPDATE, + PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT, + PROMPT_REASON_TRUSTAGENT_EXPIRED, + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + PROMPT_REASON_INCORRECT_FACE_INPUT, + PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT, + PROMPT_REASON_FACE_LOCKED_OUT, + PROMPT_REASON_FINGERPRINT_LOCKED_OUT, + PROMPT_REASON_DEFAULT, + PROMPT_REASON_NONE, + PROMPT_REASON_RESTART, + PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT, +) +annotation class BouncerPromptReason + +/** + * Helper method that provides the relevant bouncer message that should be shown for different + * scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to + * provide a more specific message. + */ +@JvmOverloads +fun getBouncerMessage( + @BouncerPromptReason reason: Int, + securityMode: SecurityMode, + fpAllowedInBouncer: Boolean = false +): BouncerMessage { + return when (reason) { + PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode) + PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode) + PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode) + PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode) + PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode) + PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode) + PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode) + PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode) + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT -> + if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode) + else incorrectSecurityInput(securityMode) + PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT -> + if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode) + else nonStrongAuthTimeout(securityMode) + PROMPT_REASON_TRUSTAGENT_EXPIRED -> + if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode) + else trustAgentDisabled(securityMode) + PROMPT_REASON_INCORRECT_FACE_INPUT -> + if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode) + else incorrectFaceInput(securityMode) + PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode) + PROMPT_REASON_DEFAULT -> + if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode) + else defaultMessage(securityMode) + PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode) + else -> emptyBouncerMessage() + } +} + +fun defaultMessage(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0) + SecurityMode.Password -> Pair(keyguard_enter_password, 0) + SecurityMode.PIN -> Pair(keyguard_enter_pin, 0) + else -> Pair(0, 0) + } +} + +fun defaultMessageWithFingerprint(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, 0) + SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, 0) + SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, 0) + else -> Pair(0, 0) + } +} + +fun incorrectSecurityInput(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, 0) + SecurityMode.Password -> Pair(kg_wrong_password_try_again, 0) + SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, 0) + else -> Pair(0, 0) + } +} + +fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, kg_wrong_input_try_fp_suggestion) + SecurityMode.Password -> Pair(kg_wrong_password_try_again, kg_wrong_input_try_fp_suggestion) + SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, kg_wrong_input_try_fp_suggestion) + else -> Pair(0, 0) + } +} + +fun incorrectFingerprintInput(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pattern) + SecurityMode.Password -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_password) + SecurityMode.PIN -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pin) + else -> Pair(0, 0) + } +} + +fun incorrectFaceInput(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pattern) + SecurityMode.Password -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_password) + SecurityMode.PIN -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pin) + else -> Pair(0, 0) + } +} + +fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, bouncer_face_not_recognized) + SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, bouncer_face_not_recognized) + SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, bouncer_face_not_recognized) + else -> Pair(0, 0) + } +} + +fun biometricLockout(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_bio_too_many_attempts_pin) + else -> Pair(0, 0) + } +} + +fun authRequiredAfterReboot(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_reason_restart_pattern) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_reason_restart_password) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin) + else -> Pair(0, 0) + } +} + +fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_dpm_lock) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_dpm_lock) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock) + else -> Pair(0, 0) + } +} + +fun authRequiredAfterUserLockdown(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_user_lockdown_pattern) + SecurityMode.Password -> + Pair(keyguard_enter_password, kg_prompt_after_user_lockdown_password) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin) + else -> Pair(0, 0) + } +} + +fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_unattended_update) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_unattended_update) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_unattended_update) + else -> Pair(0, 0) + } +} + +fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_password_auth_timeout) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout) + else -> Pair(0, 0) + } +} + +fun nonStrongAuthTimeout(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_auth_timeout) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_auth_timeout) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_auth_timeout) + else -> Pair(0, 0) + } +} + +fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_prompt_auth_timeout) + SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_prompt_auth_timeout) + SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_prompt_auth_timeout) + else -> Pair(0, 0) + } +} + +fun faceUnlockUnavailable(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_face_locked_out) + else -> Pair(0, 0) + } +} + +fun fingerprintUnlockUnavailable(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_fp_locked_out) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_fp_locked_out) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_fp_locked_out) + else -> Pair(0, 0) + } +} + +fun trustAgentDisabled(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_trust_agent_disabled) + SecurityMode.Password -> Pair(keyguard_enter_password, kg_trust_agent_disabled) + SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_trust_agent_disabled) + else -> Pair(0, 0) + } +} + +fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_trust_agent_disabled) + SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_trust_agent_disabled) + SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_trust_agent_disabled) + else -> Pair(0, 0) + } +} + +fun primaryAuthLockedOut(securityMode: SecurityMode): BouncerMessage { + return when (securityMode) { + SecurityMode.Pattern -> + Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pattern) + SecurityMode.Password -> + Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_password) + SecurityMode.PIN -> + Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pin) + else -> Pair(0, 0) + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java index 22ad725faaa2..419303d71f97 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -67,6 +67,42 @@ public interface KeyguardSecurityView { int PROMPT_REASON_TRUSTAGENT_EXPIRED = 8; /** + * Prompt that is shown when there is an incorrect primary authentication input. + */ + int PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT = 9; + + /** + * Prompt that is shown when there is an incorrect face biometric input. + */ + int PROMPT_REASON_INCORRECT_FACE_INPUT = 10; + + /** + * Prompt that is shown when there is an incorrect fingerprint biometric input. + */ + int PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT = 11; + + /** + * Prompt that is shown when face authentication is in locked out state. + */ + int PROMPT_REASON_FACE_LOCKED_OUT = 12; + + /** + * Prompt that is shown when fingerprint authentication is in locked out state. + */ + int PROMPT_REASON_FINGERPRINT_LOCKED_OUT = 13; + + /** + * Default prompt that is shown on the bouncer. + */ + int PROMPT_REASON_DEFAULT = 14; + + /** + * Prompt that is shown when primary authentication is in locked out state after too many + * attempts + */ + int PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT = 15; + + /** * Reset the view and prepare to take input. This should do things like clearing the * password or pattern and clear error messages. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 9573913e5e2f..7d7b276fbe44 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -522,6 +522,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FACE_AUTH_TRIGGERED_TRUST_DISABLED); } + mLogger.logTrustChanged(wasTrusted, enabled, userId); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onTrustChanged(userId); + } + } + if (enabled) { String message = null; if (KeyguardUpdateMonitor.getCurrentUser() == userId @@ -560,14 +568,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } } - - mLogger.logTrustChanged(wasTrusted, enabled, userId); - for (int i = 0; i < mCallbacks.size(); i++) { - KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); - if (cb != null) { - cb.onTrustChanged(userId); - } - } } /** @@ -4379,9 +4379,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ public void startBiometricWatchdog() { if (mFaceManager != null && !isFaceAuthInteractorEnabled()) { + mLogger.scheduleWatchdog("face"); mFaceManager.scheduleWatchdog(); } if (mFpm != null) { + mLogger.scheduleWatchdog("fingerprint"); mFpm.scheduleWatchdog(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 90587546cc28..c2d22c3e1d14 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -67,8 +67,10 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { "ActiveUnlock", DEBUG, { int1 = wakeReason }, - { "Skip requesting active unlock from wake reason that doesn't trigger face auth" + - " reason=${PowerManager.wakeReasonToString(int1)}" } + { + "Skip requesting active unlock from wake reason that doesn't trigger face auth" + + " reason=${PowerManager.wakeReasonToString(int1)}" + } ) } @@ -207,17 +209,27 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { } fun logFaceDetected(userId: Int, isStrongBiometric: Boolean) { - logBuffer.log(TAG, DEBUG, { - int1 = userId - bool1 = isStrongBiometric - }, {"Face detected: userId: $int1, isStrongBiometric: $bool1"}) + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + bool1 = isStrongBiometric + }, + { "Face detected: userId: $int1, isStrongBiometric: $bool1" } + ) } fun logFingerprintDetected(userId: Int, isStrongBiometric: Boolean) { - logBuffer.log(TAG, DEBUG, { - int1 = userId - bool1 = isStrongBiometric - }, {"Fingerprint detected: userId: $int1, isStrongBiometric: $bool1"}) + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + bool1 = isStrongBiometric + }, + { "Fingerprint detected: userId: $int1, isStrongBiometric: $bool1" } + ) } fun logFingerprintError(msgId: Int, originalErrMsg: String) { @@ -669,13 +681,10 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { } fun logHandleBatteryUpdate(isInteresting: Boolean) { - logBuffer.log( - TAG, - DEBUG, - { - bool1 = isInteresting - }, - { "handleBatteryUpdate: $bool1" } - ) + logBuffer.log(TAG, DEBUG, { bool1 = isInteresting }, { "handleBatteryUpdate: $bool1" }) + } + + fun scheduleWatchdog(@CompileTimeConstant watchdogType: String) { + logBuffer.log(TAG, DEBUG, "Scheduling biometric watchdog for $watchdogType") } } diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index a3e7d71a92f6..48805be50aa9 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -110,6 +110,10 @@ class FaceScanningOverlay( if (showScanningAnimNow == showScanningAnim) { return } + logger.cameraProtectionShownOrHidden(keyguardUpdateMonitor.isFaceDetectionRunning, + authController.isShowing, + show, + showScanningAnim) showScanningAnim = showScanningAnimNow updateProtectionBoundingPath() // Delay the relayout until the end of the animation when hiding, @@ -352,6 +356,7 @@ class FaceScanningOverlay( if (biometricSourceType == BiometricSourceType.FACE) { post { faceAuthSucceeded = true + logger.biometricEvent("biometricAuthenticated") enableShowProtection(true) } } @@ -372,6 +377,7 @@ class FaceScanningOverlay( if (biometricSourceType == BiometricSourceType.FACE) { post { faceAuthSucceeded = false + logger.biometricEvent("biometricFailed") enableShowProtection(false) } } @@ -385,6 +391,7 @@ class FaceScanningOverlay( if (biometricSourceType == BiometricSourceType.FACE) { post { faceAuthSucceeded = false + logger.biometricEvent("biometricError") enableShowProtection(false) } } diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index adc04128a93a..ea0f343e80f4 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -90,6 +90,8 @@ import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.ThreadFactory; import com.android.systemui.util.settings.SecureSettings; +import kotlin.Pair; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -98,8 +100,6 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -import kotlin.Pair; - /** * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout) * for antialiasing and emulation purposes. @@ -254,11 +254,13 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { new CameraAvailabilityListener.CameraTransitionCallback() { @Override public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { + mLogger.cameraProtectionEvent("onApplyCameraProtection"); showCameraProtection(protectionPath, bounds); } @Override public void onHideCameraProtection() { + mLogger.cameraProtectionEvent("onHideCameraProtection"); hideCameraProtection(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt index 4b5c50ff7fa5..5499d2c07181 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt @@ -19,7 +19,6 @@ package com.android.systemui.biometrics import android.annotation.RawRes import android.content.Context import android.content.Context.FINGERPRINT_SERVICE -import android.content.res.Configuration import android.hardware.fingerprint.FingerprintManager import android.view.DisplayInfo import android.view.Surface @@ -35,21 +34,18 @@ import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION -import com.android.systemui.unfold.compat.ScreenSizeFoldProvider -import com.android.systemui.unfold.updates.FoldProvider /** Fingerprint only icon animator for BiometricPrompt. */ open class AuthBiometricFingerprintIconController( context: Context, iconView: LottieAnimationView, protected val iconViewOverlay: LottieAnimationView -) : AuthIconController(context, iconView), FoldProvider.FoldCallback { +) : AuthIconController(context, iconView) { - private var isDeviceFolded: Boolean = false private val isSideFps: Boolean private val isReverseDefaultRotation = context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) - private val screenSizeFoldProvider: ScreenSizeFoldProvider = ScreenSizeFoldProvider(context) + var iconLayoutParamSize: Pair<Int, Int> = Pair(1, 1) set(value) { if (field == value) { @@ -77,20 +73,16 @@ open class AuthBiometricFingerprintIconController( if (isSideFps && getRotationFromDefault(displayInfo.rotation) == Surface.ROTATION_180) { iconView.rotation = 180f } - screenSizeFoldProvider.registerCallback(this, context.mainExecutor) - screenSizeFoldProvider.onConfigurationChange(context.resources.configuration) } private fun updateIconSideFps(@BiometricState lastState: Int, @BiometricState newState: Int) { val displayInfo = DisplayInfo() context.display?.getDisplayInfo(displayInfo) val rotation = getRotationFromDefault(displayInfo.rotation) - val iconAnimation = getSideFpsAnimationForTransition(rotation) val iconViewOverlayAnimation = getSideFpsOverlayAnimationForTransition(lastState, newState, rotation) ?: return if (!(lastState == STATE_AUTHENTICATING_ANIMATING_IN && newState == STATE_AUTHENTICATING)) { - iconView.setAnimation(iconAnimation) iconViewOverlay.setAnimation(iconViewOverlayAnimation) } @@ -132,10 +124,6 @@ open class AuthBiometricFingerprintIconController( LottieColorUtils.applyDynamicColors(context, iconView) } - override fun onConfigurationChanged(newConfig: Configuration) { - screenSizeFoldProvider.onConfigurationChange(newConfig) - } - override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) { if (isSideFps) { updateIconSideFps(lastState, newState) @@ -230,25 +218,6 @@ open class AuthBiometricFingerprintIconController( if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation @RawRes - private fun getSideFpsAnimationForTransition(rotation: Int): Int = when (rotation) { - Surface.ROTATION_90 -> if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_topleft - } else { - R.raw.biometricprompt_portrait_base_topleft - } - Surface.ROTATION_270 -> if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_bottomright - } else { - R.raw.biometricprompt_portrait_base_bottomright - } - else -> if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_default - } else { - R.raw.biometricprompt_landscape_base - } - } - - @RawRes private fun getSideFpsOverlayAnimationForTransition( @BiometricState oldState: Int, @BiometricState newState: Int, @@ -357,8 +326,4 @@ open class AuthBiometricFingerprintIconController( ) } } - - override fun onFoldUpdated(isFolded: Boolean) { - isDeviceFolded = isFolded - } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index e04dd0680060..f330c34bfaa0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -124,6 +124,7 @@ public abstract class AuthBiometricView extends LinearLayout { protected final int mTextColorHint; private AuthPanelController mPanelController; + private PromptInfo mPromptInfo; private boolean mRequireConfirmation; private int mUserId; @@ -266,11 +267,9 @@ public abstract class AuthBiometricView extends LinearLayout { /** Create the controller for managing the icons transitions during the prompt.*/ @NonNull protected abstract AuthIconController createIconController(); - void setPanelController(AuthPanelController panelController) { mPanelController = panelController; } - void setPromptInfo(PromptInfo promptInfo) { mPromptInfo = promptInfo; } @@ -463,9 +462,16 @@ public abstract class AuthBiometricView extends LinearLayout { return false; } + /** + * Updates mIconView animation on updates to fold state, device rotation, or rear display mode + * @param animation new asset to use for iconw + */ + public void updateIconViewAnimation(int animation) { + mIconView.setAnimation(animation); + } + public void updateState(@BiometricState int newState) { Log.v(TAG, "newState: " + newState); - mIconController.updateState(mState, newState); switch (newState) { @@ -625,7 +631,6 @@ public abstract class AuthBiometricView extends LinearLayout { public void restoreState(@Nullable Bundle savedState) { mSavedState = savedState; } - private void setTextOrHide(TextView view, CharSequence charSequence) { if (TextUtils.isEmpty(charSequence)) { view.setVisibility(View.GONE); @@ -658,7 +663,6 @@ public abstract class AuthBiometricView extends LinearLayout { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - mIconController.onConfigurationChanged(newConfig); if (mSavedState != null) { updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index aeebb010eb1e..b386bc9d050b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -67,6 +67,8 @@ import com.android.systemui.animation.Interpolators; import com.android.systemui.biometrics.AuthController.ScaleFactorProvider; import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; import com.android.systemui.biometrics.ui.CredentialView; +import com.android.systemui.biometrics.ui.binder.AuthBiometricFingerprintViewBinder; +import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel; import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -126,6 +128,8 @@ public class AuthContainerView extends LinearLayout // TODO: these should be migrated out once ready private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor; + private final Provider<AuthBiometricFingerprintViewModel> + mAuthBiometricFingerprintViewModelProvider; private final Provider<CredentialViewModel> mCredentialViewModelProvider; @VisibleForTesting final BiometricCallback mBiometricCallback; @@ -241,12 +245,14 @@ public class AuthContainerView extends LinearLayout @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<AuthBiometricFingerprintViewModel> + authBiometricFingerprintViewModelProvider, @NonNull Provider<CredentialViewModel> credentialViewModelProvider) { mConfig.mSensorIds = sensorIds; return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, jankMonitor, - biometricPromptInteractor, credentialViewModelProvider, - new Handler(Looper.getMainLooper()), bgExecutor); + biometricPromptInteractor, authBiometricFingerprintViewModelProvider, + credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor); } } @@ -339,6 +345,8 @@ public class AuthContainerView extends LinearLayout @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<AuthBiometricFingerprintViewModel> + authBiometricFingerprintViewModelProvider, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull Handler mainHandler, @NonNull @Background DelayableExecutor bgExecutor) { @@ -368,6 +376,7 @@ public class AuthContainerView extends LinearLayout mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; mBiometricPromptInteractor = biometricPromptInteractor; + mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider; mCredentialViewModelProvider = credentialViewModelProvider; // Inflate biometric view only if necessary. @@ -386,6 +395,9 @@ public class AuthContainerView extends LinearLayout fingerprintAndFaceView.updateOverrideIconLayoutParamsSize(); fingerprintAndFaceView.setFaceClass3( faceProperties.sensorStrength == STRENGTH_STRONG); + final AuthBiometricFingerprintViewModel fpAndFaceViewModel = + mAuthBiometricFingerprintViewModelProvider.get(); + AuthBiometricFingerprintViewBinder.bind(fingerprintAndFaceView, fpAndFaceViewModel); mBiometricView = fingerprintAndFaceView; } else if (fpProperties != null) { final AuthBiometricFingerprintView fpView = @@ -394,6 +406,9 @@ public class AuthContainerView extends LinearLayout fpView.setSensorProperties(fpProperties); fpView.setScaleFactorProvider(config.mScaleProvider); fpView.updateOverrideIconLayoutParamsSize(); + final AuthBiometricFingerprintViewModel fpViewModel = + mAuthBiometricFingerprintViewModelProvider.get(); + AuthBiometricFingerprintViewBinder.bind(fpView, fpViewModel); mBiometricView = fpView; } else if (faceProperties != null) { mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 6eb3c708e9b5..fd9cee0d6144 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -73,6 +73,7 @@ import com.android.settingslib.udfps.UdfpsUtils; import com.android.systemui.CoreStartable; import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; import com.android.systemui.biometrics.domain.interactor.LogContextInteractor; +import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel; import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -127,6 +128,9 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, // TODO: these should be migrated out once ready @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor; + + @NonNull private final Provider<AuthBiometricFingerprintViewModel> + mAuthBiometricFingerprintViewModelProvider; @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider; @NonNull private final LogContextInteractor mLogContextInteractor; @@ -722,7 +726,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } onDialogDismissed(reason); } - @Inject public AuthController(Context context, Execution execution, @@ -741,6 +744,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull UdfpsLogger udfpsLogger, @NonNull LogContextInteractor logContextInteractor, @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<AuthBiometricFingerprintViewModel> + authBiometricFingerprintViewModelProvider, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull InteractionJankMonitor jankMonitor, @Main Handler handler, @@ -771,6 +776,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mLogContextInteractor = logContextInteractor; mBiometricPromptInteractor = biometricPromptInteractor; + mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider; mCredentialViewModelProvider = credentialViewModelProvider; mOrientationListener = new BiometricDisplayListener( @@ -823,9 +829,9 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, final Rect overlayBounds = new Rect( 0, /* left */ - 0, /* top */ + mCachedDisplayInfo.getNaturalHeight() / 2, /* top */ mCachedDisplayInfo.getNaturalWidth(), /* right */ - mCachedDisplayInfo.getNaturalHeight() /* botom */); + mCachedDisplayInfo.getNaturalHeight() /* bottom */); mUdfpsOverlayParams = new UdfpsOverlayParams( mUdfpsBounds, @@ -1299,7 +1305,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, .build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, mInteractionJankMonitor, mBiometricPromptInteractor, - mCredentialViewModelProvider); + mAuthBiometricFingerprintViewModelProvider, mCredentialViewModelProvider); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt index d6ad4da04dbe..f5f46405660f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt @@ -18,7 +18,6 @@ package com.android.systemui.biometrics import android.annotation.DrawableRes import android.content.Context -import android.content.res.Configuration import android.graphics.drawable.Animatable2 import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable @@ -94,8 +93,6 @@ abstract class AuthIconController( /** Called during [onAnimationEnd] if the controller is not [deactivated]. */ open fun handleAnimationEnd(drawable: Drawable) {} - open fun onConfigurationChanged(newConfig: Configuration) {} - // TODO(b/251476085): Migrate this to an extension at the appropriate level? /** Load the given [rawResources] immediately so they are cached for use in the [context]. */ protected fun cacheLottieAssetsInContext(context: Context, vararg rawResources: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index 4319f01bed14..962140fa2cff 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -55,6 +55,7 @@ import com.airbnb.lottie.model.KeyPath import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.R +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -84,6 +85,7 @@ constructor( private val activityTaskManager: ActivityTaskManager, overviewProxyService: OverviewProxyService, displayManager: DisplayManager, + private val displayStateInteractor: DisplayStateInteractor, @Main private val mainExecutor: DelayableExecutor, @Main private val handler: Handler, private val alternateBouncerInteractor: AlternateBouncerInteractor, @@ -203,14 +205,16 @@ constructor( request: SideFpsUiRequestSource, @BiometricOverlayConstants.ShowReason reason: Int = BiometricOverlayConstants.REASON_UNKNOWN ) { - requests.add(request) - mainExecutor.execute { - if (overlayView == null) { - traceSection("SideFpsController#show(request=${request.name}, reason=$reason") { - createOverlayForDisplay(reason) + if (!displayStateInteractor.isInRearDisplayMode.value) { + requests.add(request) + mainExecutor.execute { + if (overlayView == null) { + traceSection("SideFpsController#show(request=${request.name}, reason=$reason") { + createOverlayForDisplay(reason) + } + } else { + Log.v(TAG, "overlay already shown") } - } else { - Log.v(TAG, "overlay already shown") } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index f876affb2a9c..d953a885fe1f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -364,7 +364,12 @@ class UdfpsControllerOverlay @JvmOverloads constructor( if (accessibilityManager.isTouchExplorationEnabled && isEnrollment) { Rect(overlayParams.sensorBounds) } else { - Rect(overlayParams.overlayBounds) + Rect( + 0, + 0, + overlayParams.naturalDisplayWidth, + overlayParams.naturalDisplayHeight + ) } } else { Rect(overlayParams.sensorBounds) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index f0b9f670f1e0..c83166385ac6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -21,8 +21,12 @@ import com.android.systemui.biometrics.data.repository.FingerprintPropertyReposi import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepositoryImpl import com.android.systemui.biometrics.data.repository.PromptRepository import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl +import com.android.systemui.biometrics.data.repository.RearDisplayStateRepository +import com.android.systemui.biometrics.data.repository.RearDisplayStateRepositoryImpl import com.android.systemui.biometrics.domain.interactor.CredentialInteractor import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.biometrics.domain.interactor.LogContextInteractor import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl import com.android.systemui.dagger.SysUISingleton @@ -45,6 +49,9 @@ interface BiometricsModule { @SysUISingleton fun fingerprintRepository(impl: FingerprintPropertyRepositoryImpl): FingerprintPropertyRepository + @Binds + @SysUISingleton + fun rearDisplayStateRepository(impl: RearDisplayStateRepositoryImpl): RearDisplayStateRepository @Binds @SysUISingleton @@ -52,6 +59,10 @@ interface BiometricsModule { @Binds @SysUISingleton + fun providesDisplayStateInteractor(impl: DisplayStateInteractorImpl): DisplayStateInteractor + + @Binds + @SysUISingleton fun bindsLogContextInteractor(impl: LogContextInteractorImpl): LogContextInteractor companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/RearDisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/RearDisplayStateRepository.kt new file mode 100644 index 000000000000..d17d961b5279 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/RearDisplayStateRepository.kt @@ -0,0 +1,85 @@ +/* + * 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.data.repository + +import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import com.android.internal.util.ArrayUtils +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +/** Provide current rear display state. */ +interface RearDisplayStateRepository { + /** Provides the current rear display state. */ + val isInRearDisplayMode: StateFlow<Boolean> +} + +@SysUISingleton +class RearDisplayStateRepositoryImpl +@Inject +constructor( + @Application applicationScope: CoroutineScope, + @Application context: Context, + deviceStateManager: DeviceStateManager, + @Main mainExecutor: Executor +) : RearDisplayStateRepository { + override val isInRearDisplayMode: StateFlow<Boolean> = + conflatedCallbackFlow { + val sendRearDisplayStateUpdate = { state: Boolean -> + trySendWithFailureLogging( + state, + TAG, + "Error sending rear display state update to $state" + ) + } + + val callback = + DeviceStateManager.DeviceStateCallback { state -> + val isInRearDisplayMode = + ArrayUtils.contains( + context.resources.getIntArray( + com.android.internal.R.array.config_rearDisplayDeviceStates + ), + state + ) + sendRearDisplayStateUpdate(isInRearDisplayMode) + } + + sendRearDisplayStateUpdate(false) + deviceStateManager.registerCallback(mainExecutor, callback) + awaitClose { deviceStateManager.unregisterCallback(callback) } + } + .stateIn( + applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + companion object { + const val TAG = "RearDisplayStateRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt new file mode 100644 index 000000000000..c935aa290e21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt @@ -0,0 +1,101 @@ +/* + * 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.domain.interactor + +import android.content.Context +import android.content.res.Configuration +import com.android.systemui.biometrics.data.repository.RearDisplayStateRepository +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.unfold.compat.ScreenSizeFoldProvider +import com.android.systemui.unfold.updates.FoldProvider +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +/** Aggregates display state information. */ +interface DisplayStateInteractor { + + /** Whether the device is currently in rear display mode. */ + val isInRearDisplayMode: StateFlow<Boolean> + + /** Whether the device is currently folded. */ + val isFolded: Flow<Boolean> + + /** Called on configuration changes, used to keep the display state in sync */ + fun onConfigurationChanged(newConfig: Configuration) +} + +/** Encapsulates logic for interacting with the display state. */ +class DisplayStateInteractorImpl +@Inject +constructor( + @Application applicationScope: CoroutineScope, + @Application context: Context, + @Main mainExecutor: Executor, + rearDisplayStateRepository: RearDisplayStateRepository, +) : DisplayStateInteractor { + private var screenSizeFoldProvider: ScreenSizeFoldProvider = ScreenSizeFoldProvider(context) + + fun setScreenSizeFoldProvider(foldProvider: ScreenSizeFoldProvider) { + screenSizeFoldProvider = foldProvider + } + + override val isFolded: Flow<Boolean> = + conflatedCallbackFlow { + val sendFoldStateUpdate = { state: Boolean -> + trySendWithFailureLogging( + state, + TAG, + "Error sending fold state update to $state" + ) + } + + val callback = + object : FoldProvider.FoldCallback { + override fun onFoldUpdated(isFolded: Boolean) { + sendFoldStateUpdate(isFolded) + } + } + sendFoldStateUpdate(false) + screenSizeFoldProvider.registerCallback(callback, mainExecutor) + awaitClose { screenSizeFoldProvider.unregisterCallback(callback) } + } + .stateIn( + applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + override val isInRearDisplayMode: StateFlow<Boolean> = + rearDisplayStateRepository.isInRearDisplayMode + + override fun onConfigurationChanged(newConfig: Configuration) { + screenSizeFoldProvider.onConfigurationChange(newConfig) + } + + companion object { + private const val TAG = "DisplayStateInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt new file mode 100644 index 000000000000..e776ab44ee42 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.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.biometrics.ui.binder + +import android.view.Surface +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.biometrics.AuthBiometricFingerprintView +import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.launch + +object AuthBiometricFingerprintViewBinder { + + /** Binds a [AuthBiometricFingerprintView] to a [AuthBiometricFingerprintViewModel]. */ + @JvmStatic + fun bind(view: AuthBiometricFingerprintView, viewModel: AuthBiometricFingerprintViewModel) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.onConfigurationChanged(view.context.resources.configuration) + viewModel.setRotation(view.context.display?.orientation ?: Surface.ROTATION_0) + launch { + viewModel.iconAsset.collect { iconAsset -> + view.updateIconViewAnimation(iconAsset) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt new file mode 100644 index 000000000000..617d80cee09d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt @@ -0,0 +1,84 @@ +/* + * 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.ui.viewmodel + +import android.annotation.RawRes +import android.content.res.Configuration +import android.view.Surface +import com.android.systemui.R +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** Models UI of AuthBiometricFingerprintView to support rear display state changes. */ +class AuthBiometricFingerprintViewModel +@Inject +constructor(private val interactor: DisplayStateInteractor) { + /** Current device rotation. */ + private var rotation: Int = Surface.ROTATION_0 + + /** Current AuthBiometricFingerprintView asset. */ + val iconAsset: Flow<Int> = + combine(interactor.isFolded, interactor.isInRearDisplayMode) { + isFolded: Boolean, + isInRearDisplayMode: Boolean -> + getSideFpsAnimationAsset(isFolded, isInRearDisplayMode) + } + + @RawRes + private fun getSideFpsAnimationAsset( + isDeviceFolded: Boolean, + isInRearDisplayMode: Boolean, + ): Int = + when (rotation) { + Surface.ROTATION_90 -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_portrait_reverse_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_topleft + } else { + R.raw.biometricprompt_portrait_base_topleft + } + Surface.ROTATION_270 -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_portrait_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_bottomright + } else { + R.raw.biometricprompt_portrait_base_bottomright + } + else -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_landscape_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_default + } else { + R.raw.biometricprompt_landscape_base + } + } + + /** Called on configuration changes */ + fun onConfigurationChanged(newConfig: Configuration) { + interactor.onConfigurationChanged(newConfig) + } + + fun setRotation(newRotation: Int) { + rotation = newRotation + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt new file mode 100644 index 000000000000..0542e13ba6da --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt @@ -0,0 +1,60 @@ +/* + * 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.clipboardoverlay + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import android.util.Size +import com.android.systemui.R +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import java.io.IOException +import java.util.function.Consumer +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull + +class ClipboardImageLoader +@Inject +constructor( + private val context: Context, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val mainScope: CoroutineScope +) { + private val TAG: String = "ClipboardImageLoader" + + suspend fun load(uri: Uri, timeoutMs: Long = 300) = + withTimeoutOrNull(timeoutMs) { + withContext(bgDispatcher) { + try { + val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale) + context.contentResolver.loadThumbnail(uri, Size(size, size * 4), null) + } catch (e: IOException) { + Log.e(TAG, "Thumbnail loading failed!", e) + null + } + } + } + + fun loadAsync(uri: Uri, callback: Consumer<Bitmap?>) { + mainScope.launch { callback.accept(load(uri)) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index 0aeab10101f6..757ebf45e9ad 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -32,6 +32,7 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT; +import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -90,6 +91,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private final ClipboardOverlayUtils mClipboardUtils; private final FeatureFlags mFeatureFlags; private final Executor mBgExecutor; + private final ClipboardImageLoader mClipboardImageLoader; private final ClipboardOverlayView mView; @@ -109,6 +111,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private Runnable mOnUiUpdate; + private boolean mShowingUi; private boolean mIsMinimized; private ClipboardModel mClipboardModel; @@ -175,9 +178,11 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv FeatureFlags featureFlags, ClipboardOverlayUtils clipboardUtils, @Background Executor bgExecutor, + ClipboardImageLoader clipboardImageLoader, UiEventLogger uiEventLogger) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; + mClipboardImageLoader = clipboardImageLoader; mClipboardLogger = new ClipboardLogger(uiEventLogger); @@ -260,21 +265,42 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting; mClipboardModel = model; mClipboardLogger.setClipSource(mClipboardModel.getSource()); - if (shouldAnimate) { - reset(); - mClipboardLogger.setClipSource(mClipboardModel.getSource()); - if (shouldShowMinimized(mWindow.getWindowInsets())) { - mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED); - mIsMinimized = true; - mView.setMinimized(true); - } else { - mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED); + if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) { + if (shouldAnimate) { + reset(); + mClipboardLogger.setClipSource(mClipboardModel.getSource()); + if (shouldShowMinimized(mWindow.getWindowInsets())) { + mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED); + mIsMinimized = true; + mView.setMinimized(true); + } else { + mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED); + setExpandedView(this::animateIn); + } + mView.announceForAccessibility( + getAccessibilityAnnouncement(mClipboardModel.getType())); + } else if (!mIsMinimized) { + setExpandedView(() -> { + }); + } + } else { + if (shouldAnimate) { + reset(); + mClipboardLogger.setClipSource(mClipboardModel.getSource()); + if (shouldShowMinimized(mWindow.getWindowInsets())) { + mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED); + mIsMinimized = true; + mView.setMinimized(true); + } else { + mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED); + setExpandedView(); + animateIn(); + } + mView.announceForAccessibility( + getAccessibilityAnnouncement(mClipboardModel.getType())); + } else if (!mIsMinimized) { setExpandedView(); } - animateIn(); - mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType())); - } else if (!mIsMinimized) { - setExpandedView(); } if (mClipboardModel.isRemote()) { mTimeoutHandler.cancelTimeout(); @@ -285,6 +311,58 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv } } + private void setExpandedView(Runnable onViewReady) { + final ClipboardModel model = mClipboardModel; + mView.setMinimized(false); + switch (model.getType()) { + case TEXT: + if (model.isRemote() || DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) { + if (model.getTextLinks() != null) { + classifyText(model); + } + } + if (model.isSensitive()) { + mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true); + } else { + mView.showTextPreview(model.getText().toString(), false); + } + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = this::editText; + onViewReady.run(); + break; + case IMAGE: + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = () -> editImage(model.getUri()); + if (model.isSensitive()) { + mView.showImagePreview(null); + onViewReady.run(); + } else { + mClipboardImageLoader.loadAsync(model.getUri(), (bitmap) -> mView.post(() -> { + if (bitmap == null) { + mView.showDefaultTextPreview(); + } else { + mView.showImagePreview(bitmap); + } + onViewReady.run(); + })); + } + break; + case URI: + case OTHER: + mView.showDefaultTextPreview(); + onViewReady.run(); + break; + } + if (!model.isRemote()) { + maybeShowRemoteCopy(model.getClipData()); + } + if (model.getType() != ClipboardModel.Type.OTHER) { + mOnShareTapped = () -> shareContent(model.getClipData()); + mView.showShareChip(); + } + } + private void setExpandedView() { final ClipboardModel model = mClipboardModel; mView.setMinimized(false); @@ -350,8 +428,12 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED); mIsMinimized = false; } - setExpandedView(); - animateIn(); + if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) { + setExpandedView(() -> animateIn()); + } else { + setExpandedView(); + animateIn(); + } } }); mEnterAnimator.start(); @@ -412,7 +494,8 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mInputMonitor.getInputChannel(), Looper.getMainLooper()) { @Override public void onInputEvent(InputEvent event) { - if (event instanceof MotionEvent) { + if ((!mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT) || mShowingUi) + && event instanceof MotionEvent) { MotionEvent motionEvent = (MotionEvent) event; if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { if (!mView.isInTouchRegion( @@ -452,6 +535,12 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mEnterAnimator = mView.getEnterAnimation(); mEnterAnimator.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mShowingUi = true; + } + + @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (mOnUiUpdate != null) { @@ -518,6 +607,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mOnRemoteCopyTapped = null; mOnShareTapped = null; mOnPreviewTapped = null; + mShowingUi = false; mView.reset(); mTimeoutHandler.cancelTimeout(); mClipboardLogger.reset(); diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/CommonRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/CommonRepositoryModule.kt new file mode 100644 index 000000000000..9b0c3fa7e92b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/CommonRepositoryModule.kt @@ -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.common.ui.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface CommonRepositoryModule { + @Binds fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt new file mode 100644 index 000000000000..3e6ac86e2417 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt @@ -0,0 +1,118 @@ +/* + * 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.common.ui.data.repository + +import android.content.Context +import android.content.res.Configuration +import android.view.DisplayInfo +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.wrapper.DisplayUtilsWrapper +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +interface ConfigurationRepository { + /** Called whenever ui mode, theme or configuration has changed. */ + val onAnyConfigurationChange: Flow<Unit> + val scaleForResolution: Flow<Float> + + fun getResolutionScale(): Float +} + +@ExperimentalCoroutinesApi +@SysUISingleton +class ConfigurationRepositoryImpl +@Inject +constructor( + private val configurationController: ConfigurationController, + private val context: Context, + @Application private val scope: CoroutineScope, + private val displayUtils: DisplayUtilsWrapper, +) : ConfigurationRepository { + private val displayInfo = MutableStateFlow(DisplayInfo()) + + override val onAnyConfigurationChange: Flow<Unit> = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : ConfigurationController.ConfigurationListener { + override fun onUiModeChanged() { + sendUpdate("ConfigurationRepository#onUiModeChanged") + } + + override fun onThemeChanged() { + sendUpdate("ConfigurationRepository#onThemeChanged") + } + + override fun onConfigChanged(newConfig: Configuration) { + sendUpdate("ConfigurationRepository#onConfigChanged") + } + + fun sendUpdate(reason: String) { + trySendWithFailureLogging(Unit, reason) + } + } + configurationController.addCallback(callback) + awaitClose { configurationController.removeCallback(callback) } + } + + private val configurationChange: Flow<Unit> = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration) { + trySendWithFailureLogging(Unit, "ConfigurationRepository#onConfigChanged") + } + } + configurationController.addCallback(callback) + awaitClose { configurationController.removeCallback(callback) } + } + + override val scaleForResolution: StateFlow<Float> = + configurationChange + .mapLatest { getResolutionScale() } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale()) + + override fun getResolutionScale(): Float { + context.display.getDisplayInfo(displayInfo.value) + val maxDisplayMode = + displayUtils.getMaximumResolutionDisplayMode(displayInfo.value.supportedModes) + maxDisplayMode?.let { + val scaleFactor = + displayUtils.getPhysicalPixelDisplaySizeRatio( + maxDisplayMode.physicalWidth, + maxDisplayMode.physicalHeight, + displayInfo.value.naturalWidth, + displayInfo.value.naturalHeight + ) + return if (scaleFactor == Float.POSITIVE_INFINITY) 1f else scaleFactor + } + return 1f + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 8e6e0dd37a90..89c45d7f5e12 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -39,6 +39,7 @@ import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.biometrics.dagger.UdfpsModule; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; +import com.android.systemui.common.ui.data.repository.CommonRepositoryModule; import com.android.systemui.complication.dagger.ComplicationComponent; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; @@ -155,6 +156,7 @@ import javax.inject.Named; ClipboardOverlayModule.class, ClockInfoModule.class, ClockRegistryModule.class, + CommonRepositoryModule.class, ConnectivityModule.class, CoroutinesModule.class, DreamModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt index d853e04fed90..10578521f182 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt @@ -24,4 +24,8 @@ class BurnInHelperWrapper @Inject constructor() { fun burnInOffset(amplitude: Int, xAxis: Boolean): Int { return getBurnInOffset(amplitude, xAxis) } + + fun burnInProgressOffset(): Float { + return getBurnInProgressOffset() + } } diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt index 1e01c9cfd6a5..2d57633e47a8 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt @@ -16,12 +16,12 @@ package com.android.systemui.dump -import android.util.ArrayMap import com.android.systemui.Dumpable import com.android.systemui.ProtoDumpable import com.android.systemui.dump.nano.SystemUIProtoDump import com.android.systemui.log.LogBuffer import java.io.PrintWriter +import java.util.TreeMap import javax.inject.Inject import javax.inject.Singleton @@ -36,8 +36,9 @@ import javax.inject.Singleton */ @Singleton open class DumpManager @Inject constructor() { - private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = ArrayMap() - private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = ArrayMap() + // NOTE: Using TreeMap ensures that iteration is in a predictable & alphabetical order. + private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = TreeMap() + private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = TreeMap() /** See [registerCriticalDumpable]. */ fun registerCriticalDumpable(module: Dumpable) { @@ -132,7 +133,8 @@ open class DumpManager @Inject constructor() { } /** - * Dumps the first dumpable or buffer whose registered name ends with [target] + * Dumps the alphabetically first, shortest-named dumpable or buffer whose registered name ends + * with [target]. */ @Synchronized fun dumpTarget( @@ -141,19 +143,14 @@ open class DumpManager @Inject constructor() { args: Array<String>, tailLength: Int, ) { - for (dumpable in dumpables.values) { - if (dumpable.name.endsWith(target)) { - dumpDumpable(dumpable, pw, args) - return + sequence { + findBestTargetMatch(dumpables, target)?.let { + yield(it.name to { dumpDumpable(it, pw, args) }) } - } - - for (buffer in buffers.values) { - if (buffer.name.endsWith(target)) { - dumpBuffer(buffer, pw, tailLength) - return + findBestTargetMatch(buffers, target)?.let { + yield(it.name to { dumpBuffer(it, pw, tailLength) }) } - } + }.sortedBy { it.first }.minByOrNull { it.first.length }?.second?.invoke() } @Synchronized @@ -162,11 +159,8 @@ open class DumpManager @Inject constructor() { protoDump: SystemUIProtoDump, args: Array<String> ) { - for (dumpable in dumpables.values) { - if (dumpable.dumpable is ProtoDumpable && dumpable.name.endsWith(target)) { - dumpProtoDumpable(dumpable.dumpable, protoDump, args) - return - } + findBestProtoTargetMatch(dumpables, target)?.let { + dumpProtoDumpable(it, protoDump, args) } } @@ -303,6 +297,22 @@ open class DumpManager @Inject constructor() { val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable return existingDumpable == null || newDumpable == existingDumpable } + + private fun <V : Any> findBestTargetMatch(map: Map<String, V>, target: String): V? = map + .asSequence() + .filter { it.key.endsWith(target) } + .minByOrNull { it.key.length } + ?.value + + private fun findBestProtoTargetMatch( + map: Map<String, RegisteredDumpable<Dumpable>>, + target: String + ): ProtoDumpable? = map + .asSequence() + .filter { it.key.endsWith(target) } + .filter { it.value.dumpable is ProtoDumpable } + .minByOrNull { it.key.length } + ?.value?.dumpable as? ProtoDumpable } private data class RegisteredDumpable<T>( diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 3c6353101e13..6ca409f07f6f 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -206,11 +206,6 @@ object Flags { ) /** Whether to inflate the bouncer view on a background thread. */ - // TODO(b/272091103): Tracking Bug - @JvmField - val ASYNC_INFLATE_BOUNCER = releasedFlag(229, "async_inflate_bouncer") - - /** Whether to inflate the bouncer view on a background thread. */ // TODO(b/273341787): Tracking Bug @JvmField val PREVENT_BYPASS_KEYGUARD = releasedFlag(230, "prevent_bypass_keyguard") @@ -236,6 +231,11 @@ object Flags { val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag(234, "revamped_bouncer_messages") + /** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */ + // TODO(b/279794160): Tracking bug. + @JvmField + val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer") + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") @@ -391,7 +391,7 @@ object Flags { val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE = releasedFlag(912, "media_ttt_dismiss_gesture") // TODO(b/266157412): Tracking Bug - val MEDIA_RETAIN_SESSIONS = releasedFlag(913, "media_retain_sessions") + val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions") // TODO(b/266739309): Tracking Bug @JvmField @@ -401,10 +401,10 @@ object Flags { val MEDIA_RESUME_PROGRESS = releasedFlag(915, "media_resume_progress") // TODO(b/267166152) : Tracking Bug - val MEDIA_RETAIN_RECOMMENDATIONS = releasedFlag(916, "media_retain_recommendations") + val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations") // TODO(b/270437894): Tracking Bug - val MEDIA_REMOTE_RESUME = releasedFlag(917, "media_remote_resume") + val MEDIA_REMOTE_RESUME = unreleasedFlag(917, "media_remote_resume") // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") @@ -600,6 +600,8 @@ object Flags { // 1700 - clipboard @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") + // TODO(b/278714186) Tracking Bug + @JvmField val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout") // 1800 - shade container // TODO(b/265944639): Tracking Bug @@ -623,7 +625,7 @@ object Flags { // TODO(b/269132640): Tracking Bug @JvmField val APP_PANELS_REMOVE_APPS_ALLOWED = - unreleasedFlag(2003, "app_panels_remove_apps_allowed", teamfood = true) + releasedFlag(2003, "app_panels_remove_apps_allowed") // 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.) // TODO(b/259264861): Tracking Bug diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt index 5f2178df4346..5b71a2ed1991 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt @@ -32,6 +32,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -42,6 +44,7 @@ import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.log.SessionTracker @@ -63,6 +66,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn @@ -135,6 +139,7 @@ constructor( @FaceDetectTableLog private val faceDetectLog: TableLogBuffer, @FaceAuthTableLog private val faceAuthLog: TableLogBuffer, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val featureFlags: FeatureFlags, dumpManager: DumpManager, ) : DeviceEntryFaceAuthRepository, Dumpable { private var authCancellationSignal: CancellationSignal? = null @@ -212,15 +217,21 @@ constructor( .collect(Collectors.toSet()) dumpManager.registerCriticalDumpable("DeviceEntryFaceAuthRepositoryImpl", this) - observeFaceAuthGatingChecks() - observeFaceDetectGatingChecks() - observeFaceAuthResettingConditions() - listenForSchedulingWatchdog() + if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) { + observeFaceAuthGatingChecks() + observeFaceDetectGatingChecks() + observeFaceAuthResettingConditions() + listenForSchedulingWatchdog() + } } private fun listenForSchedulingWatchdog() { keyguardTransitionInteractor.anyStateToGoneTransition - .onEach { faceManager?.scheduleWatchdog() } + .filter { it.transitionState == TransitionState.FINISHED } + .onEach { + faceAuthLogger.watchdogScheduled() + faceManager?.scheduleWatchdog() + } .launchIn(applicationScope) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt new file mode 100644 index 000000000000..252982fd019f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt @@ -0,0 +1,125 @@ +/* + * 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 android.content.Context +import androidx.annotation.DimenRes +import com.android.systemui.R +import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** Encapsulates business-logic related to Ambient Display burn-in offsets. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class BurnInInteractor +@Inject +constructor( + private val context: Context, + private val burnInHelperWrapper: BurnInHelperWrapper, + @Application private val scope: CoroutineScope, + private val configurationRepository: ConfigurationRepository, + private val systemClock: SystemClock, +) { + private val _dozeTimeTick = MutableStateFlow<Long>(0) + val dozeTimeTick: StateFlow<Long> = _dozeTimeTick.asStateFlow() + + val udfpsBurnInXOffset: StateFlow<Int> = + burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_x, isXAxis = true) + val udfpsBurnInYOffset: StateFlow<Int> = + burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_y, isXAxis = false) + val udfpsBurnInProgress: StateFlow<Float> = + dozeTimeTick + .mapLatest { burnInHelperWrapper.burnInProgressOffset() } + .stateIn(scope, SharingStarted.Lazily, burnInHelperWrapper.burnInProgressOffset()) + + fun dozeTimeTick() { + _dozeTimeTick.value = systemClock.uptimeMillis() + } + + /** + * Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the + * max burn-in offset on any configuration changes. If the max burn-in offset is specified in + * pixels, use [burnInOffsetDefinedInPixels]. + */ + private fun burnInOffset( + @DimenRes maxBurnInOffsetResourceId: Int, + isXAxis: Boolean, + ): StateFlow<Int> { + return configurationRepository.onAnyConfigurationChange + .flatMapLatest { + val maxBurnInOffsetPixels = + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId) + dozeTimeTick.mapLatest { calculateOffset(maxBurnInOffsetPixels, isXAxis) } + } + .stateIn( + scope, + SharingStarted.Lazily, + calculateOffset( + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId), + isXAxis, + ) + ) + } + + /** + * Use for max burn-in offBurn-in offsets that ARE specified in pixels. This flow will apply the + * a scale for any resolution changes. If the max burn-in offset is specified in dp, use + * [burnInOffset]. + */ + private fun burnInOffsetDefinedInPixels( + @DimenRes maxBurnInOffsetResourceId: Int, + isXAxis: Boolean, + ): StateFlow<Int> { + return configurationRepository.scaleForResolution + .flatMapLatest { scale -> + val maxBurnInOffsetPixels = + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId) + dozeTimeTick.mapLatest { calculateOffset(maxBurnInOffsetPixels, isXAxis, scale) } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + calculateOffset( + context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId), + isXAxis, + configurationRepository.getResolutionScale(), + ) + ) + } + + private fun calculateOffset( + maxBurnInOffsetPixels: Int, + isXAxis: Boolean, + scale: Float = 1f + ): Int { + return (burnInHelperWrapper.burnInOffset(maxBurnInOffsetPixels, isXAxis) * scale).toInt() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt index 6bbc6f61cc6f..c1aefc7bcbd7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt @@ -112,8 +112,6 @@ object KeyguardBouncerViewBinder { viewModel.isShowing.collect { isShowing -> view.visibility = if (isShowing) View.VISIBLE else View.INVISIBLE if (isShowing) { - // Reset Security Container entirely. - view.visibility = View.VISIBLE securityContainerController.reinflateViewFlipper { // Reset Security Container entirely. securityContainerController.onBouncerVisibilityChanged( diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt index 62e420759186..fefa1b29b576 100644 --- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt @@ -260,4 +260,8 @@ constructor( { "Attempting face auth again because of HW error: retry attempt $int1" } ) } + + fun watchdogScheduled() { + logBuffer.log(TAG, DEBUG, "FaceManager Biometric watchdog scheduled.") + } } diff --git a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt index af248269c77e..f7277842c026 100644 --- a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt @@ -21,9 +21,11 @@ import android.graphics.Rect import android.graphics.RectF import androidx.core.graphics.toRectF import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.LogLevel.ERROR import com.android.systemui.log.dagger.ScreenDecorationsLog +import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject private const val TAG = "ScreenDecorationsLog" @@ -130,4 +132,36 @@ constructor( fun onSensorLocationChanged() { logBuffer.log(TAG, DEBUG, "AuthControllerCallback in ScreenDecorations triggered") } + + fun cameraProtectionShownOrHidden( + faceDetectionRunning: Boolean, + biometricPromptShown: Boolean, + requestedState: Boolean, + currentlyShowing: Boolean + ) { + logBuffer.log( + TAG, + DEBUG, + { + bool1 = faceDetectionRunning + bool2 = biometricPromptShown + bool3 = requestedState + bool4 = currentlyShowing + }, + { + "isFaceDetectionRunning: $bool1, " + + "isBiometricPromptShowing: $bool2, " + + "requestedState: $bool3, " + + "currentState: $bool4" + } + ) + } + + fun biometricEvent(@CompileTimeConstant info: String) { + logBuffer.log(TAG, DEBUG, info) + } + + fun cameraProtectionEvent(@CompileTimeConstant cameraProtectionEvent: String) { + logBuffer.log(TAG, DEBUG, cameraProtectionEvent) + } } 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 90ef8a787a10..9be18ace79fa 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -209,7 +209,7 @@ public class LogModule { @SysUISingleton @CollapsedSbFragmentLog public static LogBuffer provideCollapsedSbFragmentLogBuffer(LogBufferFactory factory) { - return factory.create("CollapsedSbFragmentLog", 20); + return factory.create("CollapsedSbFragmentLog", 40); } /** diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt index 42fdd689df6c..592044e514be 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt @@ -27,19 +27,27 @@ import androidx.annotation.VisibleForTesting */ data class TableChange( var timestamp: Long = 0, - var columnPrefix: String = "", - var columnName: String = "", - var isInitial: Boolean = false, - var type: DataType = DataType.EMPTY, - var bool: Boolean = false, - var int: Int? = null, - var str: String? = null, + private var columnPrefix: String = "", + private var columnName: String = "", + private var isInitial: Boolean = false, + private var type: DataType = DataType.EMPTY, + private var bool: Boolean = false, + private var int: Int? = null, + private var str: String? = null, ) { + init { + // Truncate any strings that were passed into the constructor. [reset] and [set] will take + // care of the rest of the truncation. + this.columnPrefix = columnPrefix.take(MAX_STRING_LENGTH) + this.columnName = columnName.take(MAX_STRING_LENGTH) + this.str = str?.take(MAX_STRING_LENGTH) + } + /** Resets to default values so that the object can be recycled. */ fun reset(timestamp: Long, columnPrefix: String, columnName: String, isInitial: Boolean) { this.timestamp = timestamp - this.columnPrefix = columnPrefix - this.columnName = columnName + this.columnPrefix = columnPrefix.take(MAX_STRING_LENGTH) + this.columnName = columnName.take(MAX_STRING_LENGTH) this.isInitial = isInitial this.type = DataType.EMPTY this.bool = false @@ -50,7 +58,7 @@ data class TableChange( /** Sets this to store a string change. */ fun set(value: String?) { type = DataType.STRING - str = value + str = value?.take(MAX_STRING_LENGTH) } /** Sets this to store a boolean change. */ @@ -89,6 +97,8 @@ data class TableChange( } } + fun getColumnName() = columnName + fun getVal(): String { val value = when (type) { @@ -109,5 +119,8 @@ data class TableChange( companion object { @VisibleForTesting const val IS_INITIAL_PREFIX = "**" + // Don't allow any strings larger than this length so that we have a hard upper limit on the + // size of the data stored by the buffer. + @VisibleForTesting const val MAX_STRING_LENGTH = 500 } } 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 b936e833a56a..1d785ae62d87 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt @@ -291,7 +291,7 @@ class TableLogBuffer( private fun echoToDesiredEndpoints(change: TableChange) { if ( logcatEchoTracker.isBufferLoggable(bufferName = name, LogLevel.DEBUG) || - logcatEchoTracker.isTagLoggable(change.columnName, LogLevel.DEBUG) + logcatEchoTracker.isTagLoggable(change.getColumnName(), LogLevel.DEBUG) ) { if (change.hasData()) { localLogcat.d(name, change.logcatRepresentation()) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 9997730fa938..fa4211487d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -43,7 +43,6 @@ import android.media.session.PlaybackState import android.net.Uri import android.os.Parcelable import android.os.Process -import android.os.RemoteException import android.os.UserHandle import android.provider.Settings import android.service.notification.StatusBarNotification @@ -53,7 +52,6 @@ import android.util.Log import android.util.Pair as APair import androidx.media.utils.MediaConstants import com.android.internal.logging.InstanceId -import com.android.internal.statusbar.IStatusBarService import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Dumpable import com.android.systemui.R @@ -139,8 +137,6 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA = expiryTimeMs = 0, ) -const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank." - fun isMediaNotification(sbn: StatusBarNotification): Boolean { return sbn.notification.isMediaNotification() } @@ -185,7 +181,6 @@ class MediaDataManager( private val logger: MediaUiEventLogger, private val smartspaceManager: SmartspaceManager, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val statusBarService: IStatusBarService, ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener { companion object { @@ -257,7 +252,6 @@ class MediaDataManager( mediaFlags: MediaFlags, logger: MediaUiEventLogger, smartspaceManager: SmartspaceManager, - statusBarService: IStatusBarService, keyguardUpdateMonitor: KeyguardUpdateMonitor, ) : this( context, @@ -283,7 +277,6 @@ class MediaDataManager( logger, smartspaceManager, keyguardUpdateMonitor, - statusBarService, ) private val appChangeReceiver = @@ -385,21 +378,21 @@ class MediaDataManager( fun onNotificationAdded(key: String, sbn: StatusBarNotification) { if (useQsMediaPlayer && isMediaNotification(sbn)) { - var isNewlyActiveEntry = false + var logEvent = false Assert.isMainThread() val oldKey = findExistingEntry(key, sbn.packageName) if (oldKey == null) { val instanceId = logger.getNewInstanceId() val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId) mediaEntries.put(key, temp) - isNewlyActiveEntry = true + logEvent = true } else if (oldKey != key) { // Resume -> active conversion; move to new key val oldData = mediaEntries.remove(oldKey)!! - isNewlyActiveEntry = true + logEvent = true mediaEntries.put(key, oldData) } - loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) + loadMediaData(key, sbn, oldKey, logEvent) } else { onNotificationRemoved(key) } @@ -482,9 +475,9 @@ class MediaDataManager( key: String, sbn: StatusBarNotification, oldKey: String?, - isNewlyActiveEntry: Boolean = false, + logEvent: Boolean = false ) { - backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, logEvent) } } /** Add a listener for changes in this class */ @@ -608,11 +601,9 @@ class MediaDataManager( } } - private fun removeEntry(key: String, logEvent: Boolean = true) { + private fun removeEntry(key: String) { mediaEntries.remove(key)?.let { - if (logEvent) { - logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) - } + logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) } notifyMediaDataRemoved(key) } @@ -760,7 +751,7 @@ class MediaDataManager( key: String, sbn: StatusBarNotification, oldKey: String?, - isNewlyActiveEntry: Boolean = false, + logEvent: Boolean = false ) { val token = sbn.notification.extras.getParcelable( @@ -774,34 +765,6 @@ class MediaDataManager( val metadata = mediaController.metadata val notif: Notification = sbn.notification - // Song name - var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) - if (song == null) { - song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) - } - if (song == null) { - song = HybridGroupManager.resolveTitle(notif) - } - // Media data must have a title. - if (song.isNullOrBlank()) { - try { - statusBarService.onNotificationError( - sbn.packageName, - sbn.tag, - sbn.id, - sbn.uid, - sbn.initialPid, - MEDIA_TITLE_ERROR_MESSAGE, - sbn.user.identifier - ) - } catch (e: RemoteException) { - Log.e(TAG, "cancelNotification failed: $e") - } - // Only add log for media removed if active media is updated with invalid title. - foregroundExecutor.execute { removeEntry(key, !isNewlyActiveEntry) } - return - } - val appInfo = notif.extras.getParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, @@ -830,6 +793,15 @@ class MediaDataManager( // App Icon val smallIcon = sbn.notification.smallIcon + // Song name + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song == null) { + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song == null) { + song = HybridGroupManager.resolveTitle(notif) + } + // Explicit Indicator var isExplicit = false if (mediaFlags.isExplicitIndicatorEnabled()) { @@ -901,7 +873,7 @@ class MediaDataManager( val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val appUid = appInfo?.uid ?: Process.INVALID_UID - if (isNewlyActiveEntry) { + if (logEvent) { logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) } else if (playbackLocation != currentEntry?.playbackLocation) { diff --git a/packages/SystemUI/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivity.kt new file mode 100644 index 000000000000..c209a000deb4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivity.kt @@ -0,0 +1,54 @@ +/* + * 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.notetask + +import android.os.Bundle +import androidx.activity.ComponentActivity +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import javax.inject.Inject + +/** + * An internal proxy activity that starts the notes role setting. + * + * This activity is introduced mainly for the error handling of the notes app lock screen shortcut + * picker, which only supports package + action but not extras. See + * [KeyguardQuickAffordanceConfig.PickerScreenState.Disabled.actionComponentName]. + */ +class LaunchNotesRoleSettingsTrampolineActivity +@Inject +constructor( + private val controller: NoteTaskController, +) : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val entryPoint = + if (intent?.action == ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE) { + NoteTaskEntryPoint.QUICK_AFFORDANCE + } else { + null + } + controller.startNotesRoleSetting(this, entryPoint) + finish() + } + + companion object { + const val ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE = + "com.android.systemui.action.MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index d4052f54b3da..7e9b346c9577 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -31,15 +31,14 @@ import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ShortcutManager import android.graphics.drawable.Icon -import android.os.Build import android.os.UserHandle import android.os.UserManager -import android.util.Log import android.widget.Toast import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled +import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity @@ -92,10 +91,10 @@ constructor( if (info.launchMode != NoteTaskLaunchMode.AppBubble) return if (isExpanding) { - logDebug { "onBubbleExpandChanged - expanding: $info" } + debugLog { "onBubbleExpandChanged - expanding: $info" } eventLogger.logNoteTaskOpened(info) } else { - logDebug { "onBubbleExpandChanged - collapsing: $info" } + debugLog { "onBubbleExpandChanged - collapsing: $info" } eventLogger.logNoteTaskClosed(info) } } @@ -112,6 +111,43 @@ constructor( ) } + /** Starts the notes role setting. */ + fun startNotesRoleSetting(activityContext: Context, entryPoint: NoteTaskEntryPoint?) { + val user = + if (entryPoint == null) { + userTracker.userHandle + } else { + getUserForHandlingNotesTaking(entryPoint) + } + activityContext.startActivityAsUser( + Intent(Intent.ACTION_MANAGE_DEFAULT_APP).apply { + putExtra(Intent.EXTRA_ROLE_NAME, ROLE_NOTES) + }, + user + ) + } + + /** + * Returns the [UserHandle] of an android user that should handle the notes taking [entryPoint]. + * + * On company owned personally enabled (COPE) devices, if the given [entryPoint] is in the + * [FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES] list, the default notes app in the work + * profile user will always be launched. + * + * On non managed devices or devices with other management modes, the current [UserHandle] is + * returned. + */ + fun getUserForHandlingNotesTaking(entryPoint: NoteTaskEntryPoint): UserHandle = + if ( + entryPoint in FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES && + devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile + ) { + userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle + ?: userTracker.userHandle + } else { + userTracker.userHandle + } + /** * Shows a note task. How the task is shown will depend on when the method is invoked. * @@ -122,30 +158,13 @@ constructor( * bubble is already opened. * * That will let users open other apps in full screen, and take contextual notes. - * - * On company owned personally enabled (COPE) devices, if the given [entryPoint] is in the - * [FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES] list, the default notes app in the work - * profile user will always be launched. */ fun showNoteTask( entryPoint: NoteTaskEntryPoint, ) { if (!isEnabled) return - val user: UserHandle = - if ( - entryPoint in FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES && - devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile - ) { - userTracker.userProfiles - .firstOrNull { userManager.isManagedProfile(it.id) } - ?.userHandle - ?: userTracker.userHandle - } else { - userTracker.userHandle - } - - showNoteTaskAsUser(entryPoint, user) + showNoteTaskAsUser(entryPoint, getUserForHandlingNotesTaking(entryPoint)) } /** A variant of [showNoteTask] which launches note task in the given [user]. */ @@ -168,14 +187,14 @@ constructor( isKeyguardLocked && devicePolicyManager.areKeyguardShortcutsDisabled(userId = user.identifier) ) { - logDebug { "Enterprise policy disallows launching note app when the screen is locked." } + debugLog { "Enterprise policy disallows launching note app when the screen is locked." } return } val info = resolver.resolveInfo(entryPoint, isKeyguardLocked, user) if (info == null) { - logDebug { "Default notes app isn't set" } + debugLog { "Default notes app isn't set" } showNoDefaultNotesAppToast() return } @@ -184,7 +203,7 @@ constructor( try { // TODO(b/266686199): We should handle when app not available. For now, we log. - logDebug { "onShowNoteTask - start: $info on user#${user.identifier}" } + debugLog { "onShowNoteTask - start: $info on user#${user.identifier}" } when (info.launchMode) { is NoteTaskLaunchMode.AppBubble -> { val intent = createNoteTaskIntent(info) @@ -192,7 +211,7 @@ constructor( Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) bubbles.showOrHideAppBubble(intent, user, icon) // App bubble logging happens on `onBubbleExpandChanged`. - logDebug { "onShowNoteTask - opened as app bubble: $info" } + debugLog { "onShowNoteTask - opened as app bubble: $info" } } is NoteTaskLaunchMode.Activity -> { if (activityManager.isInForeground(info.packageName)) { @@ -200,20 +219,20 @@ constructor( val intent = createHomeIntent() context.startActivityAsUser(intent, user) eventLogger.logNoteTaskClosed(info) - logDebug { "onShowNoteTask - closed as activity: $info" } + debugLog { "onShowNoteTask - closed as activity: $info" } } else { val intent = createNoteTaskIntent(info) context.startActivityAsUser(intent, user) eventLogger.logNoteTaskOpened(info) - logDebug { "onShowNoteTask - opened as activity: $info" } + debugLog { "onShowNoteTask - opened as activity: $info" } } } } - logDebug { "onShowNoteTask - success: $info" } + debugLog { "onShowNoteTask - success: $info" } } catch (e: ActivityNotFoundException) { - logDebug { "onShowNoteTask - failed: $info" } + debugLog { "onShowNoteTask - failed: $info" } } - logDebug { "onShowNoteTask - completed: $info" } + debugLog { "onShowNoteTask - completed: $info" } } @VisibleForTesting @@ -253,7 +272,7 @@ constructor( PackageManager.DONT_KILL_APP, ) - logDebug { "setNoteTaskShortcutEnabled - completed: $isEnabled" } + debugLog { "setNoteTaskShortcutEnabled - completed: $isEnabled" } } /** @@ -352,11 +371,6 @@ private fun createNoteTaskIntent(info: NoteTaskInfo): Intent = } } -/** [Log.println] a [Log.DEBUG] message, only when [Build.IS_DEBUGGABLE]. */ -private inline fun Any.logDebug(message: () -> String) { - if (Build.IS_DEBUGGABLE) Log.d(this::class.java.simpleName.orEmpty(), message()) -} - /** Creates an [Intent] which forces the current app to background by calling home. */ private fun createHomeIntent(): Intent = Intent(Intent.ACTION_MAIN).apply { diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index 2c62ffd4841f..4d5173a1ec0a 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -45,6 +45,10 @@ interface NoteTaskModule { @[Binds IntoMap ClassKey(LaunchNoteTaskManagedProfileProxyActivity::class)] fun LaunchNoteTaskManagedProfileProxyActivity.bindNoteTaskLauncherProxyActivity(): Activity + @[Binds IntoMap ClassKey(LaunchNotesRoleSettingsTrampolineActivity::class)] + fun LaunchNotesRoleSettingsTrampolineActivity.bindLaunchNotesRoleSettingsTrampolineActivity(): + Activity + companion object { @[Provides NoteTaskEnabledKey] diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt index 2da5b7621723..444407c2341a 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt @@ -16,9 +16,12 @@ package com.android.systemui.notetask.quickaffordance +import android.app.role.OnRoleHoldersChangedListener +import android.app.role.RoleManager import android.content.Context import android.hardware.input.InputSettings import android.os.Build +import android.os.UserHandle import android.os.UserManager import android.util.Log import com.android.keyguard.KeyguardUpdateMonitor @@ -27,17 +30,22 @@ import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity.Companion.ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEnabledKey -import com.android.systemui.notetask.NoteTaskEntryPoint +import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE +import com.android.systemui.notetask.NoteTaskInfoResolver +import com.android.systemui.shared.customization.data.content.CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable.COMPONENT_NAME_SEPARATOR import com.android.systemui.stylus.StylusManager import dagger.Lazy +import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking @@ -49,13 +57,16 @@ import kotlinx.coroutines.flow.onEach class NoteTaskQuickAffordanceConfig @Inject constructor( - context: Context, + private val context: Context, private val controller: NoteTaskController, + private val noteTaskInfoResolver: NoteTaskInfoResolver, private val stylusManager: StylusManager, + private val roleManager: RoleManager, private val keyguardMonitor: KeyguardUpdateMonitor, private val userManager: UserManager, private val lazyRepository: Lazy<KeyguardQuickAffordanceRepository>, @NoteTaskEnabledKey private val isEnabled: Boolean, + @Background private val backgroundExecutor: Executor, ) : KeyguardQuickAffordanceConfig { override val key = BuiltInKeyguardQuickAffordanceKeys.CREATE_NOTE @@ -73,15 +84,24 @@ constructor( val configSelectedFlow = repository.createConfigSelectedFlow(key) val stylusEverUsedFlow = stylusManager.createStylusEverUsedFlow(context) val userUnlockedFlow = userManager.createUserUnlockedFlow(keyguardMonitor) - combine(userUnlockedFlow, stylusEverUsedFlow, configSelectedFlow) { + val defaultNotesAppFlow = + roleManager.createNotesRoleFlow(backgroundExecutor, controller, noteTaskInfoResolver) + combine(userUnlockedFlow, stylusEverUsedFlow, configSelectedFlow, defaultNotesAppFlow) { isUserUnlocked, isStylusEverUsed, - isConfigSelected -> + isConfigSelected, + isDefaultNotesAppSet -> logDebug { "lockScreenState:isUserUnlocked=$isUserUnlocked" } logDebug { "lockScreenState:isStylusEverUsed=$isStylusEverUsed" } logDebug { "lockScreenState:isConfigSelected=$isConfigSelected" } + logDebug { "lockScreenState:isDefaultNotesAppSet=$isDefaultNotesAppSet" } - if (isEnabled && isUserUnlocked && (isConfigSelected || isStylusEverUsed)) { + if ( + isEnabled && + isUserUnlocked && + isDefaultNotesAppSet && + (isConfigSelected || isStylusEverUsed) + ) { val contentDescription = ContentDescription.Resource(pickerNameResourceId) val icon = Icon.Resource(pickerIconResourceId, contentDescription) LockScreenState.Visible(icon) @@ -92,15 +112,34 @@ constructor( .onEach { state -> logDebug { "lockScreenState=$state" } } } - override suspend fun getPickerScreenState() = - if (isEnabled) { - PickerScreenState.Default() - } else { - PickerScreenState.UnavailableOnDevice + override suspend fun getPickerScreenState(): PickerScreenState { + val isDefaultNotesAppSet = + noteTaskInfoResolver.resolveInfo( + QUICK_AFFORDANCE, + user = controller.getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + ) != null + return when { + isEnabled && isDefaultNotesAppSet -> PickerScreenState.Default() + isEnabled -> { + PickerScreenState.Disabled( + listOf( + context.getString( + R.string.keyguard_affordance_enablement_dialog_notes_app_instruction + ) + ), + context.getString( + R.string.keyguard_affordance_enablement_dialog_notes_app_action + ), + "${context.packageName}$COMPONENT_NAME_SEPARATOR" + + "$ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE", + ) + } + else -> PickerScreenState.UnavailableOnDevice } + } override fun onTriggered(expandable: Expandable?): OnTriggeredResult { - controller.showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) + controller.showNoteTask(entryPoint = QUICK_AFFORDANCE) return OnTriggeredResult.Handled } } @@ -129,6 +168,27 @@ private fun StylusManager.createStylusEverUsedFlow(context: Context) = callbackF awaitClose { unregisterCallback(callback) } } +private fun RoleManager.createNotesRoleFlow( + executor: Executor, + noteTaskController: NoteTaskController, + noteTaskInfoResolver: NoteTaskInfoResolver, +) = callbackFlow { + fun isDefaultNotesAppSetForUser() = + noteTaskInfoResolver.resolveInfo( + QUICK_AFFORDANCE, + user = noteTaskController.getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + ) != null + + trySendBlocking(isDefaultNotesAppSetForUser()) + val callback = OnRoleHoldersChangedListener { roleName, _ -> + if (roleName == RoleManager.ROLE_NOTES) { + trySendBlocking(isDefaultNotesAppSetForUser()) + } + } + addOnRoleHoldersChangedListenerAsUser(executor, callback, UserHandle.ALL) + awaitClose { removeOnRoleHoldersChangedListenerAsUser(callback, UserHandle.ALL) } +} + private fun KeyguardQuickAffordanceRepository.createConfigSelectedFlow(key: String) = selections.map { selected -> selected.values.flatten().any { selectedConfig -> selectedConfig.key == key } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt index 0f38d32e0b64..8ca13b9776bb 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -18,12 +18,11 @@ package com.android.systemui.notetask.shortcut import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle import android.os.UserHandle import android.os.UserManager -import android.util.Log import androidx.activity.ComponentActivity +import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEntryPoint import com.android.systemui.settings.UserTracker @@ -68,7 +67,7 @@ constructor( val mainUser: UserHandle? = userManager.mainUser if (userManager.isManagedProfile) { if (mainUser == null) { - logDebug { "Can't find the main user. Skipping the notes app launch." } + debugLog { "Can't find the main user. Skipping the notes app launch." } } else { controller.startNoteTaskProxyActivityForUser(mainUser) } @@ -89,8 +88,3 @@ constructor( } } } - -/** [Log.println] a [Log.DEBUG] message, only when [Build.IS_DEBUGGABLE]. */ -private inline fun Any.logDebug(message: () -> String) { - if (Build.IS_DEBUGGABLE) Log.d(this::class.java.simpleName.orEmpty(), message()) -} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt index 79167f276576..166ba9fba166 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt @@ -15,6 +15,7 @@ package com.android.systemui.privacy import android.content.Context +import android.content.res.Configuration import android.util.AttributeSet import android.view.Gravity.CENTER_VERTICAL import android.view.Gravity.END @@ -102,6 +103,11 @@ class OngoingPrivacyChip @JvmOverloads constructor( R.string.ongoing_privacy_chip_content_multiple_apps, typesText) } + override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + updateResources() + } + private fun updateResources() { iconMargin = context.resources .getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_margin) @@ -110,8 +116,11 @@ class OngoingPrivacyChip @JvmOverloads constructor( iconColor = Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) + val height = context.resources + .getDimensionPixelSize(R.dimen.ongoing_appops_chip_height) val padding = context.resources .getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding) + iconsContainer.layoutParams.height = height iconsContainer.setPaddingRelative(padding, 0, padding, 0) iconsContainer.background = context.getDrawable(R.drawable.statusbar_privacy_chip_bg) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index b7f9f6bc0e4a..1afc885cb70d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -38,7 +38,9 @@ import java.io.PrintWriter; */ public class QSContainerImpl extends FrameLayout implements Dumpable { + private int mFancyClippingLeftInset; private int mFancyClippingTop; + private int mFancyClippingRightInset; private int mFancyClippingBottom; private final float[] mFancyClippingRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0}; private final Path mFancyClippingPath = new Path(); @@ -53,6 +55,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { private boolean mQsDisabled; private int mContentHorizontalPadding = -1; private boolean mClippingEnabled; + private boolean mIsFullWidth; public QSContainerImpl(Context context, AttributeSet attrs) { super(context, attrs); @@ -237,7 +240,8 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { /** * Clip QS bottom using a concave shape. */ - public void setFancyClipping(int top, int bottom, int radius, boolean enabled) { + public void setFancyClipping(int leftInset, int top, int rightInset, int bottom, int radius, + boolean enabled, boolean fullWidth) { boolean updatePath = false; if (mFancyClippingRadii[0] != radius) { mFancyClippingRadii[0] = radius; @@ -246,10 +250,18 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { mFancyClippingRadii[3] = radius; updatePath = true; } + if (mFancyClippingLeftInset != leftInset) { + mFancyClippingLeftInset = leftInset; + updatePath = true; + } if (mFancyClippingTop != top) { mFancyClippingTop = top; updatePath = true; } + if (mFancyClippingRightInset != rightInset) { + mFancyClippingRightInset = rightInset; + updatePath = true; + } if (mFancyClippingBottom != bottom) { mFancyClippingBottom = bottom; updatePath = true; @@ -258,6 +270,10 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { mClippingEnabled = enabled; updatePath = true; } + if (mIsFullWidth != fullWidth) { + mIsFullWidth = fullWidth; + updatePath = true; + } if (updatePath) { updateClippingPath(); @@ -281,15 +297,21 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { return; } - mFancyClippingPath.addRoundRect(0, mFancyClippingTop, getWidth(), + int clippingLeft = mIsFullWidth ? -mFancyClippingLeftInset : 0; + int clippingRight = mIsFullWidth ? getWidth() + mFancyClippingRightInset : getWidth(); + mFancyClippingPath.addRoundRect(clippingLeft, mFancyClippingTop, clippingRight, mFancyClippingBottom, mFancyClippingRadii, Path.Direction.CW); invalidate(); } @Override public void dump(PrintWriter pw, String[] args) { - pw.println(getClass().getSimpleName() + " updateClippingPath: top(" - + mFancyClippingTop + ") bottom(" + mFancyClippingBottom + ") mClippingEnabled(" - + mClippingEnabled + ")"); + pw.println(getClass().getSimpleName() + " updateClippingPath: " + + "leftInset(" + mFancyClippingLeftInset + ") " + + "top(" + mFancyClippingTop + ") " + + "rightInset(" + mFancyClippingRightInset + ") " + + "bottom(" + mFancyClippingBottom + ") " + + "mClippingEnabled(" + mClippingEnabled + ") " + + "mIsFullWidth(" + mIsFullWidth + ")"); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index d806afa91dd1..fd3f70148a69 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -18,9 +18,10 @@ import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; import static com.android.systemui.media.dagger.MediaModule.QS_PANEL; import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL; -import static com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; +import static com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.res.Configuration; @@ -395,9 +396,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } @Override - public void setFancyClipping(int top, int bottom, int cornerRadius, boolean visible) { + public void setFancyClipping(int leftInset, int top, int rightInset, int bottom, + int cornerRadius, boolean visible, boolean fullWidth) { if (getView() instanceof QSContainerImpl) { - ((QSContainerImpl) getView()).setFancyClipping(top, bottom, cornerRadius, visible); + ((QSContainerImpl) getView()).setFancyClipping(leftInset, top, rightInset, bottom, + cornerRadius, visible, fullWidth); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java index 7f7f8ad6a4c1..2d9f7dd038bc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -68,6 +68,7 @@ public class RotationLockTile extends QSTileImpl<BooleanState> implements private final SensorPrivacyManager mPrivacyManager; private final BatteryController mBatteryController; private final SettingObserver mSetting; + private final boolean mAllowRotationResolver; @Inject public RotationLockTile( @@ -105,6 +106,8 @@ public class RotationLockTile extends QSTileImpl<BooleanState> implements } }; mBatteryController.observe(getLifecycle(), this); + mAllowRotationResolver = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_allowRotationResolver); } @Override @@ -145,7 +148,7 @@ public class RotationLockTile extends QSTileImpl<BooleanState> implements final boolean powerSave = mBatteryController.isPowerSave(); final boolean cameraLocked = mPrivacyManager.isSensorPrivacyEnabled(CAMERA); - final boolean cameraRotation = + final boolean cameraRotation = mAllowRotationResolver && !powerSave && !cameraLocked && hasSufficientPermission(mContext) && mController.isCameraRotationEnabled(); state.value = !rotationLocked; diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 69008cca6fe3..84f358c303ca 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -33,6 +33,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; @@ -60,11 +61,10 @@ public class RecordingService extends Service implements ScreenMediaRecorderList public static final int REQUEST_CODE = 2; private static final int USER_ID_NOT_SPECIFIED = -1; - private static final int NOTIFICATION_RECORDING_ID = 4274; - private static final int NOTIFICATION_PROCESSING_ID = 4275; - private static final int NOTIFICATION_VIEW_ID = 4273; + private static final int NOTIF_BASE_ID = 4273; private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; + private static final String GROUP_KEY = "screen_record_saved"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; private static final String EXTRA_PATH = "extra_path"; private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; @@ -89,6 +89,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList private final UiEventLogger mUiEventLogger; private final NotificationManager mNotificationManager; private final UserContextProvider mUserContextTracker; + private int mNotificationId = NOTIF_BASE_ID; @Inject public RecordingService(RecordingController controller, @LongRunning Executor executor, @@ -134,14 +135,23 @@ public class RecordingService extends Service implements ScreenMediaRecorderList } String action = intent.getAction(); Log.d(TAG, "onStartCommand " + action); + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + getString(R.string.screenrecord_title), + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(getString(R.string.screenrecord_channel_description)); + channel.enableVibration(true); + mNotificationManager.createNotificationChannel(channel); int currentUserId = mUserContextTracker.getUserContext().getUserId(); UserHandle currentUser = new UserHandle(currentUserId); switch (action) { case ACTION_START: + // Get a unique ID for this recording's notifications + mNotificationId = NOTIF_BASE_ID + (int) SystemClock.uptimeMillis(); mAudioSource = ScreenRecordingAudioSource .values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)]; - Log.d(TAG, "recording with audio source" + mAudioSource); + Log.d(TAG, "recording with audio source " + mAudioSource); mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false); MediaProjectionCaptureTarget captureTarget = intent.getParcelableExtra(EXTRA_CAPTURE_TARGET, @@ -169,7 +179,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList } else { updateState(false); createErrorNotification(); - stopForeground(true); + stopForeground(STOP_FOREGROUND_DETACH); stopSelf(); return Service.START_NOT_STICKY; } @@ -200,7 +210,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList startActivity(Intent.createChooser(shareIntent, shareLabel) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); // Remove notification - mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); + mNotificationManager.cancelAsUser(null, mNotificationId, currentUser); return false; }, false, false); @@ -260,14 +270,6 @@ public class RecordingService extends Service implements ScreenMediaRecorderList @VisibleForTesting protected void createErrorNotification() { Resources res = getResources(); - NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, - getString(R.string.screenrecord_title), - NotificationManager.IMPORTANCE_DEFAULT); - channel.setDescription(getString(R.string.screenrecord_channel_description)); - channel.enableVibration(true); - mNotificationManager.createNotificationChannel(channel); - Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, res.getString(R.string.screenrecord_title)); @@ -277,7 +279,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList .setSmallIcon(R.drawable.ic_screenrecord) .setContentTitle(notificationTitle) .addExtras(extras); - startForeground(NOTIFICATION_RECORDING_ID, builder.build()); + startForeground(mNotificationId, builder.build()); } @VisibleForTesting @@ -288,14 +290,6 @@ public class RecordingService extends Service implements ScreenMediaRecorderList @VisibleForTesting protected void createRecordingNotification() { Resources res = getResources(); - NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, - getString(R.string.screenrecord_title), - NotificationManager.IMPORTANCE_DEFAULT); - channel.setDescription(getString(R.string.screenrecord_channel_description)); - channel.enableVibration(true); - mNotificationManager.createNotificationChannel(channel); - Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, res.getString(R.string.screenrecord_title)); @@ -323,7 +317,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) .addAction(stopAction) .addExtras(extras); - startForeground(NOTIFICATION_RECORDING_ID, builder.build()); + startForeground(mNotificationId, builder.build()); } @VisibleForTesting @@ -337,11 +331,12 @@ public class RecordingService extends Service implements ScreenMediaRecorderList extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, res.getString(R.string.screenrecord_title)); - Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID) + Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) .setContentTitle(notificationTitle) .setContentText( getResources().getString(R.string.screenrecord_background_processing_label)) .setSmallIcon(R.drawable.ic_screenrecord) + .setGroup(GROUP_KEY) .addExtras(extras); return builder.build(); } @@ -378,6 +373,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList PendingIntent.FLAG_IMMUTABLE)) .addAction(shareAction) .setAutoCancel(true) + .setGroup(GROUP_KEY) .addExtras(extras); // Add thumbnail if available @@ -391,6 +387,24 @@ public class RecordingService extends Service implements ScreenMediaRecorderList return builder.build(); } + /** + * Adds a group notification so that save notifications from multiple recordings are + * grouped together, and the foreground service recording notification is not + */ + private void postGroupNotification(UserHandle currentUser) { + Bundle extras = new Bundle(); + extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, + getResources().getString(R.string.screenrecord_title)); + Notification groupNotif = new Notification.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_screenrecord) + .setContentTitle(getResources().getString(R.string.screenrecord_save_title)) + .setGroup(GROUP_KEY) + .setGroupSummary(true) + .setExtras(extras) + .build(); + mNotificationManager.notifyAsUser(TAG, NOTIF_BASE_ID, groupNotif, currentUser); + } + private void stopService() { stopService(USER_ID_NOT_SPECIFIED); } @@ -423,27 +437,26 @@ public class RecordingService extends Service implements ScreenMediaRecorderList Log.e(TAG, "stopRecording called, but recorder was null"); } updateState(false); + stopForeground(STOP_FOREGROUND_DETACH); stopSelf(); } private void saveRecording(int userId) { UserHandle currentUser = new UserHandle(userId); - mNotificationManager.notifyAsUser(null, NOTIFICATION_PROCESSING_ID, + mNotificationManager.notifyAsUser(null, mNotificationId, createProcessingNotification(), currentUser); mLongExecutor.execute(() -> { try { Log.d(TAG, "saving recording"); Notification notification = createSaveNotification(getRecorder().save()); - if (!mController.isRecording()) { - mNotificationManager.notifyAsUser(null, NOTIFICATION_VIEW_ID, notification, - currentUser); - } + postGroupNotification(currentUser); + mNotificationManager.notifyAsUser(null, mNotificationId, notification, + currentUser); } catch (IOException e) { Log.e(TAG, "Error saving screen recording: " + e.getMessage()); showErrorToast(R.string.screenrecord_delete_error); - } finally { - mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser); + mNotificationManager.cancelAsUser(null, mNotificationId, currentUser); } }); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 51179153fc73..137a99ef39b8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -4474,7 +4474,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mDisplayTopInset = combinedInsets.top; mDisplayRightInset = combinedInsets.right; mDisplayLeftInset = combinedInsets.left; - mQsController.setDisplayInsets(mDisplayRightInset, mDisplayLeftInset); + mQsController.setDisplayInsets(mDisplayLeftInset, mDisplayRightInset); mNavigationBarBottomHeight = insets.getStableInsetBottom(); updateMaxHeadsUpTranslation(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index 92c9a80e0c8c..a1fa8fbf9075 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -887,7 +887,9 @@ public class QuickSettingsController { } void setOverScrollAmount(int overExpansion) { - mQs.setOverScrollAmount(overExpansion); + if (mQs != null) { + mQs.setOverScrollAmount(overExpansion); + } } private void setOverScrolling(boolean overscrolling) { @@ -1230,10 +1232,13 @@ public class QuickSettingsController { mVisible = qsVisible; mQs.setQsVisible(qsVisible); mQs.setFancyClipping( + mDisplayLeftInset, clipTop, + mDisplayRightInset, clipBottom, radius, - qsVisible && !mSplitShadeEnabled); + qsVisible && !mSplitShadeEnabled, + mIsFullWidth); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 77550038c94a..129c8594e48e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -26,6 +26,7 @@ import android.annotation.IntDef; import android.app.ActivityManager; import android.app.Notification; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -75,9 +76,9 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi */ private static final float DARK_ALPHA_BOOST = 0.67f; /** - * Status icons are currently drawn with the intention of being 17dp tall, but we - * want to scale them (in a way that doesn't require an asset dump) down 2dp. So - * 17dp * (15 / 17) = 15dp, the new height. After the first call to {@link #reloadDimens} all + * Status icons are currently drawn with the intention of being 17sp tall, but we + * want to scale them (in a way that doesn't require an asset dump) down 2sp. So + * 17sp * (15 / 17) = 15sp, the new height. After the first call to {@link #reloadDimens} all * values will be in px. */ private float mSystemIconDesiredHeight = 15f; @@ -144,7 +145,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi private String mNumberText; private StatusBarNotification mNotification; private final boolean mBlocked; - private int mDensity; + private Configuration mConfiguration; private boolean mNightMode; private float mIconScale = 1.0f; private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -198,9 +199,8 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi mNumberPain.setAntiAlias(true); setNotification(sbn); setScaleType(ScaleType.CENTER); - mDensity = context.getResources().getDisplayMetrics().densityDpi; - Configuration configuration = context.getResources().getConfiguration(); - mNightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) + mConfiguration = new Configuration(context.getResources().getConfiguration()); + mNightMode = (mConfiguration.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; initializeDecorColor(); reloadDimens(); @@ -214,7 +214,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi mAlwaysScaleIcon = true; reloadDimens(); maybeUpdateIconScaleDimens(); - mDensity = context.getResources().getDisplayMetrics().densityDpi; + mConfiguration = new Configuration(context.getResources().getConfiguration()); } /** Should always be preceded by {@link #reloadDimens()} */ @@ -231,12 +231,17 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi private void updateIconScaleForNotifications() { final float imageBounds = mIncreasedSize ? mStatusBarIconDrawingSizeIncreased : mStatusBarIconDrawingSize; - final int outerBounds = mStatusBarIconSize; - mIconScale = imageBounds / (float)outerBounds; + float iconHeight = getIconHeight(); + if (iconHeight != 0) { + mIconScale = imageBounds / iconHeight; + } else { + final int outerBounds = mStatusBarIconSize; + mIconScale = imageBounds / (float) outerBounds; + } updatePivot(); } - // Makes sure that all icons are scaled to the same height (15dp). If we cannot get a height + // Makes sure that all icons are scaled to the same height (15sp). If we cannot get a height // for the icon, it uses the default SCALE (15f / 17f) which is the old behavior private void updateIconScaleForSystemIcons() { float iconHeight = getIconHeight(); @@ -267,12 +272,10 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - int density = newConfig.densityDpi; - if (density != mDensity) { - mDensity = density; - reloadDimens(); - updateDrawable(); - maybeUpdateIconScaleDimens(); + final int configDiff = newConfig.diff(mConfiguration); + mConfiguration.setTo(newConfig); + if ((configDiff & (ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_FONT_SCALE)) != 0) { + updateIconDimens(); } boolean nightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; @@ -282,6 +285,15 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi } } + /** + * Update the icon dimens and drawable with current resources + */ + public void updateIconDimens() { + reloadDimens(); + updateDrawable(); + maybeUpdateIconScaleDimens(); + } + private void reloadDimens() { boolean applyRadius = mDotRadius == mStaticDotRadius; Resources res = getResources(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 66d4c3a97773..285dd9766169 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -733,12 +733,16 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView super.dump(pw, args); if (DUMP_VERBOSE) { DumpUtilsKt.withIncreasedIndent(pw, () -> { - pw.println("mBackgroundNormal: " + mBackgroundNormal); - if (mBackgroundNormal != null) { - DumpUtilsKt.withIncreasedIndent(pw, () -> { - mBackgroundNormal.dump(pw, args); - }); - } + dumpBackgroundView(pw, args); + }); + } + } + + protected void dumpBackgroundView(IndentingPrintWriter pw, String[] args) { + pw.println("Background View: " + mBackgroundNormal); + if (DUMP_VERBOSE && mBackgroundNormal != null) { + DumpUtilsKt.withIncreasedIndent(pw, () -> { + mBackgroundNormal.dump(pw, args); }); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 597813344d6e..1dc58b59339d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -3593,6 +3593,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // Skip super call; dump viewState ourselves pw.println("Notification: " + mEntry.getKey()); DumpUtilsKt.withIncreasedIndent(pw, () -> { + pw.println(this); pw.print("visibility: " + getVisibility()); pw.print(", alpha: " + getAlpha()); pw.print(", translation: " + getTranslation()); @@ -3612,6 +3613,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.println("no viewState!!!"); } pw.println(getRoundableState().debugString()); + dumpBackgroundView(pw, args); int transientViewCount = mChildrenContainer == null ? 0 : mChildrenContainer.getTransientViewCount(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 5edff5f4e5d2..3e01dd3ab330 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.notification.Roundable; import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; +import com.android.systemui.util.Compile; import com.android.systemui.util.DumpUtilsKt; import java.io.PrintWriter; @@ -52,7 +53,8 @@ import java.util.List; public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable { private static final String TAG = "ExpandableView"; /** whether the dump() for this class should include verbose details */ - protected static final boolean DUMP_VERBOSE = false; + protected static final boolean DUMP_VERBOSE = + Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private RoundableState mRoundableState = null; protected OnHeightChangedListener mOnHeightChangedListener; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index da8d2d524456..647505cea71b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.util.ColorUtilKt.hexColorString; + import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Canvas; @@ -27,6 +29,9 @@ import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.util.ArrayUtils; import com.android.systemui.Dumpable; import com.android.systemui.R; @@ -44,6 +49,7 @@ public class NotificationBackgroundView extends View implements Dumpable { private int mClipTopAmount; private int mClipBottomAmount; private int mTintColor; + @Nullable private Integer mRippleColor; private final float[] mCornerRadii = new float[8]; private boolean mBottomIsRounded; private boolean mBottomAmountClips = true; @@ -127,6 +133,7 @@ public class NotificationBackgroundView extends View implements Dumpable { unscheduleDrawable(mBackground); } mBackground = background; + mRippleColor = null; mBackground.mutate(); if (mBackground != null) { mBackground.setCallback(this); @@ -215,6 +222,9 @@ public class NotificationBackgroundView extends View implements Dumpable { if (mBackground instanceof RippleDrawable) { RippleDrawable ripple = (RippleDrawable) mBackground; ripple.setColor(ColorStateList.valueOf(color)); + mRippleColor = color; + } else { + mRippleColor = null; } } @@ -290,7 +300,7 @@ public class NotificationBackgroundView extends View implements Dumpable { } @Override - public void dump(PrintWriter pw, String[] args) { + public void dump(PrintWriter pw, @NonNull String[] args) { pw.println("mDontModifyCorners: " + mDontModifyCorners); pw.println("mClipTopAmount: " + mClipTopAmount); pw.println("mClipBottomAmount: " + mClipBottomAmount); @@ -299,5 +309,8 @@ public class NotificationBackgroundView extends View implements Dumpable { pw.println("mBottomAmountClips: " + mBottomAmountClips); pw.println("mActualWidth: " + mActualWidth); pw.println("mActualHeight: " + mActualHeight); + pw.println("mTintColor: " + hexColorString(mTintColor)); + pw.println("mRippleColor: " + hexColorString(mRippleColor)); + pw.println("mBackground: " + mBackground); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 89c3946939a2..618120d406cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -37,6 +37,7 @@ import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.BurnInInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -56,10 +57,12 @@ import java.util.ArrayList; import javax.inject.Inject; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + /** * Implementation of DozeHost for SystemUI. */ -@SysUISingleton +@ExperimentalCoroutinesApi @SysUISingleton public final class DozeServiceHost implements DozeHost { private static final String TAG = "DozeServiceHost"; private final ArrayList<Callback> mCallbacks = new ArrayList<>(); @@ -89,6 +92,7 @@ public final class DozeServiceHost implements DozeHost { private NotificationShadeWindowViewController mNotificationShadeWindowViewController; private final AuthController mAuthController; private final NotificationIconAreaController mNotificationIconAreaController; + private final BurnInInteractor mBurnInInteractor; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private ShadeViewController mNotificationPanel; private View mAmbientIndicationContainer; @@ -110,7 +114,8 @@ public final class DozeServiceHost implements DozeHost { NotificationShadeWindowController notificationShadeWindowController, NotificationWakeUpCoordinator notificationWakeUpCoordinator, AuthController authController, - NotificationIconAreaController notificationIconAreaController) { + NotificationIconAreaController notificationIconAreaController, + BurnInInteractor burnInInteractor) { super(); mDozeLog = dozeLog; mPowerManager = powerManager; @@ -129,6 +134,7 @@ public final class DozeServiceHost implements DozeHost { mNotificationWakeUpCoordinator = notificationWakeUpCoordinator; mAuthController = authController; mNotificationIconAreaController = notificationIconAreaController; + mBurnInInteractor = burnInInteractor; mHeadsUpManagerPhone.addListener(mOnHeadsUpChangedListener); } @@ -304,6 +310,7 @@ public final class DozeServiceHost implements DozeHost { if (mAmbientIndicationContainer instanceof DozeReceiver) { ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick(); } + mBurnInInteractor.dozeTimeTick(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 006a029de8e0..b9a12e28b8ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -306,7 +306,7 @@ public class NotificationIconContainer extends ViewGroup { public void applyIconStates() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); - ViewState childState = mIconStates.get(child); + IconState childState = mIconStates.get(child); if (childState != null) { childState.applyToView(child); } @@ -339,6 +339,7 @@ public class NotificationIconContainer extends ViewGroup { } } if (child instanceof StatusBarIconView) { + ((StatusBarIconView) child).updateIconDimens(); ((StatusBarIconView) child).setDozing(mDozing, false, 0); } } @@ -447,9 +448,14 @@ public class NotificationIconContainer extends ViewGroup { @VisibleForTesting boolean isOverflowing(boolean isLastChild, float translationX, float layoutEnd, float iconSize) { - // Layout end, as used here, does not include padding end. - final float overflowX = isLastChild ? layoutEnd : layoutEnd - iconSize; - return translationX >= overflowX; + if (isLastChild) { + return translationX + iconSize > layoutEnd; + } else { + // If the child is not the last child, we need to ensure that we have room for the next + // icon and the dot. The dot could be as large as an icon, so verify that we have room + // for 2 icons. + return translationX + iconSize * 2f > layoutEnd; + } } /** @@ -489,10 +495,7 @@ public class NotificationIconContainer extends ViewGroup { // First icon to overflow. if (firstOverflowIndex == -1 && isOverflowing) { firstOverflowIndex = i; - mVisualOverflowStart = layoutEnd - mIconSize; - if (forceOverflow || mIsStaticLayout) { - mVisualOverflowStart = Math.min(translationX, mVisualOverflowStart); - } + mVisualOverflowStart = translationX; } final float drawingScale = mOnLockScreen && view instanceof StatusBarIconView ? ((StatusBarIconView) view).getIconScaleIncreased() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index a8a834f1e8f4..678873c0165c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -203,8 +203,7 @@ public interface StatusBarIconController { @Override protected LayoutParams onCreateLayoutParams() { - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize); + LinearLayout.LayoutParams lp = super.onCreateLayoutParams(); lp.setMargins(mIconHPadding, 0, mIconHPadding, 0); return lp; } @@ -370,7 +369,7 @@ public interface StatusBarIconController { private final MobileIconsViewModel mMobileIconsViewModel; protected final Context mContext; - protected final int mIconSize; + protected int mIconSize; // Whether or not these icons show up in dumpsys protected boolean mShouldLog = false; private StatusBarIconController mController; @@ -395,10 +394,10 @@ public interface StatusBarIconController { mStatusBarPipelineFlags = statusBarPipelineFlags; mMobileContextProvider = mobileContextProvider; mContext = group.getContext(); - mIconSize = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_icon_size); mLocation = location; + reloadDimens(); + if (statusBarPipelineFlags.runNewMobileIconsBackend()) { // This starts the flow for the new pipeline, and will notify us of changes if // {@link StatusBarPipelineFlags#useNewMobileIcons} is also true. @@ -609,13 +608,9 @@ public interface StatusBarIconController { mGroup.removeAllViews(); } - protected void onDensityOrFontScaleChanged() { - for (int i = 0; i < mGroup.getChildCount(); i++) { - View child = mGroup.getChildAt(i); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize); - child.setLayoutParams(lp); - } + protected void reloadDimens() { + mIconSize = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_icon_size); } private void setHeightAndCenter(ImageView imageView, int height) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 3a184239ac43..80d5651a65dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -109,6 +109,7 @@ public class StatusBarIconControllerImpl implements Tunable, } group.setController(this); + group.reloadDimens(); mIconGroups.add(group); List<Slot> allSlots = mStatusBarIconList.getSlots(); for (int i = 0; i < allSlots.size(); i++) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java index 26c17674ab10..ddbfd43f9bf6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java @@ -22,6 +22,8 @@ import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -72,13 +74,16 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { // Any ignored icon will never be added as a child private ArrayList<String> mIgnoredSlots = new ArrayList<>(); + private Configuration mConfiguration; + public StatusIconContainer(Context context) { this(context, null); } public StatusIconContainer(Context context, AttributeSet attrs) { super(context, attrs); - initDimens(); + mConfiguration = new Configuration(context.getResources().getConfiguration()); + reloadDimens(); setWillNotDraw(!DEBUG_OVERFLOW); } @@ -95,7 +100,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { return mShouldRestrictIcons; } - private void initDimens() { + private void reloadDimens() { // This is the same value that StatusBarIconView uses mIconDotFrameWidth = getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); @@ -211,6 +216,16 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { child.setTag(R.id.status_bar_view_state_tag, null); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + final int configDiff = newConfig.diff(mConfiguration); + mConfiguration.setTo(newConfig); + if ((configDiff & (ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_FONT_SCALE)) != 0) { + reloadDimens(); + } + } + /** * Add a name of an icon slot to be ignored. It will not show up nor be measured * @param slotName name of the icon as it exists in @@ -348,13 +363,17 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { int totalVisible = mLayoutStates.size(); int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; - mUnderflowStart = 0; + // Init mUnderflowStart value with the offset to let the dot be placed next to battery icon. + // It to prevent if the underflow happens at rightest(totalVisible - 1) child then break the + // for loop with mUnderflowStart staying 0(initial value), causing the dot be placed at the + // leftest side. + mUnderflowStart = (int) Math.max(contentStart, width - getPaddingEnd() - mUnderflowWidth); int visible = 0; int firstUnderflowIndex = -1; for (int i = totalVisible - 1; i >= 0; i--) { StatusIconState state = mLayoutStates.get(i); // Allow room for underflow if we found we need it in onMeasure - if (mNeedsUnderflow && (state.getXTranslation() < (contentStart + mUnderflowWidth)) + if ((mNeedsUnderflow && (state.getXTranslation() < (contentStart + mUnderflowWidth))) || (mShouldRestrictIcons && (visible >= maxVisible))) { firstUnderflowIndex = i; break; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index 453dd1bb6f81..620d2824acfa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -14,11 +14,7 @@ package com.android.systemui.statusbar.phone.fragment; -import static android.app.StatusBarManager.DISABLE2_SYSTEM_ICONS; -import static android.app.StatusBarManager.DISABLE_CLOCK; -import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS; -import static android.app.StatusBarManager.DISABLE_ONGOING_CALL_CHIP; -import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO; + import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE; import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT; @@ -112,8 +108,13 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private View mClockView; private View mOngoingCallChip; private View mNotificationIconAreaInner; - private int mDisabled1; - private int mDisabled2; + // Visibilities come in from external system callers via disable flags, but we also sometimes + // modify the visibilities internally. We need to store both so that we don't accidentally + // propagate our internally modified flags for too long. + private StatusBarVisibilityModel mLastSystemVisibility = + StatusBarVisibilityModel.createDefaultModel(); + private StatusBarVisibilityModel mLastModifiedVisibility = + StatusBarVisibilityModel.createDefaultModel(); private DarkIconManager mDarkIconManager; private final StatusBarFragmentComponent.Factory mStatusBarFragmentComponentFactory; private final CommandQueue mCommandQueue; @@ -141,7 +142,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() { @Override public void onOngoingCallStateChanged(boolean animate) { - disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); + updateStatusBarVisibilities(animate); } }; private OperatorNameViewController mOperatorNameViewController; @@ -388,8 +389,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } notificationIconArea.addView(mNotificationIconAreaInner); - // #disable should have already been called, so use the disable values to set visibility. - updateNotificationIconAreaAndCallChip(mDisabled1, false); + updateNotificationIconAreaAndCallChip(/* animate= */ false); } /** @@ -408,49 +408,50 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue if (displayId != getContext().getDisplayId()) { return; } + mCollapsedStatusBarFragmentLogger + .logDisableFlagChange(new DisableState(state1, state2)); + mLastSystemVisibility = + StatusBarVisibilityModel.createModelFromFlags(state1, state2); + updateStatusBarVisibilities(animate); + } - int state1BeforeAdjustment = state1; - state1 = adjustDisableFlags(state1); - - mCollapsedStatusBarFragmentLogger.logDisableFlagChange( - /* new= */ new DisableState(state1BeforeAdjustment, state2), - /* newAfterLocalModification= */ new DisableState(state1, state2)); - - final int old1 = mDisabled1; - final int diff1 = state1 ^ old1; - final int old2 = mDisabled2; - final int diff2 = state2 ^ old2; - mDisabled1 = state1; - mDisabled2 = state2; - if ((diff1 & DISABLE_SYSTEM_INFO) != 0 || ((diff2 & DISABLE2_SYSTEM_ICONS) != 0)) { - if ((state1 & DISABLE_SYSTEM_INFO) != 0 || ((state2 & DISABLE2_SYSTEM_ICONS) != 0)) { - hideEndSideContent(animate); - hideOperatorName(animate); - } else { + private void updateStatusBarVisibilities(boolean animate) { + StatusBarVisibilityModel previousModel = mLastModifiedVisibility; + StatusBarVisibilityModel newModel = calculateInternalModel(mLastSystemVisibility); + mCollapsedStatusBarFragmentLogger.logVisibilityModel(newModel); + mLastModifiedVisibility = newModel; + + if (newModel.getShowSystemInfo() != previousModel.getShowSystemInfo()) { + if (newModel.getShowSystemInfo()) { showEndSideContent(animate); showOperatorName(animate); + } else { + hideEndSideContent(animate); + hideOperatorName(animate); } } // The ongoing call chip and notification icon visibilities are intertwined, so update both // if either change. - if (((diff1 & DISABLE_ONGOING_CALL_CHIP) != 0) - || ((diff1 & DISABLE_NOTIFICATION_ICONS) != 0)) { - updateNotificationIconAreaAndCallChip(state1, animate); + if (newModel.getShowNotificationIcons() != previousModel.getShowNotificationIcons() + || newModel.getShowOngoingCallChip() != previousModel.getShowOngoingCallChip()) { + updateNotificationIconAreaAndCallChip(animate); } // The clock may have already been hidden, but we might want to shift its // visibility to GONE from INVISIBLE or vice versa - if ((diff1 & DISABLE_CLOCK) != 0 || mClockView.getVisibility() != clockHiddenMode()) { - if ((state1 & DISABLE_CLOCK) != 0) { - hideClock(animate); - } else { + if (newModel.getShowClock() != previousModel.getShowClock() + || mClockView.getVisibility() != clockHiddenMode()) { + if (newModel.getShowClock()) { showClock(animate); + } else { + hideClock(animate); } } } - protected int adjustDisableFlags(int state) { + private StatusBarVisibilityModel calculateInternalModel( + StatusBarVisibilityModel externalModel) { boolean headsUpVisible = mStatusBarFragmentComponent.getHeadsUpAppearanceController().shouldBeVisible(); @@ -459,34 +460,31 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue && shouldHideNotificationIcons() && !(mStatusBarStateController.getState() == StatusBarState.KEYGUARD && headsUpVisible)) { - state |= DISABLE_NOTIFICATION_ICONS; - state |= DISABLE_SYSTEM_INFO; - state |= DISABLE_CLOCK; - } - - if (mOngoingCallController.hasOngoingCall()) { - state &= ~DISABLE_ONGOING_CALL_CHIP; - } else { - state |= DISABLE_ONGOING_CALL_CHIP; - } - - if (headsUpVisible) { - // Disable everything on the left side of the status bar, since the app name for the - // heads up notification appears there instead. - state |= DISABLE_CLOCK; - state |= DISABLE_ONGOING_CALL_CHIP; + // Hide everything + return new StatusBarVisibilityModel( + /* showClock= */ false, + /* showNotificationIcons= */ false, + /* showOngoingCallChip= */ false, + /* showSystemInfo= */ false); } - return state; + boolean showClock = externalModel.getShowClock() && !headsUpVisible; + boolean showOngoingCallChip = mOngoingCallController.hasOngoingCall() && !headsUpVisible; + return new StatusBarVisibilityModel( + showClock, + externalModel.getShowNotificationIcons(), + showOngoingCallChip, + externalModel.getShowSystemInfo()); } /** * Updates the visibility of the notification icon area and ongoing call chip based on disabled1 * state. */ - private void updateNotificationIconAreaAndCallChip(int state1, boolean animate) { - boolean disableNotifications = (state1 & DISABLE_NOTIFICATION_ICONS) != 0; - boolean hasOngoingCall = (state1 & DISABLE_ONGOING_CALL_CHIP) == 0; + private void updateNotificationIconAreaAndCallChip(boolean animate) { + StatusBarVisibilityModel visibilityModel = mLastModifiedVisibility; + boolean disableNotifications = !visibilityModel.getShowNotificationIcons(); + boolean hasOngoingCall = visibilityModel.getShowOngoingCallChip(); // Hide notifications if the disable flag is set or we have an ongoing call. if (disableNotifications || hasOngoingCall) { @@ -683,7 +681,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue @Override public void onDozingChanged(boolean isDozing) { - disable(getContext().getDisplayId(), mDisabled1, mDisabled2, false /* animate */); + updateStatusBarVisibilities(/* animate= */ false); } @Nullable @@ -698,10 +696,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue return mSystemEventAnimator.onSystemEventAnimationFinish(hasPersistentDot); } - private boolean isSystemIconAreaDisabled() { - return (mDisabled1 & DISABLE_SYSTEM_INFO) != 0 || (mDisabled2 & DISABLE2_SYSTEM_ICONS) != 0; - } - private void updateStatusBarLocation(int left, int right) { int leftMargin = left - mStatusBar.getLeft(); int rightMargin = mStatusBar.getRight() - right; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt index 0c86b51a4995..8c19fb4f43c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt @@ -37,7 +37,6 @@ class CollapsedStatusBarFragmentLogger @Inject constructor( */ fun logDisableFlagChange( new: DisableFlagsLogger.DisableState, - newAfterLocalModification: DisableFlagsLogger.DisableState ) { buffer.log( TAG, @@ -45,19 +44,34 @@ class CollapsedStatusBarFragmentLogger @Inject constructor( { int1 = new.disable1 int2 = new.disable2 - long1 = newAfterLocalModification.disable1.toLong() - long2 = newAfterLocalModification.disable2.toLong() }, { disableFlagsLogger.getDisableFlagsString( old = null, new = DisableFlagsLogger.DisableState(int1, int2), - newAfterLocalModification = - DisableFlagsLogger.DisableState(long1.toInt(), long2.toInt()) ) } ) } + + fun logVisibilityModel(model: StatusBarVisibilityModel) { + buffer.log( + TAG, + LogLevel.INFO, + { + bool1 = model.showClock + bool2 = model.showNotificationIcons + bool3 = model.showOngoingCallChip + bool4 = model.showSystemInfo + }, + { "New visibilities calculated internally. " + + "showClock=$bool1 " + + "showNotificationIcons=$bool2 " + + "showOngoingCallChip=$bool3 " + + "showSystemInfo=$bool4" + } + ) + } } private const val TAG = "CollapsedSbFragment" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt new file mode 100644 index 000000000000..cf54cb7aa954 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.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.statusbar.phone.fragment + +import android.app.StatusBarManager.DISABLE2_NONE +import android.app.StatusBarManager.DISABLE2_SYSTEM_ICONS +import android.app.StatusBarManager.DISABLE_CLOCK +import android.app.StatusBarManager.DISABLE_NONE +import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS +import android.app.StatusBarManager.DISABLE_ONGOING_CALL_CHIP +import android.app.StatusBarManager.DISABLE_SYSTEM_INFO + +/** A model for which parts of the status bar should be visible or not visible. */ +data class StatusBarVisibilityModel( + val showClock: Boolean, + val showNotificationIcons: Boolean, + val showOngoingCallChip: Boolean, + val showSystemInfo: Boolean, +) { + companion object { + /** Creates the default model. */ + @JvmStatic + fun createDefaultModel(): StatusBarVisibilityModel { + return createModelFromFlags(DISABLE_NONE, DISABLE2_NONE) + } + + /** + * Given a set of disabled flags, converts them into the correct visibility statuses. + * + * See [CommandQueue.Callbacks.disable]. + */ + @JvmStatic + fun createModelFromFlags(disabled1: Int, disabled2: Int): StatusBarVisibilityModel { + return StatusBarVisibilityModel( + showClock = (disabled1 and DISABLE_CLOCK) == 0, + showNotificationIcons = (disabled1 and DISABLE_NOTIFICATION_ICONS) == 0, + // TODO(b/279899176): [CollapsedStatusBarFragment] always overwrites this with the + // value of [OngoingCallController]. Do we need to process the flag here? + showOngoingCallChip = (disabled1 and DISABLE_ONGOING_CALL_CHIP) == 0, + showSystemInfo = + (disabled1 and DISABLE_SYSTEM_INFO) == 0 && + (disabled2 and DISABLE2_SYSTEM_ICONS) == 0 + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 412b3150489c..27aaa6828036 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -22,7 +22,6 @@ import android.content.Context import android.hardware.BatteryState import android.hardware.input.InputManager import android.hardware.input.InputSettings -import android.os.Build import android.os.Handler import android.util.ArrayMap import android.util.Log @@ -35,6 +34,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.shared.hardware.hasInputDevice import com.android.systemui.shared.hardware.isInternalStylusSource import java.util.concurrent.CopyOnWriteArrayList @@ -81,7 +81,7 @@ constructor( fun startListener() { handler.post { if (hasStarted) return@post - logDebug { "Listener has started." } + debugLog { "Listener has started." } hasStarted = true isInUsiSession = @@ -109,7 +109,7 @@ constructor( val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return - logDebug { + debugLog { "Stylus InputDevice added: $deviceId ${device.name}, " + "External: ${device.isExternal}" } @@ -134,7 +134,7 @@ constructor( val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return - logDebug { "Stylus InputDevice changed: $deviceId ${device.name}" } + debugLog { "Stylus InputDevice changed: $deviceId ${device.name}" } val currAddress: String? = device.bluetoothAddress val prevAddress: String? = inputDeviceAddressMap[deviceId] @@ -155,7 +155,7 @@ constructor( if (!hasStarted) return if (!inputDeviceAddressMap.contains(deviceId)) return - logDebug { "Stylus InputDevice removed: $deviceId" } + debugLog { "Stylus InputDevice removed: $deviceId" } unregisterBatteryListener(deviceId) @@ -180,7 +180,7 @@ constructor( val isCharging = String(value) == "true" - logDebug { + debugLog { "Charging state metadata changed for device $inputDeviceId " + "${device.address}: $isCharging" } @@ -199,7 +199,7 @@ constructor( handler.post { if (!hasStarted) return@post - logDebug { + debugLog { "Battery state changed for $deviceId. " + "batteryState present: ${batteryState.isPresent}, " + "capacity: ${batteryState.capacity}" @@ -247,7 +247,7 @@ constructor( if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return if (InputSettings.isStylusEverUsed(context)) return - logDebug { "Stylus used for the first time." } + debugLog { "Stylus used for the first time." } InputSettings.setStylusEverUsed(context, true) executeStylusCallbacks { cb -> cb.onStylusFirstUsed() } } @@ -264,7 +264,7 @@ constructor( val hasBtConnection = if (inputDeviceBtSessionIdMap.isEmpty()) 0 else 1 if (batteryStateValid && usiSessionId == null) { - logDebug { "USI battery newly present, entering new USI session: $deviceId" } + debugLog { "USI battery newly present, entering new USI session: $deviceId" } usiSessionId = instanceIdSequence.newInstanceId() uiEventLogger.logWithInstanceIdAndPosition( StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED, @@ -274,7 +274,7 @@ constructor( hasBtConnection, ) } else if (!batteryStateValid && usiSessionId != null) { - logDebug { "USI battery newly absent, exiting USI session: $deviceId" } + debugLog { "USI battery newly absent, exiting USI session: $deviceId" } uiEventLogger.logWithInstanceIdAndPosition( StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED, 0, @@ -291,7 +291,7 @@ constructor( btAddress: String, btConnected: Boolean ) { - logDebug { + debugLog { "Bluetooth stylus ${if (btConnected) "connected" else "disconnected"}:" + " $deviceId $btAddress" } @@ -386,9 +386,3 @@ constructor( val TAG = StylusManager::class.simpleName.orEmpty() } } - -private inline fun logDebug(message: () -> String) { - if (Build.IS_DEBUGGABLE) { - Log.d(StylusManager.TAG, message()) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt index 21b0efadb8d5..6eddd9eb7ad2 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt @@ -26,7 +26,6 @@ import android.content.Intent import android.content.IntentFilter import android.hardware.BatteryState import android.hardware.input.InputManager -import android.os.Build import android.os.Bundle import android.os.Handler import android.os.UserHandle @@ -40,6 +39,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.shared.hardware.hasInputDevice import com.android.systemui.shared.hardware.isAnyStylusSource import com.android.systemui.util.NotificationChannels @@ -110,7 +110,7 @@ constructor( inputDeviceId = deviceId batteryCapacity = batteryState.capacity - logDebug { + debugLog { "Updating notification battery state to $batteryCapacity " + "for InputDevice $deviceId." } @@ -130,14 +130,14 @@ constructor( handler.post updateSuppressed@{ if (suppressed == suppress) return@updateSuppressed - logDebug { "Updating notification suppression to $suppress." } + debugLog { "Updating notification suppression to $suppress." } suppressed = suppress refresh() } } private fun hideNotification() { - logDebug { "Cancelling USI low battery notification." } + debugLog { "Cancelling USI low battery notification." } instanceId = null notificationManager.cancel(USI_NOTIFICATION_ID) } @@ -160,7 +160,7 @@ constructor( .setAutoCancel(true) .build() - logDebug { "Show or update USI low battery notification at $batteryCapacity." } + debugLog { "Show or update USI low battery notification at $batteryCapacity." } logUiEvent(StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_SHOWN) notificationManager.notify(USI_NOTIFICATION_ID, notification) } @@ -188,12 +188,12 @@ constructor( override fun onReceive(context: Context, intent: Intent) { when (intent.action) { ACTION_DISMISSED_LOW_BATTERY -> { - logDebug { "USI low battery notification dismissed." } + debugLog { "USI low battery notification dismissed." } logUiEvent(StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_DISMISSED) updateSuppression(true) } ACTION_CLICKED_LOW_BATTERY -> { - logDebug { "USI low battery notification clicked." } + debugLog { "USI low battery notification clicked." } logUiEvent(StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_CLICKED) updateSuppression(true) if (inputDeviceId == null) return @@ -263,9 +263,3 @@ constructor( @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args" } } - -private inline fun logDebug(message: () -> String) { - if (Build.IS_DEBUGGABLE) { - Log.d(StylusUsiPowerUI.TAG, message()) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/ColorUtil.kt b/packages/SystemUI/src/com/android/systemui/util/ColorUtil.kt index 27a53bf2ceda..41b3145ed36e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ColorUtil.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ColorUtil.kt @@ -19,6 +19,7 @@ package com.android.systemui.util import android.content.res.TypedArray import android.graphics.Color import android.view.ContextThemeWrapper +import androidx.annotation.ColorInt /** Returns an ARGB color version of [color] at the given [alpha]. */ fun getColorWithAlpha(color: Int, alpha: Float): Int = @@ -35,8 +36,11 @@ fun getColorWithAlpha(color: Int, alpha: Float): Int = * otherwise, returns the color from the private attribute {@param privAttrId}. */ fun getPrivateAttrColorIfUnset( - ctw: ContextThemeWrapper, attrArray: TypedArray, - attrIndex: Int, defColor: Int, privAttrId: Int + ctw: ContextThemeWrapper, + attrArray: TypedArray, + attrIndex: Int, + defColor: Int, + privAttrId: Int ): Int { // If the index is specified, use that value var a = attrArray @@ -51,3 +55,8 @@ fun getPrivateAttrColorIfUnset( a.recycle() return color } + +/** Returns the color as a HTML hex color (or null) */ +fun hexColorString(@ColorInt color: Int?): String = color + ?.let { String.format("#%08x", it) } + ?: "null" diff --git a/packages/SystemUI/src/com/android/systemui/util/wrapper/DisplayUtilsWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/DisplayUtilsWrapper.kt new file mode 100644 index 000000000000..cfca7f9da686 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/DisplayUtilsWrapper.kt @@ -0,0 +1,42 @@ +/* + * 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.wrapper + +import android.util.DisplayUtils +import android.view.Display +import javax.inject.Inject + +/** Injectable wrapper around `DisplayUtils` functions */ +class DisplayUtilsWrapper @Inject constructor() { + fun getPhysicalPixelDisplaySizeRatio( + physicalWidth: Int, + physicalHeight: Int, + currentWidth: Int, + currentHeight: Int + ): Float { + return DisplayUtils.getPhysicalPixelDisplaySizeRatio( + physicalWidth, + physicalHeight, + currentWidth, + currentHeight + ) + } + + fun getMaximumResolutionDisplayMode(modes: Array<Display.Mode>?): Display.Mode? { + return DisplayUtils.getMaximumResolutionDisplayMode(modes) + } +} diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index e2b568cfea77..080be6d8cf25 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -196,6 +196,17 @@ android:exported="false" android:permission="com.android.systemui.permission.SELF" android:excludeFromRecents="true" /> + + <activity + android:name="com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity" + android:exported="false" + android:permission="com.android.systemui.permission.SELF" + android:excludeFromRecents="true" > + <intent-filter> + <action android:name="com.android.systemui.action.MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> </application> <instrumentation android:name="android.testing.TestableInstrumentation" diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index f4df26dec89e..8a05a37ad0dd 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -682,8 +682,7 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { } @Test - public void testReinflateViewFlipper_asyncBouncerFlagOn() { - when(mFeatureFlags.isEnabled(Flags.ASYNC_INFLATE_BOUNCER)).thenReturn(true); + public void testReinflateViewFlipper() { KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback = controller -> { }; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 2962c14b813a..ddd9a084bbd2 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -151,7 +151,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import org.mockito.internal.util.reflection.FieldSetter; @@ -2737,6 +2739,36 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verifyFingerprintAuthenticateCall(); } + @Test + public void onTrustChangedCallbacksCalledBeforeOnTrustGrantedForCurrentUserCallback() { + // GIVEN device is interactive + deviceIsInteractive(); + + // GIVEN callback is registered + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN onTrustChanged enabled=true + mKeyguardUpdateMonitor.onTrustChanged( + true /* enabled */, + true /* newlyUnlocked */, + getCurrentUser() /* userId */, + TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD /* flags */, + null /* trustGrantedMessages */); + + // THEN onTrustChanged is called FIRST + final InOrder inOrder = Mockito.inOrder(callback); + inOrder.verify(callback).onTrustChanged(eq(getCurrentUser())); + + // AND THEN onTrustGrantedForCurrentUser callback called + inOrder.verify(callback).onTrustGrantedForCurrentUser( + eq(true) /* dismissKeyguard */, + eq(true) /* newlyUnlocked */, + eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD)), + eq(null) /* message */ + ); + } + private void verifyFingerprintAuthenticateNeverCalled() { verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), any()); verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), anyInt(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt index d7aa6e063d1e..14ad3acf7fb0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt @@ -39,10 +39,6 @@ import org.mockito.Mockito.verify import kotlin.math.ceil -private val PAINT = TextPaint().apply { - textSize = 32f -} - @RunWith(AndroidTestingRunner::class) @SmallTest class TextAnimatorTest : SysuiTestCase() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt index 063757acc1a1..f6fcd16cfd00 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt @@ -49,7 +49,7 @@ private val VF_FONT = Font.Builder(File("/system/fonts/Roboto-Regular.ttf")).bui private fun Font.toTypeface() = Typeface.CustomFallbackBuilder(FontFamily.Builder(this).build()).build() -private val PAINT = TextPaint().apply { +internal val PAINT = TextPaint().apply { typeface = Font.Builder(VF_FONT).setFontVariationSettings("'wght' 400").build().toTypeface() textSize = 32f } @@ -79,7 +79,7 @@ class TextInterpolatorTest : SysuiTestCase() { @Before fun setup() { - typefaceCache = TypefaceVariantCacheImpl() + typefaceCache = TypefaceVariantCacheImpl(PAINT.typeface) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index a20875b5d9de..6e37ee791bd2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -40,14 +40,19 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl +import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope import org.junit.After import org.junit.Ignore import org.junit.Rule @@ -87,13 +92,26 @@ class AuthContainerViewTest : SysuiTestCase() { @Mock lateinit var interactionJankMonitor: InteractionJankMonitor + private val testScope = TestScope(StandardTestDispatcher()) + private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val biometricPromptRepository = FakePromptRepository() + private val rearDisplayStateRepository = FakeRearDisplayStateRepository() private val credentialInteractor = FakeCredentialInteractor() private val bpCredentialInteractor = BiometricPromptCredentialInteractor( Dispatchers.Main.immediate, biometricPromptRepository, credentialInteractor ) + private val displayStateInteractor = DisplayStateInteractorImpl( + testScope.backgroundScope, + mContext, + fakeExecutor, + rearDisplayStateRepository + ) + + private val authBiometricFingerprintViewModel = AuthBiometricFingerprintViewModel( + displayStateInteractor + ) private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) private var authContainer: TestAuthContainerView? = null @@ -469,9 +487,10 @@ class AuthContainerViewTest : SysuiTestCase() { lockPatternUtils, interactionJankMonitor, { bpCredentialInteractor }, + { authBiometricFingerprintViewModel }, { credentialViewModel }, Handler(TestableLooper.get(this).looper), - FakeExecutor(FakeSystemClock()) + fakeExecutor ) { override fun postOnAnimation(runnable: Runnable) { runnable.run() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 4f24b3ab5e09..a326cc7045dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -93,6 +93,7 @@ import com.android.systemui.RoboPilotTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; import com.android.systemui.biometrics.domain.interactor.LogContextInteractor; +import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel; import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.statusbar.CommandQueue; @@ -172,6 +173,8 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor; @Mock + private AuthBiometricFingerprintViewModel mAuthBiometricFingerprintViewModel; + @Mock private CredentialViewModel mCredentialViewModel; @Mock private UdfpsUtils mUdfpsUtils; @@ -995,8 +998,9 @@ public class AuthControllerTest extends SysuiTestCase { () -> mSideFpsController, mDisplayManager, mWakefulnessLifecycle, mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger, mLogContextInteractor, () -> mBiometricPromptCredentialInteractor, - () -> mCredentialViewModel, mInteractionJankMonitor, mHandler, - mBackgroundExecutor, mVibratorHelper, mUdfpsUtils); + () -> mAuthBiometricFingerprintViewModel, () -> mCredentialViewModel, + mInteractionJankMonitor, mHandler, mBackgroundExecutor, mVibratorHelper, + mUdfpsUtils); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt index e6334cf2e4a1..40d9009ee81d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt @@ -55,6 +55,9 @@ import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext +import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository @@ -66,7 +69,9 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.TestScope import org.junit.Before import org.junit.Rule import org.junit.Test @@ -90,6 +95,8 @@ import org.mockito.junit.MockitoJUnit private const val DISPLAY_ID = 2 private const val SENSOR_ID = 1 +private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3 + @SmallTest @RoboPilotTest @RunWith(AndroidJUnit4::class) @@ -112,7 +119,12 @@ class SideFpsControllerTest : SysuiTestCase() { private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor + private lateinit var displayStateInteractor: DisplayStateInteractor + private val executor = FakeExecutor(FakeSystemClock()) + private val rearDisplayStateRepository = FakeRearDisplayStateRepository() + private val testScope = TestScope(StandardTestDispatcher()) + private lateinit var overlayController: ISidefpsController private lateinit var sideFpsController: SideFpsController @@ -142,6 +154,13 @@ class SideFpsControllerTest : SysuiTestCase() { FakeDeviceEntryFingerprintAuthRepository(), FakeSystemClock(), ) + displayStateInteractor = + DisplayStateInteractorImpl( + testScope.backgroundScope, + context, + executor, + rearDisplayStateRepository + ) context.addMockSystemService(DisplayManager::class.java, displayManager) context.addMockSystemService(WindowManager::class.java, windowManager) @@ -168,6 +187,7 @@ class SideFpsControllerTest : SysuiTestCase() { isReverseDefaultRotation: Boolean = false, initInfo: DisplayInfo.() -> Unit = {}, windowInsets: WindowInsets = insetsForSmallNavbar(), + inRearDisplayMode: Boolean = false, block: () -> Unit ) { this.deviceConfig = deviceConfig @@ -228,6 +248,13 @@ class SideFpsControllerTest : SysuiTestCase() { isReverseDefaultRotation ) + val rearDisplayDeviceStates = + if (inRearDisplayMode) intArrayOf(REAR_DISPLAY_MODE_DEVICE_STATE) else intArrayOf() + sideFpsControllerContext.orCreateTestableResources.addOverride( + com.android.internal.R.array.config_rearDisplayDeviceStates, + rearDisplayDeviceStates + ) + sideFpsController = SideFpsController( sideFpsControllerContext, @@ -237,12 +264,14 @@ class SideFpsControllerTest : SysuiTestCase() { activityTaskManager, overviewProxyService, displayManager, + displayStateInteractor, executor, handler, alternateBouncerInteractor, TestCoroutineScope(), - dumpManager, + dumpManager ) + rearDisplayStateRepository.setIsInRearDisplayMode(inRearDisplayMode) overlayController = ArgumentCaptor.forClass(ISidefpsController::class.java) @@ -584,10 +613,62 @@ class SideFpsControllerTest : SysuiTestCase() { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) } + @Test + fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_0() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 }, + inRearDisplayMode = true, + ) { + verifySfpsIndicator_notAdded_InRearDisplayMode() + } + + @Test + fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_90() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_90 }, + inRearDisplayMode = true, + ) { + verifySfpsIndicator_notAdded_InRearDisplayMode() + } + + @Test + fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_180() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_180 }, + inRearDisplayMode = true, + ) { + verifySfpsIndicator_notAdded_InRearDisplayMode() + } + + @Test + fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_270() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_270 }, + inRearDisplayMode = true, + ) { + verifySfpsIndicator_notAdded_InRearDisplayMode() + } + private fun verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible: Boolean) { sideFpsController.overlayOffsets = sensorLocation } + private fun verifySfpsIndicator_notAdded_InRearDisplayMode() { + sideFpsController.overlayOffsets = sensorLocation + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager, never()).addView(any(), any()) + } + fun alternateBouncerVisibility_showAndHideSideFpsUI() = testWithDisplay { // WHEN alternate bouncer is visible keyguardBouncerRepository.setAlternateVisible(true) @@ -624,7 +705,7 @@ class SideFpsControllerTest : SysuiTestCase() { * in other rotations have been omitted. */ @Test - fun verifiesIndicatorPlacementForXAlignedSensor_0() { + fun verifiesIndicatorPlacementForXAlignedSensor_0() = testWithDisplay( deviceConfig = DeviceConfig.X_ALIGNED, isReverseDefaultRotation = false, @@ -641,7 +722,6 @@ class SideFpsControllerTest : SysuiTestCase() { assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) } - } /** * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270 @@ -650,7 +730,7 @@ class SideFpsControllerTest : SysuiTestCase() { * correctly, tests for indicator placement in other rotations have been omitted. */ @Test - fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() { + fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() = testWithDisplay( deviceConfig = DeviceConfig.X_ALIGNED, isReverseDefaultRotation = true, @@ -667,7 +747,6 @@ class SideFpsControllerTest : SysuiTestCase() { assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) } - } /** * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 2747e83acb23..8a62ea0d669e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -25,7 +25,6 @@ import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROL import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater import android.view.MotionEvent @@ -35,6 +34,7 @@ import android.view.Surface.Rotation import android.view.View import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor import com.android.settingslib.udfps.UdfpsOverlayParams @@ -69,8 +69,8 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` as whenever private const val REQUEST_ID = 2L @@ -340,4 +340,22 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { assertThat(lp.height).isEqualTo(overlayParams.sensorBounds.height()) } } + + @Test + fun fullScreenOverlayWithNewTouchDetectionEnabled() = withRotation(ROTATION_0) { + withReason(REASON_AUTH_KEYGUARD) { + whenever(featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(true) + + controllerOverlay.show(udfpsController, overlayParams) + verify(windowManager).addView( + eq(controllerOverlay.overlayView), + layoutParamsCaptor.capture() + ) + + // Layout params should use natural display width and height + val lp = layoutParamsCaptor.value + assertThat(lp.width).isEqualTo(overlayParams.naturalDisplayWidth) + assertThat(lp.height).isEqualTo(overlayParams.naturalDisplayHeight) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/RearDisplayStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/RearDisplayStateRepositoryTest.kt new file mode 100644 index 000000000000..dfe8d36504d0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/RearDisplayStateRepositoryTest.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.keyguard.data.repository + +import android.hardware.devicestate.DeviceStateManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.RearDisplayStateRepository +import com.android.systemui.biometrics.data.repository.RearDisplayStateRepositoryImpl +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +private const val NORMAL_DISPLAY_MODE_DEVICE_STATE = 2 +private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class RearDisplayStateRepositoryTest : SysuiTestCase() { + @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() + @Mock private lateinit var deviceStateManager: DeviceStateManager + private lateinit var underTest: RearDisplayStateRepository + + private val testScope = TestScope(StandardTestDispatcher()) + private val fakeExecutor = FakeExecutor(FakeSystemClock()) + + @Captor + private lateinit var callbackCaptor: ArgumentCaptor<DeviceStateManager.DeviceStateCallback> + + @Before + fun setUp() { + val rearDisplayDeviceStates = intArrayOf(REAR_DISPLAY_MODE_DEVICE_STATE) + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.array.config_rearDisplayDeviceStates, + rearDisplayDeviceStates + ) + + underTest = + RearDisplayStateRepositoryImpl( + testScope.backgroundScope, + mContext, + deviceStateManager, + fakeExecutor + ) + } + + @Test + fun updatesIsInRearDisplayMode_whenRearDisplayStateChanges() = + testScope.runTest { + val isInRearDisplayMode = collectLastValue(underTest.isInRearDisplayMode) + runCurrent() + + val callback = deviceStateManager.captureCallback() + + callback.onStateChanged(NORMAL_DISPLAY_MODE_DEVICE_STATE) + assertThat(isInRearDisplayMode()).isFalse() + + callback.onStateChanged(REAR_DISPLAY_MODE_DEVICE_STATE) + assertThat(isInRearDisplayMode()).isTrue() + } +} + +private fun DeviceStateManager.captureCallback() = + withArgCaptor<DeviceStateManager.DeviceStateCallback> { + verify(this@captureCallback).registerCallback(any(), capture()) + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorImplTest.kt new file mode 100644 index 000000000000..2217c5c677d5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorImplTest.kt @@ -0,0 +1,84 @@ +package com.android.systemui.biometrics.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.unfold.compat.ScreenSizeFoldProvider +import com.android.systemui.unfold.updates.FoldProvider +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class DisplayStateInteractorImplTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + private val fakeExecutor = FakeExecutor(FakeSystemClock()) + private val testScope = TestScope(StandardTestDispatcher()) + private val rearDisplayStateRepository = FakeRearDisplayStateRepository() + + @Mock private lateinit var screenSizeFoldProvider: ScreenSizeFoldProvider + private lateinit var interactor: DisplayStateInteractorImpl + + @Before + fun setup() { + interactor = + DisplayStateInteractorImpl( + testScope.backgroundScope, + mContext, + fakeExecutor, + rearDisplayStateRepository + ) + interactor.setScreenSizeFoldProvider(screenSizeFoldProvider) + } + + @Test + fun isInRearDisplayModeChanges() = + testScope.runTest { + val isInRearDisplayMode = collectLastValue(interactor.isInRearDisplayMode) + + rearDisplayStateRepository.setIsInRearDisplayMode(false) + assertThat(isInRearDisplayMode()).isFalse() + + rearDisplayStateRepository.setIsInRearDisplayMode(true) + assertThat(isInRearDisplayMode()).isTrue() + } + + @Test + fun isFoldedChanges() = + testScope.runTest { + val isFolded = collectLastValue(interactor.isFolded) + runCurrent() + val callback = screenSizeFoldProvider.captureCallback() + + callback.onFoldUpdated(isFolded = true) + assertThat(isFolded()).isTrue() + + callback.onFoldUpdated(isFolded = false) + assertThat(isFolded()).isFalse() + } +} + +private fun FoldProvider.captureCallback() = + withArgCaptor<FoldProvider.FoldCallback> { + verify(this@captureCallback).registerCallback(capture(), any()) + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt new file mode 100644 index 000000000000..0c210e51cb90 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt @@ -0,0 +1,69 @@ +package com.android.systemui.biometrics.ui.viewmodel + +import android.content.res.Configuration +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class AuthBiometricFingerprintViewModelTest : SysuiTestCase() { + + private val rearDisplayStateRepository = FakeRearDisplayStateRepository() + private val testScope = TestScope(StandardTestDispatcher()) + private val fakeExecutor = FakeExecutor(FakeSystemClock()) + + private lateinit var interactor: DisplayStateInteractor + private lateinit var viewModel: AuthBiometricFingerprintViewModel + + @Before + fun setup() { + interactor = + DisplayStateInteractorImpl( + testScope.backgroundScope, + mContext, + fakeExecutor, + rearDisplayStateRepository + ) + viewModel = AuthBiometricFingerprintViewModel(interactor) + } + + @Test + fun iconUpdates_onConfigurationChanged() { + testScope.runTest { + runCurrent() + val testConfig = Configuration() + val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1 + val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1 + val currentIcon = collectLastValue(viewModel.iconAsset) + + testConfig.smallestScreenWidthDp = folded + viewModel.onConfigurationChanged(testConfig) + val foldedIcon = currentIcon() + + testConfig.smallestScreenWidthDp = unfolded + viewModel.onConfigurationChanged(testConfig) + val unfoldedIcon = currentIcon() + + assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) + } + } +} + +internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600 diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt new file mode 100644 index 000000000000..21516d4917b5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt @@ -0,0 +1,83 @@ +/* + * 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.clipboardoverlay + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.whenever +import java.io.IOException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class ClipboardImageLoaderTest : SysuiTestCase() { + @Mock private lateinit var mockContext: Context + + @Mock private lateinit var mockContentResolver: ContentResolver + + private lateinit var clipboardImageLoader: ClipboardImageLoader + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + @Throws(IOException::class) + fun test_imageLoadSuccess() = runTest { + val testDispatcher = StandardTestDispatcher(this.testScheduler) + clipboardImageLoader = + ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher)) + val testUri = Uri.parse("testUri") + whenever(mockContext.contentResolver).thenReturn(mockContentResolver) + whenever(mockContext.resources).thenReturn(context.resources) + + clipboardImageLoader.load(testUri) + + verify(mockContentResolver).loadThumbnail(eq(testUri), any(), any()) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + @Throws(IOException::class) + fun test_imageLoadFailure() = runTest { + val testDispatcher = StandardTestDispatcher(this.testScheduler) + clipboardImageLoader = + ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher)) + val testUri = Uri.parse("testUri") + whenever(mockContext.contentResolver).thenReturn(mockContentResolver) + whenever(mockContext.resources).thenReturn(context.resources) + + val res = clipboardImageLoader.load(testUri) + + verify(mockContentResolver).loadThumbnail(eq(testUri), any(), any()) + assertNull(res) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index fe5fa1fdd39f..39fb7b4cda2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -25,6 +25,7 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_EXPANDED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; +import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -90,6 +91,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Mock private ClipboardOverlayUtils mClipboardUtils; @Mock + private ClipboardImageLoader mClipboardImageLoader; + @Mock private UiEventLogger mUiEventLogger; private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @@ -120,6 +123,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, new ClipData.Item("Test Item")); + mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, true); // turned off for legacy tests mOverlayController = new ClipboardOverlayController( mContext, @@ -131,6 +135,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mFeatureFlags, mClipboardUtils, mExecutor, + mClipboardImageLoader, mUiEventLogger); verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture()); mCallbacks = mOverlayCallbacksCaptor.getValue(); @@ -142,6 +147,69 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { } @Test + public void test_setClipData_invalidImageData_legacy() { + ClipData clipData = new ClipData("", new String[]{"image/png"}, + new ClipData.Item(Uri.parse(""))); + mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); + + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_nonImageUri_legacy() { + ClipData clipData = new ClipData("", new String[]{"resource/png"}, + new ClipData.Item(Uri.parse(""))); + mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); + + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_textData_legacy() { + mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); + mOverlayController.setClipData(mSampleClipData, "abc"); + + verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false); + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "abc"); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_sensitiveTextData_legacy() { + mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); + ClipDescription description = mSampleClipData.getDescription(); + PersistableBundle b = new PersistableBundle(); + b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true); + description.setExtras(b); + ClipData data = new ClipData(description, mSampleClipData.getItemAt(0)); + mOverlayController.setClipData(data, ""); + + verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_repeatedCalls_legacy() { + when(mAnimator.isRunning()).thenReturn(true); + mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); + + mOverlayController.setClipData(mSampleClipData, ""); + mOverlayController.setClipData(mSampleClipData, ""); + + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test public void test_setClipData_invalidImageData() { ClipData clipData = new ClipData("", new String[]{"image/png"}, new ClipData.Item(Uri.parse(""))); diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt new file mode 100644 index 000000000000..a308c8ee38ca --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt @@ -0,0 +1,196 @@ +/* + * 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.common.ui.data.repository + +import android.content.res.Configuration +import android.view.Display +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.wrapper.DisplayUtilsWrapper +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.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class ConfigurationRepositoryImplTest : SysuiTestCase() { + private var displaySizeRatio = 0f + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var displayUtils: DisplayUtilsWrapper + + private lateinit var testScope: TestScope + private lateinit var underTest: ConfigurationRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + setPhysicalPixelDisplaySizeRatio(displaySizeRatio) + + testScope = TestScope() + underTest = + ConfigurationRepositoryImpl( + configurationController, + context, + testScope.backgroundScope, + displayUtils, + ) + } + + @Test + fun onAnyConfigurationChange_updatesOnUiModeChanged() = + testScope.runTest { + val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange) + assertThat(lastAnyConfigurationChange).isNull() + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + configurationCallback.onUiModeChanged() + runCurrent() + assertThat(lastAnyConfigurationChange).isNotNull() + } + + @Test + fun onAnyConfigurationChange_updatesOnThemeChanged() = + testScope.runTest { + val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange) + assertThat(lastAnyConfigurationChange).isNull() + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + configurationCallback.onThemeChanged() + runCurrent() + assertThat(lastAnyConfigurationChange).isNotNull() + } + + @Test + fun onAnyConfigurationChange_updatesOnConfigChanged() = + testScope.runTest { + val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange) + assertThat(lastAnyConfigurationChange).isNull() + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + runCurrent() + assertThat(lastAnyConfigurationChange).isNotNull() + } + + @Test + fun onResolutionScale_updatesOnConfigurationChange() = + testScope.runTest { + val scaleForResolution by collectLastValue(underTest.scaleForResolution) + assertThat(scaleForResolution).isEqualTo(displaySizeRatio) + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + setPhysicalPixelDisplaySizeRatio(2f) + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + assertThat(scaleForResolution).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(.21f) + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + assertThat(scaleForResolution).isEqualTo(displaySizeRatio) + } + + @Test + fun onResolutionScale_nullMaxResolution() = + testScope.runTest { + val scaleForResolution by collectLastValue(underTest.scaleForResolution) + runCurrent() + + givenNullMaxResolutionDisplayMode() + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + configurationCallback.onConfigChanged(mock(Configuration::class.java)) + assertThat(scaleForResolution).isEqualTo(1f) + } + + @Test + fun getResolutionScale_nullMaxResolutionDisplayMode() { + givenNullMaxResolutionDisplayMode() + assertThat(underTest.getResolutionScale()).isEqualTo(1f) + } + + @Test + fun getResolutionScale_infiniteDisplayRatios() { + setPhysicalPixelDisplaySizeRatio(Float.POSITIVE_INFINITY) + assertThat(underTest.getResolutionScale()).isEqualTo(1f) + } + + @Test + fun getResolutionScale_differentDisplayRatios() { + setPhysicalPixelDisplaySizeRatio(.5f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(.283f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(3.58f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(0f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + + setPhysicalPixelDisplaySizeRatio(1f) + assertThat(underTest.getResolutionScale()).isEqualTo(displaySizeRatio) + } + + private fun givenNullMaxResolutionDisplayMode() { + whenever(displayUtils.getMaximumResolutionDisplayMode(any())).thenReturn(null) + } + + private fun setPhysicalPixelDisplaySizeRatio(ratio: Float) { + displaySizeRatio = ratio + whenever(displayUtils.getMaximumResolutionDisplayMode(any())) + .thenReturn(Display.Mode(0, 0, 0, 90f)) + whenever( + displayUtils.getPhysicalPixelDisplaySizeRatio( + anyInt(), + anyInt(), + anyInt(), + anyInt() + ) + ) + .thenReturn(ratio) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt new file mode 100644 index 000000000000..b2a1668df7aa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt @@ -0,0 +1,44 @@ +/* + * 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.common.ui.data.repository + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow + +class FakeConfigurationRepository : ConfigurationRepository { + private val onAnyConfigurationChangeChannel = Channel<Unit>() + override val onAnyConfigurationChange: Flow<Unit> = + onAnyConfigurationChangeChannel.receiveAsFlow() + + private val _scaleForResolution = MutableStateFlow(1f) + override val scaleForResolution: Flow<Float> = _scaleForResolution.asStateFlow() + + suspend fun onAnyConfigurationChange() { + onAnyConfigurationChangeChannel.send(Unit) + } + + fun setScaleForResolution(scale: Float) { + _scaleForResolution.value = scale + } + + override fun getResolutionScale(): Float { + return _scaleForResolution.value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt index d22c409d8590..02555cfa783a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt @@ -60,16 +60,14 @@ class DumpManagerTest : SysuiTestCase() { // WHEN a dumpable is dumped explicitly val args = arrayOf<String>() - dumpManager.dumpTarget("dumpable2", pw, arrayOf(), tailLength = 0) + dumpManager.dumpTarget("dumpable2", pw, args, tailLength = 0) // THEN only the requested one has their dump() method called - verify(dumpable1, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) + verify(dumpable1, never()).dump(any(), any()) verify(dumpable2).dump(pw, args) - verify(dumpable3, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt()) - verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) } @Test @@ -82,17 +80,15 @@ class DumpManagerTest : SysuiTestCase() { dumpManager.registerBuffer("buffer2", buffer2) // WHEN a buffer is dumped explicitly - dumpManager.dumpTarget("buffer1", pw, arrayOf(), tailLength = 14) + val args = arrayOf<String>() + dumpManager.dumpTarget("buffer1", pw, args, tailLength = 14) // THEN only the requested one has their dump() method called - verify(dumpable1, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(dumpable2, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(dumpable2, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) verify(buffer1).dump(pw, tailLength = 14) - verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) } @Test @@ -109,6 +105,122 @@ class DumpManagerTest : SysuiTestCase() { } @Test + fun testDumpTarget_selectsShortestNamedDumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("first-dumpable", dumpable1) + dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2) + dumpManager.registerCriticalDumpable("third-dumpable", dumpable3) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf<String>() + dumpManager.dumpTarget("dumpable", pw, args, tailLength = 0) + + // THEN the matching dumpable with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2).dump(pw, args) + verify(dumpable3, never()).dump(any(), any()) + } + + @Test + fun testDumpTarget_selectsShortestNamedBuffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerBuffer("first-buffer", buffer1) + dumpManager.registerBuffer("scnd-buffer", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf<String>() + dumpManager.dumpTarget("buffer", pw, args, tailLength = 14) + + // THEN the matching buffer with the shorter name is dumped + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2).dump(pw, tailLength = 14) + } + + @Test + fun testDumpTarget_selectsShortestNamedMatch_dumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("big-buffer1", buffer1) + dumpManager.registerBuffer("big-buffer2", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf<String>() + dumpManager.dumpTarget("2", pw, args, tailLength = 14) + + // THEN the matching dumpable with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2).dump(pw, args) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + } + + @Test + fun testDumpTarget_selectsShortestNamedMatch_buffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("dumpable1", dumpable1) + dumpManager.registerCriticalDumpable("dumpable2", dumpable2) + dumpManager.registerCriticalDumpable("dumpable3", dumpable3) + dumpManager.registerBuffer("buffer1", buffer1) + dumpManager.registerBuffer("buffer2", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf<String>() + dumpManager.dumpTarget("2", pw, args, tailLength = 14) + + // THEN the matching buffer with the shorter name is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2).dump(pw, tailLength = 14) + } + + @Test + fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("d1x", dumpable1) + dumpManager.registerCriticalDumpable("d2x", dumpable2) + dumpManager.registerCriticalDumpable("a3x", dumpable3) + dumpManager.registerBuffer("ab1x", buffer1) + dumpManager.registerBuffer("b2x", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf<String>() + dumpManager.dumpTarget("x", pw, args, tailLength = 14) + + // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3).dump(pw, args) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) + } + + @Test + fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() { + // GIVEN a variety of registered dumpables and buffers + dumpManager.registerCriticalDumpable("d1x", dumpable1) + dumpManager.registerCriticalDumpable("d2x", dumpable2) + dumpManager.registerCriticalDumpable("az1x", dumpable3) + dumpManager.registerBuffer("b1x", buffer1) + dumpManager.registerBuffer("b2x", buffer2) + + // WHEN a dumpable is dumped by a suffix that matches multiple options + val args = arrayOf<String>() + dumpManager.dumpTarget("x", pw, args, tailLength = 14) + + // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1).dump(pw, tailLength = 14) + verify(buffer2, never()).dump(any(), anyInt()) + } + + @Test fun testDumpDumpables() { // GIVEN a variety of registered dumpables and buffers dumpManager.registerCriticalDumpable("dumpable1", dumpable1) @@ -125,8 +237,8 @@ class DumpManagerTest : SysuiTestCase() { verify(dumpable1).dump(pw, args) verify(dumpable2).dump(pw, args) verify(dumpable3).dump(pw, args) - verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt()) - verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) } @Test @@ -142,12 +254,9 @@ class DumpManagerTest : SysuiTestCase() { dumpManager.dumpBuffers(pw, tailLength = 1) // THEN all buffers are dumped (and no dumpables) - verify(dumpable1, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(dumpable2, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(dumpable3, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) verify(buffer1).dump(pw, tailLength = 1) verify(buffer2).dump(pw, tailLength = 1) } @@ -168,10 +277,9 @@ class DumpManagerTest : SysuiTestCase() { // THEN only critical modules are dumped (and no buffers) verify(dumpable1).dump(pw, args) verify(dumpable2).dump(pw, args) - verify(dumpable3, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt()) - verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt()) + verify(dumpable3, never()).dump(any(), any()) + verify(buffer1, never()).dump(any(), anyInt()) + verify(buffer2, never()).dump(any(), anyInt()) } @Test @@ -188,10 +296,8 @@ class DumpManagerTest : SysuiTestCase() { dumpManager.dumpNormal(pw, args, tailLength = 2) // THEN the normal module and all buffers are dumped - verify(dumpable1, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(dumpable2, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) + verify(dumpable1, never()).dump(any(), any()) + verify(dumpable2, never()).dump(any(), any()) verify(dumpable3).dump(pw, args) verify(buffer1).dump(pw, tailLength = 2) verify(buffer2).dump(pw, tailLength = 2) @@ -213,9 +319,7 @@ class DumpManagerTest : SysuiTestCase() { // THEN the unregistered dumpables (both normal and critical) are not dumped verify(dumpable1).dump(pw, args) - verify(dumpable2, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) - verify(dumpable3, never()) - .dump(any(PrintWriter::class.java), any(Array<String>::class.java)) + verify(dumpable2, never()).dump(any(), any()) + verify(dumpable3, never()).dump(any(), any()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 1d0b58a8e0f4..d73c2c76272e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -54,6 +54,7 @@ import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.shared.model.WakeSleepReason import com.android.systemui.keyguard.shared.model.WakefulnessModel @@ -234,6 +235,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { faceDetectBuffer, faceAuthBuffer, keyguardTransitionInteractor, + featureFlags, dumpManager, ) } @@ -612,6 +614,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { authStatus() detectStatus() authRunning() + bypassEnabled() lockedOut() canFaceAuthRun() authenticated() @@ -847,7 +850,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromDozing() = testScope.runTest { keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.DOZING, to = KeyguardState.GONE) + TransitionStep( + from = KeyguardState.DOZING, + to = KeyguardState.GONE, + transitionState = TransitionState.FINISHED + ) ) runCurrent() @@ -858,7 +865,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromAod() = testScope.runTest { keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE) + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.GONE, + transitionState = TransitionState.FINISHED + ) ) runCurrent() @@ -869,7 +880,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromLockscreen() = testScope.runTest { keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.FINISHED + ) ) runCurrent() @@ -880,7 +895,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromBouncer() = testScope.runTest { keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.GONE) + TransitionStep( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + transitionState = TransitionState.FINISHED + ) ) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt new file mode 100644 index 000000000000..069a4862ccdc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt @@ -0,0 +1,137 @@ +/* + * 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.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class BurnInInteractorTest : SysuiTestCase() { + private val burnInOffset = 7 + private var burnInProgress = 0f + + @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper + + private lateinit var configurationRepository: FakeConfigurationRepository + private lateinit var systemClock: FakeSystemClock + private lateinit var testScope: TestScope + private lateinit var underTest: BurnInInteractor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + configurationRepository = FakeConfigurationRepository() + systemClock = FakeSystemClock() + + whenever(burnInHelperWrapper.burnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset) + setBurnInProgress(.65f) + + testScope = TestScope() + underTest = + BurnInInteractor( + context, + burnInHelperWrapper, + testScope.backgroundScope, + configurationRepository, + systemClock, + ) + } + + @Test + fun dozeTimeTick_updatesOnDozeTimeTick() = + testScope.runTest { + // Initial state set to 0 + val lastDozeTimeTick by collectLastValue(underTest.dozeTimeTick) + assertEquals(0L, lastDozeTimeTick) + + // WHEN dozeTimeTick updated + incrementUptimeMillis() + underTest.dozeTimeTick() + + // THEN listeners were updated to the latest uptime millis + assertThat(systemClock.uptimeMillis()).isEqualTo(lastDozeTimeTick) + } + + @Test + fun udfpsBurnInOffset_updatesOnResolutionScaleChange() = + testScope.runTest { + val udfpsBurnInOffsetX by collectLastValue(underTest.udfpsBurnInXOffset) + val udfpsBurnInOffsetY by collectLastValue(underTest.udfpsBurnInYOffset) + assertThat(udfpsBurnInOffsetX).isEqualTo(burnInOffset) + assertThat(udfpsBurnInOffsetY).isEqualTo(burnInOffset) + + configurationRepository.setScaleForResolution(3f) + assertThat(udfpsBurnInOffsetX).isEqualTo(burnInOffset * 3) + assertThat(udfpsBurnInOffsetY).isEqualTo(burnInOffset * 3) + + configurationRepository.setScaleForResolution(.5f) + assertThat(udfpsBurnInOffsetX).isEqualTo(burnInOffset / 2) + assertThat(udfpsBurnInOffsetY).isEqualTo(burnInOffset / 2) + } + + @Test + fun udfpsBurnInProgress_updatesOnDozeTimeTick() = + testScope.runTest { + val udfpsBurnInProgress by collectLastValue(underTest.udfpsBurnInProgress) + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + + setBurnInProgress(.88f) + incrementUptimeMillis() + underTest.dozeTimeTick() + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + + setBurnInProgress(.92f) + incrementUptimeMillis() + underTest.dozeTimeTick() + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + + setBurnInProgress(.32f) + incrementUptimeMillis() + underTest.dozeTimeTick() + assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress) + } + + private fun incrementUptimeMillis() { + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 5) + } + + private fun setBurnInProgress(progress: Float) { + burnInProgress = progress + whenever(burnInHelperWrapper.burnInProgressOffset()).thenReturn(burnInProgress) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt index a003e1d9d1f3..43e430fd7fa2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt @@ -19,7 +19,9 @@ package com.android.systemui.log.table import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableChange.Companion.IS_INITIAL_PREFIX +import com.android.systemui.log.table.TableChange.Companion.MAX_STRING_LENGTH import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertTrue import org.junit.Test @SmallTest @@ -326,4 +328,152 @@ class TableChangeTest : SysuiTestCase() { assertThat(underTest.getVal()).doesNotContain(IS_INITIAL_PREFIX) } + + @Test + fun constructor_columnAndValueTooLong_truncated() { + val underTest = + TableChange( + columnPrefix = "P".repeat(MAX_STRING_LENGTH + 10), + columnName = "N".repeat(MAX_STRING_LENGTH + 10), + type = TableChange.DataType.STRING, + str = "V".repeat(MAX_STRING_LENGTH + 10), + ) + + assertThat(underTest.getName()).contains("P".repeat(MAX_STRING_LENGTH)) + assertThat(underTest.getName()).doesNotContain("P".repeat(MAX_STRING_LENGTH + 1)) + assertThat(underTest.getName()).contains("N".repeat(MAX_STRING_LENGTH)) + assertThat(underTest.getName()).doesNotContain("N".repeat(MAX_STRING_LENGTH + 1)) + assertThat(underTest.getVal()).isEqualTo("V".repeat(MAX_STRING_LENGTH)) + } + + @Test + fun constructor_columnNameNotTooLong_noReallocation() { + val inputColumnName = "fakeName" + val inputValue = "fakeValue" + val underTest = + TableChange( + columnPrefix = "", + columnName = inputColumnName, + type = TableChange.DataType.STRING, + str = inputValue, + ) + + // Use referential equality to verify we didn't reallocate a new string when the string is + // *not* too long. + assertTrue(underTest.getColumnName() === inputColumnName) + } + + @Test + fun reset_columnPrefixTooLong_truncated() { + val underTest = TableChange() + + underTest.reset( + timestamp = 1L, + columnPrefix = "P".repeat(MAX_STRING_LENGTH + 10), + columnName = "name", + isInitial = false, + ) + + assertThat(underTest.getName()).contains("P".repeat(MAX_STRING_LENGTH)) + assertThat(underTest.getName()).doesNotContain("P".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun reset_columnNameTooLong_truncated() { + val underTest = TableChange() + + underTest.reset( + timestamp = 1L, + columnPrefix = "prefix", + columnName = "N".repeat(MAX_STRING_LENGTH + 10), + isInitial = false, + ) + + assertThat(underTest.getName()).contains("N".repeat(MAX_STRING_LENGTH)) + assertThat(underTest.getName()).doesNotContain("N".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun reset_columnNameNotTooLong_noReallocation() { + val underTest = TableChange() + val shortColumnName = "shortColumnName" + + underTest.reset( + timestamp = 1L, + columnPrefix = "prefix", + columnName = shortColumnName, + isInitial = false, + ) + + // Use referential equality to verify we didn't reallocate a new string when the string is + // *not* too long. + assertTrue(underTest.getColumnName() === shortColumnName) + } + + @Test + fun setString_valueTooLong_truncated() { + val underTest = TableChange() + + underTest.set("V".repeat(MAX_STRING_LENGTH + 1)) + + assertThat(underTest.getVal()).isEqualTo("V".repeat(MAX_STRING_LENGTH)) + } + + @Test + fun updateTo_newColumnPrefixTooLong_truncated() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set(42) + + val new = + TableChange( + columnPrefix = "P".repeat(MAX_STRING_LENGTH + 10), + columnName = "name", + ) + underTest.updateTo(new) + + assertThat(underTest.getName()).contains("P".repeat(MAX_STRING_LENGTH)) + assertThat(underTest.getName()).doesNotContain("P".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun updateTo_newColumnNameTooLong_truncated() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set(42) + + val new = + TableChange( + columnPrefix = "prefix", + columnName = "N".repeat(MAX_STRING_LENGTH + 10), + ) + underTest.updateTo(new) + + assertThat(underTest.getName()).contains("N".repeat(MAX_STRING_LENGTH)) + assertThat(underTest.getName()).doesNotContain("N".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun updateTo_columnNameNotTooLong_noReallocation() { + val underTest = TableChange() + val shortColumnName = "shortColumnName" + val new = TableChange(columnPrefix = "prefix", columnName = shortColumnName) + + underTest.updateTo(new) + + // Use referential equality to verify we didn't reallocate a new string when the string is + // *not* too long. + assertTrue(underTest.getColumnName() === shortColumnName) + } + + @Test + fun updateTo_newValTooLong_truncated() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set("value") + + val new = TableChange() + new.set("V".repeat(MAX_STRING_LENGTH + 10)) + + underTest.updateTo(new) + + assertThat(underTest.getVal()).isEqualTo("V".repeat(MAX_STRING_LENGTH)) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt index df8a71826401..12f46898ab8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt @@ -21,6 +21,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.log.LogLevel import com.android.systemui.log.LogcatEchoTracker import com.android.systemui.log.table.TableChange.Companion.IS_INITIAL_PREFIX +import com.android.systemui.log.table.TableChange.Companion.MAX_STRING_LENGTH import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock @@ -576,6 +577,112 @@ class TableLogBufferTest : SysuiTestCase() { } @Test + fun dumpChanges_tooLongColumnPrefix_viaLogChange_truncated() { + underTest.logChange( + prefix = "P".repeat(MAX_STRING_LENGTH + 10), + columnName = "name", + value = true, + ) + + val dumpedString = dumpChanges() + + assertThat(dumpedString).contains("P".repeat(MAX_STRING_LENGTH)) + assertThat(dumpedString).doesNotContain("P".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun dumpChanges_tooLongColumnPrefix_viaLogDiffs_truncated() { + val prevDiffable = object : TestDiffable() {} + val nextDiffable = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + row.logChange("status", "value") + } + } + + // WHEN the column prefix is too large + underTest.logDiffs( + columnPrefix = "P".repeat(MAX_STRING_LENGTH + 10), + prevDiffable, + nextDiffable, + ) + + val dumpedString = dumpChanges() + + // THEN it's truncated to the max length + assertThat(dumpedString).contains("P".repeat(MAX_STRING_LENGTH)) + assertThat(dumpedString).doesNotContain("P".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun dumpChanges_tooLongColumnName_viaLogChange_truncated() { + underTest.logChange( + prefix = "prefix", + columnName = "N".repeat(MAX_STRING_LENGTH + 10), + value = 10, + ) + + val dumpedString = dumpChanges() + + assertThat(dumpedString).contains("N".repeat(MAX_STRING_LENGTH)) + assertThat(dumpedString).doesNotContain("N".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun dumpChanges_tooLongColumnName_viaLogDiffs_truncated() { + val prevDiffable = object : TestDiffable() {} + val nextDiffable = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + // WHEN the column name is too large + row.logChange(columnName = "N".repeat(MAX_STRING_LENGTH + 10), "value") + } + } + + underTest.logDiffs(columnPrefix = "prefix", prevDiffable, nextDiffable) + + val dumpedString = dumpChanges() + + // THEN it's truncated to the max length + assertThat(dumpedString).contains("N".repeat(MAX_STRING_LENGTH)) + assertThat(dumpedString).doesNotContain("N".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun dumpChanges_tooLongValue_viaLogChange_truncated() { + underTest.logChange( + prefix = "prefix", + columnName = "name", + value = "V".repeat(MAX_STRING_LENGTH + 10), + ) + + val dumpedString = dumpChanges() + + assertThat(dumpedString).contains("V".repeat(MAX_STRING_LENGTH)) + assertThat(dumpedString).doesNotContain("V".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test + fun dumpChanges_tooLongValue_viaLogDiffs_truncated() { + val prevDiffable = object : TestDiffable() {} + val nextDiffable = + object : TestDiffable() { + override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) { + // WHEN the value is too large + row.logChange("columnName", value = "V".repeat(MAX_STRING_LENGTH + 10)) + } + } + + underTest.logDiffs(columnPrefix = "prefix", prevDiffable, nextDiffable) + + val dumpedString = dumpChanges() + + // THEN it's truncated to the max length + assertThat(dumpedString).contains("V".repeat(MAX_STRING_LENGTH)) + assertThat(dumpedString).doesNotContain("V".repeat(MAX_STRING_LENGTH + 1)) + } + + @Test fun dumpChanges_rotatesIfBufferIsFull() { lateinit var valToDump: String diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index 0a1db60e075c..d428db7b9dda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -40,7 +40,6 @@ import android.testing.TestableLooper.RunWithLooper import androidx.media.utils.MediaConstants import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId -import com.android.internal.statusbar.IStatusBarService import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.R @@ -131,7 +130,6 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var activityStarter: ActivityStarter @Mock lateinit var smartspaceManager: SmartspaceManager @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock lateinit var statusBarService: IStatusBarService lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget @Mock private lateinit var mediaRecommendationItem: SmartspaceAction @@ -194,8 +192,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaFlags = mediaFlags, logger = logger, smartspaceManager = smartspaceManager, - keyguardUpdateMonitor = keyguardUpdateMonitor, - statusBarService = statusBarService, + keyguardUpdateMonitor = keyguardUpdateMonitor ) verify(tunerService) .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) @@ -520,136 +517,19 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test - fun testOnNotificationAdded_emptyTitle_notLoaded() { - // GIVEN that the manager has a notification with an empty title. + fun testOnNotificationRemoved_emptyTitle_notConverted() { + // GIVEN that the manager has a notification with a resume action and empty title. whenever(controller.metadata) .thenReturn( metadataBuilder .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) .build() ) - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(statusBarService) - .onNotificationError( - eq(PACKAGE_NAME), - eq(mediaNotification.tag), - eq(mediaNotification.id), - eq(mediaNotification.uid), - eq(mediaNotification.initialPid), - eq(MEDIA_TITLE_ERROR_MESSAGE), - eq(mediaNotification.user.identifier) - ) - verify(listener, never()) - .onMediaDataLoaded( - eq(KEY), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) - verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) - verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any()) - } - - @Test - fun testOnNotificationAdded_blankTitle_notLoaded() { - // GIVEN that the manager has a notification with a blank title. - whenever(controller.metadata) - .thenReturn( - metadataBuilder - .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) - .build() - ) - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(statusBarService) - .onNotificationError( - eq(PACKAGE_NAME), - eq(mediaNotification.tag), - eq(mediaNotification.id), - eq(mediaNotification.uid), - eq(mediaNotification.initialPid), - eq(MEDIA_TITLE_ERROR_MESSAGE), - eq(mediaNotification.user.identifier) - ) - verify(listener, never()) - .onMediaDataLoaded( - eq(KEY), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) - verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) - verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any()) - } - - @Test - fun testOnNotificationUpdated_invalidTitle_logMediaRemoved() { - addNotificationAndLoad() - val data = mediaDataCaptor.value - - verify(listener) - .onMediaDataLoaded( - eq(KEY), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) - - reset(listener) - whenever(controller.metadata) - .thenReturn( - metadataBuilder - .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) - .build() - ) - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(statusBarService) - .onNotificationError( - eq(PACKAGE_NAME), - eq(mediaNotification.tag), - eq(mediaNotification.id), - eq(mediaNotification.uid), - eq(mediaNotification.initialPid), - eq(MEDIA_TITLE_ERROR_MESSAGE), - eq(mediaNotification.user.identifier) - ) - verify(listener, never()) - .onMediaDataLoaded( - eq(KEY), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) - verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) - } - - @Test - fun testOnNotificationRemoved_emptyTitle_notConverted() { - // GIVEN that the manager has a notification with a resume action and empty title. addNotificationAndLoad() val data = mediaDataCaptor.value val instanceId = data.instanceId assertThat(data.resumption).isFalse() - mediaDataManager.onMediaDataLoaded( - KEY, - null, - data.copy(song = SESSION_EMPTY_TITLE, resumeAction = Runnable {}) - ) + mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) // WHEN the notification is removed reset(listener) @@ -674,15 +554,17 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testOnNotificationRemoved_blankTitle_notConverted() { // GIVEN that the manager has a notification with a resume action and blank title. + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) + .build() + ) addNotificationAndLoad() val data = mediaDataCaptor.value val instanceId = data.instanceId assertThat(data.resumption).isFalse() - mediaDataManager.onMediaDataLoaded( - KEY, - null, - data.copy(song = SESSION_BLANK_TITLE, resumeAction = Runnable {}) - ) + mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) // WHEN the notification is removed reset(listener) diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt new file mode 100644 index 000000000000..36b913fc3e7b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt @@ -0,0 +1,86 @@ +/* + * 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.notetask + +import android.content.Context +import android.content.Intent +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import androidx.test.rule.ActivityTestRule +import androidx.test.runner.intercepting.SingleActivityFactory +import com.android.dx.mockito.inline.extended.ExtendedMockito.verify +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity.Companion.ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE +import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +@TestableLooper.RunWithLooper +class LaunchNotesRoleSettingsTrampolineActivityTest : SysuiTestCase() { + + @Mock lateinit var noteTaskController: NoteTaskController + + @Rule + @JvmField + val activityRule = + ActivityTestRule<LaunchNotesRoleSettingsTrampolineActivity>( + /* activityFactory= */ object : + SingleActivityFactory<LaunchNotesRoleSettingsTrampolineActivity>( + LaunchNotesRoleSettingsTrampolineActivity::class.java + ) { + override fun create(intent: Intent?) = + LaunchNotesRoleSettingsTrampolineActivity(noteTaskController) + }, + /* initialTouchMode= */ false, + /* launchActivity= */ false, + ) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @After + fun tearDown() { + activityRule.finishActivity() + } + + @Test + fun startActivity_noAction_shouldLaunchNotesRoleSettingTaskWithNullEntryPoint() { + activityRule.launchActivity(/* startIntent= */ null) + + verify(noteTaskController).startNotesRoleSetting(any(Context::class.java), eq(null)) + } + + @Test + fun startActivity_quickAffordanceAction_shouldLaunchNotesRoleSettingTaskWithQuickAffordanceEntryPoint() { // ktlint-disable max-line-length + activityRule.launchActivity(Intent(ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE)) + + verify(noteTaskController) + .startNotesRoleSetting(any(Context::class.java), eq(QUICK_AFFORDANCE)) + } +} 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 5dbcd33ab0e6..5f897050099a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -47,6 +47,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskController.Companion.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE import com.android.systemui.notetask.NoteTaskController.Companion.SETTINGS_CREATE_NOTE_TASK_SHORTCUT_COMPONENT import com.android.systemui.notetask.NoteTaskController.Companion.SHORTCUT_ID +import com.android.systemui.notetask.NoteTaskEntryPoint.APP_CLIPS +import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE +import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity import com.android.systemui.settings.FakeUserTracker @@ -493,7 +496,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) + createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -509,7 +512,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) + createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -525,7 +528,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) + createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) } @@ -541,7 +544,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) + createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) } @@ -553,7 +556,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(listOf(mainUserInfo), mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) + createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) } @@ -563,7 +566,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) + createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle) } @@ -734,6 +737,129 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } // endregion + // region getUserForHandlingNotesTaking + @Test + fun getUserForHandlingNotesTaking_cope_quickAffordance_shouldReturnWorkProfileUser() { + whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + val user = createNoteTaskController().getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + + assertThat(user).isEqualTo(UserHandle.of(workUserInfo.id)) + } + + @Test + fun getUserForHandlingNotesTaking_cope_tailButton_shouldReturnWorkProfileUser() { + whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON) + + assertThat(user).isEqualTo(UserHandle.of(workUserInfo.id)) + } + + @Test + fun getUserForHandlingNotesTaking_cope_appClip_shouldReturnCurrentUser() { + whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + val user = createNoteTaskController().getUserForHandlingNotesTaking(APP_CLIPS) + + assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + + @Test + fun getUserForHandlingNotesTaking_noManagement_quickAffordance_shouldReturnCurrentUser() { + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + val user = createNoteTaskController().getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + + assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + + @Test + fun getUserForHandlingNotesTaking_noManagement_tailButton_shouldReturnCurrentUser() { + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON) + + assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + + @Test + fun getUserForHandlingNotesTaking_noManagement_appClip_shouldReturnCurrentUser() { + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + val user = createNoteTaskController().getUserForHandlingNotesTaking(APP_CLIPS) + + assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + // endregion + + // startregion startNotesRoleSetting + @Test + fun startNotesRoleSetting_cope_quickAffordance_shouldStartNoteRoleIntentWithWorkProfileUser() { + whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + createNoteTaskController().startNotesRoleSetting(context, QUICK_AFFORDANCE) + + 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(userCaptor.value).isEqualTo(UserHandle.of(workUserInfo.id)) + } + + @Test + fun startNotesRoleSetting_cope_nullEntryPoint_shouldStartNoteRoleIntentWithCurrentUser() { + whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + createNoteTaskController().startNotesRoleSetting(context, entryPoint = null) + + 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(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + + @Test + fun startNotesRoleSetting_noManagement_quickAffordance_shouldStartNoteRoleIntentWithCurrentUser() { // ktlint-disable max-line-length + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + createNoteTaskController().startNotesRoleSetting(context, QUICK_AFFORDANCE) + + 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(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + + @Test + fun startNotesRoleSetting_noManagement_nullEntryPoint_shouldStartNoteRoleIntentWithCurrentUser() { // ktlint-disable max-line-length + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + createNoteTaskController().startNotesRoleSetting(context, entryPoint = null) + + 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(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + // endregion + private companion object { const val NOTE_TASK_SHORT_LABEL = "Notetaking" const val NOTE_TASK_ACTIVITY_NAME = "NoteTaskActivity" diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt index 42ef2b5ad3ab..452658004733 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt @@ -18,7 +18,12 @@ package com.android.systemui.notetask.quickaffordance +import android.app.role.RoleManager +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags import android.hardware.input.InputSettings +import android.os.UserHandle import android.os.UserManager import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner @@ -31,11 +36,18 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity.Companion.ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEntryPoint +import com.android.systemui.notetask.NoteTaskInfoResolver +import com.android.systemui.shared.customization.data.content.CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable.COMPONENT_NAME_SEPARATOR import com.android.systemui.stylus.StylusManager +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -45,6 +57,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.anyString import org.mockito.Mockito.verify import org.mockito.MockitoSession import org.mockito.quality.Strictness @@ -58,6 +71,8 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { @Mock lateinit var stylusManager: StylusManager @Mock lateinit var repository: KeyguardQuickAffordanceRepository @Mock lateinit var userManager: UserManager + @Mock lateinit var roleManager: RoleManager + @Mock lateinit var packageManager: PackageManager private lateinit var mockitoSession: MockitoSession @@ -69,6 +84,23 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { .mockStatic(InputSettings::class.java) .strictness(Strictness.LENIENT) .startMocking() + + whenever( + packageManager.getApplicationInfoAsUser( + anyString(), + any(ApplicationInfoFlags::class.java), + any(UserHandle::class.java) + ) + ) + .thenReturn(ApplicationInfo()) + whenever(controller.getUserForHandlingNotesTaking(any())).thenReturn(UserHandle.SYSTEM) + whenever( + roleManager.getRoleHoldersAsUser( + eq(RoleManager.ROLE_NOTES), + any(UserHandle::class.java) + ) + ) + .thenReturn(listOf("com.google.test.notes")) } @After @@ -85,6 +117,9 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { keyguardMonitor = mock(), lazyRepository = { repository }, isEnabled = isEnabled, + backgroundExecutor = FakeExecutor(FakeSystemClock()), + roleManager = roleManager, + noteTaskInfoResolver = NoteTaskInfoResolver(roleManager, packageManager) ) private fun createLockScreenStateVisible(): LockScreenState = @@ -112,6 +147,27 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { } @Test + fun lockScreenState_stylusUsed_userUnlocked_isSelected_noDefaultNotesAppSet_shouldEmitHidden() = + runTest { + TestConfig() + .setStylusEverUsed(true) + .setUserUnlocked(true) + .setConfigSelections(mock<NoteTaskQuickAffordanceConfig>()) + whenever( + roleManager.getRoleHoldersAsUser( + eq(RoleManager.ROLE_NOTES), + any(UserHandle::class.java) + ) + ) + .thenReturn(emptyList()) + + val underTest = createUnderTest() + val actual by collectLastValue(underTest.lockScreenState) + + assertThat(actual).isEqualTo(LockScreenState.Hidden) + } + + @Test fun lockScreenState_stylusUnused_userUnlocked_isSelected_shouldEmitHidden() = runTest { TestConfig() .setStylusEverUsed(false) @@ -217,6 +273,39 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { verify(controller).showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) } + // region getPickerScreenState + @Test + fun getPickerScreenState_defaultNoteAppSet_shouldReturnDefault() = runTest { + val underTest = createUnderTest(isEnabled = true) + + assertThat(underTest.getPickerScreenState()) + .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default()) + } + + @Test + fun getPickerScreenState_nodefaultNoteAppSet_shouldReturnDisable() = runTest { + val underTest = createUnderTest(isEnabled = true) + whenever( + roleManager.getRoleHoldersAsUser( + eq(RoleManager.ROLE_NOTES), + any(UserHandle::class.java) + ) + ) + .thenReturn(emptyList()) + + assertThat(underTest.getPickerScreenState()) + .isEqualTo( + KeyguardQuickAffordanceConfig.PickerScreenState.Disabled( + listOf("Select a default notes app to use the notetaking shortcut"), + actionText = "Select app", + actionComponentName = + "${context.packageName}$COMPONENT_NAME_SEPARATOR" + + "$ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE" + ) + ) + } + // endregion + private inner class TestConfig { fun setStylusEverUsed(value: Boolean) = also { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java index e106741499ab..41545fc2abfd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java @@ -30,6 +30,7 @@ import android.os.Handler; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.testing.TestableResources; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; @@ -94,14 +95,18 @@ public class RotationLockTileTest extends SysuiTestCase { private RotationLockController mController; private TestableLooper mTestableLooper; private RotationLockTile mLockTile; + private TestableResources mTestableResources; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mTestableLooper = TestableLooper.get(this); + mTestableResources = mContext.getOrCreateTestableResources(); when(mHost.getContext()).thenReturn(mContext); when(mHost.getUserContext()).thenReturn(mContext); + mTestableResources.addOverride(com.android.internal.R.bool.config_allowRotationResolver, + true); mController = new RotationLockControllerImpl(mRotationPolicyWrapper, mDeviceStateRotationLockSettingController, DEFAULT_SETTINGS); @@ -208,6 +213,32 @@ public class RotationLockTileTest extends SysuiTestCase { } @Test + public void testSecondaryString_rotationResolverDisabled_isEmpty() { + mTestableResources.addOverride(com.android.internal.R.bool.config_allowRotationResolver, + false); + mLockTile = new RotationLockTile( + mHost, + mUiEventLogger, + mTestableLooper.getLooper(), + new Handler(mTestableLooper.getLooper()), + new FalsingManagerFake(), + mMetricsLogger, + mStatusBarStateController, + mActivityStarter, + mQSLogger, + mController, + mPrivacyManager, + mBatteryController, + new FakeSettings() + ); + + mLockTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertEquals("", mLockTile.getState().secondaryLabel.toString()); + } + + @Test public void testIcon_whenDisabled_isOffState() { QSTile.BooleanState state = new QSTile.BooleanState(); disableAutoRotation(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index a1d78cbba7f9..7c30843bb70b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -18,7 +18,6 @@ package com.android.systemui.screenrecord; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -94,6 +93,7 @@ public class RecordingServiceTest extends SysuiTestCase { doReturn(mContext.getUserId()).when(mRecordingService).getUserId(); doReturn(mContext.getPackageName()).when(mRecordingService).getPackageName(); doReturn(mContext.getContentResolver()).when(mRecordingService).getContentResolver(); + doReturn(mContext.getResources()).when(mRecordingService).getResources(); // Mock notifications doNothing().when(mRecordingService).createRecordingNotification(); @@ -101,7 +101,7 @@ public class RecordingServiceTest extends SysuiTestCase { doReturn(mNotification).when(mRecordingService).createSaveNotification(any()); doNothing().when(mRecordingService).createErrorNotification(); doNothing().when(mRecordingService).showErrorToast(anyInt()); - doNothing().when(mRecordingService).stopForeground(anyBoolean()); + doNothing().when(mRecordingService).stopForeground(anyInt()); doNothing().when(mRecordingService).startForeground(anyInt(), any()); doReturn(mScreenMediaRecorder).when(mRecordingService).getRecorder(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 2831d2fad02a..163369f0412f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -41,6 +41,7 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.BurnInInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -92,6 +93,7 @@ public class DozeServiceHostTest extends SysuiTestCase { @Mock private BiometricUnlockController mBiometricUnlockController; @Mock private AuthController mAuthController; @Mock private DozeHost.Callback mCallback; + @Mock private BurnInInteractor mBurnInInteractor; @Before public void setup() { @@ -102,7 +104,8 @@ public class DozeServiceHostTest extends SysuiTestCase { () -> mAssistManager, mDozeScrimController, mKeyguardUpdateMonitor, mPulseExpansionHandler, mNotificationShadeWindowController, mNotificationWakeUpCoordinator, - mAuthController, mNotificationIconAreaController); + mAuthController, mNotificationIconAreaController, + mBurnInInteractor); mDozeServiceHost.initialize( mCentralSurfaces, @@ -213,4 +216,12 @@ public class DozeServiceHostTest extends SysuiTestCase { assertFalse(mDozeServiceHost.isPulsePending()); verify(mDozeScrimController).pulseOutNow(); } + @Test + public void dozeTimeTickSentTBurnInInteractor() { + // WHEN dozeTimeTick + mDozeServiceHost.dozeTimeTick(); + + // THEN burnInInteractor's dozeTimeTick is updated + verify(mBurnInInteractor).dozeTimeTick(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt index b80b825d87dc..c282c1ef0cf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt @@ -21,6 +21,8 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue @@ -49,7 +51,7 @@ class NotificationIconContainerTest : SysuiTestCase() { fun calculateWidthFor_oneIcon_widthForOneIcon() { iconContainer.setActualPaddingStart(10f) iconContainer.setActualPaddingEnd(10f) - iconContainer.setIconSize(10); + iconContainer.setIconSize(10) assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 1f), /* actual= */ 30f) @@ -59,7 +61,7 @@ class NotificationIconContainerTest : SysuiTestCase() { fun calculateWidthFor_fourIcons_widthForFourIcons() { iconContainer.setActualPaddingStart(10f) iconContainer.setActualPaddingEnd(10f) - iconContainer.setIconSize(10); + iconContainer.setIconSize(10) assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 4f), /* actual= */ 60f) @@ -69,7 +71,7 @@ class NotificationIconContainerTest : SysuiTestCase() { fun calculateWidthFor_fiveIcons_widthForFourIcons() { iconContainer.setActualPaddingStart(10f) iconContainer.setActualPaddingEnd(10f) - iconContainer.setIconSize(10); + iconContainer.setIconSize(10) assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 5f), /* actual= */ 60f) } @@ -78,7 +80,7 @@ class NotificationIconContainerTest : SysuiTestCase() { fun calculateIconXTranslations_shortShelfOneIcon_atCorrectXWithoutOverflowDot() { iconContainer.setActualPaddingStart(10f) iconContainer.setActualPaddingEnd(10f) - iconContainer.setIconSize(10); + iconContainer.setIconSize(10) val icon = mockStatusBarIcon() iconContainer.addView(icon) @@ -99,7 +101,7 @@ class NotificationIconContainerTest : SysuiTestCase() { fun calculateIconXTranslations_shortShelfFourIcons_atCorrectXWithoutOverflowDot() { iconContainer.setActualPaddingStart(10f) iconContainer.setActualPaddingEnd(10f) - iconContainer.setIconSize(10); + iconContainer.setIconSize(10) val iconOne = mockStatusBarIcon() val iconTwo = mockStatusBarIcon() @@ -128,7 +130,7 @@ class NotificationIconContainerTest : SysuiTestCase() { fun calculateIconXTranslations_shortShelfFiveIcons_atCorrectXWithOverflowDot() { iconContainer.setActualPaddingStart(10f) iconContainer.setActualPaddingEnd(10f) - iconContainer.setIconSize(10); + iconContainer.setIconSize(10) val iconOne = mockStatusBarIcon() val iconTwo = mockStatusBarIcon() @@ -154,6 +156,55 @@ class NotificationIconContainerTest : SysuiTestCase() { } @Test + fun calculateIconXTranslations_givenWidthEnoughForThreeIcons_atCorrectXWithoutOverflowDot() { + iconContainer.setActualPaddingStart(0f) + iconContainer.setActualPaddingEnd(0f) + iconContainer.setActualLayoutWidth(30) + iconContainer.setIconSize(10) + + val iconOne = mockStatusBarIcon() + val iconTwo = mockStatusBarIcon() + val iconThree = mockStatusBarIcon() + + iconContainer.addView(iconOne) + iconContainer.addView(iconTwo) + iconContainer.addView(iconThree) + assertEquals(3, iconContainer.childCount) + + iconContainer.calculateIconXTranslations() + assertEquals(0f, iconContainer.getIconState(iconOne).xTranslation) + assertEquals(10f, iconContainer.getIconState(iconTwo).xTranslation) + assertEquals(20f, iconContainer.getIconState(iconThree).xTranslation) + assertFalse(iconContainer.areIconsOverflowing()) + } + + @Test + fun calculateIconXTranslations_givenWidthNotEnoughForFourIcons_atCorrectXWithOverflowDot() { + iconContainer.setActualPaddingStart(0f) + iconContainer.setActualPaddingEnd(0f) + iconContainer.setActualLayoutWidth(35) + iconContainer.setIconSize(10) + + val iconOne = mockStatusBarIcon() + val iconTwo = mockStatusBarIcon() + val iconThree = mockStatusBarIcon() + val iconFour = mockStatusBarIcon() + + iconContainer.addView(iconOne) + iconContainer.addView(iconTwo) + iconContainer.addView(iconThree) + iconContainer.addView(iconFour) + assertEquals(4, iconContainer.childCount) + + iconContainer.calculateIconXTranslations() + assertEquals(0f, iconContainer.getIconState(iconOne).xTranslation) + assertEquals(10f, iconContainer.getIconState(iconTwo).xTranslation) + assertEquals(STATE_DOT, iconContainer.getIconState(iconThree).visibleState) + assertEquals(STATE_HIDDEN, iconContainer.getIconState(iconFour).visibleState) + assertTrue(iconContainer.areIconsOverflowing()) + } + + @Test fun shouldForceOverflow_appearingAboveSpeedBump_true() { val forceOverflow = iconContainer.shouldForceOverflow( /* i= */ 1, @@ -161,7 +212,7 @@ class NotificationIconContainerTest : SysuiTestCase() { /* iconAppearAmount= */ 1f, /* maxVisibleIcons= */ 5 ) - assertTrue(forceOverflow); + assertTrue(forceOverflow) } @Test @@ -172,7 +223,7 @@ class NotificationIconContainerTest : SysuiTestCase() { /* iconAppearAmount= */ 0f, /* maxVisibleIcons= */ 5 ) - assertTrue(forceOverflow); + assertTrue(forceOverflow) } @Test @@ -183,7 +234,7 @@ class NotificationIconContainerTest : SysuiTestCase() { /* iconAppearAmount= */ 0f, /* maxVisibleIcons= */ 5 ) - assertFalse(forceOverflow); + assertFalse(forceOverflow) } @Test @@ -210,6 +261,17 @@ class NotificationIconContainerTest : SysuiTestCase() { } @Test + fun isOverflowing_lastChildXGreaterThanDotX_true() { + val isOverflowing = iconContainer.isOverflowing( + /* isLastChild= */ true, + /* translationX= */ 9f, + /* layoutEnd= */ 10f, + /* iconSize= */ 2f, + ) + assertTrue(isOverflowing) + } + + @Test fun isOverflowing_lastChildXGreaterThanLayoutEnd_true() { val isOverflowing = iconContainer.isOverflowing( /* isLastChild= */ true, @@ -253,7 +315,7 @@ class NotificationIconContainerTest : SysuiTestCase() { assertTrue(isOverflowing) } - private fun mockStatusBarIcon() : StatusBarIconView { + private fun mockStatusBarIcon(): StatusBarIconView { val iconView = mock(StatusBarIconView::class.java) whenever(iconView.width).thenReturn(10) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt index 65e2964ea332..9bc49ae02436 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt @@ -43,15 +43,39 @@ class CollapsedStatusBarFragmentLoggerTest : SysuiTestCase() { fun logDisableFlagChange_bufferHasStates() { val state = DisableFlagsLogger.DisableState(0, 1) - logger.logDisableFlagChange(state, state) + logger.logDisableFlagChange(state) val stringWriter = StringWriter() buffer.dump(PrintWriter(stringWriter), tailLength = 0) val actualString = stringWriter.toString() - val expectedLogString = disableFlagsLogger.getDisableFlagsString( - old = null, new = state, newAfterLocalModification = state - ) + val expectedLogString = + disableFlagsLogger.getDisableFlagsString( + old = null, + new = state, + newAfterLocalModification = null, + ) assertThat(actualString).contains(expectedLogString) } + + @Test + fun logVisibilityModel_bufferCorrect() { + logger.logVisibilityModel( + StatusBarVisibilityModel( + showClock = false, + showNotificationIcons = true, + showOngoingCallChip = false, + showSystemInfo = true, + ) + ) + + val stringWriter = StringWriter() + buffer.dump(PrintWriter(stringWriter), tailLength = 0) + val actualString = stringWriter.toString() + + assertThat(actualString).contains("showClock=false") + assertThat(actualString).contains("showNotificationIcons=true") + assertThat(actualString).contains("showOngoingCallChip=false") + assertThat(actualString).contains("showSystemInfo=true") + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 4b1caca86bea..21769922c899 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone.fragment; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN; import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN; import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT; import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE; @@ -93,6 +95,7 @@ import java.util.List; public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { private NotificationIconAreaController mMockNotificationAreaController; + private ShadeExpansionStateManager mShadeExpansionStateManager; private View mNotificationAreaInner; private OngoingCallController mOngoingCallController; private SystemStatusAnimationScheduler mAnimationScheduler; @@ -173,6 +176,10 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { fragment.disable(DEFAULT_DISPLAY, 0, 0, false); assertEquals(View.VISIBLE, getEndSideContentView().getVisibility()); + + fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false); + + assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility()); } @Test @@ -278,6 +285,10 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { fragment.disable(DEFAULT_DISPLAY, 0, 0, false); Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE)); + + fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false); + + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE)); } @Test @@ -291,6 +302,70 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { fragment.disable(DEFAULT_DISPLAY, 0, 0, false); assertEquals(View.VISIBLE, getClockView().getVisibility()); + + fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_CLOCK, 0, false); + + assertEquals(View.GONE, getClockView().getVisibility()); + } + + @Test + public void disable_shadeOpenAndShouldHide_everythingHidden() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + // WHEN the shade is open and configured to hide the status bar icons + mShadeExpansionStateManager.updateState(STATE_OPEN); + when(mShadeViewController.shouldHideStatusBarIconsWhenExpanded()).thenReturn(true); + + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + // THEN all views are hidden + assertEquals(View.INVISIBLE, getClockView().getVisibility()); + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE)); + assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility()); + } + + @Test + public void disable_shadeOpenButNotShouldHide_everythingShown() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + // WHEN the shade is open but *not* configured to hide the status bar icons + mShadeExpansionStateManager.updateState(STATE_OPEN); + when(mShadeViewController.shouldHideStatusBarIconsWhenExpanded()).thenReturn(false); + + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + // THEN all views are shown + assertEquals(View.VISIBLE, getClockView().getVisibility()); + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE)); + assertEquals(View.VISIBLE, getEndSideContentView().getVisibility()); + } + + /** Regression test for b/279790651. */ + @Test + public void disable_shadeOpenAndShouldHide_thenShadeNotOpenAndDozingUpdate_everythingShown() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + // WHEN the shade is open and configured to hide the status bar icons + mShadeExpansionStateManager.updateState(STATE_OPEN); + when(mShadeViewController.shouldHideStatusBarIconsWhenExpanded()).thenReturn(true); + + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + // THEN all views are hidden + assertEquals(View.INVISIBLE, getClockView().getVisibility()); + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE)); + assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility()); + + // WHEN the shade is updated to no longer be open + mShadeExpansionStateManager.updateState(STATE_CLOSED); + + // AND we internally request an update via dozing change + fragment.onDozingChanged(true); + + // THEN all views are shown + assertEquals(View.VISIBLE, getClockView().getVisibility()); + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE)); + assertEquals(View.VISIBLE, getEndSideContentView().getVisibility()); } @Test @@ -323,7 +398,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility()); Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE)); - } @Test @@ -356,20 +430,26 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { public void disable_ongoingCallEnded_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - // Ongoing call started + when(mOngoingCallController.hasOngoingCall()).thenReturn(true); fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility()); // Ongoing call ended when(mOngoingCallController.hasOngoingCall()).thenReturn(false); - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); assertEquals(View.GONE, mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility()); + + // Ongoing call started + when(mOngoingCallController.hasOngoingCall()).thenReturn(true); + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + assertEquals(View.VISIBLE, + mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility()); } @Test @@ -494,6 +574,8 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager); mSecureSettings = mock(SecureSettings.class); + mShadeExpansionStateManager = new ShadeExpansionStateManager(); + setUpNotificationIconAreaController(); return new CollapsedStatusBarFragment( mStatusBarFragmentComponentFactory, @@ -501,7 +583,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mAnimationScheduler, mLocationPublisher, mMockNotificationAreaController, - new ShadeExpansionStateManager(), + mShadeExpansionStateManager, mock(FeatureFlags.class), mStatusBarIconController, mIconManagerFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt new file mode 100644 index 000000000000..8e789cb2cae6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.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.statusbar.phone.fragment + +import android.app.StatusBarManager.DISABLE2_SYSTEM_ICONS +import android.app.StatusBarManager.DISABLE_CLOCK +import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS +import android.app.StatusBarManager.DISABLE_ONGOING_CALL_CHIP +import android.app.StatusBarManager.DISABLE_SYSTEM_INFO +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.fragment.StatusBarVisibilityModel.Companion.createDefaultModel +import com.android.systemui.statusbar.phone.fragment.StatusBarVisibilityModel.Companion.createModelFromFlags +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class StatusBarVisibilityModelTest : SysuiTestCase() { + @Test + fun createDefaultModel_everythingEnabled() { + val result = createDefaultModel() + + val expected = + StatusBarVisibilityModel( + showClock = true, + showNotificationIcons = true, + showOngoingCallChip = true, + showSystemInfo = true, + ) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun createModelFromFlags_clockNotDisabled_showClockTrue() { + val result = createModelFromFlags(disabled1 = 0, disabled2 = 0) + + assertThat(result.showClock).isTrue() + } + + @Test + fun createModelFromFlags_clockDisabled_showClockFalse() { + val result = createModelFromFlags(disabled1 = DISABLE_CLOCK, disabled2 = 0) + + assertThat(result.showClock).isFalse() + } + + @Test + fun createModelFromFlags_notificationIconsNotDisabled_showNotificationIconsTrue() { + val result = createModelFromFlags(disabled1 = 0, disabled2 = 0) + + assertThat(result.showNotificationIcons).isTrue() + } + + @Test + fun createModelFromFlags_notificationIconsDisabled_showNotificationIconsFalse() { + val result = createModelFromFlags(disabled1 = DISABLE_NOTIFICATION_ICONS, disabled2 = 0) + + assertThat(result.showNotificationIcons).isFalse() + } + + @Test + fun createModelFromFlags_ongoingCallChipNotDisabled_showOngoingCallChipTrue() { + val result = createModelFromFlags(disabled1 = 0, disabled2 = 0) + + assertThat(result.showOngoingCallChip).isTrue() + } + + @Test + fun createModelFromFlags_ongoingCallChipDisabled_showOngoingCallChipFalse() { + val result = createModelFromFlags(disabled1 = DISABLE_ONGOING_CALL_CHIP, disabled2 = 0) + + assertThat(result.showOngoingCallChip).isFalse() + } + + @Test + fun createModelFromFlags_systemInfoAndIconsNotDisabled_showSystemInfoTrue() { + val result = createModelFromFlags(disabled1 = 0, disabled2 = 0) + + assertThat(result.showSystemInfo).isTrue() + } + + @Test + fun createModelFromFlags_disable1SystemInfoDisabled_showSystemInfoFalse() { + val result = createModelFromFlags(disabled1 = DISABLE_SYSTEM_INFO, disabled2 = 0) + + assertThat(result.showSystemInfo).isFalse() + } + + @Test + fun createModelFromFlags_disable2SystemIconsDisabled_showSystemInfoFalse() { + val result = createModelFromFlags(disabled1 = 0, disabled2 = DISABLE2_SYSTEM_ICONS) + + assertThat(result.showSystemInfo).isFalse() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeRearDisplayStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeRearDisplayStateRepository.kt new file mode 100644 index 000000000000..fd9139165c85 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeRearDisplayStateRepository.kt @@ -0,0 +1,31 @@ +/* + * 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.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeRearDisplayStateRepository : RearDisplayStateRepository { + private val _isInRearDisplayMode = MutableStateFlow<Boolean>(false) + override val isInRearDisplayMode: StateFlow<Boolean> = _isInRearDisplayMode.asStateFlow() + + fun setIsInRearDisplayMode(isInRearDisplayMode: Boolean) { + _isInRearDisplayMode.value = isInRearDisplayMode + } +} diff --git a/services/autofill/java/com/android/server/autofill/ClientSuggestionsSession.java b/services/autofill/java/com/android/server/autofill/ClientSuggestionsSession.java deleted file mode 100644 index 715697d82cad..000000000000 --- a/services/autofill/java/com/android/server/autofill/ClientSuggestionsSession.java +++ /dev/null @@ -1,293 +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.autofill; - -import static android.service.autofill.FillRequest.INVALID_REQUEST_ID; - -import static com.android.server.autofill.Helper.sVerbose; - -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.app.AppGlobals; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.ICancellationSignal; -import android.os.RemoteException; -import android.service.autofill.Dataset; -import android.service.autofill.FillResponse; -import android.service.autofill.IFillCallback; -import android.service.autofill.SaveInfo; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.Slog; -import android.view.autofill.AutofillId; -import android.view.autofill.IAutoFillManagerClient; -import android.view.inputmethod.InlineSuggestionsRequest; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.infra.AndroidFuture; - -import java.util.List; -import java.util.concurrent.CancellationException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Maintains a client suggestions session with the - * {@link android.view.autofill.AutofillRequestCallback} through the {@link IAutoFillManagerClient}. - * - */ -final class ClientSuggestionsSession { - - private static final String TAG = "ClientSuggestionsSession"; - private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 15 * DateUtils.SECOND_IN_MILLIS; - - private final int mSessionId; - private final IAutoFillManagerClient mClient; - private final Handler mHandler; - private final ComponentName mComponentName; - - private final RemoteFillService.FillServiceCallbacks mCallbacks; - - private final Object mLock = new Object(); - @GuardedBy("mLock") - private AndroidFuture<FillResponse> mPendingFillRequest; - @GuardedBy("mLock") - private int mPendingFillRequestId = INVALID_REQUEST_ID; - - ClientSuggestionsSession(int sessionId, IAutoFillManagerClient client, Handler handler, - ComponentName componentName, RemoteFillService.FillServiceCallbacks callbacks) { - mSessionId = sessionId; - mClient = client; - mHandler = handler; - mComponentName = componentName; - mCallbacks = callbacks; - } - - void onFillRequest(int requestId, InlineSuggestionsRequest inlineRequest, int flags) { - final AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); - final AtomicReference<AndroidFuture<FillResponse>> futureRef = new AtomicReference<>(); - final AndroidFuture<FillResponse> fillRequest = new AndroidFuture<>(); - - mHandler.post(() -> { - if (sVerbose) { - Slog.v(TAG, "calling onFillRequest() for id=" + requestId); - } - - try { - mClient.requestFillFromClient(requestId, inlineRequest, - new FillCallbackImpl(fillRequest, futureRef, cancellationSink)); - } catch (RemoteException e) { - fillRequest.completeExceptionally(e); - } - }); - - fillRequest.orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS); - futureRef.set(fillRequest); - - synchronized (mLock) { - mPendingFillRequest = fillRequest; - mPendingFillRequestId = requestId; - } - - fillRequest.whenComplete((res, err) -> mHandler.post(() -> { - synchronized (mLock) { - mPendingFillRequest = null; - mPendingFillRequestId = INVALID_REQUEST_ID; - } - if (err == null) { - processAutofillId(res); - mCallbacks.onFillRequestSuccess(requestId, res, - mComponentName.getPackageName(), flags); - } else { - Slog.e(TAG, "Error calling on client fill request", err); - if (err instanceof TimeoutException) { - dispatchCancellationSignal(cancellationSink.get()); - mCallbacks.onFillRequestTimeout(requestId); - } else if (err instanceof CancellationException) { - dispatchCancellationSignal(cancellationSink.get()); - } else { - mCallbacks.onFillRequestFailure(requestId, err.getMessage()); - } - } - })); - } - - /** - * Gets the application info for the component. - */ - @Nullable - static ApplicationInfo getAppInfo(ComponentName comp, @UserIdInt int userId) { - try { - ApplicationInfo si = AppGlobals.getPackageManager().getApplicationInfo( - comp.getPackageName(), - PackageManager.GET_META_DATA, - userId); - if (si != null) { - return si; - } - } catch (RemoteException e) { - } - return null; - } - - /** - * Gets the user-visible name of the application. - */ - @Nullable - @GuardedBy("mLock") - static CharSequence getAppLabelLocked(Context context, ApplicationInfo appInfo) { - return appInfo == null ? null : appInfo.loadSafeLabel( - context.getPackageManager(), 0 /* do not ellipsize */, - TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); - } - - /** - * Gets the user-visible icon of the application. - */ - @Nullable - @GuardedBy("mLock") - static Drawable getAppIconLocked(Context context, ApplicationInfo appInfo) { - return appInfo == null ? null : appInfo.loadIcon(context.getPackageManager()); - } - - int cancelCurrentRequest() { - synchronized (mLock) { - return mPendingFillRequest != null && mPendingFillRequest.cancel(false) - ? mPendingFillRequestId - : INVALID_REQUEST_ID; - } - } - - /** - * The {@link AutofillId} which the client gets from its view is not contain the session id, - * but Autofill framework is using the {@link AutofillId} with a session id. So before using - * those ids in the Autofill framework, applies the current session id. - * - * @param res which response need to apply for a session id - */ - private void processAutofillId(FillResponse res) { - if (res == null) { - return; - } - - final List<Dataset> datasets = res.getDatasets(); - if (datasets != null && !datasets.isEmpty()) { - for (int i = 0; i < datasets.size(); i++) { - final Dataset dataset = datasets.get(i); - if (dataset != null) { - applySessionId(dataset.getFieldIds()); - } - } - } - - final SaveInfo saveInfo = res.getSaveInfo(); - if (saveInfo != null) { - applySessionId(saveInfo.getOptionalIds()); - applySessionId(saveInfo.getRequiredIds()); - applySessionId(saveInfo.getSanitizerValues()); - applySessionId(saveInfo.getTriggerId()); - } - } - - private void applySessionId(List<AutofillId> ids) { - if (ids == null || ids.isEmpty()) { - return; - } - - for (int i = 0; i < ids.size(); i++) { - applySessionId(ids.get(i)); - } - } - - private void applySessionId(AutofillId[][] ids) { - if (ids == null) { - return; - } - for (int i = 0; i < ids.length; i++) { - applySessionId(ids[i]); - } - } - - private void applySessionId(AutofillId[] ids) { - if (ids == null) { - return; - } - for (int i = 0; i < ids.length; i++) { - applySessionId(ids[i]); - } - } - - private void applySessionId(AutofillId id) { - if (id == null) { - return; - } - id.setSessionId(mSessionId); - } - - private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) { - if (signal == null) { - return; - } - try { - signal.cancel(); - } catch (RemoteException e) { - Slog.e(TAG, "Error requesting a cancellation", e); - } - } - - private class FillCallbackImpl extends IFillCallback.Stub { - final AndroidFuture<FillResponse> mFillRequest; - final AtomicReference<AndroidFuture<FillResponse>> mFutureRef; - final AtomicReference<ICancellationSignal> mCancellationSink; - - FillCallbackImpl(AndroidFuture<FillResponse> fillRequest, - AtomicReference<AndroidFuture<FillResponse>> futureRef, - AtomicReference<ICancellationSignal> cancellationSink) { - mFillRequest = fillRequest; - mFutureRef = futureRef; - mCancellationSink = cancellationSink; - } - - @Override - public void onCancellable(ICancellationSignal cancellation) { - AndroidFuture<FillResponse> future = mFutureRef.get(); - if (future != null && future.isCancelled()) { - dispatchCancellationSignal(cancellation); - } else { - mCancellationSink.set(cancellation); - } - } - - @Override - public void onSuccess(FillResponse response) { - mFillRequest.complete(response); - } - - @Override - public void onFailure(int requestId, CharSequence message) { - String errorMessage = message == null ? "" : String.valueOf(message); - mFillRequest.completeExceptionally( - new RuntimeException(errorMessage)); - } - } -} diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index f83d7342b6e3..3736262e0673 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -36,7 +36,6 @@ import static android.view.autofill.AutofillManager.ACTION_VALUE_CHANGED; import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED; import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED; import static android.view.autofill.AutofillManager.COMMIT_REASON_UNKNOWN; -import static android.view.autofill.AutofillManager.FLAG_ENABLED_CLIENT_SUGGESTIONS; import static android.view.autofill.AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM; import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString; @@ -446,9 +445,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState */ private final PccAssistDataReceiverImpl mPccAssistReceiver = new PccAssistDataReceiverImpl(); - @Nullable - private ClientSuggestionsSession mClientSuggestionsSession; - private final ClassificationState mClassificationState = new ClassificationState(); // TODO(b/216576510): Share one BroadcastReceiver between all Sessions instead of creating a @@ -590,9 +586,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState /** Whether the current {@link FillResponse} is expired. */ private boolean mExpiredResponse; - /** Whether the client is using {@link android.view.autofill.AutofillRequestCallback}. */ - private boolean mClientSuggestionsEnabled; - /** Whether the fill dialog UI is disabled. */ private boolean mFillDialogDisabled; } @@ -623,21 +616,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } mWaitForInlineRequest = inlineSuggestionsRequest != null; mPendingInlineSuggestionsRequest = inlineSuggestionsRequest; - mWaitForInlineRequest = inlineSuggestionsRequest != null; - maybeRequestFillFromServiceLocked(); + maybeRequestFillLocked(); viewState.resetState(ViewState.STATE_PENDING_CREATE_INLINE_REQUEST); } } : null; } - void newAutofillRequestLocked(@Nullable InlineSuggestionsRequest inlineRequest) { - mPendingFillRequest = null; - mWaitForInlineRequest = inlineRequest != null; - mPendingInlineSuggestionsRequest = inlineRequest; - } - @GuardedBy("mLock") - void maybeRequestFillFromServiceLocked() { + void maybeRequestFillLocked() { if (mPendingFillRequest == null) { return; } @@ -647,15 +633,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } - if (mPendingInlineSuggestionsRequest.isServiceSupported()) { - mPendingFillRequest = new FillRequest(mPendingFillRequest.getId(), - mPendingFillRequest.getFillContexts(), - mPendingFillRequest.getHints(), - mPendingFillRequest.getClientState(), - mPendingFillRequest.getFlags(), - mPendingInlineSuggestionsRequest, - mPendingFillRequest.getDelayedFillIntentSender()); - } + mPendingFillRequest = new FillRequest(mPendingFillRequest.getId(), + mPendingFillRequest.getFillContexts(), + mPendingFillRequest.getHints(), + mPendingFillRequest.getClientState(), + mPendingFillRequest.getFlags(), + mPendingInlineSuggestionsRequest, + mPendingFillRequest.getDelayedFillIntentSender()); } mLastFillRequest = mPendingFillRequest; @@ -777,7 +761,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState : mDelayedFillPendingIntent.getIntentSender()); mPendingFillRequest = request; - maybeRequestFillFromServiceLocked(); + maybeRequestFillLocked(); } if (mActivityToken != null) { @@ -1099,39 +1083,30 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } /** - * Cancels the last request sent to the {@link #mRemoteFillService} or the - * {@link #mClientSuggestionsSession}. + * Cancels the last request sent to the {@link #mRemoteFillService}. */ @GuardedBy("mLock") private void cancelCurrentRequestLocked() { - if (mRemoteFillService == null && mClientSuggestionsSession == null) { - wtf(null, "cancelCurrentRequestLocked() called without a remote service or a " - + "client suggestions session. mForAugmentedAutofillOnly: %s", - mSessionFlags.mAugmentedAutofillOnly); + if (mRemoteFillService == null) { + wtf(null, "cancelCurrentRequestLocked() called without a remote service. " + + "mForAugmentedAutofillOnly: %s", mSessionFlags.mAugmentedAutofillOnly); return; } + final int canceledRequest = mRemoteFillService.cancelCurrentRequest(); - if (mRemoteFillService != null) { - final int canceledRequest = mRemoteFillService.cancelCurrentRequest(); - - // Remove the FillContext as there will never be a response for the service - if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) { - final int numContexts = mContexts.size(); + // Remove the FillContext as there will never be a response for the service + if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) { + final int numContexts = mContexts.size(); - // It is most likely the last context, hence search backwards - for (int i = numContexts - 1; i >= 0; i--) { - if (mContexts.get(i).getRequestId() == canceledRequest) { - if (sDebug) Slog.d(TAG, "cancelCurrentRequest(): id = " + canceledRequest); - mContexts.remove(i); - break; - } + // It is most likely the last context, hence search backwards + for (int i = numContexts - 1; i >= 0; i--) { + if (mContexts.get(i).getRequestId() == canceledRequest) { + if (sDebug) Slog.d(TAG, "cancelCurrentRequest(): id = " + canceledRequest); + mContexts.remove(i); + break; } } } - - if (mClientSuggestionsSession != null) { - mClientSuggestionsSession.cancelCurrentRequest(); - } } private boolean isViewFocusedLocked(int flags) { @@ -1225,30 +1200,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState requestAssistStructureForPccLocked(flags | FLAG_PCC_DETECTION); } - // Only ask IME to create inline suggestions request when - // 1. Autofill provider supports it or client enabled client suggestions. - // 2. The render service is available. - // 3. The view is focused. (The view may not be focused if the autofill is triggered - // manually.) + // Only ask IME to create inline suggestions request if Autofill provider supports it and + // the render service is available except the autofill is triggered manually and the view + // is also not focused. final RemoteInlineSuggestionRenderService remoteRenderService = mService.getRemoteInlineSuggestionRenderServiceLocked(); - if ((mSessionFlags.mInlineSupportedByService || mSessionFlags.mClientSuggestionsEnabled) - && remoteRenderService != null - && (isViewFocusedLocked(flags) || (isRequestSupportFillDialog(flags)))) { - final Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer; - if (mSessionFlags.mClientSuggestionsEnabled) { - final int finalRequestId = requestId; - inlineSuggestionsRequestConsumer = (inlineSuggestionsRequest) -> { - // Using client suggestions - synchronized (mLock) { - onClientFillRequestLocked(finalRequestId, inlineSuggestionsRequest); - } - viewState.resetState(ViewState.STATE_PENDING_CREATE_INLINE_REQUEST); - }; - } else { - inlineSuggestionsRequestConsumer = mAssistReceiver.newAutofillRequestLocked( - viewState, /* isInlineRequest= */ true); - } + if (mSessionFlags.mInlineSupportedByService + && remoteRenderService != null + && (isViewFocusedLocked(flags) || isRequestSupportFillDialog(flags))) { + Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer = + mAssistReceiver.newAutofillRequestLocked(viewState, + /* isInlineRequest= */ true); if (inlineSuggestionsRequestConsumer != null) { final AutofillId focusedId = mCurrentViewId; final int requestIdCopy = requestId; @@ -1264,18 +1226,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState ); viewState.setState(ViewState.STATE_PENDING_CREATE_INLINE_REQUEST); } - } else if (mSessionFlags.mClientSuggestionsEnabled) { - // Request client suggestions for the dropdown mode - onClientFillRequestLocked(requestId, null); } else { mAssistReceiver.newAutofillRequestLocked(viewState, /* isInlineRequest= */ false); } - if (mSessionFlags.mClientSuggestionsEnabled) { - // Using client suggestions, unnecessary request AssistStructure - return; - } - // Now request the assist structure data. requestAssistStructureLocked(requestId, flags); } @@ -1380,11 +1334,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mSessionFlags = new SessionFlags(); mSessionFlags.mAugmentedAutofillOnly = forAugmentedAutofillOnly; mSessionFlags.mInlineSupportedByService = mService.isInlineSuggestionsEnabledLocked(); - if (mContext.checkCallingPermission(PROVIDE_OWN_AUTOFILL_SUGGESTIONS) - == PackageManager.PERMISSION_GRANTED) { - mSessionFlags.mClientSuggestionsEnabled = - (mFlags & FLAG_ENABLED_CLIENT_SUGGESTIONS) != 0; - } setClientLocked(client); } @@ -1522,15 +1471,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (requestLog != null) { requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, -1); } - processNullResponseOrFallbackLocked(requestId, requestFlags); + processNullResponseLocked(requestId, requestFlags); return; } // TODO: Check if this is required. We can still present datasets to the user even if // traditional field classification is disabled. fieldClassificationIds = response.getFieldClassificationIds(); - if (!mSessionFlags.mClientSuggestionsEnabled && fieldClassificationIds != null - && !mService.isFieldClassificationEnabledLocked()) { + if (fieldClassificationIds != null && !mService.isFieldClassificationEnabledLocked()) { Slog.w(TAG, "Ignoring " + response + " because field detection is disabled"); processNullResponseLocked(requestId, requestFlags); return; @@ -1643,9 +1591,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState || (ArrayUtils.isEmpty(saveInfo.getOptionalIds()) && ArrayUtils.isEmpty(saveInfo.getRequiredIds()) && ((saveInfo.getFlags() & SaveInfo.FLAG_DELAY_SAVE) == 0))) - && (ArrayUtils.isEmpty(response.getFieldClassificationIds()) - || (!mSessionFlags.mClientSuggestionsEnabled - && !mService.isFieldClassificationEnabledLocked()))); + && (ArrayUtils.isEmpty(response.getFieldClassificationIds()))); } } @@ -1975,40 +1921,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } - @GuardedBy("mLock") - private void processNullResponseOrFallbackLocked(int requestId, int flags) { - if (!mSessionFlags.mClientSuggestionsEnabled) { - processNullResponseLocked(requestId, flags); - return; - } - - // fallback to the default platform password manager - mSessionFlags.mClientSuggestionsEnabled = false; - mLastFillDialogTriggerIds = null; - // Log the existing FillResponse event. - mFillResponseEventLogger.logAndEndEvent(); - - final InlineSuggestionsRequest inlineRequest = - (mLastInlineSuggestionsRequest != null - && mLastInlineSuggestionsRequest.first == requestId) - ? mLastInlineSuggestionsRequest.second : null; - - // Start a new FillRequest logger for client suggestion fallback. - mFillRequestEventLogger.startLogForNewRequest(); - mRequestCount++; - mFillRequestEventLogger.maybeSetAppPackageUid(uid); - mFillRequestEventLogger.maybeSetFlags( - flags & ~FLAG_ENABLED_CLIENT_SUGGESTIONS); - mFillRequestEventLogger.maybeSetRequestTriggerReason( - TRIGGER_REASON_NORMAL_TRIGGER); - mFillRequestEventLogger.maybeSetIsClientSuggestionFallback(true); - - mAssistReceiver.newAutofillRequestLocked(inlineRequest); - requestAssistStructureLocked(requestId, - flags & ~FLAG_ENABLED_CLIENT_SUGGESTIONS); - return; - } - // FillServiceCallbacks @Override @SuppressWarnings("GuardedBy") @@ -4205,22 +4117,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState filterText = value.getTextValue().toString(); } - final CharSequence targetLabel; - final Drawable targetIcon; - synchronized (mLock) { - if (mSessionFlags.mClientSuggestionsEnabled) { - final ApplicationInfo appInfo = ClientSuggestionsSession.getAppInfo(mComponentName, - mService.getUserId()); - targetLabel = ClientSuggestionsSession.getAppLabelLocked( - mService.getMaster().getContext(), appInfo); - targetIcon = ClientSuggestionsSession.getAppIconLocked( - mService.getMaster().getContext(), appInfo); - } else { - targetLabel = mService.getServiceLabelLocked(); - targetIcon = mService.getServiceIconLocked(); - } + final CharSequence serviceLabel; + final Drawable serviceIcon; + synchronized (this.mService.mLock) { + serviceLabel = mService.getServiceLabelLocked(); + serviceIcon = mService.getServiceIconLocked(); } - if (targetLabel == null || targetIcon == null) { + if (serviceLabel == null || serviceIcon == null) { wtf(null, "onFillReady(): no service label or icon"); return; } @@ -4281,7 +4184,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState getUiForShowing().showFillUi(filledId, response, filterText, mService.getServicePackageName(), mComponentName, - targetLabel, targetIcon, this, mContext, id, mCompatMode); + serviceLabel, serviceIcon, this, mContext, id, mCompatMode); synchronized (mLock) { mPresentationStatsEventLogger.maybeSetCountShown( @@ -4477,17 +4380,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return false; } - final InlineSuggestionsRequest request = inlineSuggestionsRequest.get(); - if (mSessionFlags.mClientSuggestionsEnabled && !request.isClientSupported() - || !mSessionFlags.mClientSuggestionsEnabled && !request.isServiceSupported()) { - if (sDebug) { - Slog.d(TAG, "Inline suggestions not supported for " - + (mSessionFlags.mClientSuggestionsEnabled ? "client" : "service") - + ". Falling back to dropdown."); - } - return false; - } - final RemoteInlineSuggestionRenderService remoteRenderService = mService.getRemoteInlineSuggestionRenderServiceLocked(); if (remoteRenderService == null) { @@ -4502,8 +4394,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } final InlineFillUi.InlineFillUiInfo inlineFillUiInfo = - new InlineFillUi.InlineFillUiInfo(request, focusedId, - filterText, remoteRenderService, userId, id); + new InlineFillUi.InlineFillUiInfo(inlineSuggestionsRequest.get(), focusedId, + filterText, remoteRenderService, userId, id); InlineFillUi inlineFillUi = InlineFillUi.forAutofill(inlineFillUiInfo, response, new InlineFillUi.InlineSuggestionUiCallback() { @Override @@ -5154,26 +5046,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } - @GuardedBy("mLock") - private void onClientFillRequestLocked(int requestId, - InlineSuggestionsRequest inlineSuggestionsRequest) { - if (mClientSuggestionsSession == null) { - mClientSuggestionsSession = new ClientSuggestionsSession(id, mClient, mHandler, - mComponentName, this); - } - - if (mContexts == null) { - mContexts = new ArrayList<>(1); - } - mContexts.add(new FillContext(requestId, new AssistStructure(), mCurrentViewId)); - - if (inlineSuggestionsRequest != null && !inlineSuggestionsRequest.isClientSupported()) { - inlineSuggestionsRequest = null; - } - - mClientSuggestionsSession.onFillRequest(requestId, inlineSuggestionsRequest, mFlags); - } - /** * The result of checking whether to show the save dialog, when session can be saved. * diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index d8fbd08a0e70..0172eaf03fa7 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -107,6 +107,9 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.transport.CompanionTransportManager; import com.android.server.pm.UserManagerInternal; @@ -116,6 +119,7 @@ import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -199,6 +203,8 @@ public class CompanionDeviceManagerService extends SystemService { private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners = new RemoteCallbackList<>(); + private CrossDeviceSyncController mCrossDeviceSyncController; + public CompanionDeviceManagerService(Context context) { super(context); @@ -238,6 +244,8 @@ public class CompanionDeviceManagerService extends SystemService { mTransportManager = new CompanionTransportManager(context, mAssociationStore); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); + // TODO(b/279663946): move context sync to a dedicated system service + mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager); // Publish "binder" service. final CompanionDeviceManagerImpl impl = new CompanionDeviceManagerImpl(); @@ -1353,6 +1361,39 @@ public class CompanionDeviceManagerService extends SystemService { public void removeInactiveSelfManagedAssociations() { CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations(); } + + @Override + public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.registerCallMetadataSyncCallback(callback); + } + } + + @Override + public void crossDeviceSync(int userId, Collection<CrossDeviceCall> calls) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.syncToAllDevicesForUserId(userId, calls); + } + } + + @Override + public void crossDeviceSync(AssociationInfo associationInfo, + Collection<CrossDeviceCall> calls) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.syncToSingleDevice(associationInfo, calls); + } + } + + @Override + public void sendCrossDeviceSyncMessage(int associationId, byte[] message) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.syncMessageToDevice(associationId, message); + } + } } /** diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java index 36492407d463..3b108e63e13d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -16,12 +16,41 @@ package com.android.server.companion; +import android.companion.AssociationInfo; + +import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback; + +import java.util.Collection; + /** * Companion Device Manager Local System Service Interface. */ -interface CompanionDeviceManagerServiceInternal { +public interface CompanionDeviceManagerServiceInternal { /** * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations */ void removeInactiveSelfManagedAssociations(); + + /** + * Registers a callback from an InCallService / ConnectionService to CDM to process sync + * requests and perform call control actions. + */ + void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback); + + /** + * Requests a sync from an InCallService / ConnectionService to CDM, for the given association + * and message. + */ + void sendCrossDeviceSyncMessage(int associationId, byte[] message); + + /** + * Requests a sync from an InCallService to CDM, for the given user and call metadata. + */ + void crossDeviceSync(int userId, Collection<CrossDeviceCall> calls); + + /** + * Requests a sync from an InCallService to CDM, for the given association and call metadata. + */ + void crossDeviceSync(AssociationInfo associationInfo, Collection<CrossDeviceCall> calls); } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java index ae4766ac9fda..443a732eb6f1 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java @@ -17,15 +17,20 @@ package com.android.server.companion.datatransfer.contextsync; import android.annotation.Nullable; +import android.companion.AssociationInfo; import android.telecom.Call; import android.telecom.InCallService; import android.telecom.TelecomManager; +import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; import com.android.server.companion.CompanionDeviceConfig; +import com.android.server.companion.CompanionDeviceManagerServiceInternal; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.stream.Collectors; @@ -35,90 +40,132 @@ import java.util.stream.Collectors; */ public class CallMetadataSyncInCallService extends InCallService { + private static final String TAG = "CallMetadataIcs"; private static final long NOT_VALID = -1L; + private CompanionDeviceManagerServiceInternal mCdmsi; + @VisibleForTesting final Map<Call, CrossDeviceCall> mCurrentCalls = new HashMap<>(); - @VisibleForTesting - boolean mShouldSync; + @VisibleForTesting int mNumberOfActiveSyncAssociations; final Call.Callback mTelecomCallback = new Call.Callback() { @Override public void onDetailsChanged(Call call, Call.Details details) { - mCurrentCalls.get(call).updateCallDetails(details); + if (mNumberOfActiveSyncAssociations > 0) { + final CrossDeviceCall crossDeviceCall = mCurrentCalls.get(call); + if (crossDeviceCall != null) { + crossDeviceCall.updateCallDetails(details); + sync(getUserId()); + } else { + Slog.w(TAG, "Could not update details for nonexistent call"); + } + } } }; - final CallMetadataSyncCallback mCallMetadataSyncCallback = new CallMetadataSyncCallback() { - @Override - void processCallControlAction(int crossDeviceCallId, int callControlAction) { - final CrossDeviceCall crossDeviceCall = getCallForId(crossDeviceCallId, - mCurrentCalls.values()); - switch (callControlAction) { - case android.companion.Telecom.Call.ACCEPT: - if (crossDeviceCall != null) { - crossDeviceCall.doAccept(); - } - break; - case android.companion.Telecom.Call.REJECT: - if (crossDeviceCall != null) { - crossDeviceCall.doReject(); + final CrossDeviceSyncControllerCallback + mCrossDeviceSyncControllerCallback = new CrossDeviceSyncControllerCallback() { + @Override + void processContextSyncMessage(int associationId, + CallMetadataSyncData callMetadataSyncData) { + final Iterator<CallMetadataSyncData.Call> iterator = + callMetadataSyncData.getRequests().iterator(); + while (iterator.hasNext()) { + final CallMetadataSyncData.Call call = iterator.next(); + if (call.getId() != 0) { + // The call is already assigned an id; treat as control invocations. + for (int control : call.getControls()) { + processCallControlAction(call.getId(), control); + } + } + iterator.remove(); } - break; - case android.companion.Telecom.Call.SILENCE: - doSilence(); - break; - case android.companion.Telecom.Call.MUTE: - doMute(); - break; - case android.companion.Telecom.Call.UNMUTE: - doUnmute(); - break; - case android.companion.Telecom.Call.END: - if (crossDeviceCall != null) { - crossDeviceCall.doEnd(); - } - break; - case android.companion.Telecom.Call.PUT_ON_HOLD: - if (crossDeviceCall != null) { - crossDeviceCall.doPutOnHold(); - } - break; - case android.companion.Telecom.Call.TAKE_OFF_HOLD: - if (crossDeviceCall != null) { - crossDeviceCall.doTakeOffHold(); + } + + private void processCallControlAction(long crossDeviceCallId, + int callControlAction) { + final CrossDeviceCall crossDeviceCall = getCallForId(crossDeviceCallId, + mCurrentCalls.values()); + switch (callControlAction) { + case android.companion.Telecom.Call.ACCEPT: + if (crossDeviceCall != null) { + crossDeviceCall.doAccept(); + } + break; + case android.companion.Telecom.Call.REJECT: + if (crossDeviceCall != null) { + crossDeviceCall.doReject(); + } + break; + case android.companion.Telecom.Call.SILENCE: + doSilence(); + break; + case android.companion.Telecom.Call.MUTE: + doMute(); + break; + case android.companion.Telecom.Call.UNMUTE: + doUnmute(); + break; + case android.companion.Telecom.Call.END: + if (crossDeviceCall != null) { + crossDeviceCall.doEnd(); + } + break; + case android.companion.Telecom.Call.PUT_ON_HOLD: + if (crossDeviceCall != null) { + crossDeviceCall.doPutOnHold(); + } + break; + case android.companion.Telecom.Call.TAKE_OFF_HOLD: + if (crossDeviceCall != null) { + crossDeviceCall.doTakeOffHold(); + } + break; + default: } - break; - default: - } - } + } - @Override - void requestCrossDeviceSync(int userId) { - } + @Override + void requestCrossDeviceSync(AssociationInfo associationInfo) { + if (associationInfo.getUserId() == getUserId()) { + sync(associationInfo); + } + } - @Override - void updateStatus(int userId, boolean shouldSyncCallMetadata) { - if (userId == getUserId()) { - mShouldSync = shouldSyncCallMetadata; - if (shouldSyncCallMetadata) { - initializeCalls(); - } else { - mCurrentCalls.clear(); + @Override + void updateNumberOfActiveSyncAssociations(int userId, boolean added) { + if (userId == getUserId()) { + final boolean wasActivelySyncing = mNumberOfActiveSyncAssociations > 0; + if (added) { + mNumberOfActiveSyncAssociations++; + } else { + mNumberOfActiveSyncAssociations--; + } + if (!wasActivelySyncing && mNumberOfActiveSyncAssociations > 0) { + initializeCalls(); + } else if (wasActivelySyncing && mNumberOfActiveSyncAssociations <= 0) { + mCurrentCalls.clear(); + } + } } - } - } }; @Override public void onCreate() { super.onCreate(); - initializeCalls(); + if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCdmsi = LocalServices.getService(CompanionDeviceManagerServiceInternal.class); + mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback); + } } private void initializeCalls() { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.putAll(getCalls().stream().collect(Collectors.toMap(call -> call, call -> new CrossDeviceCall(getPackageManager(), call, getCallAudioState())))); + mCurrentCalls.keySet().forEach(call -> call.registerCallback(mTelecomCallback, + getMainThreadHandler())); + sync(getUserId()); } } @@ -139,33 +186,39 @@ public class CallMetadataSyncInCallService extends InCallService { @Override public void onCallAdded(Call call) { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.put(call, new CrossDeviceCall(getPackageManager(), call, getCallAudioState())); + call.registerCallback(mTelecomCallback); + sync(getUserId()); } } @Override public void onCallRemoved(Call call) { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.remove(call); + call.unregisterCallback(mTelecomCallback); + sync(getUserId()); } } @Override public void onMuteStateChanged(boolean isMuted) { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.values().forEach(call -> call.updateMuted(isMuted)); + sync(getUserId()); } } @Override public void onSilenceRinger() { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.values().forEach(call -> call.updateSilencedIfRinging()); + sync(getUserId()); } } @@ -183,4 +236,12 @@ public class CallMetadataSyncInCallService extends InCallService { telecomManager.silenceRinger(); } } + + private void sync(int userId) { + mCdmsi.crossDeviceSync(userId, mCurrentCalls.values()); + } + + private void sync(AssociationInfo associationInfo) { + mCdmsi.crossDeviceSync(associationInfo, mCurrentCalls.values()); + } }
\ No newline at end of file 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 3d8fb7a8d5bf..adc5faf24f2c 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 @@ -16,27 +16,32 @@ package com.android.server.companion.datatransfer.contextsync; +import static com.android.server.companion.transport.Transport.MESSAGE_REQUEST_CONTEXT_SYNC; + import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; +import android.companion.IOnMessageReceivedListener; +import android.companion.IOnTransportsChangedListener; import android.companion.Telecom; -import android.companion.Telecom.Call; import android.content.Context; +import android.os.Binder; import android.os.UserHandle; -import android.util.Pair; import android.util.Slog; +import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; +import android.util.proto.ProtoParseException; +import android.util.proto.ProtoUtils; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.companion.CompanionDeviceConfig; +import com.android.server.companion.transport.CompanionTransportManager; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -45,149 +50,308 @@ import java.util.Set; public class CrossDeviceSyncController { private static final String TAG = "CrossDeviceSyncController"; - private static final int BYTE_ARRAY_SIZE = 64; + + private static final int VERSION_1 = 1; + private static final int CURRENT_VERSION = VERSION_1; private final Context mContext; - private final Callback mCdmCallback; - private final Map<Integer, List<AssociationInfo>> mUserIdToAssociationInfo = new HashMap<>(); - private final Map<Integer, Pair<InputStream, OutputStream>> mAssociationIdToStreams = - new HashMap<>(); + private final CompanionTransportManager mCompanionTransportManager; + private final List<AssociationInfo> mConnectedAssociations = new ArrayList<>(); private final Set<Integer> mBlocklist = new HashSet<>(); - private CallMetadataSyncCallback mInCallServiceCallMetadataSyncCallback; + private CrossDeviceSyncControllerCallback mCrossDeviceSyncControllerCallback; - public CrossDeviceSyncController(Context context, Callback callback) { + public CrossDeviceSyncController(Context context, + CompanionTransportManager companionTransportManager) { mContext = context; - mCdmCallback = callback; + mCompanionTransportManager = companionTransportManager; + mCompanionTransportManager.addListener(new IOnTransportsChangedListener.Stub() { + @Override + public void onTransportsChanged(List<AssociationInfo> newAssociations) { + final long token = Binder.clearCallingIdentity(); + try { + if (!CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + return; + } + } finally { + Binder.restoreCallingIdentity(token); + } + final List<AssociationInfo> existingAssociations = new ArrayList<>( + mConnectedAssociations); + mConnectedAssociations.clear(); + mConnectedAssociations.addAll(newAssociations); + + if (mCrossDeviceSyncControllerCallback == null) { + Slog.w(TAG, "No callback to report transports changed"); + return; + } + for (AssociationInfo associationInfo : newAssociations) { + if (!existingAssociations.contains(associationInfo) + && !isAssociationBlocked(associationInfo.getId())) { + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); + } + } + for (AssociationInfo associationInfo : existingAssociations) { + if (!newAssociations.contains(associationInfo)) { + if (isAssociationBlocked(associationInfo.getId())) { + mBlocklist.remove(associationInfo.getId()); + } else { + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ false); + } + } + } + } + }); + mCompanionTransportManager.addListener(MESSAGE_REQUEST_CONTEXT_SYNC, + new IOnMessageReceivedListener.Stub() { + @Override + public void onMessageReceived(int associationId, byte[] data) { + if (mCrossDeviceSyncControllerCallback == null) { + Slog.w(TAG, "No callback to process context sync message"); + return; + } + mCrossDeviceSyncControllerCallback.processContextSyncMessage(associationId, + processTelecomDataFromSync(data)); + } + }); + } + + private boolean isAssociationBlocked(int associationId) { + return mBlocklist.contains(associationId); } /** Registers the call metadata callback. */ - public void registerCallMetadataSyncCallback(CallMetadataSyncCallback callback) { - mInCallServiceCallMetadataSyncCallback = callback; + public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback) { + mCrossDeviceSyncControllerCallback = callback; + for (AssociationInfo associationInfo : mConnectedAssociations) { + if (!isAssociationBlocked(associationInfo.getId())) { + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); + } + } } /** Allow specific associated devices to enable / disable syncing. */ public void setSyncEnabled(AssociationInfo associationInfo, boolean enabled) { if (enabled) { - if (mBlocklist.contains(associationInfo.getId())) { + if (isAssociationBlocked(associationInfo.getId())) { mBlocklist.remove(associationInfo.getId()); - openChannel(associationInfo); + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); } } else { - if (!mBlocklist.contains(associationInfo.getId())) { + if (!isAssociationBlocked(associationInfo.getId())) { mBlocklist.add(associationInfo.getId()); - closeChannel(associationInfo); + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ false); + // Send empty message to device to clear its data (otherwise it will get stale) + syncMessageToDevice(associationInfo.getId(), createEmptyMessage()); } } } + private boolean isAdminBlocked(int userId) { + return mContext.getSystemService(DevicePolicyManager.class) + .getBluetoothContactSharingDisabled(UserHandle.of(userId)); + } + /** - * Opens channels to newly associated devices, and closes channels to newly disassociated - * devices. + * Sync data to associated devices. * - * TODO(b/265466098): this needs to be limited to just connected devices + * @param userId The user whose data should be synced. + * @param calls The full list of current calls for all users. */ - public void onAssociationsChanged(int userId, List<AssociationInfo> newAssociationInfoList) { - final List<AssociationInfo> existingAssociationInfoList = mUserIdToAssociationInfo.get( - userId); - // Close channels to newly-disconnected devices. - for (AssociationInfo existingAssociationInfo : existingAssociationInfoList) { - if (!newAssociationInfoList.contains(existingAssociationInfo) && !mBlocklist.contains( - existingAssociationInfo.getId())) { - closeChannel(existingAssociationInfo); + public void syncToAllDevicesForUserId(int userId, Collection<CrossDeviceCall> calls) { + final Set<Integer> associationIds = new HashSet<>(); + for (AssociationInfo associationInfo : mConnectedAssociations) { + if (associationInfo.getUserId() == userId && !isAssociationBlocked( + associationInfo.getId())) { + associationIds.add(associationInfo.getId()); } } - // Open channels to newly-connected devices. - for (AssociationInfo newAssociationInfo : newAssociationInfoList) { - if (!existingAssociationInfoList.contains(newAssociationInfo) && !mBlocklist.contains( - newAssociationInfo.getId())) { - openChannel(newAssociationInfo); - } + if (associationIds.isEmpty()) { + Slog.w(TAG, "No eligible devices to sync to"); + return; } - mUserIdToAssociationInfo.put(userId, newAssociationInfoList); - } - private boolean isAdminBlocked(int userId) { - return mContext.getSystemService(DevicePolicyManager.class) - .getBluetoothContactSharingDisabled(UserHandle.of(userId)); + mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, + createCallUpdateMessage(calls, userId), + associationIds.stream().mapToInt(Integer::intValue).toArray()); } - /** Stop reading, close streams, and close secure channel. */ - private void closeChannel(AssociationInfo associationInfo) { - // TODO(b/265466098): stop reading from secure channel - final Pair<InputStream, OutputStream> streams = mAssociationIdToStreams.get( - associationInfo.getId()); - if (streams != null) { - try { - if (streams.first != null) { - streams.first.close(); - } - if (streams.second != null) { - streams.second.close(); - } - } catch (IOException e) { - Slog.e(TAG, "Could not close streams for association " + associationInfo.getId(), - e); - } + /** + * Sync data to associated devices. + * + * @param associationInfo The association whose data should be synced. + * @param calls The full list of current calls for all users. + */ + public void syncToSingleDevice(AssociationInfo associationInfo, + Collection<CrossDeviceCall> calls) { + if (isAssociationBlocked(associationInfo.getId())) { + Slog.e(TAG, "Cannot sync to requested device; connection is blocked"); + return; } - mCdmCallback.closeSecureChannel(associationInfo.getId()); - } - /** Sync initial snapshot and start reading. */ - private void openChannel(AssociationInfo associationInfo) { - final InputStream is = new ByteArrayInputStream(new byte[BYTE_ARRAY_SIZE]); - final OutputStream os = new ByteArrayOutputStream(BYTE_ARRAY_SIZE); - mAssociationIdToStreams.put(associationInfo.getId(), new Pair<>(is, os)); - mCdmCallback.createSecureChannel(associationInfo.getId(), is, os); - // TODO(b/265466098): only requestSync for this specific association / connection? - mInCallServiceCallMetadataSyncCallback.requestCrossDeviceSync(associationInfo.getUserId()); - // TODO(b/265466098): start reading from secure channel + mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, + createCallUpdateMessage(calls, associationInfo.getUserId()), + new int[]{associationInfo.getId()}); } /** * Sync data to associated devices. * - * @param userId The user whose data should be synced. - * @param calls The full list of current calls for all users. + * @param associationId The association whose data should be synced. + * @param message The message to sync. */ - public void crossDeviceSync(int userId, Collection<CrossDeviceCall> calls) { - final boolean isAdminBlocked = isAdminBlocked(userId); - for (AssociationInfo associationInfo : mUserIdToAssociationInfo.get(userId)) { - final Pair<InputStream, OutputStream> streams = mAssociationIdToStreams.get( - associationInfo.getId()); - final ProtoOutputStream pos = new ProtoOutputStream(streams.second); - final long telecomToken = pos.start(ContextSyncMessage.TELECOM); - for (CrossDeviceCall call : calls) { - final long callsToken = pos.start(Telecom.CALLS); - pos.write(Call.ID, call.getId()); - final long originToken = pos.start(Call.ORIGIN); - pos.write(Call.Origin.CALLER_ID, call.getReadableCallerId(isAdminBlocked)); - pos.write(Call.Origin.APP_ICON, call.getCallingAppIcon()); - pos.write(Call.Origin.APP_NAME, call.getCallingAppName()); - pos.end(originToken); - pos.write(Call.STATUS, call.getStatus()); - for (int control : call.getControls()) { - pos.write(Call.CONTROLS_AVAILABLE, control); + public void syncMessageToDevice(int associationId, byte[] message) { + if (isAssociationBlocked(associationId)) { + Slog.e(TAG, "Cannot sync to requested device; connection is blocked"); + return; + } + + mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, message, + new int[]{associationId}); + } + + @VisibleForTesting + CallMetadataSyncData processTelecomDataFromSync(byte[] data) { + final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData(); + final ProtoInputStream pis = new ProtoInputStream(data); + try { + int version = -1; + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) ContextSyncMessage.VERSION: + version = pis.readInt(ContextSyncMessage.VERSION); + Slog.e(TAG, "Processing context sync message version " + version); + break; + case (int) ContextSyncMessage.TELECOM: + if (version == VERSION_1) { + final long telecomToken = pis.start(ContextSyncMessage.TELECOM); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) Telecom.CALLS) { + final long callsToken = pis.start(Telecom.CALLS); + callMetadataSyncData.addCall(processCallDataFromSync(pis)); + pis.end(callsToken); + } else if (pis.getFieldNumber() == (int) Telecom.REQUESTS) { + final long requestsToken = pis.start(Telecom.REQUESTS); + callMetadataSyncData.addRequest(processCallDataFromSync(pis)); + pis.end(requestsToken); + } else { + Slog.e(TAG, "Unhandled field in Telecom:" + + ProtoUtils.currentFieldToString(pis)); + } + } + pis.end(telecomToken); + } else { + Slog.e(TAG, "Cannot process unsupported version " + version); + } + break; + default: + Slog.e(TAG, "Unhandled field in ContextSyncMessage:" + + ProtoUtils.currentFieldToString(pis)); } - pos.end(callsToken); } - pos.end(telecomToken); - pos.flush(); + } catch (IOException | ProtoParseException e) { + throw new RuntimeException(e); } + return callMetadataSyncData; } - /** - * Callback to be implemented by CompanionDeviceManagerService. - */ - public interface Callback { - /** - * Create a secure channel to send messages. - */ - void createSecureChannel(int associationId, InputStream input, OutputStream output); - - /** - * Close the secure channel created previously. - */ - void closeSecureChannel(int associationId); + @VisibleForTesting + CallMetadataSyncData.Call processCallDataFromSync(ProtoInputStream pis) throws IOException { + final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) Telecom.Call.ID: + call.setId(pis.readLong(Telecom.Call.ID)); + break; + case (int) Telecom.Call.ORIGIN: + final long originToken = pis.start(Telecom.Call.ORIGIN); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) Telecom.Call.Origin.APP_ICON: + call.setAppIcon(pis.readBytes(Telecom.Call.Origin.APP_ICON)); + break; + case (int) Telecom.Call.Origin.APP_NAME: + call.setAppName(pis.readString(Telecom.Call.Origin.APP_NAME)); + break; + case (int) Telecom.Call.Origin.CALLER_ID: + call.setCallerId(pis.readString(Telecom.Call.Origin.CALLER_ID)); + break; + case (int) Telecom.Call.Origin.APP_IDENTIFIER: + call.setAppIdentifier( + pis.readString(Telecom.Call.Origin.APP_IDENTIFIER)); + break; + default: + Slog.e(TAG, "Unhandled field in Origin:" + + ProtoUtils.currentFieldToString(pis)); + } + } + pis.end(originToken); + break; + case (int) Telecom.Call.STATUS: + call.setStatus(pis.readInt(Telecom.Call.STATUS)); + break; + case (int) Telecom.Call.CONTROLS: + call.addControl(pis.readInt(Telecom.Call.CONTROLS)); + break; + default: + Slog.e(TAG, + "Unhandled field in Telecom:" + ProtoUtils.currentFieldToString(pis)); + } + } + return call; + } + + @VisibleForTesting + byte[] createCallUpdateMessage(Collection<CrossDeviceCall> calls, int userId) { + final ProtoOutputStream pos = new ProtoOutputStream(); + pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION); + final long telecomToken = pos.start(ContextSyncMessage.TELECOM); + for (CrossDeviceCall call : calls) { + final long callsToken = pos.start(Telecom.CALLS); + pos.write(Telecom.Call.ID, call.getId()); + final long originToken = pos.start(Telecom.Call.ORIGIN); + pos.write(Telecom.Call.Origin.CALLER_ID, + call.getReadableCallerId(isAdminBlocked(userId))); + pos.write(Telecom.Call.Origin.APP_ICON, call.getCallingAppIcon()); + pos.write(Telecom.Call.Origin.APP_NAME, call.getCallingAppName()); + pos.write(Telecom.Call.Origin.APP_IDENTIFIER, call.getCallingAppPackageName()); + pos.end(originToken); + pos.write(Telecom.Call.STATUS, call.getStatus()); + for (int control : call.getControls()) { + pos.write(Telecom.Call.CONTROLS, control); + } + pos.end(callsToken); + } + pos.end(telecomToken); + return pos.getBytes(); + } + + /** Create a call control message. */ + public static byte[] createCallControlMessage(long callId, int control) { + final ProtoOutputStream pos = new ProtoOutputStream(); + pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION); + final long telecomToken = pos.start(ContextSyncMessage.TELECOM); + final long requestsToken = pos.start(Telecom.REQUESTS); + pos.write(Telecom.Call.ID, callId); + pos.write(Telecom.Call.CONTROLS, control); + pos.end(requestsToken); + pos.end(telecomToken); + return pos.getBytes(); + } + + /** Create an empty context sync message, used to clear state. */ + public static byte[] createEmptyMessage() { + final ProtoOutputStream pos = new ProtoOutputStream(); + pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION); + return pos.getBytes(); } } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncCallback.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java index 7c339d213483..31e10a814568 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncCallback.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java @@ -16,12 +16,14 @@ package com.android.server.companion.datatransfer.contextsync; +import android.companion.AssociationInfo; + /** Callback for call metadata syncing. */ -public abstract class CallMetadataSyncCallback { +public abstract class CrossDeviceSyncControllerCallback { - abstract void processCallControlAction(int crossDeviceCallId, int callControlAction); + void processContextSyncMessage(int associationId, CallMetadataSyncData callMetadataSyncData) {} - abstract void requestCrossDeviceSync(int userId); + void requestCrossDeviceSync(AssociationInfo associationInfo) {} - abstract void updateStatus(int userId, boolean shouldSyncCallMetadata); + void updateNumberOfActiveSyncAssociations(int userId, boolean added) {} } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 678d58232f15..ca50af8075c6 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -52,6 +52,7 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVI import static android.os.PowerExemptionManager.REASON_ACTIVE_DEVICE_ADMIN; import static android.os.PowerExemptionManager.REASON_ACTIVITY_STARTER; import static android.os.PowerExemptionManager.REASON_ACTIVITY_VISIBILITY_GRACE_PERIOD; +import static android.os.PowerExemptionManager.REASON_ALARM_MANAGER_ALARM_CLOCK; import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE; import static android.os.PowerExemptionManager.REASON_BACKGROUND_ACTIVITY_PERMISSION; import static android.os.PowerExemptionManager.REASON_BACKGROUND_FGS_PERMISSION; @@ -458,11 +459,12 @@ public final class ActiveServices { public void updateBackgroundRestrictedForUidPackage(int uid, String packageName, boolean restricted) { synchronized (mAm) { - if (!isForegroundServiceAllowedInBackgroundRestricted(uid, packageName)) { - stopAllForegroundServicesLocked(uid, packageName); - } mAm.mProcessList.updateBackgroundRestrictedForUidPackageLocked( uid, packageName, restricted); + if (!isForegroundServiceAllowedInBackgroundRestricted(uid, packageName) + && !isTempAllowedByAlarmClock(uid)) { + stopAllForegroundServicesLocked(uid, packageName); + } } } } @@ -475,7 +477,11 @@ public final class ActiveServices { final ServiceRecord r = smap.mServicesByInstanceName.valueAt(i); if (uid == r.serviceInfo.applicationInfo.uid || packageName.equals(r.serviceInfo.packageName)) { - if (r.isForeground) { + // If the FGS is started by temp allowlist of alarm-clock + // (REASON_ALARM_MANAGER_ALARM_CLOCK), allow it to continue and do not stop it, + // even the app is background-restricted. + if (r.isForeground + && r.mAllowStartForegroundAtEntering != REASON_ALARM_MANAGER_ALARM_CLOCK) { toStop.add(r); } } @@ -762,6 +768,15 @@ public final class ActiveServices { } } + private static void traceInstant(@NonNull String message, @NonNull ServiceRecord service) { + if (!Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + return; + } + final String serviceName = (service.getComponentName() != null) + ? service.getComponentName().toShortString() : "(?)"; + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, message + serviceName); + } + ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType, int callingPid, int callingUid, boolean fgRequired, String callingPackage, @Nullable String callingFeatureId, final int userId, boolean isSdkSandboxService, @@ -818,6 +833,9 @@ public final class ActiveServices { } ServiceRecord r = res.record; + + traceInstant("startService(): ", r); + // Note, when startService() or startForegroundService() is called on an already // running SHORT_SERVICE FGS, the call will succeed (i.e. we won't throw // ForegroundServiceStartNotAllowedException), even when the service is already timed @@ -860,7 +878,9 @@ public final class ActiveServices { // start analogously to the legacy-app forced-restrictions case, regardless // of its target SDK version. boolean forcedStandby = false; - if (bgLaunch && appRestrictedAnyInBackground(appUid, appPackageName)) { + if (bgLaunch + && appRestrictedAnyInBackground(appUid, appPackageName) + && !isTempAllowedByAlarmClock(appUid)) { if (DEBUG_FOREGROUND_SERVICE) { Slog.d(TAG, "Forcing bg-only service start only for " + r.shortInstanceName + " : bgLaunch=" + bgLaunch + " callerFg=" + callerFg); @@ -1407,6 +1427,7 @@ public final class ActiveServices { } private void stopServiceLocked(ServiceRecord service, boolean enqueueOomAdj) { + traceInstant("stopService(): ", service); try { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "stopServiceLocked()"); if (service.delayed) { @@ -1918,6 +1939,20 @@ public final class ActiveServices { && isForegroundServiceAllowedInBackgroundRestricted(app); } + /* + * If the FGS start is temp allowlisted by alarm-clock(REASON_ALARM_MANAGER_ALARM_CLOCK), it is + * allowed even the app is background-restricted. + */ + private boolean isTempAllowedByAlarmClock(int uid) { + final ActivityManagerService.FgsTempAllowListItem item = + mAm.isAllowlistedForFgsStartLOSP(uid); + if (item != null) { + return item.mReasonCode == REASON_ALARM_MANAGER_ALARM_CLOCK; + } else { + return false; + } + } + void logFgsApiBeginLocked(int uid, int pid, int apiType) { synchronized (mFGSLogger) { mFGSLogger.logForegroundServiceApiEventBegin(uid, pid, apiType, ""); @@ -1946,6 +1981,7 @@ public final class ActiveServices { if (notification == null) { throw new IllegalArgumentException("null notification"); } + traceInstant("startForeground(): ", r); final int foregroundServiceStartType = foregroundServiceType; // Instant apps need permission to create foreground services. if (r.appInfo.isInstantApp()) { @@ -2050,7 +2086,8 @@ public final class ActiveServices { // Apps that are TOP or effectively similar may call startForeground() on // their services even if they are restricted from doing that while in bg. if (!ignoreForeground - && !isForegroundServiceAllowedInBackgroundRestricted(r.app)) { + && !isForegroundServiceAllowedInBackgroundRestricted(r.app) + && !isTempAllowedByAlarmClock(r.app.uid)) { Slog.w(TAG, "Service.startForeground() not allowed due to bg restriction: service " + r.shortInstanceName); @@ -2484,6 +2521,7 @@ public final class ActiveServices { } } else { if (r.isForeground) { + traceInstant("stopForeground(): ", r); final ServiceMap smap = getServiceMapLocked(r.userId); if (smap != null) { decActiveForegroundAppLocked(smap, r); @@ -3311,6 +3349,7 @@ public final class ActiveServices { Slog.i(TAG_SERVICE, "Short FGS started: " + sr); } } + traceInstant("short FGS start/extend: ", sr); sr.setShortFgsInfo(SystemClock.uptimeMillis()); // We'll restart the timeout. @@ -3356,10 +3395,11 @@ public final class ActiveServices { return; } Slog.e(TAG_SERVICE, "Short FGS timed out: " + sr); - final long now = SystemClock.uptimeMillis(); + traceInstant("short FGS timeout: ", sr); + logFGSStateChangeLocked(sr, FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT, - now > sr.mFgsEnterTime ? (int) (now - sr.mFgsEnterTime) : 0, + nowUptime > sr.mFgsEnterTime ? (int) (nowUptime - sr.mFgsEnterTime) : 0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN); try { @@ -3410,6 +3450,7 @@ public final class ActiveServices { } Slog.e(TAG_SERVICE, "Short FGS procstate demoted: " + sr); + traceInstant("short FGS demote: ", sr); mAm.updateOomAdjLocked(sr.app, OOM_ADJ_REASON_SHORT_FGS_TIMEOUT); } @@ -3440,6 +3481,9 @@ public final class ActiveServices { } else { Slog.e(TAG_SERVICE, message); } + + traceInstant("short FGS ANR: ", sr); + mAm.appNotResponding(sr.app, tr); // TODO: Can we close the ANR dialog here, if it's still shown? Currently, the ANR @@ -8196,7 +8240,11 @@ public final class ActiveServices { : ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT, 0 /* api_sate */, null /* api_type */, - null /* api_timestamp */); + null /* api_timestamp */, + mAm.getUidStateLocked(r.appInfo.uid), + mAm.getUidProcessCapabilityLocked(r.appInfo.uid), + mAm.getUidStateLocked(r.mRecentCallingUid), + mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid)); int event = 0; if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index a54e8e95b155..1a5d4253b5ba 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5190,7 +5190,10 @@ public class ActivityManagerService extends IActivityManager.Stub throw new IllegalArgumentException( "Can't use FLAG_RECEIVER_BOOT_UPGRADE here"); } - if (PendingIntent.isNewMutableDisallowedImplicitPendingIntent(flags, intent)) { + boolean isActivityResultType = + type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT; + if (PendingIntent.isNewMutableDisallowedImplicitPendingIntent(flags, intent, + isActivityResultType)) { boolean isChangeEnabled = CompatChanges.isChangeEnabled( PendingIntent.BLOCK_MUTABLE_IMPLICIT_PENDING_INTENT, owningUid); @@ -6915,7 +6918,7 @@ public class ActivityManagerService extends IActivityManager.Stub mActivityTaskManager.unhandledBack(); } - // TODO: Move to ContentProviderHelper? + // TODO: Replace this method with one that returns a bound IContentProvider. public ParcelFileDescriptor openContentUri(String uriString) throws RemoteException { enforceNotIsolatedCaller("openContentUri"); final int userId = UserHandle.getCallingUserId(); @@ -6944,6 +6947,16 @@ public class ActivityManagerService extends IActivityManager.Stub Log.e(TAG, "Cannot find package for uid: " + uid); return null; } + + final ApplicationInfo appInfo = mPackageManagerInt.getApplicationInfo( + androidPackage.getPackageName(), /*flags*/0, Process.SYSTEM_UID, + UserHandle.USER_SYSTEM); + if (!appInfo.isVendor() && !appInfo.isSystemApp() && !appInfo.isSystemExt() + && !appInfo.isProduct()) { + Log.e(TAG, "openContentUri may only be used by vendor/system/product."); + return null; + } + final AttributionSource attributionSource = new AttributionSource( Binder.getCallingUid(), androidPackage.getPackageName(), null); pfd = cph.provider.openFile(attributionSource, uri, "r", null); diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 1426cfd65286..3bc5de91b2bd 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -2100,9 +2100,12 @@ public final class CachedAppOptimizer { final boolean frozen; final ProcessCachedOptimizerRecord opt = proc.mOptRecord; - opt.setPendingFreeze(false); - synchronized (mProcLock) { + // someone has canceled this freeze + if (!opt.isPendingFreeze()) { + return; + } + opt.setPendingFreeze(false); pid = proc.getPid(); if (mFreezerOverride) { @@ -2148,7 +2151,6 @@ public final class CachedAppOptimizer { try { traceAppFreeze(proc.processName, pid, -1); Process.setProcessFrozen(pid, proc.uid, true); - opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis()); opt.setFrozen(true); opt.setHasCollectedFrozenPSS(false); diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java index 8f84b08ff6a6..79089074dd1c 100644 --- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java +++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java @@ -28,12 +28,13 @@ import static android.app.ActivityManager.FOREGROUND_SERVICE_API_TYPE_USB; import static android.os.Process.INVALID_UID; import android.annotation.IntDef; +import android.app.ActivityManager; import android.app.ActivityManager.ForegroundServiceApiType; import android.app.ForegroundServiceDelegationOptions; import android.content.ComponentName; import android.content.pm.ServiceInfo; import android.util.ArrayMap; -import android.util.Log; +import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; @@ -191,13 +192,20 @@ public class ForegroundServiceTypeLoggerModule { final ArrayList<Integer> apiTypes = convertFgsTypeToApiTypes(record.foregroundServiceType); final UidState uidState = mUids.get(uid); if (uidState == null) { - Log.e(TAG, "FGS stop call being logged with no start call for UID " + uid); + Slog.wtfStack(TAG, "FGS stop call being logged with no start call for UID for UID " + + uid + + " in package " + record.packageName); return; } final ArrayList<Integer> apisFound = new ArrayList<>(); final ArrayList<Long> timestampsFound = new ArrayList<>(); for (int i = 0, size = apiTypes.size(); i < size; i++) { - int apiType = apiTypes.get(i); + final int apiType = apiTypes.get(i); + if (!uidState.mOpenWithFgsCount.contains(apiType)) { + Slog.wtfStack(TAG, "Logger should be tracking FGS types correctly for UID " + uid + + " in package " + record.packageName); + continue; + } // retrieve the eligible closed call // we only want to log if this is the only // open in flight call. If there are other calls, @@ -214,7 +222,8 @@ public class ForegroundServiceTypeLoggerModule { final ArrayMap<ComponentName, ServiceRecord> runningFgsOfType = uidState.mRunningFgs.get(apiType); if (runningFgsOfType == null) { - Log.w(TAG, "Could not find appropriate running FGS for FGS stop"); + Slog.w(TAG, "Could not find appropriate running FGS for FGS stop for UID " + uid + + " in package " + record.packageName); continue; } @@ -321,7 +330,7 @@ public class ForegroundServiceTypeLoggerModule { // it's not related to any FGS UidState uidState = mUids.get(uid); if (uidState == null) { - Log.w(TAG, "API event end called before start!"); + Slog.w(TAG, "API event end called before start!"); return -1; } if (uidState.mOpenWithFgsCount.contains(apiType)) { @@ -466,7 +475,11 @@ public class ForegroundServiceTypeLoggerModule { : ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT, apiState, apiType, - timestamp); + timestamp, + ActivityManager.PROCESS_STATE_UNKNOWN, + ActivityManager.PROCESS_CAPABILITY_NONE, + ActivityManager.PROCESS_STATE_UNKNOWN, + ActivityManager.PROCESS_CAPABILITY_NONE); } /** @@ -500,7 +513,11 @@ public class ForegroundServiceTypeLoggerModule { 0, apiState, apiType, - timestamp); + timestamp, + ActivityManager.PROCESS_STATE_UNKNOWN, + ActivityManager.PROCESS_CAPABILITY_NONE, + ActivityManager.PROCESS_STATE_UNKNOWN, + ActivityManager.PROCESS_CAPABILITY_NONE); } /** diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 78aafeba3b22..6551db9ad783 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -643,6 +643,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN pw.print(prefix); pw.print("lastUntrustedSetFgsRestrictionAllowedTime="); TimeUtils.formatDuration(mLastUntrustedSetFgsRestrictionAllowedTime, now, pw); + pw.println(); if (delayed) { pw.print(prefix); pw.print("delayed="); pw.println(delayed); diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java index 6da6a6ecda52..412fbe797758 100644 --- a/services/core/java/com/android/server/am/UserSwitchingDialog.java +++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java @@ -266,12 +266,11 @@ class UserSwitchingDialog extends Dialog { } private void startProgressAnimation(Runnable onAnimationEnd) { - if (mDisableAnimations) { + final AnimatedVectorDrawable avd = getSpinnerAVD(); + if (mDisableAnimations || avd == null) { onAnimationEnd.run(); return; } - final ImageView progressCircular = findViewById(R.id.progress_circular); - final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) progressCircular.getDrawable(); avd.registerAnimationCallback(new Animatable2.AnimationCallback() { @Override public void onAnimationEnd(Drawable drawable) { @@ -281,7 +280,23 @@ class UserSwitchingDialog extends Dialog { avd.start(); } + private AnimatedVectorDrawable getSpinnerAVD() { + final ImageView view = findViewById(R.id.progress_circular); + if (view != null) { + final Drawable drawable = view.getDrawable(); + if (drawable instanceof AnimatedVectorDrawable) { + return (AnimatedVectorDrawable) drawable; + } + } + return null; + } + private void startDialogAnimation(Animation animation, Runnable onAnimationEnd) { + final View view = findViewById(R.id.content); + if (mDisableAnimations || view == null) { + onAnimationEnd.run(); + return; + } animation.setDuration(DIALOG_SHOW_HIDE_ANIMATION_DURATION_MS); animation.setAnimationListener(new Animation.AnimationListener() { @Override @@ -299,7 +314,7 @@ class UserSwitchingDialog extends Dialog { } }); - findViewById(R.id.content).startAnimation(animation); + view.startAnimation(animation); } private void asyncTraceBegin(String subTag, int subCookie) { diff --git a/services/core/java/com/android/server/biometrics/sensors/SensorList.java b/services/core/java/com/android/server/biometrics/sensors/SensorList.java new file mode 100644 index 000000000000..1cff92fba31d --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/SensorList.java @@ -0,0 +1,97 @@ +/* + * 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.biometrics.sensors; + +import android.app.IActivityManager; +import android.app.SynchronousUserSwitchObserver; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; +import android.util.SparseArray; + +/** + * Keep track of the sensors that is supported by the HAL. + * @param <T> T is either face sensor or fingerprint sensor. + */ +public class SensorList<T> { + private static final String TAG = "SensorList"; + private final SparseArray<T> mSensors; + private final IActivityManager mActivityManager; + + public SensorList(IActivityManager activityManager) { + mSensors = new SparseArray<T>(); + mActivityManager = activityManager; + } + + /** + * Adding sensor to the map with the sensor id as key. Also, starts a session if the user Id is + * NULL. + */ + public void addSensor(int sensorId, T sensor, int sessionUserId, + SynchronousUserSwitchObserver userSwitchObserver) { + mSensors.put(sensorId, sensor); + registerUserSwitchObserver(sessionUserId, userSwitchObserver); + } + + private void registerUserSwitchObserver(int sessionUserId, + SynchronousUserSwitchObserver userSwitchObserver) { + try { + mActivityManager.registerUserSwitchObserver(userSwitchObserver, + TAG); + if (sessionUserId == UserHandle.USER_NULL) { + userSwitchObserver.onUserSwitching(UserHandle.USER_SYSTEM); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to register user switch observer"); + } + } + + /** + * Returns the sensor corresponding to the key at a specific position. + */ + public T valueAt(int position) { + return mSensors.valueAt(position); + } + + /** + * Returns the sensor associated with sensorId as key. + */ + public T get(int sensorId) { + return mSensors.get(sensorId); + } + + /** + * Returns the sensorId at the specified position. + */ + public int keyAt(int position) { + return mSensors.keyAt(position); + } + + /** + * Returns the number of sensors added. + */ + public int size() { + return mSensors.size(); + } + + /** + * Returns true if a sensor exists for the specified sensorId. + */ + public boolean contains(int sensorId) { + return mSensors.contains(sensorId); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index c5037b7012f2..a50164718417 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityTaskManager; +import android.app.SynchronousUserSwitchObserver; import android.app.TaskStackListener; import android.content.Context; import android.content.pm.UserInfo; @@ -41,9 +42,9 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.os.UserManager; import android.util.Slog; -import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import android.view.Surface; @@ -62,6 +63,7 @@ import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.InvalidationRequesterClient; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.PerformanceTracker; +import com.android.server.biometrics.sensors.SensorList; import com.android.server.biometrics.sensors.face.FaceUtils; import com.android.server.biometrics.sensors.face.ServiceProvider; import com.android.server.biometrics.sensors.face.UsageStats; @@ -86,7 +88,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull @VisibleForTesting - final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports + final SensorList<Sensor> mFaceSensors; @NonNull private final Context mContext; @NonNull @@ -117,8 +119,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void onTaskStackChanged() { mHandler.post(() -> { - for (int i = 0; i < mSensors.size(); i++) { - final BaseClientMonitor client = mSensors.valueAt(i).getScheduler() + for (int i = 0; i < mFaceSensors.size(); i++) { + final BaseClientMonitor client = mFaceSensors.valueAt(i).getScheduler() .getCurrentClient(); if (!(client instanceof AuthenticationClient)) { Slog.e(getTag(), "Task stack changed for client: " + client); @@ -133,7 +135,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { && !client.isAlreadyDone()) { Slog.e(getTag(), "Stopping background authentication," + " currentClient: " + client); - mSensors.valueAt(i).getScheduler().cancelAuthenticationOrDetection( + mFaceSensors.valueAt(i).getScheduler().cancelAuthenticationOrDetection( client.getToken(), client.getRequestId()); } } @@ -150,7 +152,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mContext = context; mBiometricStateCallback = biometricStateCallback; mHalInstanceName = halInstanceName; - mSensors = new SparseArray<>(); + mFaceSensors = new SensorList<>(ActivityManager.getService()); mHandler = new Handler(Looper.getMainLooper()); mUsageStats = new UsageStats(context); mLockoutResetDispatcher = lockoutResetDispatcher; @@ -178,8 +180,15 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { false /* resetLockoutRequiresChallenge */); final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler, internalProp, lockoutResetDispatcher, mBiometricContext); - - mSensors.put(sensorId, sensor); + final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFaceSensors.addSensor(sensorId, sensor, userId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + } + }); Slog.d(getTag(), "Added: " + internalProp); } } @@ -223,8 +232,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { Slog.e(getTag(), "Unable to linkToDeath", e); } - for (int i = 0; i < mSensors.size(); i++) { - final int sensorId = mSensors.keyAt(i); + for (int i = 0; i < mFaceSensors.size(); i++) { + final int sensorId = mFaceSensors.keyAt(i); scheduleLoadAuthenticatorIds(sensorId); scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(), null /* callback */); @@ -234,20 +243,20 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } private void scheduleForSensor(int sensorId, @NonNull BaseClientMonitor client) { - if (!mSensors.contains(sensorId)) { + if (!mFaceSensors.contains(sensorId)) { throw new IllegalStateException("Unable to schedule client: " + client + " for sensor: " + sensorId); } - mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); + mFaceSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); } private void scheduleForSensor(int sensorId, @NonNull BaseClientMonitor client, ClientMonitorCallback callback) { - if (!mSensors.contains(sensorId)) { + if (!mFaceSensors.contains(sensorId)) { throw new IllegalStateException("Unable to schedule client: " + client + " for sensor: " + sensorId); } - mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client, callback); + mFaceSensors.get(sensorId).getScheduler().scheduleClientMonitor(client, callback); } private void scheduleLoadAuthenticatorIds(int sensorId) { @@ -259,12 +268,12 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) { mHandler.post(() -> { final FaceGetAuthenticatorIdClient client = new FaceGetAuthenticatorIdClient( - mContext, mSensors.get(sensorId).getLazySession(), userId, + mContext, mFaceSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mSensors.get(sensorId).getAuthenticatorIds()); + mFaceSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client); }); @@ -283,15 +292,15 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public boolean containsSensor(int sensorId) { - return mSensors.contains(sensorId); + return mFaceSensors.contains(sensorId); } @NonNull @Override public List<FaceSensorPropertiesInternal> getSensorProperties() { final List<FaceSensorPropertiesInternal> props = new ArrayList<>(); - for (int i = 0; i < mSensors.size(); ++i) { - props.add(mSensors.valueAt(i).getSensorProperties()); + for (int i = 0; i < mFaceSensors.size(); ++i) { + props.add(mFaceSensors.valueAt(i).getSensorProperties()); } return props; } @@ -299,7 +308,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull @Override public FaceSensorPropertiesInternal getSensorProperties(int sensorId) { - return mSensors.get(sensorId).getSensorProperties(); + return mFaceSensors.get(sensorId).getSensorProperties(); } @NonNull @@ -318,11 +327,11 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull IInvalidationCallback callback) { mHandler.post(() -> { final FaceInvalidationClient client = new FaceInvalidationClient(mContext, - mSensors.get(sensorId).getLazySession(), userId, sensorId, + mFaceSensors.get(sensorId).getLazySession(), userId, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mSensors.get(sensorId).getAuthenticatorIds(), callback); + mFaceSensors.get(sensorId).getAuthenticatorIds(), callback); scheduleForSensor(sensorId, client); }); } @@ -335,7 +344,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public long getAuthenticatorId(int sensorId, int userId) { - return mSensors.get(sensorId).getAuthenticatorIds().getOrDefault(userId, 0L); + return mFaceSensors.get(sensorId).getAuthenticatorIds().getOrDefault(userId, 0L); } @Override @@ -348,7 +357,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull IFaceServiceReceiver receiver, String opPackageName) { mHandler.post(() -> { final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext, - mSensors.get(sensorId).getLazySession(), token, + mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), @@ -362,7 +371,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull String opPackageName, long challenge) { mHandler.post(() -> { final FaceRevokeChallengeClient client = new FaceRevokeChallengeClient(mContext, - mSensors.get(sensorId).getLazySession(), token, userId, opPackageName, sensorId, + mFaceSensors.get(sensorId).getLazySession(), token, userId, + opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, challenge); @@ -377,10 +387,10 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Nullable Surface previewSurface, boolean debugConsent) { final long id = mRequestCounter.incrementAndGet(); mHandler.post(() -> { - final int maxTemplatesPerUser = mSensors.get( + final int maxTemplatesPerUser = mFaceSensors.get( sensorId).getSensorProperties().maxEnrollmentsPerUser; final FaceEnrollClient client = new FaceEnrollClient(mContext, - mSensors.get(sensorId).getLazySession(), token, + mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, opPackageName, id, FaceUtils.getInstance(sensorId), disabledFeatures, ENROLL_TIMEOUT_SEC, previewSurface, sensorId, @@ -406,7 +416,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void cancelEnrollment(int sensorId, @NonNull IBinder token, long requestId) { mHandler.post(() -> - mSensors.get(sensorId).getScheduler().cancelEnrollment(token, requestId)); + mFaceSensors.get(sensorId).getScheduler().cancelEnrollment(token, requestId)); } @Override @@ -419,7 +429,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mHandler.post(() -> { final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId); final FaceDetectClient client = new FaceDetectClient(mContext, - mSensors.get(sensorId).getLazySession(), + mFaceSensors.get(sensorId).getLazySession(), token, id, callback, options, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric); @@ -431,7 +441,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void cancelFaceDetect(int sensorId, @NonNull IBinder token, long requestId) { - mHandler.post(() -> mSensors.get(sensorId).getScheduler() + mHandler.post(() -> mFaceSensors.get(sensorId).getScheduler() .cancelAuthenticationOrDetection(token, requestId)); } @@ -446,12 +456,12 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { final int sensorId = options.getSensorId(); final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId); final FaceAuthenticationClient client = new FaceAuthenticationClient( - mContext, mSensors.get(sensorId).getLazySession(), token, requestId, callback, - operationId, restricted, options, cookie, + mContext, mFaceSensors.get(sensorId).getLazySession(), token, requestId, + callback, operationId, restricted, options, cookie, false /* requireConfirmation */, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric, - mUsageStats, mSensors.get(sensorId).getLockoutCache(), + mUsageStats, mFaceSensors.get(sensorId).getLockoutCache(), allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId)); scheduleForSensor(sensorId, client, new ClientMonitorCallback() { @Override @@ -486,7 +496,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void cancelAuthentication(int sensorId, @NonNull IBinder token, long requestId) { - mHandler.post(() -> mSensors.get(sensorId).getScheduler() + mHandler.post(() -> mFaceSensors.get(sensorId).getScheduler() .cancelAuthenticationOrDetection(token, requestId)); } @@ -514,13 +524,13 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { int userId, @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { mHandler.post(() -> { final FaceRemovalClient client = new FaceRemovalClient(mContext, - mSensors.get(sensorId).getLazySession(), token, + mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), faceIds, userId, opPackageName, FaceUtils.getInstance(sensorId), sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mSensors.get(sensorId).getAuthenticatorIds()); + mFaceSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -529,12 +539,12 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { public void scheduleResetLockout(int sensorId, int userId, @NonNull byte[] hardwareAuthToken) { mHandler.post(() -> { final FaceResetLockoutClient client = new FaceResetLockoutClient( - mContext, mSensors.get(sensorId).getLazySession(), userId, + mContext, mFaceSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, hardwareAuthToken, - mSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, + mFaceSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId)); scheduleForSensor(sensorId, client); @@ -553,7 +563,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { return; } final FaceSetFeatureClient client = new FaceSetFeatureClient(mContext, - mSensors.get(sensorId).getLazySession(), token, + mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, mContext.getOpPackageName(), sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext, @@ -573,7 +583,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { return; } final FaceGetFeatureClient client = new FaceGetFeatureClient(mContext, - mSensors.get(sensorId).getLazySession(), token, callback, userId, + mFaceSensors.get(sensorId).getLazySession(), token, callback, userId, mContext.getOpPackageName(), sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext); scheduleForSensor(sensorId, client); @@ -583,7 +593,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void startPreparedClient(int sensorId, int cookie) { mHandler.post(() -> { - mSensors.get(sensorId).getScheduler().startPreparedClient(cookie); + mFaceSensors.get(sensorId).getScheduler().startPreparedClient(cookie); }); } @@ -599,13 +609,13 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mHandler.post(() -> { final FaceInternalCleanupClient client = new FaceInternalCleanupClient(mContext, - mSensors.get(sensorId).getLazySession(), userId, + mFaceSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_ENUMERATE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, FaceUtils.getInstance(sensorId), - mSensors.get(sensorId).getAuthenticatorIds()); + mFaceSensors.get(sensorId).getAuthenticatorIds()); if (favorHalEnrollments) { client.setFavorHalEnrollments(); } @@ -622,8 +632,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto, boolean clearSchedulerBuffer) { - if (mSensors.contains(sensorId)) { - mSensors.get(sensorId).dumpProtoState(sensorId, proto, clearSchedulerBuffer); + if (mFaceSensors.contains(sensorId)) { + mFaceSensors.get(sensorId).dumpProtoState(sensorId, proto, clearSchedulerBuffer); } } @@ -672,7 +682,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { pw.println(mBiometricContext.getAuthSessionCoordinator()); pw.println("---AuthSessionCoordinator logs end ---"); - mSensors.get(sensorId).getScheduler().dump(pw); + mFaceSensors.get(sensorId).getScheduler().dump(pw); mUsageStats.print(pw); } @@ -680,7 +690,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName) { - return mSensors.get(sensorId).createTestSession(callback); + return mFaceSensors.get(sensorId).createTestSession(callback); } @Override @@ -692,9 +702,9 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { Slog.e(getTag(), "HAL died"); mHandler.post(() -> { mDaemon = null; - for (int i = 0; i < mSensors.size(); i++) { - final Sensor sensor = mSensors.valueAt(i); - final int sensorId = mSensors.keyAt(i); + for (int i = 0; i < mFaceSensors.size(); i++) { + final Sensor sensor = mFaceSensors.valueAt(i); + final int sensorId = mFaceSensors.keyAt(i); PerformanceTracker.getInstanceForSensorId(sensorId).incrementHALDeathCount(); sensor.onBinderDied(); } @@ -708,7 +718,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void scheduleWatchdog(int sensorId) { Slog.d(getTag(), "Starting watchdog for face"); - final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler(); + final BiometricScheduler biometricScheduler = mFaceSensors.get(sensorId).getScheduler(); if (biometricScheduler == null) { return; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java index 468bf5530d34..ffbf4e12f2ae 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java @@ -18,9 +18,6 @@ package com.android.server.biometrics.sensors.face.aidl; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.SynchronousUserSwitchObserver; -import android.app.UserSwitchObserver; import android.content.Context; import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricsProtoEnums; @@ -94,14 +91,6 @@ public class Sensor { @NonNull private final Supplier<AidlSession> mLazySession; @Nullable private AidlSession mCurrentSession; - private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() { - @Override - public void onUserSwitching(int newUserId) { - mProvider.scheduleInternalCleanup( - mSensorProperties.sensorId, newUserId, null /* callback */); - } - }; - @VisibleForTesting public static class HalSessionCallback extends ISessionCallback.Stub { /** @@ -558,12 +547,6 @@ public class Sensor { mLockoutCache = new LockoutCache(); mAuthenticatorIds = new HashMap<>(); mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; - - try { - ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, mTag); - } catch (RemoteException e) { - Slog.e(mTag, "Unable to register user switch observer"); - } } @NonNull Supplier<AidlSession> getLazySession() { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 23b6f84e6954..58ece898a9fe 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityTaskManager; +import android.app.SynchronousUserSwitchObserver; import android.app.TaskStackListener; import android.content.Context; import android.content.pm.UserInfo; @@ -51,9 +52,9 @@ import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; +import android.os.UserHandle; import android.os.UserManager; import android.util.Slog; -import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; @@ -71,6 +72,7 @@ import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.InvalidationRequesterClient; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.PerformanceTracker; +import com.android.server.biometrics.sensors.SensorList; import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler; @@ -99,7 +101,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @NonNull @VisibleForTesting - final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports + final SensorList<Sensor> mFingerprintSensors; @NonNull private final Context mContext; @NonNull @@ -127,8 +129,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public void onTaskStackChanged() { mHandler.post(() -> { - for (int i = 0; i < mSensors.size(); i++) { - final BaseClientMonitor client = mSensors.valueAt(i).getScheduler() + for (int i = 0; i < mFingerprintSensors.size(); i++) { + final BaseClientMonitor client = mFingerprintSensors.valueAt(i).getScheduler() .getCurrentClient(); if (!(client instanceof AuthenticationClient)) { Slog.e(getTag(), "Task stack changed for client: " + client); @@ -143,8 +145,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi && !client.isAlreadyDone()) { Slog.e(getTag(), "Stopping background authentication," + " currentClient: " + client); - mSensors.valueAt(i).getScheduler().cancelAuthenticationOrDetection( - client.getToken(), client.getRequestId()); + mFingerprintSensors.valueAt(i).getScheduler() + .cancelAuthenticationOrDetection( + client.getToken(), client.getRequestId()); } } }); @@ -160,7 +163,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mContext = context; mBiometricStateCallback = biometricStateCallback; mHalInstanceName = halInstanceName; - mSensors = new SparseArray<>(); + mFingerprintSensors = new SensorList<>(ActivityManager.getService()); mHandler = new Handler(Looper.getMainLooper()); mLockoutResetDispatcher = lockoutResetDispatcher; mActivityTaskManager = ActivityTaskManager.getInstance(); @@ -201,8 +204,15 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler, internalProp, lockoutResetDispatcher, gestureAvailabilityDispatcher, mBiometricContext); - - mSensors.put(sensorId, sensor); + final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : + sensor.getLazySession().get().getUserId(); + mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId, + new SynchronousUserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId) { + scheduleInternalCleanup(sensorId, newUserId, null /* callback */); + } + }); Slog.d(getTag(), "Added: " + internalProp); } } @@ -250,8 +260,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi Slog.e(getTag(), "Unable to linkToDeath", e); } - for (int i = 0; i < mSensors.size(); i++) { - final int sensorId = mSensors.keyAt(i); + for (int i = 0; i < mFingerprintSensors.size(); i++) { + final int sensorId = mFingerprintSensors.keyAt(i); scheduleLoadAuthenticatorIds(sensorId); scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(), null /* callback */); @@ -261,33 +271,33 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } private void scheduleForSensor(int sensorId, @NonNull BaseClientMonitor client) { - if (!mSensors.contains(sensorId)) { + if (!mFingerprintSensors.contains(sensorId)) { throw new IllegalStateException("Unable to schedule client: " + client + " for sensor: " + sensorId); } - mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); + mFingerprintSensors.get(sensorId).getScheduler().scheduleClientMonitor(client); } private void scheduleForSensor(int sensorId, @NonNull BaseClientMonitor client, ClientMonitorCallback callback) { - if (!mSensors.contains(sensorId)) { + if (!mFingerprintSensors.contains(sensorId)) { throw new IllegalStateException("Unable to schedule client: " + client + " for sensor: " + sensorId); } - mSensors.get(sensorId).getScheduler().scheduleClientMonitor(client, callback); + mFingerprintSensors.get(sensorId).getScheduler().scheduleClientMonitor(client, callback); } @Override public boolean containsSensor(int sensorId) { - return mSensors.contains(sensorId); + return mFingerprintSensors.contains(sensorId); } @NonNull @Override public List<FingerprintSensorPropertiesInternal> getSensorProperties() { final List<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); - for (int i = 0; i < mSensors.size(); i++) { - props.add(mSensors.valueAt(i).getSensorProperties()); + for (int i = 0; i < mFingerprintSensors.size(); i++) { + props.add(mFingerprintSensors.valueAt(i).getSensorProperties()); } return props; } @@ -295,12 +305,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Nullable @Override public FingerprintSensorPropertiesInternal getSensorProperties(int sensorId) { - if (mSensors.size() == 0) { + if (mFingerprintSensors.size() == 0) { return null; } else if (sensorId == SENSOR_ID_ANY) { - return mSensors.valueAt(0).getSensorProperties(); + return mFingerprintSensors.valueAt(0).getSensorProperties(); } else { - final Sensor sensor = mSensors.get(sensorId); + final Sensor sensor = mFingerprintSensors.get(sensorId); return sensor != null ? sensor.getSensorProperties() : null; } } @@ -315,12 +325,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mHandler.post(() -> { final FingerprintGetAuthenticatorIdClient client = new FingerprintGetAuthenticatorIdClient(mContext, - mSensors.get(sensorId).getLazySession(), userId, + mFingerprintSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mSensors.get(sensorId).getAuthenticatorIds()); + mFingerprintSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client); }); } @@ -340,12 +350,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi public void scheduleResetLockout(int sensorId, int userId, @Nullable byte[] hardwareAuthToken) { mHandler.post(() -> { final FingerprintResetLockoutClient client = new FingerprintResetLockoutClient( - mContext, mSensors.get(sensorId).getLazySession(), userId, + mContext, mFingerprintSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, hardwareAuthToken, - mSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, + mFingerprintSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId)); scheduleForSensor(sensorId, client); }); @@ -357,7 +367,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mHandler.post(() -> { final FingerprintGenerateChallengeClient client = new FingerprintGenerateChallengeClient(mContext, - mSensors.get(sensorId).getLazySession(), token, + mFingerprintSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), @@ -372,7 +382,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mHandler.post(() -> { final FingerprintRevokeChallengeClient client = new FingerprintRevokeChallengeClient(mContext, - mSensors.get(sensorId).getLazySession(), token, + mFingerprintSensors.get(sensorId).getLazySession(), token, userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), @@ -388,16 +398,16 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @FingerprintManager.EnrollReason int enrollReason) { final long id = mRequestCounter.incrementAndGet(); mHandler.post(() -> { - final int maxTemplatesPerUser = mSensors.get(sensorId).getSensorProperties() + final int maxTemplatesPerUser = mFingerprintSensors.get(sensorId).getSensorProperties() .maxEnrollmentsPerUser; final FingerprintEnrollClient client = new FingerprintEnrollClient(mContext, - mSensors.get(sensorId).getLazySession(), token, id, + mFingerprintSensors.get(sensorId).getLazySession(), token, id, new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, opPackageName, FingerprintUtils.getInstance(sensorId), sensorId, createLogger(BiometricsProtoEnums.ACTION_ENROLL, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mSensors.get(sensorId).getSensorProperties(), + mFingerprintSensors.get(sensorId).getSensorProperties(), mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay, maxTemplatesPerUser, enrollReason); scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( @@ -419,7 +429,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public void cancelEnrollment(int sensorId, @NonNull IBinder token, long requestId) { mHandler.post(() -> - mSensors.get(sensorId).getScheduler().cancelEnrollment(token, requestId)); + mFingerprintSensors.get(sensorId).getScheduler() + .cancelEnrollment(token, requestId)); } @Override @@ -432,7 +443,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi final int sensorId = options.getSensorId(); final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId); final FingerprintDetectClient client = new FingerprintDetectClient(mContext, - mSensors.get(sensorId).getLazySession(), token, id, callback, + mFingerprintSensors.get(sensorId).getLazySession(), token, id, callback, options, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, @@ -454,15 +465,15 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi final int sensorId = options.getSensorId(); final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId); final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient( - mContext, mSensors.get(sensorId).getLazySession(), token, requestId, callback, - operationId, restricted, options, cookie, + mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, requestId, + callback, operationId, restricted, options, cookie, false /* requireConfirmation */, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric, - mTaskStackListener, mSensors.get(sensorId).getLockoutCache(), + mTaskStackListener, mFingerprintSensors.get(sensorId).getLockoutCache(), mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay, allowBackgroundAuthentication, - mSensors.get(sensorId).getSensorProperties(), mHandler, + mFingerprintSensors.get(sensorId).getSensorProperties(), mHandler, Utils.getCurrentStrength(sensorId), SystemClock.elapsedRealtimeClock()); scheduleForSensor(sensorId, client, new ClientMonitorCallback() { @@ -505,12 +516,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public void startPreparedClient(int sensorId, int cookie) { - mHandler.post(() -> mSensors.get(sensorId).getScheduler().startPreparedClient(cookie)); + mHandler.post(() -> mFingerprintSensors.get(sensorId).getScheduler() + .startPreparedClient(cookie)); } @Override public void cancelAuthentication(int sensorId, @NonNull IBinder token, long requestId) { - mHandler.post(() -> mSensors.get(sensorId).getScheduler() + mHandler.post(() -> mFingerprintSensors.get(sensorId).getScheduler() .cancelAuthenticationOrDetection(token, requestId)); } @@ -541,13 +553,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @NonNull String opPackageName) { mHandler.post(() -> { final FingerprintRemovalClient client = new FingerprintRemovalClient(mContext, - mSensors.get(sensorId).getLazySession(), token, + mFingerprintSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), fingerprintIds, userId, opPackageName, FingerprintUtils.getInstance(sensorId), sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mSensors.get(sensorId).getAuthenticatorIds()); + mFingerprintSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -564,13 +576,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mHandler.post(() -> { final FingerprintInternalCleanupClient client = new FingerprintInternalCleanupClient(mContext, - mSensors.get(sensorId).getLazySession(), userId, + mFingerprintSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_ENUMERATE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, FingerprintUtils.getInstance(sensorId), - mSensors.get(sensorId).getAuthenticatorIds()); + mFingerprintSensors.get(sensorId).getAuthenticatorIds()); if (favorHalEnrollments) { client.setFavorHalEnrollments(); } @@ -612,11 +624,11 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mHandler.post(() -> { final FingerprintInvalidationClient client = new FingerprintInvalidationClient(mContext, - mSensors.get(sensorId).getLazySession(), userId, sensorId, + mFingerprintSensors.get(sensorId).getLazySession(), userId, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mSensors.get(sensorId).getAuthenticatorIds(), callback); + mFingerprintSensors.get(sensorId).getAuthenticatorIds(), callback); scheduleForSensor(sensorId, client); }); } @@ -629,40 +641,43 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public long getAuthenticatorId(int sensorId, int userId) { - return mSensors.get(sensorId).getAuthenticatorIds().getOrDefault(userId, 0L); + return mFingerprintSensors.get(sensorId).getAuthenticatorIds().getOrDefault(userId, 0L); } @Override public void onPointerDown(long requestId, int sensorId, PointerContext pc) { - mSensors.get(sensorId).getScheduler().getCurrentClientIfMatches(requestId, (client) -> { - if (!(client instanceof Udfps)) { - Slog.e(getTag(), "onPointerDown received during client: " + client); - return; - } - ((Udfps) client).onPointerDown(pc); - }); + mFingerprintSensors.get(sensorId).getScheduler().getCurrentClientIfMatches( + requestId, (client) -> { + if (!(client instanceof Udfps)) { + Slog.e(getTag(), "onPointerDown received during client: " + client); + return; + } + ((Udfps) client).onPointerDown(pc); + }); } @Override public void onPointerUp(long requestId, int sensorId, PointerContext pc) { - mSensors.get(sensorId).getScheduler().getCurrentClientIfMatches(requestId, (client) -> { - if (!(client instanceof Udfps)) { - Slog.e(getTag(), "onPointerUp received during client: " + client); - return; - } - ((Udfps) client).onPointerUp(pc); - }); + mFingerprintSensors.get(sensorId).getScheduler().getCurrentClientIfMatches( + requestId, (client) -> { + if (!(client instanceof Udfps)) { + Slog.e(getTag(), "onPointerUp received during client: " + client); + return; + } + ((Udfps) client).onPointerUp(pc); + }); } @Override public void onUiReady(long requestId, int sensorId) { - mSensors.get(sensorId).getScheduler().getCurrentClientIfMatches(requestId, (client) -> { - if (!(client instanceof Udfps)) { - Slog.e(getTag(), "onUiReady received during client: " + client); - return; - } - ((Udfps) client).onUiReady(); - }); + mFingerprintSensors.get(sensorId).getScheduler().getCurrentClientIfMatches( + requestId, (client) -> { + if (!(client instanceof Udfps)) { + Slog.e(getTag(), "onUiReady received during client: " + client); + return; + } + ((Udfps) client).onUiReady(); + }); } @Override @@ -672,8 +687,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public void onPowerPressed() { - for (int i = 0; i < mSensors.size(); i++) { - final Sensor sensor = mSensors.valueAt(i); + for (int i = 0; i < mFingerprintSensors.size(); i++) { + final Sensor sensor = mFingerprintSensors.valueAt(i); BaseClientMonitor client = sensor.getScheduler().getCurrentClient(); if (client == null) { return; @@ -698,8 +713,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto, boolean clearSchedulerBuffer) { - if (mSensors.contains(sensorId)) { - mSensors.get(sensorId).dumpProtoState(sensorId, proto, clearSchedulerBuffer); + if (mFingerprintSensors.contains(sensorId)) { + mFingerprintSensors.get(sensorId).dumpProtoState(sensorId, proto, clearSchedulerBuffer); } } @@ -748,14 +763,15 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi pw.println(mBiometricContext.getAuthSessionCoordinator()); pw.println("---AuthSessionCoordinator logs end ---"); - mSensors.get(sensorId).getScheduler().dump(pw); + mFingerprintSensors.get(sensorId).getScheduler().dump(pw); } @NonNull @Override public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName) { - return mSensors.get(sensorId).createTestSession(callback, mBiometricStateCallback); + return mFingerprintSensors.get(sensorId).createTestSession(callback, + mBiometricStateCallback); } @Override @@ -764,9 +780,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mHandler.post(() -> { mDaemon = null; - for (int i = 0; i < mSensors.size(); i++) { - final Sensor sensor = mSensors.valueAt(i); - final int sensorId = mSensors.keyAt(i); + for (int i = 0; i < mFingerprintSensors.size(); i++) { + final Sensor sensor = mFingerprintSensors.valueAt(i); + final int sensorId = mFingerprintSensors.keyAt(i); PerformanceTracker.getInstanceForSensorId(sensorId).incrementHALDeathCount(); sensor.onBinderDied(); } @@ -821,7 +837,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public void scheduleWatchdog(int sensorId) { Slog.d(getTag(), "Starting watchdog for fingerprint"); - final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler(); + final BiometricScheduler biometricScheduler = mFingerprintSensors.get(sensorId) + .getScheduler(); if (biometricScheduler == null) { return; } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java index 22ca816ba56c..c0dde721b962 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java @@ -18,9 +18,6 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.SynchronousUserSwitchObserver; -import android.app.UserSwitchObserver; import android.content.Context; import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricsProtoEnums; @@ -96,14 +93,6 @@ public class Sensor { @Nullable private AidlSession mCurrentSession; @NonNull private final Supplier<AidlSession> mLazySession; - private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() { - @Override - public void onUserSwitching(int newUserId) { - mProvider.scheduleInternalCleanup( - mSensorProperties.sensorId, newUserId, null /* callback */); - } - }; - @VisibleForTesting public static class HalSessionCallback extends ISessionCallback.Stub { @@ -512,12 +501,6 @@ public class Sensor { }); mAuthenticatorIds = new HashMap<>(); mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; - - try { - ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, mTag); - } catch (RemoteException e) { - Slog.e(mTag, "Unable to register user switch observer"); - } } @NonNull Supplier<AidlSession> getLazySession() { diff --git a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java index 1f82961efd22..6d4306198aa2 100644 --- a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java +++ b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java @@ -41,6 +41,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.BitUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.RingBuffer; @@ -278,6 +279,11 @@ public class NetdEventListenerService extends BaseNetdEventListener { } } + private boolean hasWifiTransport(Network network) { + final NetworkCapabilities nc = mCm.getNetworkCapabilities(network); + return nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI); + } + @Override public synchronized void onWakeupEvent(String prefix, int uid, int ethertype, int ipNextHeader, byte[] dstHw, String srcIp, String dstIp, int srcPort, int dstPort, long timestampNs) { @@ -286,12 +292,21 @@ public class NetdEventListenerService extends BaseNetdEventListener { throw new IllegalArgumentException("Prefix " + prefix + " required in format <nethandle>:<interface>"); } + final long netHandle = Long.parseLong(prefixParts[0]); + final Network network = Network.fromNetworkHandle(netHandle); final WakeupEvent event = new WakeupEvent(); event.iface = prefixParts[1]; event.uid = uid; event.ethertype = ethertype; - event.dstHwAddr = MacAddress.fromBytes(dstHw); + if (ArrayUtils.isEmpty(dstHw)) { + if (hasWifiTransport(network)) { + Log.e(TAG, "Empty mac address on WiFi transport, network: " + network); + } + event.dstHwAddr = null; + } else { + event.dstHwAddr = MacAddress.fromBytes(dstHw); + } event.srcIp = srcIp; event.dstIp = dstIp; event.ipNextHeader = ipNextHeader; @@ -306,14 +321,12 @@ public class NetdEventListenerService extends BaseNetdEventListener { final BatteryStatsInternal bsi = LocalServices.getService(BatteryStatsInternal.class); if (bsi != null) { - final long netHandle = Long.parseLong(prefixParts[0]); final long elapsedMs = SystemClock.elapsedRealtime() + event.timestampMs - System.currentTimeMillis(); - bsi.noteCpuWakingNetworkPacket(Network.fromNetworkHandle(netHandle), elapsedMs, - event.uid); + bsi.noteCpuWakingNetworkPacket(network, elapsedMs, event.uid); } - final String dstMac = event.dstHwAddr.toString(); + final String dstMac = String.valueOf(event.dstHwAddr); FrameworkStatsLog.write(FrameworkStatsLog.PACKET_WAKEUP_OCCURRED, uid, event.iface, ethertype, dstMac, srcIp, dstIp, ipNextHeader, srcPort, dstPort); } diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index fd94be9a2921..17f928af1c09 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -65,7 +65,6 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; -import com.android.internal.display.RefreshRateSettingsUtils; import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; import com.android.server.display.DisplayDeviceConfig; @@ -1067,10 +1066,10 @@ public class DisplayModeDirector { @VisibleForTesting final class SettingsObserver extends ContentObserver { - private final Uri mSmoothDisplaySetting = - Settings.System.getUriFor(Settings.System.SMOOTH_DISPLAY); - private final Uri mForcePeakRefreshRateSetting = - Settings.System.getUriFor(Settings.System.FORCE_PEAK_REFRESH_RATE); + private final Uri mPeakRefreshRateSetting = + Settings.System.getUriFor(Settings.System.PEAK_REFRESH_RATE); + private final Uri mMinRefreshRateSetting = + Settings.System.getUriFor(Settings.System.MIN_REFRESH_RATE); private final Uri mLowPowerModeSetting = Settings.Global.getUriFor(Settings.Global.LOW_POWER_MODE); private final Uri mMatchContentFrameRateSetting = @@ -1106,8 +1105,9 @@ public class DisplayModeDirector { public void observe() { final ContentResolver cr = mContext.getContentResolver(); - mInjector.registerSmoothDisplayObserver(cr, this); - mInjector.registerForcePeakRefreshRateObserver(cr, this); + mInjector.registerPeakRefreshRateObserver(cr, this); + cr.registerContentObserver(mMinRefreshRateSetting, false /*notifyDescendants*/, this, + UserHandle.USER_SYSTEM); cr.registerContentObserver(mLowPowerModeSetting, false /*notifyDescendants*/, this, UserHandle.USER_SYSTEM); cr.registerContentObserver(mMatchContentFrameRateSetting, false /*notifyDescendants*/, @@ -1149,8 +1149,8 @@ public class DisplayModeDirector { @Override public void onChange(boolean selfChange, Uri uri, int userId) { synchronized (mLock) { - if (mSmoothDisplaySetting.equals(uri) - || mForcePeakRefreshRateSetting.equals(uri)) { + if (mPeakRefreshRateSetting.equals(uri) + || mMinRefreshRateSetting.equals(uri)) { updateRefreshRateSettingLocked(); } else if (mLowPowerModeSetting.equals(uri)) { updateLowPowerModeSettingLocked(); @@ -1205,9 +1205,12 @@ public class DisplayModeDirector { } private void updateRefreshRateSettingLocked() { - updateRefreshRateSettingLocked(RefreshRateSettingsUtils.getMinRefreshRate(mContext), - RefreshRateSettingsUtils.getPeakRefreshRate(mContext, mDefaultPeakRefreshRate), - mDefaultRefreshRate); + final ContentResolver cr = mContext.getContentResolver(); + float minRefreshRate = Settings.System.getFloatForUser(cr, + Settings.System.MIN_REFRESH_RATE, 0f, cr.getUserId()); + float peakRefreshRate = Settings.System.getFloatForUser(cr, + Settings.System.PEAK_REFRESH_RATE, mDefaultPeakRefreshRate, cr.getUserId()); + updateRefreshRateSettingLocked(minRefreshRate, peakRefreshRate, mDefaultRefreshRate); } private void updateRefreshRateSettingLocked( @@ -2840,17 +2843,12 @@ public class DisplayModeDirector { } interface Injector { - Uri SMOOTH_DISPLAY_URI = Settings.System.getUriFor(Settings.System.SMOOTH_DISPLAY); - Uri FORCE_PEAK_REFRESH_RATE_URI = - Settings.System.getUriFor(Settings.System.FORCE_PEAK_REFRESH_RATE); + Uri PEAK_REFRESH_RATE_URI = Settings.System.getUriFor(Settings.System.PEAK_REFRESH_RATE); @NonNull DeviceConfigInterface getDeviceConfig(); - void registerSmoothDisplayObserver(@NonNull ContentResolver cr, - @NonNull ContentObserver observer); - - void registerForcePeakRefreshRateObserver(@NonNull ContentResolver cr, + void registerPeakRefreshRateObserver(@NonNull ContentResolver cr, @NonNull ContentObserver observer); void registerDisplayListener(@NonNull DisplayManager.DisplayListener listener, @@ -2890,16 +2888,9 @@ public class DisplayModeDirector { } @Override - public void registerSmoothDisplayObserver(@NonNull ContentResolver cr, - @NonNull ContentObserver observer) { - cr.registerContentObserver(SMOOTH_DISPLAY_URI, false /*notifyDescendants*/, - observer, UserHandle.USER_SYSTEM); - } - - @Override - public void registerForcePeakRefreshRateObserver(@NonNull ContentResolver cr, + public void registerPeakRefreshRateObserver(@NonNull ContentResolver cr, @NonNull ContentObserver observer) { - cr.registerContentObserver(FORCE_PEAK_REFRESH_RATE_URI, false /*notifyDescendants*/, + cr.registerContentObserver(PEAK_REFRESH_RATE_URI, false /*notifyDescendants*/, observer, UserHandle.USER_SYSTEM); } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java index 220a438d55ee..b82129bdf82f 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java @@ -35,6 +35,7 @@ public class HdmiCecMessageValidator { ERROR_DESTINATION, ERROR_PARAMETER, ERROR_PARAMETER_SHORT, + ERROR_PARAMETER_LONG, }) public @interface ValidationResult {}; @@ -43,6 +44,7 @@ public class HdmiCecMessageValidator { static final int ERROR_DESTINATION = 2; static final int ERROR_PARAMETER = 3; static final int ERROR_PARAMETER_SHORT = 4; + static final int ERROR_PARAMETER_LONG = 5; interface ParameterValidator { /** @@ -159,11 +161,13 @@ public class HdmiCecMessageValidator { addValidationInfo(Constants.MESSAGE_SET_MENU_LANGUAGE, new AsciiValidator(3), DEST_BROADCAST); - ParameterValidator statusRequestValidator = new OneByteRangeValidator(0x01, 0x03); + ParameterValidator statusRequestValidator = new MinimumOneByteRangeValidator(0x01, 0x03); addValidationInfo( - Constants.MESSAGE_DECK_CONTROL, new OneByteRangeValidator(0x01, 0x04), DEST_DIRECT); + Constants.MESSAGE_DECK_CONTROL, + new MinimumOneByteRangeValidator(0x01, 0x04), DEST_DIRECT); addValidationInfo( - Constants.MESSAGE_DECK_STATUS, new OneByteRangeValidator(0x11, 0x1F), DEST_DIRECT); + Constants.MESSAGE_DECK_STATUS, + new MinimumOneByteRangeValidator(0x11, 0x1F), DEST_DIRECT); addValidationInfo(Constants.MESSAGE_GIVE_DECK_STATUS, statusRequestValidator, DEST_DIRECT); addValidationInfo(Constants.MESSAGE_PLAY, new PlayModeValidator(), DEST_DIRECT); @@ -201,9 +205,11 @@ public class HdmiCecMessageValidator { // Messages for the Device Menu Control. addValidationInfo( - Constants.MESSAGE_MENU_REQUEST, new OneByteRangeValidator(0x00, 0x02), DEST_DIRECT); + Constants.MESSAGE_MENU_REQUEST, + new MinimumOneByteRangeValidator(0x00, 0x02), DEST_DIRECT); addValidationInfo( - Constants.MESSAGE_MENU_STATUS, new OneByteRangeValidator(0x00, 0x01), DEST_DIRECT); + Constants.MESSAGE_MENU_STATUS, + new MinimumOneByteRangeValidator(0x00, 0x01), DEST_DIRECT); // Messages for the Remote Control Passthrough. addValidationInfo( @@ -214,7 +220,7 @@ public class HdmiCecMessageValidator { // Messages for the Power Status. addValidationInfo( Constants.MESSAGE_REPORT_POWER_STATUS, - new OneByteRangeValidator(0x00, 0x03), + new MinimumOneByteRangeValidator(0x00, 0x03), DEST_DIRECT | DEST_BROADCAST); // Messages for the General Protocol. @@ -229,17 +235,17 @@ public class HdmiCecMessageValidator { oneByteValidator, DEST_DIRECT); addValidationInfo( Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE, - new OneByteRangeValidator(0x00, 0x01), + new MinimumOneByteRangeValidator(0x00, 0x01), DEST_ALL); addValidationInfo( Constants.MESSAGE_SYSTEM_AUDIO_MODE_STATUS, - new OneByteRangeValidator(0x00, 0x01), + new SingleByteRangeValidator(0x00, 0x01), DEST_DIRECT); // Messages for the Audio Rate Control. addValidationInfo( Constants.MESSAGE_SET_AUDIO_RATE, - new OneByteRangeValidator(0x00, 0x06), + new MinimumOneByteRangeValidator(0x00, 0x06), DEST_DIRECT); // Messages for Feature Discovery. @@ -900,11 +906,32 @@ public class HdmiCecMessageValidator { } } - /** Check if the given parameters are one byte parameters and within range. */ - private static class OneByteRangeValidator implements ParameterValidator { + /** + * Check if the given parameters are at least one byte parameters + * and the first byte is within range. + */ + private static class MinimumOneByteRangeValidator implements ParameterValidator { + private final int mMinValue, mMaxValue; + + MinimumOneByteRangeValidator(int minValue, int maxValue) { + mMinValue = minValue; + mMaxValue = maxValue; + } + + @Override + public int isValid(byte[] params) { + if (params.length < 1) { + return ERROR_PARAMETER_SHORT; + } + return toErrorCode(isWithinRange(params[0], mMinValue, mMaxValue)); + } + } + + /** Check if the given parameters are exactly one byte parameters and within range. */ + private static class SingleByteRangeValidator implements ParameterValidator { private final int mMinValue, mMaxValue; - OneByteRangeValidator(int minValue, int maxValue) { + SingleByteRangeValidator(int minValue, int maxValue) { mMinValue = minValue; mMaxValue = maxValue; } @@ -913,6 +940,8 @@ public class HdmiCecMessageValidator { public int isValid(byte[] params) { if (params.length < 1) { return ERROR_PARAMETER_SHORT; + } else if (params.length > 1) { + return ERROR_PARAMETER_LONG; } return toErrorCode(isWithinRange(params[0], mMinValue, mMaxValue)); } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 75fe63a66206..9cd5272b356b 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1611,6 +1611,7 @@ public class HdmiControlService extends SystemService { @HdmiCecMessageValidator.ValidationResult int validationResult = message.getValidationResult(); if (validationResult == HdmiCecMessageValidator.ERROR_PARAMETER + || validationResult == HdmiCecMessageValidator.ERROR_PARAMETER_LONG || !verifyPhysicalAddresses(message)) { return Constants.ABORT_INVALID_OPERAND; } else if (validationResult != HdmiCecMessageValidator.OK diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index c70d55510493..57f8d1478905 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -856,13 +856,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private final WeakHashMap<IBinder, IBinder> mImeTargetWindowMap = new WeakHashMap<>(); - private static final class SoftInputShowHideHistory { + @VisibleForTesting + static final class SoftInputShowHideHistory { private final Entry[] mEntries = new Entry[16]; private int mNextIndex = 0; private static final AtomicInteger sSequenceNumber = new AtomicInteger(0); - private static final class Entry { + static final class Entry { final int mSequenceNumber = sSequenceNumber.getAndIncrement(); + @Nullable final ClientState mClientState; @SoftInputModeFlags final int mFocusedWindowSoftInputMode; @@ -874,7 +876,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean mInFullscreenMode; @NonNull final String mFocusedWindowName; - @NonNull + @Nullable final EditorInfo mEditorInfo; @NonNull final String mRequestWindowName; @@ -953,9 +955,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub pw.print(prefix); pw.print(" editorInfo: "); - pw.print(" inputType=" + entry.mEditorInfo.inputType); - pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions); - pw.println(" fieldId (viewId)=" + entry.mEditorInfo.fieldId); + if (entry.mEditorInfo != null) { + pw.print(" inputType=" + entry.mEditorInfo.inputType); + pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions); + pw.println(" fieldId (viewId)=" + entry.mEditorInfo.fieldId); + } else { + pw.println("null"); + } pw.print(prefix); pw.println(" focusedWindowSoftInputMode=" + InputMethodDebug.softInputModeToString( diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 33e6a8f15df2..f0ab815db2c1 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -316,7 +316,6 @@ import com.android.server.notification.toast.TextToastRecord; import com.android.server.notification.toast.ToastRecord; import com.android.server.pm.PackageManagerService; import com.android.server.pm.UserManagerInternal; -import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.android.server.policy.PermissionPolicyInternal; import com.android.server.powerstats.StatsPullAtomCallbackImpl; import com.android.server.statusbar.StatusBarManagerInternal; @@ -2559,8 +2558,8 @@ public class NotificationManagerService extends SystemService { Context.STATS_MANAGER), getContext().getSystemService(TelephonyManager.class), LocalServices.getService(ActivityManagerInternal.class), - createToastRateLimiter(), new PermissionHelper(LocalServices.getService( - PermissionManagerServiceInternal.class), AppGlobals.getPackageManager(), + createToastRateLimiter(), new PermissionHelper(getContext(), + AppGlobals.getPackageManager(), AppGlobals.getPermissionManager()), LocalServices.getService(UsageStatsManagerInternal.class), getContext().getSystemService(TelecomManager.class), @@ -11599,6 +11598,8 @@ public class NotificationManagerService extends SystemService { StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn); try { listener.onNotificationPosted(sbnHolder, rankingUpdate); + } catch (android.os.DeadObjectException ex) { + Slog.wtf(TAG, "unable to notify listener (posted): " + info, ex); } catch (RemoteException ex) { Slog.e(TAG, "unable to notify listener (posted): " + info, ex); } @@ -11620,6 +11621,8 @@ public class NotificationManagerService extends SystemService { reason = REASON_LISTENER_CANCEL; } listener.onNotificationRemoved(sbnHolder, rankingUpdate, stats, reason); + } catch (android.os.DeadObjectException ex) { + Slog.wtf(TAG, "unable to notify listener (removed): " + info, ex); } catch (RemoteException ex) { Slog.e(TAG, "unable to notify listener (removed): " + info, ex); } @@ -11630,6 +11633,8 @@ public class NotificationManagerService extends SystemService { final INotificationListener listener = (INotificationListener) info.service; try { listener.onNotificationRankingUpdate(rankingUpdate); + } catch (android.os.DeadObjectException ex) { + Slog.wtf(TAG, "unable to notify listener (ranking update): " + info, ex); } catch (RemoteException ex) { Slog.e(TAG, "unable to notify listener (ranking update): " + info, ex); } diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java index b6fd822b7687..93c83e181ec1 100644 --- a/services/core/java/com/android/server/notification/PermissionHelper.java +++ b/services/core/java/com/android/server/notification/PermissionHelper.java @@ -25,6 +25,7 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.Manifest; import android.annotation.NonNull; import android.annotation.UserIdInt; +import android.content.Context; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -37,7 +38,6 @@ import android.util.Pair; import android.util.Slog; import com.android.internal.util.ArrayUtils; -import com.android.server.pm.permission.PermissionManagerServiceInternal; import java.util.Collections; import java.util.HashSet; @@ -53,13 +53,13 @@ public final class PermissionHelper { private static final String NOTIFICATION_PERMISSION = Manifest.permission.POST_NOTIFICATIONS; - private final PermissionManagerServiceInternal mPmi; + private final Context mContext; private final IPackageManager mPackageManager; private final IPermissionManager mPermManager; - public PermissionHelper(PermissionManagerServiceInternal pmi, IPackageManager packageManager, + public PermissionHelper(Context context, IPackageManager packageManager, IPermissionManager permManager) { - mPmi = pmi; + mContext = context; mPackageManager = packageManager; mPermManager = permManager; } @@ -71,7 +71,7 @@ public final class PermissionHelper { public boolean hasPermission(int uid) { final long callingId = Binder.clearCallingIdentity(); try { - return mPmi.checkUidPermission(uid, NOTIFICATION_PERMISSION) == PERMISSION_GRANTED; + return mContext.checkPermission(NOTIFICATION_PERMISSION, -1, uid) == PERMISSION_GRANTED; } finally { Binder.restoreCallingIdentity(callingId); } @@ -193,8 +193,8 @@ public final class PermissionHelper { return; } - boolean currentlyGranted = mPmi.checkPermission(packageName, NOTIFICATION_PERMISSION, - userId) != PackageManager.PERMISSION_DENIED; + int uid = mPackageManager.getPackageUid(packageName, 0, userId); + boolean currentlyGranted = hasPermission(uid); if (grant && !currentlyGranted) { mPermManager.grantRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId); } else if (!grant && currentlyGranted) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 29c5adaea844..97e7f6f41703 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -1148,12 +1148,21 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { info.userId = userId; info.installerPackageName = mInstallSource.mInstallerPackageName; info.installerAttributionTag = mInstallSource.mInstallerAttributionTag; + info.resolvedBaseCodePath = null; if (mContext.checkCallingOrSelfPermission( Manifest.permission.READ_INSTALLED_SESSION_PATHS) - == PackageManager.PERMISSION_GRANTED && mResolvedBaseFile != null) { - info.resolvedBaseCodePath = mResolvedBaseFile.getAbsolutePath(); - } else { - info.resolvedBaseCodePath = null; + == PackageManager.PERMISSION_GRANTED) { + File file = mResolvedBaseFile; + if (file == null) { + // Try to guess mResolvedBaseFile file. + final List<File> addedFiles = getAddedApksLocked(); + if (addedFiles.size() > 0) { + file = addedFiles.get(0); + } + } + if (file != null) { + info.resolvedBaseCodePath = file.getAbsolutePath(); + } } info.progress = progress; info.sealed = mSealed; @@ -1355,9 +1364,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private String[] getStageDirContentsLocked() { + if (stageDir == null) { + return EmptyArray.STRING; + } String[] result = stageDir.list(); if (result == null) { - result = EmptyArray.STRING; + return EmptyArray.STRING; } return result; } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 84a9888d2458..2f0cea363d17 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -93,7 +93,6 @@ import android.provider.DeviceConfig; import android.text.TextUtils; import android.text.format.TimeMigrationUtils; import android.util.ArraySet; -import android.util.AtomicFile; import android.util.KeyValueListParser; import android.util.Log; import android.util.Slog; @@ -149,6 +148,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -321,8 +321,7 @@ public class ShortcutService extends IShortcutService.Stub { private final ArrayList<LauncherApps.ShortcutChangeCallback> mShortcutChangeCallbacks = new ArrayList<>(1); - @GuardedBy("mLock") - private long mRawLastResetTime; + private final AtomicLong mRawLastResetTime = new AtomicLong(0); /** * User ID -> UserShortcuts @@ -756,10 +755,15 @@ public class ShortcutService extends IShortcutService.Stub { } /** Return the base state file name */ - private AtomicFile getBaseStateFile() { - final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE); - path.mkdirs(); - return new AtomicFile(path); + final ResilientAtomicFile getBaseStateFile() { + File mainFile = new File(injectSystemDataPath(), FILENAME_BASE_STATE); + File temporaryBackup = new File(injectSystemDataPath(), + FILENAME_BASE_STATE + ".backup"); + File reserveCopy = new File(injectSystemDataPath(), + FILENAME_BASE_STATE + ".reservecopy"); + int fileMode = FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH; + return new ResilientAtomicFile(mainFile, temporaryBackup, reserveCopy, fileMode, + "base shortcut", null); } /** @@ -976,80 +980,91 @@ public class ShortcutService extends IShortcutService.Stub { writeAttr(out, name, intent.toUri(/* flags =*/ 0)); } - @GuardedBy("mLock") @VisibleForTesting - void saveBaseStateLocked() { - final AtomicFile file = getBaseStateFile(); - if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "Saving to " + file.getBaseFile()); - } + void saveBaseState() { + try (ResilientAtomicFile file = getBaseStateFile()) { + if (DEBUG || DEBUG_REBOOT) { + Slog.d(TAG, "Saving to " + file.getBaseFile()); + } - FileOutputStream outs = null; - try { - outs = file.startWrite(); + FileOutputStream outs = null; + try { + synchronized (mLock) { + outs = file.startWrite(); + } - // Write to XML - TypedXmlSerializer out = Xml.resolveSerializer(outs); - out.startDocument(null, true); - out.startTag(null, TAG_ROOT); + // Write to XML + TypedXmlSerializer out = Xml.resolveSerializer(outs); + out.startDocument(null, true); + out.startTag(null, TAG_ROOT); - // Body. - writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime); + // Body. + // No locking required. Ok to add lock later if we save more data. + writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime.get()); - // Epilogue. - out.endTag(null, TAG_ROOT); - out.endDocument(); + // Epilogue. + out.endTag(null, TAG_ROOT); + out.endDocument(); - // Close. - file.finishWrite(outs); - } catch (IOException e) { - Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); - file.failWrite(outs); + // Close. + file.finishWrite(outs); + } catch (IOException e) { + Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); + file.failWrite(outs); + } } } @GuardedBy("mLock") private void loadBaseStateLocked() { - mRawLastResetTime = 0; - - final AtomicFile file = getBaseStateFile(); - if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "Loading from " + file.getBaseFile()); - } - try (FileInputStream in = file.openRead()) { - TypedXmlPullParser parser = Xml.resolvePullParser(in); + mRawLastResetTime.set(0); - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (type != XmlPullParser.START_TAG) { - continue; + try (ResilientAtomicFile file = getBaseStateFile()) { + if (DEBUG || DEBUG_REBOOT) { + Slog.d(TAG, "Loading from " + file.getBaseFile()); + } + FileInputStream in = null; + try { + in = file.openRead(); + if (in == null) { + throw new FileNotFoundException(file.getBaseFile().getAbsolutePath()); } - final int depth = parser.getDepth(); - // Check the root tag - final String tag = parser.getName(); - if (depth == 1) { - if (!TAG_ROOT.equals(tag)) { - Slog.e(TAG, "Invalid root tag: " + tag); - return; + + TypedXmlPullParser parser = Xml.resolvePullParser(in); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + // Check the root tag + final String tag = parser.getName(); + if (depth == 1) { + if (!TAG_ROOT.equals(tag)) { + Slog.e(TAG, "Invalid root tag: " + tag); + return; + } + continue; + } + // Assume depth == 2 + switch (tag) { + case TAG_LAST_RESET_TIME: + mRawLastResetTime.set(parseLongAttribute(parser, ATTR_VALUE)); + break; + default: + Slog.e(TAG, "Invalid tag: " + tag); + break; } - continue; - } - // Assume depth == 2 - switch (tag) { - case TAG_LAST_RESET_TIME: - mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE); - break; - default: - Slog.e(TAG, "Invalid tag: " + tag); - break; } + } catch (FileNotFoundException e) { + // Use the default + } catch (IOException | XmlPullParserException e) { + // Remove corrupted file and retry. + file.failRead(in, e); + loadBaseStateLocked(); + return; } - } catch (FileNotFoundException e) { - // Use the default - } catch (IOException | XmlPullParserException e) { - Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); - - mRawLastResetTime = 0; } // Adjust the last reset time. getLastResetTimeLocked(); @@ -1067,8 +1082,7 @@ public class ShortcutService extends IShortcutService.Stub { "user shortcut", null); } - @GuardedBy("mLock") - private void saveUserLocked(@UserIdInt int userId) { + private void saveUser(@UserIdInt int userId) { try (ResilientAtomicFile file = getUserFile(userId)) { FileOutputStream os = null; try { @@ -1076,9 +1090,10 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, "Saving to " + file); } - os = file.startWrite(); - - saveUserInternalLocked(userId, os, /* forBackup= */ false); + synchronized (mLock) { + os = file.startWrite(); + saveUserInternalLocked(userId, os, /* forBackup= */ false); + } file.finishWrite(os); @@ -1215,16 +1230,19 @@ public class ShortcutService extends IShortcutService.Stub { } try { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "shortcutSaveDirtyInfo"); + List<Integer> dirtyUserIds = new ArrayList<>(); synchronized (mLock) { - for (int i = mDirtyUserIds.size() - 1; i >= 0; i--) { - final int userId = mDirtyUserIds.get(i); - if (userId == UserHandle.USER_NULL) { // USER_NULL for base state. - saveBaseStateLocked(); - } else { - saveUserLocked(userId); - } + List<Integer> tmp = mDirtyUserIds; + mDirtyUserIds = dirtyUserIds; + dirtyUserIds = tmp; + } + for (int i = dirtyUserIds.size() - 1; i >= 0; i--) { + final int userId = dirtyUserIds.get(i); + if (userId == UserHandle.USER_NULL) { // USER_NULL for base state. + saveBaseState(); + } else { + saveUser(userId); } - mDirtyUserIds.clear(); } } catch (Exception e) { wtf("Exception in saveDirtyInfo", e); @@ -1237,14 +1255,14 @@ public class ShortcutService extends IShortcutService.Stub { @GuardedBy("mLock") long getLastResetTimeLocked() { updateTimesLocked(); - return mRawLastResetTime; + return mRawLastResetTime.get(); } /** Return the next reset time. */ @GuardedBy("mLock") long getNextResetTimeLocked() { updateTimesLocked(); - return mRawLastResetTime + mResetInterval; + return mRawLastResetTime.get() + mResetInterval; } static boolean isClockValid(long time) { @@ -1259,25 +1277,26 @@ public class ShortcutService extends IShortcutService.Stub { final long now = injectCurrentTimeMillis(); - final long prevLastResetTime = mRawLastResetTime; + final long prevLastResetTime = mRawLastResetTime.get(); + long newLastResetTime = prevLastResetTime; - if (mRawLastResetTime == 0) { // first launch. + if (newLastResetTime == 0) { // first launch. // TODO Randomize?? - mRawLastResetTime = now; - } else if (now < mRawLastResetTime) { + newLastResetTime = now; + } else if (now < newLastResetTime) { // Clock rewound. if (isClockValid(now)) { Slog.w(TAG, "Clock rewound"); // TODO Randomize?? - mRawLastResetTime = now; - } - } else { - if ((mRawLastResetTime + mResetInterval) <= now) { - final long offset = mRawLastResetTime % mResetInterval; - mRawLastResetTime = ((now / mResetInterval) * mResetInterval) + offset; + newLastResetTime = now; } + } else if ((newLastResetTime + mResetInterval) <= now) { + final long offset = newLastResetTime % mResetInterval; + newLastResetTime = ((now / mResetInterval) * mResetInterval) + offset; } - if (prevLastResetTime != mRawLastResetTime) { + + mRawLastResetTime.set(newLastResetTime); + if (prevLastResetTime != newLastResetTime) { scheduleSaveBaseState(); } } @@ -2705,9 +2724,7 @@ public class ShortcutService extends IShortcutService.Stub { } void resetAllThrottlingInner() { - synchronized (mLock) { - mRawLastResetTime = injectCurrentTimeMillis(); - } + mRawLastResetTime.set(injectCurrentTimeMillis()); scheduleSaveBaseState(); Slog.i(TAG, "ShortcutManager: throttling counter reset for all users"); } @@ -2725,8 +2742,8 @@ public class ShortcutService extends IShortcutService.Stub { } getPackageShortcutsLocked(packageName, userId) .resetRateLimitingForCommandLineNoSaving(); - saveUserLocked(userId); } + saveUser(userId); } // We override this method in unit tests to do a simpler check. @@ -4505,8 +4522,8 @@ public class ShortcutService extends IShortcutService.Stub { dumpCurrentTime(pw); pw.println(); }); - saveUserLocked(userId); } + saveUser(userId); } // === Dump === @@ -4717,9 +4734,9 @@ public class ShortcutService extends IShortcutService.Stub { pw.print(formatTime(now)); pw.print(" Raw last reset: ["); - pw.print(mRawLastResetTime); + pw.print(mRawLastResetTime.get()); pw.print("] "); - pw.print(formatTime(mRawLastResetTime)); + pw.print(formatTime(mRawLastResetTime.get())); final long last = getLastResetTimeLocked(); pw.print(" Last reset: ["); diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java index f971db9b5f0e..e796275c53e3 100644 --- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java +++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java @@ -161,7 +161,13 @@ public class TrustAgentWrapper { mDisplayTrustGrantedMessage = (flags & FLAG_GRANT_TRUST_DISPLAY_MESSAGE) != 0; if ((flags & FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0) { mWaitingForTrustableDowngrade = true; - setSecurityWindowTimer(); + resultCallback.thenAccept(result -> { + if (result.getStatus() == GrantTrustResult.STATUS_UNLOCKED_BY_GRANT) { + // if we are not unlocked by grantTrust, then we don't need to + // have the timer for the security window + setSecurityWindowTimer(); + } + }); } else { mWaitingForTrustableDowngrade = false; } @@ -562,6 +568,7 @@ public class TrustAgentWrapper { * @see android.service.trust.TrustAgentService#onDeviceLocked() */ public void onDeviceLocked() { + mWithinSecurityLockdownWindow = false; try { if (mTrustAgentService != null) mTrustAgentService.onDeviceLocked(); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 8786005d519f..1ab982399b57 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -393,6 +393,23 @@ public class TrustManagerService extends SystemService { true /* overrideHardTimeout */); } + private void cancelBothTrustableAlarms(int userId) { + TrustableTimeoutAlarmListener idleTimeout = + mIdleTrustableTimeoutAlarmListenerForUser.get( + userId); + TrustableTimeoutAlarmListener trustableTimeout = + mTrustableTimeoutAlarmListenerForUser.get( + userId); + if (idleTimeout != null && idleTimeout.isQueued()) { + idleTimeout.setQueued(false); + mAlarmManager.cancel(idleTimeout); + } + if (trustableTimeout != null && trustableTimeout.isQueued()) { + trustableTimeout.setQueued(false); + mAlarmManager.cancel(trustableTimeout); + } + } + private void handleScheduleTrustedTimeout(int userId, boolean shouldOverride) { long when = SystemClock.elapsedRealtime() + TRUST_TIMEOUT_IN_MILLIS; TrustedTimeoutAlarmListener alarm = mTrustTimeoutAlarmListenerForUser.get(userId); @@ -657,6 +674,11 @@ public class TrustManagerService extends SystemService { resultCallback.complete(new GrantTrustResult(STATUS_UNLOCKED_BY_GRANT)); } } + + if ((wasTrusted || wasTrustable) && pendingTrustState == TrustState.UNTRUSTED) { + if (DEBUG) Slog.d(TAG, "Trust was revoked, destroy trustable alarms"); + cancelBothTrustableAlarms(userId); + } } private void updateTrustUsuallyManaged(int userId, boolean managed) { @@ -1908,7 +1930,11 @@ public class TrustManagerService extends SystemService { handleScheduleTrustTimeout(shouldOverride, timeoutType); break; case MSG_REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH: - refreshTrustableTimers(msg.arg1); + TrustableTimeoutAlarmListener trustableAlarm = + mTrustableTimeoutAlarmListenerForUser.get(msg.arg1); + if (trustableAlarm != null && trustableAlarm.isQueued()) { + refreshTrustableTimers(msg.arg1); + } break; } } @@ -2160,7 +2186,7 @@ public class TrustManagerService extends SystemService { TrustedTimeoutAlarmListener otherAlarm; boolean otherAlarmPresent; if (ENABLE_ACTIVE_UNLOCK_FLAG) { - cancelBothTrustableAlarms(); + cancelBothTrustableAlarms(mUserId); otherAlarm = mTrustTimeoutAlarmListenerForUser.get(mUserId); otherAlarmPresent = (otherAlarm != null) && otherAlarm.isQueued(); if (otherAlarmPresent) { @@ -2172,23 +2198,6 @@ public class TrustManagerService extends SystemService { } } - private void cancelBothTrustableAlarms() { - TrustableTimeoutAlarmListener idleTimeout = - mIdleTrustableTimeoutAlarmListenerForUser.get( - mUserId); - TrustableTimeoutAlarmListener trustableTimeout = - mTrustableTimeoutAlarmListenerForUser.get( - mUserId); - if (idleTimeout != null && idleTimeout.isQueued()) { - idleTimeout.setQueued(false); - mAlarmManager.cancel(idleTimeout); - } - if (trustableTimeout != null && trustableTimeout.isQueued()) { - trustableTimeout.setQueued(false); - mAlarmManager.cancel(trustableTimeout); - } - } - private void disableRenewableTrustWhileNonrenewableTrustIsPresent() { // if non-renewable trust is running, we need to temporarily prevent // renewable trust from being used diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 98d2d3d4b997..9020cb3405a2 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1353,9 +1353,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub void complete() { // Only changes from home+lock to just home or lock need attention - // If setting the wallpaper fails, this callback will be called - // when the wallpaper is detached, in which case wallpapers may have - // already changed. Make sure we're not overwriting a more recent wallpaper. if (mNewWallpaper.mSystemWasBoth) { if (DEBUG) { Slog.v(TAG, "Handling change from system+lock wallpaper"); @@ -1378,7 +1375,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mOriginalSystem.wallpaperComponent; lockWp.connection = mOriginalSystem.connection; lockWp.connection.mWallpaper = lockWp; - updateEngineFlags(mOriginalSystem, FLAG_LOCK); + mOriginalSystem.mWhich = FLAG_LOCK; + updateEngineFlags(mOriginalSystem); notifyWallpaperColorsChanged(lockWp, FLAG_LOCK); } else { // Failed rename, use current system wp for both @@ -1387,7 +1385,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } WallpaperData currentSystem = mWallpaperMap.get(mNewWallpaper.userId); currentSystem.mWhich = FLAG_SYSTEM | FLAG_LOCK; - updateEngineFlags(currentSystem, FLAG_SYSTEM | FLAG_LOCK); + updateEngineFlags(currentSystem); mLockWallpaperMap.remove(mNewWallpaper.userId); } } else { @@ -1396,7 +1394,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.v(TAG, "live system+lock to system success"); } mOriginalSystem.mWhich = FLAG_LOCK; - updateEngineFlags(mOriginalSystem, FLAG_LOCK); + updateEngineFlags(mOriginalSystem); mLockWallpaperMap.put(mNewWallpaper.userId, mOriginalSystem); mLastLockWallpaper = mOriginalSystem; notifyWallpaperColorsChanged(mOriginalSystem, FLAG_LOCK); @@ -1409,7 +1407,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub WallpaperData currentSystem = mWallpaperMap.get(mNewWallpaper.userId); if (currentSystem.wallpaperId == mOriginalSystem.wallpaperId) { currentSystem.mWhich = FLAG_SYSTEM; - updateEngineFlags(currentSystem, FLAG_SYSTEM); + updateEngineFlags(currentSystem); } } } @@ -1422,24 +1420,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.v(TAG, "new lastLockWp: " + mLastLockWallpaper); } } - - private void updateEngineFlags(WallpaperData wallpaper, @SetWallpaperFlags int which) { - if (wallpaper.connection == null) { - return; - } - wallpaper.connection.forEachDisplayConnector( - connector -> { - try { - if (connector.mEngine != null) { - connector.mEngine.setWallpaperFlags(which); - mWindowManagerInternal.setWallpaperShowWhenLocked( - connector.mToken, (which & FLAG_LOCK) != 0); - } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to update wallpaper engine flags", e); - } - }); - } } class MyPackageMonitor extends PackageMonitor { @@ -3095,6 +3075,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub newWallpaper.userId); if (lockedWallpaper != null) { detachWallpaperLocked(lockedWallpaper); + if (same) { + updateEngineFlags(newWallpaper); + } } mLockWallpaperMap.remove(newWallpaper.userId); } @@ -3430,6 +3413,27 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } + // Updates the given wallpaper's Engine so that its destination flags are the same as those of + // the wallpaper, e.g., after a wallpaper has been changed from displaying on home+lock to home + // or lock only. + private void updateEngineFlags(WallpaperData wallpaper) { + if (wallpaper.connection == null) { + return; + } + wallpaper.connection.forEachDisplayConnector( + connector -> { + try { + if (connector.mEngine != null) { + connector.mEngine.setWallpaperFlags(wallpaper.mWhich); + mWindowManagerInternal.setWallpaperShowWhenLocked( + connector.mToken, (wallpaper.mWhich & FLAG_LOCK) != 0); + } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to update wallpaper engine flags", e); + } + }); + } + private void clearWallpaperComponentLocked(WallpaperData wallpaper) { wallpaper.wallpaperComponent = null; detachWallpaperLocked(wallpaper); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 78c066bdc212..24a271f93e29 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4908,9 +4908,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTransitionController.setStatusBarTransitionDelay( mPendingRemoteAnimation.getStatusBarTransitionDelay()); } else { - if (mPendingOptions == null - || mPendingOptions.getAnimationType() == ANIM_SCENE_TRANSITION) { - // Scene transition will run on the client side. + if (mPendingOptions == null) { + return; + } else if (mPendingOptions.getAnimationType() == ANIM_SCENE_TRANSITION) { + // Scene transition will run on the client side, so just notify transition + // controller but don't clear the animation information from the options since they + // need to be sent to the animating activity. + mTransitionController.setOverrideAnimation( + AnimationOptions.makeSceneTransitionAnimOptions(), null, null); return; } applyOptionsAnimation(mPendingOptions, intent); @@ -5220,6 +5225,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } logAppCompatState(); if (!visible) { + final InputTarget imeInputTarget = mDisplayContent.getImeInputTarget(); + mLastImeShown = imeInputTarget != null && imeInputTarget.getWindowState() != null + && imeInputTarget.getWindowState().mActivityRecord == this + && mDisplayContent.mInputMethodWindow != null + && mDisplayContent.mInputMethodWindow.isVisible(); finishOrAbortReplacingWindow(); } return true; @@ -5400,7 +5410,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } if (inFinishingTransition) { - // Let the finishing transition commit the visibility. + // Let the finishing transition commit the visibility, but let the controller know + // about it so that we can recover from degenerate cases. + mTransitionController.mValidateCommitVis.add(this); return; } // If we are preparing an app transition, then delay changing @@ -5607,11 +5619,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (!visible) { - final InputTarget imeInputTarget = mDisplayContent.getImeInputTarget(); - mLastImeShown = imeInputTarget != null && imeInputTarget.getWindowState() != null - && imeInputTarget.getWindowState().mActivityRecord == this - && mDisplayContent.mInputMethodWindow != null - && mDisplayContent.mInputMethodWindow.isVisible(); mImeInsetsFrozenUntilStartInput = true; } diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index bfe298653584..a6e50405e7d9 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -45,6 +45,7 @@ import android.content.pm.ResolveInfo; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; @@ -554,7 +555,25 @@ public class ActivityStartController { .execute(); } + /** + * A quick path (skip general intent/task resolving) to start recents animation if the recents + * (or home) activity is available in background. + * @return {@code true} if the recents activity is moved to front. + */ boolean startExistingRecentsIfPossible(Intent intent, ActivityOptions options) { + try { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "startExistingRecents"); + if (startExistingRecents(intent, options)) { + return true; + } + // Else follow the standard launch procedure. + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + return false; + } + + private boolean startExistingRecents(Intent intent, ActivityOptions options) { final int activityType = mService.getRecentTasks().getRecentsComponent() .equals(intent.getComponent()) ? ACTIVITY_TYPE_RECENTS : ACTIVITY_TYPE_HOME; final Task rootTask = mService.mRootWindowContainer.getDefaultTaskDisplayArea() @@ -563,6 +582,7 @@ public class ActivityStartController { final ActivityRecord r = rootTask.topRunningActivity(); if (r == null || r.isVisibleRequested() || !r.attachedToProcess() || !r.mActivityComponent.equals(intent.getComponent()) + || !mService.isCallerRecents(r.getUid()) // Recents keeps invisible while device is locked. || r.mDisplayContent.isKeyguardLocked()) { return false; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index a27f3e49457d..19a12f2b9cc0 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1468,9 +1468,8 @@ class ActivityStarter { // transition based on a sub-action. // Only do the create here (and defer requestStart) since startActivityInner might abort. final TransitionController transitionController = r.mTransitionController; - Transition newTransition = (!transitionController.isCollecting() - && transitionController.getTransitionPlayer() != null) - ? transitionController.createTransition(TRANSIT_OPEN) : null; + Transition newTransition = transitionController.isShellTransitionsEnabled() + ? transitionController.createAndStartCollecting(TRANSIT_OPEN) : null; RemoteTransition remoteTransition = r.takeRemoteTransition(); try { mService.deferWindowLayout(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 1f4606b09396..a0ea1c3dbdbf 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5753,23 +5753,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { boolean validateIncomingUser, PendingIntentRecord originatingPendingIntent, BackgroundStartPrivileges backgroundStartPrivileges) { assertPackageMatchesCallingUid(callingPackage); - // A quick path (skip general intent/task resolving) to start recents animation if the - // recents (or home) activity is available in background. - if (options != null && options.getOriginalOptions() != null - && options.getOriginalOptions().getTransientLaunch() && isCallerRecents(uid)) { - try { - synchronized (mGlobalLock) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "startExistingRecents"); - if (mActivityStartController.startExistingRecentsIfPossible( - intent, options.getOriginalOptions())) { - return ActivityManager.START_TASK_TO_FRONT; - } - // Else follow the standard launch procedure. - } - } finally { - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } - } return getActivityStartController().startActivityInPackage(uid, realCallingPid, realCallingUid, callingPackage, callingFeatureId, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, options, userId, inTask, diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 7a11120132bd..7e783938d30a 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -267,7 +267,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume op.mDrawTransaction = null; if (DEBUG) Slog.d(TAG, "finishOp merge transaction " + windowToken.getTopChild()); } - if (op.mAction == Operation.ACTION_FADE) { + if (op.mAction == Operation.ACTION_TOGGLE_IME) { + if (DEBUG) Slog.d(TAG, "finishOp fade-in IME " + windowToken.getTopChild()); + fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM, + (type, anim) -> mDisplayContent.getInsetsStateController() + .getImeSourceProvider().reportImeDrawnForOrganizer()); + } else if (op.mAction == Operation.ACTION_FADE) { if (DEBUG) Slog.d(TAG, "finishOp fade-in " + windowToken.getTopChild()); // The previous animation leash will be dropped when preparing fade-in animation, so // simply apply new animation without restoring the transformation. @@ -344,7 +349,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { final WindowToken windowToken = mTargetWindowTokens.keyAt(i); final Operation op = mTargetWindowTokens.valueAt(i); - if (op.mAction == Operation.ACTION_FADE) { + if (op.mAction == Operation.ACTION_FADE || op.mAction == Operation.ACTION_TOGGLE_IME) { fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM); op.mLeash = windowToken.getAnimationLeash(); if (DEBUG) Slog.d(TAG, "Start fade-out " + windowToken.getTopChild()); @@ -374,17 +379,19 @@ class AsyncRotationController extends FadeAnimationController implements Consume WindowManagerService.WINDOW_FREEZE_TIMEOUT_DURATION); } - /** Hides the window immediately until it is drawn in new rotation. */ - void hideImmediately(WindowToken windowToken) { - if (isTargetToken(windowToken)) return; + /** Hides the IME window immediately until it is drawn in new rotation. */ + void hideImeImmediately() { + if (mDisplayContent.mInputMethodWindow == null) return; + final WindowToken imeWindowToken = mDisplayContent.mInputMethodWindow.mToken; + if (isTargetToken(imeWindowToken)) return; final boolean original = mHideImmediately; mHideImmediately = true; - final Operation op = new Operation(Operation.ACTION_FADE); - mTargetWindowTokens.put(windowToken, op); - fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM); - op.mLeash = windowToken.getAnimationLeash(); + final Operation op = new Operation(Operation.ACTION_TOGGLE_IME); + mTargetWindowTokens.put(imeWindowToken, op); + fadeWindowToken(false /* show */, imeWindowToken, ANIMATION_TYPE_TOKEN_TRANSFORM); + op.mLeash = imeWindowToken.getAnimationLeash(); mHideImmediately = original; - if (DEBUG) Slog.d(TAG, "hideImmediately " + windowToken.getTopChild()); + if (DEBUG) Slog.d(TAG, "hideImeImmediately " + imeWindowToken.getTopChild()); } /** Returns {@code true} if the window will rotate independently. */ @@ -586,11 +593,13 @@ class AsyncRotationController extends FadeAnimationController implements Consume /** The operation to control the rotation appearance associated with window token. */ private static class Operation { @Retention(RetentionPolicy.SOURCE) - @IntDef(value = { ACTION_SEAMLESS, ACTION_FADE }) + @IntDef(value = { ACTION_SEAMLESS, ACTION_FADE, ACTION_TOGGLE_IME }) @interface Action {} static final int ACTION_SEAMLESS = 1; static final int ACTION_FADE = 2; + /** The action to toggle the IME window appearance */ + static final int ACTION_TOGGLE_IME = 3; final @Action int mAction; /** The leash of window token. It can be animation leash or the token itself. */ SurfaceControl mLeash; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 8bca1067de8c..57812c1d604c 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1896,7 +1896,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp case SOFT_INPUT_STATE_HIDDEN: return false; } - return r.mLastImeShown; + final boolean useIme = r.getWindow( + w -> WindowManager.LayoutParams.mayUseInputMethod(w.mAttrs.flags)) != null; + if (!useIme) { + return false; + } + return r.mLastImeShown || (r.mStartingData != null && r.mStartingData.hasImeSurface()); } /** Returns {@code true} if the top activity is transformed with the new rotation of display. */ @@ -4219,7 +4224,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Hide the window until the rotation is done to avoid intermediate artifacts if the // parent surface of IME container is changed. if (mAsyncRotationController != null) { - mAsyncRotationController.hideImmediately(mInputMethodWindow.mToken); + mAsyncRotationController.hideImeImmediately(); } } } diff --git a/services/core/java/com/android/server/wm/FadeAnimationController.java b/services/core/java/com/android/server/wm/FadeAnimationController.java index 561a07061bb4..7af67e63f469 100644 --- a/services/core/java/com/android/server/wm/FadeAnimationController.java +++ b/services/core/java/com/android/server/wm/FadeAnimationController.java @@ -57,14 +57,21 @@ public class FadeAnimationController { return AnimationUtils.loadAnimation(mContext, R.anim.fade_out); } + /** Run the fade in/out animation for a window token. */ + public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType) { + fadeWindowToken(show, windowToken, animationType, null); + } + /** * Run the fade in/out animation for a window token. * * @param show true for fade-in, otherwise for fade-out. * @param windowToken the window token to run the animation. * @param animationType the animation type defined in SurfaceAnimator. + * @param finishedCallback the callback after the animation finished. */ - public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType) { + public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType, + SurfaceAnimator.OnAnimationFinishedCallback finishedCallback) { if (windowToken == null || windowToken.getParent() == null) { return; } @@ -75,9 +82,8 @@ public class FadeAnimationController { if (animationAdapter == null) { return; } - windowToken.startAnimation(windowToken.getPendingTransaction(), animationAdapter, - show /* hidden */, animationType, null /* finishedCallback */); + show /* hidden */, animationType, finishedCallback); } protected FadeAnimationAdapter createAdapter(LocalAnimationAdapter.AnimationSpec animationSpec, diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index b4dffdcba243..ff2985c98421 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -145,18 +145,44 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { } boolean changed = super.updateClientVisibility(caller); if (changed && caller.isRequestedVisible(mSource.getType())) { - reportImeDrawnForOrganizer(caller); + reportImeDrawnForOrganizerIfNeeded(caller); } changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate(); return changed; } - private void reportImeDrawnForOrganizer(InsetsControlTarget caller) { - if (caller.getWindow() != null && caller.getWindow().getTask() != null) { - if (caller.getWindow().getTask().isOrganized()) { - mWindowContainer.mWmService.mAtmService.mTaskOrganizerController - .reportImeDrawnOnTask(caller.getWindow().getTask()); - } + private void reportImeDrawnForOrganizerIfNeeded(@NonNull InsetsControlTarget caller) { + final WindowState callerWindow = caller.getWindow(); + if (callerWindow == null) { + return; + } + WindowToken imeToken = mWindowContainer.asWindowState() != null + ? mWindowContainer.asWindowState().mToken : null; + if (mDisplayContent.getAsyncRotationController() != null + && mDisplayContent.getAsyncRotationController().isTargetToken(imeToken)) { + // Skip reporting IME drawn state when the control target is in fixed + // rotation, AsyncRotationController will report after the animation finished. + return; + } + reportImeDrawnForOrganizer(caller); + } + + private void reportImeDrawnForOrganizer(@NonNull InsetsControlTarget caller) { + final WindowState callerWindow = caller.getWindow(); + if (callerWindow == null || callerWindow.getTask() == null) { + return; + } + if (callerWindow.getTask().isOrganized()) { + mWindowContainer.mWmService.mAtmService.mTaskOrganizerController + .reportImeDrawnOnTask(caller.getWindow().getTask()); + } + } + + /** Report the IME has drawn on the current IME control target for its task organizer */ + void reportImeDrawnForOrganizer() { + final InsetsControlTarget imeControlTarget = getControlTarget(); + if (imeControlTarget != null) { + reportImeDrawnForOrganizer(imeControlTarget); } } diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index f5079d37b324..e47787e97f20 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -33,6 +33,7 @@ import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.Process.SYSTEM_UID; +import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; @@ -209,7 +210,8 @@ class RecentTasks { private final PointerEventListener mListener = new PointerEventListener() { @Override public void onPointerEvent(MotionEvent ev) { - if (!mFreezeTaskListReordering || ev.getAction() != MotionEvent.ACTION_DOWN) { + if (!mFreezeTaskListReordering || ev.getAction() != MotionEvent.ACTION_DOWN + || ev.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE) { // Skip if we aren't freezing or starting a gesture return; } diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index b5df3e0937ec..bbb85636f1ee 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -193,6 +193,7 @@ class ScreenRotationAnimation { .setSourceCrop(new Rect(0, 0, width, height)) .setAllowProtected(true) .setCaptureSecureLayers(true) + .setHintForSeamlessTransition(true) .build(); screenshotBuffer = ScreenCapture.captureDisplay(captureArgs); } else { @@ -202,6 +203,7 @@ class ScreenRotationAnimation { .setCaptureSecureLayers(true) .setAllowProtected(true) .setSourceCrop(new Rect(0, 0, width, height)) + .setHintForSeamlessTransition(true) .build(); screenshotBuffer = ScreenCapture.captureLayers(captureArgs); } diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 5626aa7f075f..cdb4ad645dc3 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -18,6 +18,9 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.TaskInfo.cameraCompatControlStateToString; +import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NONE; +import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL; +import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; @@ -686,8 +689,19 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { final boolean playShiftUpAnimation = !task.inMultiWindowMode(); final ActivityRecord topActivity = task.topActivityContainsStartingWindow(); if (topActivity != null) { - removalInfo.deferRemoveForIme = topActivity.mDisplayContent - .mayImeShowOnLaunchingActivity(topActivity); + // Set defer remove mode for IME + final DisplayContent dc = topActivity.getDisplayContent(); + final WindowState imeWindow = dc.mInputMethodWindow; + if (topActivity.isVisibleRequested() && imeWindow != null + && dc.mayImeShowOnLaunchingActivity(topActivity) + && dc.isFixedRotationLaunchingApp(topActivity)) { + removalInfo.deferRemoveForImeMode = DEFER_MODE_ROTATION; + } else if (dc.mayImeShowOnLaunchingActivity(topActivity)) { + removalInfo.deferRemoveForImeMode = DEFER_MODE_NORMAL; + } else { + removalInfo.deferRemoveForImeMode = DEFER_MODE_NONE; + } + final WindowState mainWindow = topActivity.findMainWindow(false/* includeStartingApp */); // No app window for this activity, app might be crashed. diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 838536b03335..fe754691314d 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -954,6 +954,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // to the transient activity. ar.supportsEnterPipOnTaskSwitch = true; } + // Make sure this activity can enter pip under the current circumstances. + // `enterPictureInPicture` internally checks, but with beforeStopping=false which + // is specifically for non-auto-enter. + if (!ar.checkEnterPictureInPictureState("enterPictureInPictureMode", + true /* beforeStopping */)) { + return false; + } return mController.mAtm.enterPictureInPictureMode(ar, ar.pictureInPictureArgs, false /* fromClient */); } @@ -1012,13 +1019,20 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Record all the now-hiding activities so that they are committed. Just use // mParticipants because we can avoid a new list this way. for (int i = 0; i < mTransientHideTasks.size(); ++i) { - // Only worry about tasks that were actually hidden. Otherwise, we could end-up - // committing visibility for activity-level changes that aren't part of this - // transition. - if (mTransientHideTasks.get(i).isVisibleRequested()) continue; - mTransientHideTasks.get(i).forAllActivities(r -> { + final Task rootTask = mTransientHideTasks.get(i); + rootTask.forAllActivities(r -> { // Only check leaf-tasks that were collected if (!mParticipants.contains(r.getTask())) return; + if (rootTask.isVisibleRequested()) { + // This transient-hide didn't hide, so don't commit anything (otherwise we + // could prematurely commit invisible on unrelated activities). To be safe, + // though, notify the controller to prevent degenerate cases. + if (!r.isVisibleRequested()) { + mController.mValidateCommitVis.add(r); + } + return; + } + // This did hide: commit immediately so that other transitions know about it. mParticipants.add(r); }); } @@ -2992,11 +3006,14 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { Rect cropBounds = new Rect(bounds); cropBounds.offsetTo(0, 0); + final boolean isDisplayRotation = wc.asDisplayContent() != null + && wc.asDisplayContent().isRotationChanging(); ScreenCapture.LayerCaptureArgs captureArgs = new ScreenCapture.LayerCaptureArgs.Builder(wc.getSurfaceControl()) .setSourceCrop(cropBounds) .setCaptureSecureLayers(true) .setAllowProtected(true) + .setHintForSeamlessTransition(isDisplayRotation) .build(); ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = ScreenCapture.captureLayers(captureArgs); @@ -3007,8 +3024,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { Slog.w(TAG, "Failed to capture screenshot for " + wc); return false; } - final boolean isDisplayRotation = wc.asDisplayContent() != null - && wc.asDisplayContent().isRotationChanging(); // Some tests may check the name "RotationLayer" to detect display rotation. final String name = isDisplayRotation ? "RotationLayer" : "transition snapshot: " + wc; SurfaceControl snapshotSurface = wc.makeAnimationLeash() diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 19ef40b61634..c9316bf6e972 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -133,6 +133,13 @@ class TransitionController { final ArrayList<Runnable> mStateValidators = new ArrayList<>(); /** + * List of activity-records whose visibility changed outside the main/tracked part of a + * transition (eg. in the finish-transaction). These will be checked when idle to recover from + * degenerate states. + */ + final ArrayList<ActivityRecord> mValidateCommitVis = new ArrayList<>(); + + /** * Currently playing transitions (in the order they were started). When finished, records are * removed from this list. */ @@ -806,6 +813,10 @@ class TransitionController { } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record); mPlayingTransitions.remove(record); + if (!inTransition()) { + // reset track-count now since shell-side is idle. + mTrackCount = 0; + } updateRunningRemoteAnimation(record, false /* isPlaying */); record.finishTransition(); for (int i = mAnimatingExitWindows.size() - 1; i >= 0; i--) { @@ -818,10 +829,9 @@ class TransitionController { } } mRunningLock.doNotifyLocked(); - // Run state-validation checks when no transitions are active anymore. + // Run state-validation checks when no transitions are active anymore (Note: sometimes + // finish can start a transition, so check afterwards -- eg. pip). if (!inTransition()) { - // Can reset track-count now that everything is idle. - mTrackCount = 0; validateStates(); mAtm.mWindowManager.onAnimationFinished(); } @@ -848,6 +858,15 @@ class TransitionController { } } mStateValidators.clear(); + for (int i = 0; i < mValidateCommitVis.size(); ++i) { + final ActivityRecord ar = mValidateCommitVis.get(i); + if (!ar.isVisibleRequested() && ar.isVisible()) { + Slog.e(TAG, "Uncommitted visibility change: " + ar); + ar.commitVisibility(ar.isVisibleRequested(), false /* layout */, + false /* fromTransition */); + } + } + mValidateCommitVis.clear(); } /** @@ -858,7 +877,7 @@ class TransitionController { tryStartCollectFromQueue(); } - private boolean canStartCollectingNow(Transition queued) { + private boolean canStartCollectingNow(@Nullable Transition queued) { if (mCollectingTransition == null) return true; // Population (collect until ready) is still serialized, so always wait for that. if (!mCollectingTransition.isPopulated()) return false; @@ -931,14 +950,14 @@ class TransitionController { * `collecting` transition. It may still ultimately block in sync-engine or become dependent * in {@link #getIsIndependent} later. */ - boolean getCanBeIndependent(Transition collecting, Transition queued) { + boolean getCanBeIndependent(Transition collecting, @Nullable Transition queued) { // For tests - if (queued.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL + if (queued != null && queued.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL && collecting.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) { return true; } // For recents - if (queued.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS) { + if (queued != null && queued.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS) { if (collecting.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS) { // Must serialize with itself. return false; @@ -1235,6 +1254,44 @@ class TransitionController { return true; } + /** + * This will create and start collecting for a transition if possible. If there's no way to + * start collecting for `parallelType` now, then this returns null. + * + * WARNING: ONLY use this if the transition absolutely cannot be deferred! + */ + @NonNull + Transition createAndStartCollecting(int type) { + if (mTransitionPlayer == null) { + return null; + } + if (!mQueuedTransitions.isEmpty()) { + // There is a queue, so it's not possible to start immediately + return null; + } + if (mSyncEngine.hasActiveSync()) { + if (isCollecting()) { + // Check if we can run in parallel here. + if (canStartCollectingNow(null /* transit */)) { + // create and collect in parallel. + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Moving #%d from" + + " collecting to waiting.", mCollectingTransition.getSyncId()); + mWaitingTransitions.add(mCollectingTransition); + mCollectingTransition = null; + Transition transit = new Transition(type, 0 /* flags */, this, mSyncEngine); + moveToCollecting(transit); + return transit; + } + } else { + Slog.w(TAG, "Ongoing Sync outside of transition."); + } + return null; + } + Transition transit = new Transition(type, 0 /* flags */, this, mSyncEngine); + moveToCollecting(transit); + return transit; + } + /** Returns {@code true} if it started collecting, {@code false} if it was queued. */ boolean startLegacySyncOrQueue(BLASTSyncEngine.SyncGroup syncGroup, Runnable applySync) { if (!mQueuedTransitions.isEmpty() || mSyncEngine.hasActiveSync()) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index d5aa520e1b6e..09312bac593f 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -972,19 +972,30 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub switch (type) { case HIERARCHY_OP_TYPE_PENDING_INTENT: { + final Bundle launchOpts = hop.getLaunchOptions(); + ActivityOptions activityOptions = launchOpts != null + ? new ActivityOptions(launchOpts) : null; + if (activityOptions != null && activityOptions.getTransientLaunch() + && mService.isCallerRecents(hop.getPendingIntent().getCreatorUid())) { + if (mService.getActivityStartController().startExistingRecentsIfPossible( + hop.getActivityIntent(), activityOptions)) { + // Start recents successfully. + break; + } + } + String resolvedType = hop.getActivityIntent() != null ? hop.getActivityIntent().resolveTypeIfNeeded( mService.mContext.getContentResolver()) : null; - ActivityOptions activityOptions = null; if (hop.getPendingIntent().isActivity()) { // Set the context display id as preferred for this activity launches, so that // it can land on caller's display. Or just brought the task to front at the // display where it was on since it has higher preference. - activityOptions = hop.getLaunchOptions() != null - ? new ActivityOptions(hop.getLaunchOptions()) - : ActivityOptions.makeBasic(); + if (activityOptions == null) { + activityOptions = ActivityOptions.makeBasic(); + } activityOptions.setCallerDisplayId(DEFAULT_DISPLAY); } final Bundle options = activityOptions != null ? activityOptions.toBundle() : null; diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index 19a0c5e8adcb..04ecd6ebd2d1 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -67,7 +67,7 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta .createNewSession(mContext, mUserId, providerInfo, this, remoteCredentialService); if (providerClearSession != null) { - Slog.d(TAG, "In startProviderSession - provider session created " + Slog.i(TAG, "Provider session created " + "and being added for: " + providerInfo.getComponentName()); mProviders.put(providerClearSession.getComponentName().flattenToString(), providerClearSession); @@ -78,12 +78,12 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta @Override // from provider session public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Slog.d(TAG, "in onStatusChanged with status: " + status + ", and source: " + source); + Slog.i(TAG, "Provider changed with status: " + status + ", and source: " + source); if (ProviderSession.isTerminatingStatus(status)) { - Slog.d(TAG, "in onProviderStatusChanged terminating status"); + Slog.i(TAG, "Provider terminating status"); onProviderTerminated(componentName); } else if (ProviderSession.isCompletionStatus(status)) { - Slog.d(TAG, "in onProviderStatusChanged isCompletionStatus status"); + Slog.i(TAG, "Provider has completion status"); onProviderResponseComplete(componentName); } } diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index fc7fd1afe58f..4b3206242d77 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -79,7 +79,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR .createNewSession(mContext, mUserId, providerInfo, this, remoteCredentialService); if (providerCreateSession != null) { - Slog.d(TAG, "In initiateProviderSession - provider session created and " + Slog.i(TAG, "Provider session created and " + "being added for: " + providerInfo.getComponentName()); mProviders.put(providerCreateSession.getComponentName().flattenToString(), providerCreateSession); @@ -98,7 +98,9 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR mRequestId, mClientRequest, mClientAppInfo.getPackageName(), PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), - Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)), + Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), + // TODO(b/279480457): populate + /*defaultProviderId=*/new ArrayList<>()), providerDataList); mClientCallback.onPendingIntent(mPendingIntent); } catch (RemoteException e) { @@ -125,7 +127,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable CreateCredentialResponse response) { - Slog.d(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); + Slog.i(TAG, "Final credential received from: " + componentName.flattenToString()); mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.collectChosenMetricViaCandidateTransfer(mProviders.get( componentName.flattenToString()).mProviderSessionMetric @@ -168,13 +170,13 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Slog.d(TAG, "in onStatusChanged with status: " + status + ", and source: " + source); + Slog.i(TAG, "Provider status changed: " + status + ", and source: " + source); // If all provider responses have been received, we can either need the UI, // or we need to respond with error. The only other case is the entry being // selected after the UI has been invoked which has a separate code path. if (!isAnyProviderPending()) { if (isUiInvocationNeeded()) { - Slog.d(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); + Slog.i(TAG, "Provider status changed - ui invocation is needed"); getProviderDataAndInitiateUi(); } else { respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREATE_OPTIONS, diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 06b96eb46ac1..7f95e058406e 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -58,7 +58,6 @@ import android.provider.Settings; import android.service.credentials.CallingAppInfo; import android.service.credentials.CredentialProviderInfoFactory; import android.text.TextUtils; -import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -437,7 +436,7 @@ public final class CredentialManagerService IGetCredentialCallback callback, final String callingPackage) { final long timestampBegan = System.nanoTime(); - Slog.d(TAG, "starting executeGetCredential with callingPackage: " + Slog.i(TAG, "starting executeGetCredential with callingPackage: " + callingPackage); ICancellationSignal cancelTransport = CancellationSignal.createTransport(); @@ -472,7 +471,7 @@ public final class CredentialManagerService GetCredentialException.TYPE_NO_CREDENTIAL, "No credentials available on this device."); } catch (RemoteException e) { - Log.i( + Slog.e( TAG, "Issue invoking onError on IGetCredentialCallback " + "callback: " @@ -528,7 +527,7 @@ public final class CredentialManagerService false, null, false, false, null)); } catch (RemoteException e) { - Log.i( + Slog.e( TAG, "Issue invoking onError on IGetCredentialCallback " + "callback: " @@ -607,7 +606,7 @@ public final class CredentialManagerService ICreateCredentialCallback callback, String callingPackage) { final long timestampBegan = System.nanoTime(); - Slog.d(TAG, "starting executeCreateCredential with callingPackage: " + Slog.i(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage); ICancellationSignal cancelTransport = CancellationSignal.createTransport(); @@ -673,7 +672,7 @@ public final class CredentialManagerService MetricUtilities.logApiCalledInitialPhase(initMetric, session.mRequestSessionMetric.returnIncrementSequence()); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: ", e); + Slog.i(TAG, "Unexpected error during metric logging: ", e); } } @@ -706,7 +705,7 @@ public final class CredentialManagerService Settings.Secure.CREDENTIAL_SERVICE, storedValue, userId)) { - Log.e(TAG, "Failed to store setting containing enabled providers"); + Slog.e(TAG, "Failed to store setting containing enabled providers"); try { callback.onError( "failed_setting_store", @@ -733,7 +732,7 @@ public final class CredentialManagerService @Override public boolean isEnabledCredentialProviderService( ComponentName componentName, String callingPackage) { - Slog.d(TAG, "isEnabledCredentialProviderService with componentName: " + Slog.i(TAG, "isEnabledCredentialProviderService with componentName: " + componentName.flattenToString()); // TODO(253157366): Check additional set of services. @@ -829,7 +828,7 @@ public final class CredentialManagerService IClearCredentialStateCallback callback, String callingPackage) { final long timestampBegan = System.nanoTime(); - Slog.d(TAG, "starting clearCredentialState with callingPackage: " + Slog.i(TAG, "starting clearCredentialState with callingPackage: " + callingPackage); final int userId = UserHandle.getCallingUserId(); int callingUid = Binder.getCallingUid(); @@ -882,7 +881,7 @@ public final class CredentialManagerService public void registerCredentialDescription( RegisterCredentialDescriptionRequest request, String callingPackage) throws IllegalArgumentException, NonCredentialProviderCallerException { - Slog.d(TAG, "registerCredentialDescription with callingPackage: " + callingPackage); + Slog.i(TAG, "registerCredentialDescription with callingPackage: " + callingPackage); if (!isCredentialDescriptionApiEnabled()) { throw new UnsupportedOperationException(); @@ -900,7 +899,7 @@ public final class CredentialManagerService public void unregisterCredentialDescription( UnregisterCredentialDescriptionRequest request, String callingPackage) throws IllegalArgumentException { - Slog.d(TAG, "unregisterCredentialDescription with callingPackage: " + Slog.i(TAG, "unregisterCredentialDescription with callingPackage: " + callingPackage); @@ -962,7 +961,6 @@ public final class CredentialManagerService @Override @GuardedBy("mLock") public void onFinishRequestSession(@UserIdInt int userId, IBinder token) { - Log.i(TAG, "In onFinishRequestSession"); if (mRequestSessions.get(userId) != null) { mRequestSessions.get(userId).remove(token); } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index 91be2a734e85..808fdaea3de6 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -48,7 +48,7 @@ public final class CredentialManagerServiceImpl extends @NonNull Object lock, int userId, String serviceName) throws PackageManager.NameNotFoundException { super(master, lock, userId); - Slog.d(TAG, "CredentialManagerServiceImpl constructed for: " + serviceName); + Slog.i(TAG, "CredentialManagerServiceImpl constructed for: " + serviceName); synchronized (mLock) { newServiceInfoLocked(ComponentName.unflattenFromString(serviceName)); } @@ -63,7 +63,7 @@ public final class CredentialManagerServiceImpl extends @NonNull CredentialManagerService master, @NonNull Object lock, int userId, CredentialProviderInfo providerInfo) { super(master, lock, userId); - Slog.d(TAG, "CredentialManagerServiceImpl constructed for: " + Slog.i(TAG, "CredentialManagerServiceImpl constructed for: " + providerInfo.getServiceInfo().getComponentName().flattenToString()); mInfo = providerInfo; } @@ -74,11 +74,11 @@ public final class CredentialManagerServiceImpl extends throws PackageManager.NameNotFoundException { // TODO : Test update flows with multiple providers if (mInfo != null) { - Slog.d(TAG, "newServiceInfoLocked, mInfo not null : " + Slog.i(TAG, "newServiceInfoLocked, mInfo not null : " + mInfo.getServiceInfo().getComponentName().flattenToString() + " , " + serviceComponent.flattenToString()); } else { - Slog.d(TAG, "newServiceInfoLocked, mInfo null, " + Slog.i(TAG, "newServiceInfoLocked, mInfo null, " + serviceComponent.flattenToString()); } mInfo = CredentialProviderInfoFactory.create( @@ -95,11 +95,11 @@ public final class CredentialManagerServiceImpl extends public ProviderSession initiateProviderSessionForRequestLocked( RequestSession requestSession, List<String> requestOptions) { if (!requestOptions.isEmpty() && !isServiceCapableLocked(requestOptions)) { - Slog.d(TAG, "Service does not have the required capabilities"); + Slog.i(TAG, "Service does not have the required capabilities"); return null; } if (mInfo == null) { - Slog.w(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, " + Slog.w(TAG, "Initiating provider session for request " + "but mInfo is null. This shouldn't happen"); return null; } diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 0271727249b1..15034104b5e0 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -72,7 +72,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, .createNewSession(mContext, mUserId, providerInfo, this, remoteCredentialService); if (providerGetSession != null) { - Slog.d(TAG, "In startProviderSession - provider session created and " + Slog.i(TAG, "Provider session created and " + "being added for: " + providerInfo.getComponentName()); mProviders.put(providerGetSession.getComponentName().flattenToString(), providerGetSession); @@ -114,7 +114,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response) { - Slog.d(TAG, "onFinalResponseReceived from: " + componentName.flattenToString()); + Slog.i(TAG, "onFinalResponseReceived from: " + componentName.flattenToString()); mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.collectChosenMetricViaCandidateTransfer( mProviders.get(componentName.flattenToString()) @@ -158,7 +158,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Slog.d(TAG, "in onStatusChanged for: " + componentName + ", with status: " + Slog.i(TAG, "Status changed for: " + componentName + ", with status: " + status + ", and source: " + source); // Auth entry was selected, and it did not have any underlying credentials @@ -172,7 +172,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, // or we need to respond with error. The only other case is the entry being // selected after the UI has been invoked which has a separate code path. if (isUiInvocationNeeded()) { - Slog.d(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); + Slog.i(TAG, "Provider status changed - ui invocation is needed"); getProviderDataAndInitiateUi(); } else { respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index 50e5163cea55..47502c24fbd7 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -19,7 +19,7 @@ package com.android.server.credentials; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; -import android.util.Log; +import android.util.Slog; import com.android.internal.util.FrameworkStatsLog; import com.android.server.credentials.metrics.ApiName; @@ -27,6 +27,7 @@ import com.android.server.credentials.metrics.ApiStatus; import com.android.server.credentials.metrics.CandidateBrowsingPhaseMetric; import com.android.server.credentials.metrics.CandidatePhaseMetric; import com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric; +import com.android.server.credentials.metrics.EntryEnum; import com.android.server.credentials.metrics.InitialPhaseMetric; import java.util.List; @@ -35,6 +36,7 @@ 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; @@ -68,7 +70,7 @@ public class MetricUtilities { componentName.getPackageName(), PackageManager.ApplicationInfoFlags.of(0)).uid; } catch (Throwable t) { - Log.i(TAG, "Couldn't find required uid"); + Slog.i(TAG, "Couldn't find required uid"); } return sessUid; } @@ -146,28 +148,28 @@ public class MetricUtilities { .getFinalFinishTimeNanoseconds()), /* chosen_provider_status */ finalPhaseMetric.getChosenProviderStatus(), /* chosen_provider_has_exception */ finalPhaseMetric.isHasException(), - /* chosen_provider_available_entries */ finalPhaseMetric.getAvailableEntries() - .stream().mapToInt(i -> i).toArray(), - /* chosen_provider_action_entry_count */ finalPhaseMetric.getActionEntryCount(), - /* chosen_provider_credential_entry_count */ - finalPhaseMetric.getCredentialEntryCount(), - /* chosen_provider_credential_entry_type_count */ - finalPhaseMetric.getCredentialEntryTypeCount(), - /* chosen_provider_remote_entry_count */ - finalPhaseMetric.getRemoteEntryCount(), - /* chosen_provider_authentication_entry_count */ - finalPhaseMetric.getAuthenticationEntryCount(), + /* chosen_provider_available_entries (deprecated) */ DEFAULT_REPEATED_INT_32, + /* chosen_provider_action_entry_count (deprecated) */ DEFAULT_INT_32, + /* chosen_provider_credential_entry_count (deprecated)*/DEFAULT_INT_32, + /* chosen_provider_credential_entry_type_count (deprecated) */ DEFAULT_INT_32, + /* chosen_provider_remote_entry_count (deprecated) */ DEFAULT_INT_32, + /* chosen_provider_authentication_entry_count (deprecated) */ DEFAULT_INT_32, /* clicked_entries */ browsedClickedEntries, /* provider_of_clicked_entry */ browsedProviderUid, /* api_status */ apiStatus, - DEFAULT_REPEATED_INT_32, - DEFAULT_REPEATED_INT_32, - DEFAULT_REPEATED_STR, - DEFAULT_REPEATED_INT_32, + /* unique_entries */ + finalPhaseMetric.getResponseCollective().getUniqueEntries(), + /* per_entry_counts */ + finalPhaseMetric.getResponseCollective().getUniqueEntryCounts(), + /* unique_response_classtypes */ + finalPhaseMetric.getResponseCollective().getUniqueResponseStrings(), + /* per_classtype_counts */ + finalPhaseMetric.getResponseCollective().getUniqueResponseCounts(), + /* framework_exception_unique_classtypes */ DEFAULT_STRING ); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during final provider uid emit: " + e); } } @@ -222,12 +224,18 @@ public class MetricUtilities { metric.getQueryFinishTimeNanoseconds()); candidateStatusList[index] = metric.getProviderQueryStatus(); candidateHasExceptionList[index] = metric.isHasException(); - candidateTotalEntryCountList[index] = metric.getNumEntriesTotal(); - candidateCredentialEntryCountList[index] = metric.getCredentialEntryCount(); - candidateCredentialTypeCountList[index] = metric.getCredentialEntryTypeCount(); - candidateActionEntryCountList[index] = metric.getActionEntryCount(); - candidateAuthEntryCountList[index] = metric.getAuthenticationEntryCount(); - candidateRemoteEntryCountList[index] = metric.getRemoteEntryCount(); + candidateTotalEntryCountList[index] = metric.getResponseCollective() + .getNumEntriesTotal(); + candidateCredentialEntryCountList[index] = metric.getResponseCollective() + .getCountForEntry(EntryEnum.CREDENTIAL_ENTRY); + candidateCredentialTypeCountList[index] = metric.getResponseCollective() + .getUniqueResponseStrings().length; + candidateActionEntryCountList[index] = metric.getResponseCollective() + .getCountForEntry(EntryEnum.ACTION_ENTRY); + candidateAuthEntryCountList[index] = metric.getResponseCollective() + .getCountForEntry(EntryEnum.AUTHENTICATION_ENTRY); + candidateRemoteEntryCountList[index] = metric.getResponseCollective() + .getCountForEntry(EntryEnum.REMOTE_ENTRY); frameworkExceptionList[index] = metric.getFrameworkException(); index++; } @@ -261,7 +269,7 @@ public class MetricUtilities { initialPhaseMetric.getUniqueRequestCounts() ); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during candidate provider uid metric emit: " + e); } } @@ -297,7 +305,7 @@ public class MetricUtilities { DEFAULT_INT_32, /* chosen_provider_status */ DEFAULT_INT_32); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during metric logging: " + e); } } @@ -330,7 +338,7 @@ public class MetricUtilities { initialPhaseMetric.isOriginSpecified() ); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during initial metric emit: " + e); } } } diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index 441c87b1569a..36bc8baa05dd 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -66,7 +66,7 @@ public class PrepareGetRequestSession extends GetRequestSession { @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Slog.d(TAG, "in onProviderStatusChanged with status: " + status + ", and " + Slog.i(TAG, "Provider Status changed with status: " + status + ", and " + "source: " + source); switch (source) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java index 8af6b56f881d..c1fb92d30fc3 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java @@ -80,7 +80,7 @@ public final class ProviderClearSession extends ProviderSession<ClearCredentialS @Override public void onProviderResponseSuccess(@Nullable Void response) { - Slog.d(TAG, "Remote provider responded with a valid response: " + mComponentName); + Slog.i(TAG, "Remote provider responded with a valid response: " + mComponentName); mProviderResponseSet = true; updateStatusAndInvokeCallback(Status.COMPLETE, /*source=*/ CredentialsSource.REMOTE_PROVIDER); diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 520b937d24c5..4cdc6f445212 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -75,7 +75,8 @@ public final class ProviderCreateSession extends ProviderSession< CreateCredentialRequest providerCreateRequest = createProviderRequest(providerInfo.getCapabilities(), createRequestSession.mClientRequest, - createRequestSession.mClientAppInfo); + createRequestSession.mClientAppInfo, + providerInfo.isSystemProvider()); if (providerCreateRequest != null) { return new ProviderCreateSession( context, @@ -92,7 +93,7 @@ public final class ProviderCreateSession extends ProviderSession< createRequestSession.mHybridService ); } - Slog.d(TAG, "Unable to create provider session for: " + Slog.i(TAG, "Unable to create provider session for: " + providerInfo.getComponentName()); return null; } @@ -114,9 +115,16 @@ public final class ProviderCreateSession extends ProviderSession< } @Nullable - private static CreateCredentialRequest createProviderRequest(List<String> providerCapabilities, + private static CreateCredentialRequest createProviderRequest( + List<String> providerCapabilities, android.credentials.CreateCredentialRequest clientRequest, - CallingAppInfo callingAppInfo) { + CallingAppInfo callingAppInfo, + boolean isSystemProvider) { + if (clientRequest.isSystemProviderRequired() && !isSystemProvider) { + // Request requires system provider but this session does not correspond to a + // system service + return null; + } String capability = clientRequest.getType(); if (providerCapabilities.contains(capability)) { return new CreateCredentialRequest(callingAppInfo, capability, @@ -145,7 +153,7 @@ public final class ProviderCreateSession extends ProviderSession< @Override public void onProviderResponseSuccess( @Nullable BeginCreateCredentialResponse response) { - Slog.d(TAG, "Remote provider responded with a valid response: " + mComponentName); + Slog.i(TAG, "Remote provider responded with a valid response: " + mComponentName); onSetInitialRemoteResponse(response); } @@ -200,7 +208,7 @@ public final class ProviderCreateSession extends ProviderSession< protected CreateCredentialProviderData prepareUiData() throws IllegalArgumentException { if (!ProviderSession.isUiInvokingStatus(getStatus())) { - Slog.d(TAG, "No data for UI from: " + mComponentName.flattenToString()); + Slog.i(TAG, "No data for UI from: " + mComponentName.flattenToString()); return null; } @@ -216,7 +224,7 @@ public final class ProviderCreateSession extends ProviderSession< switch (entryType) { case SAVE_ENTRY_KEY: if (mProviderResponseDataHandler.getCreateEntry(entryKey) == null) { - Slog.w(TAG, "Unexpected save entry key"); + Slog.i(TAG, "Unexpected save entry key"); invokeCallbackOnInternalInvalidState(); return; } @@ -224,14 +232,14 @@ public final class ProviderCreateSession extends ProviderSession< break; case REMOTE_ENTRY_KEY: if (mProviderResponseDataHandler.getRemoteEntry(entryKey) == null) { - Slog.w(TAG, "Unexpected remote entry key"); + Slog.i(TAG, "Unexpected remote entry key"); invokeCallbackOnInternalInvalidState(); return; } onRemoteEntrySelected(providerPendingIntentResponse); break; default: - Slog.w(TAG, "Unsupported entry type selected"); + Slog.i(TAG, "Unsupported entry type selected"); invokeCallbackOnInternalInvalidState(); } } @@ -266,7 +274,7 @@ public final class ProviderCreateSession extends ProviderSession< if (credentialResponse != null) { mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse); } else { - Slog.w(TAG, "onSaveEntrySelected - no response or error found in pending " + Slog.i(TAG, "onSaveEntrySelected - no response or error found in pending " + "intent response"); invokeCallbackOnInternalInvalidState(); } @@ -282,14 +290,14 @@ public final class ProviderCreateSession extends ProviderSession< private CreateCredentialException maybeGetPendingIntentException( ProviderPendingIntentResponse pendingIntentResponse) { if (pendingIntentResponse == null) { - Slog.w(TAG, "pendingIntentResponse is null"); + Slog.i(TAG, "pendingIntentResponse is null"); return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREATE_OPTIONS); } if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { CreateCredentialException exception = PendingIntentResultHandler .extractCreateCredentialException(pendingIntentResponse.getResultData()); if (exception != null) { - Slog.d(TAG, "Pending intent contains provider exception"); + Slog.i(TAG, "Pending intent contains provider exception"); return exception; } } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index a62d9e805a93..8070fa7ca9aa 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -40,7 +40,6 @@ import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderService; import android.service.credentials.GetCredentialRequest; import android.service.credentials.RemoteEntry; -import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -115,7 +114,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential getRequestSession.mHybridService ); } - Slog.d(TAG, "Unable to create provider session for: " + Slog.i(TAG, "Unable to create provider session for: " + providerInfo.getComponentName()); return null; } @@ -147,13 +146,13 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential android.credentials.GetCredentialRequest clientRequest, CredentialProviderInfo info ) { - Slog.d(TAG, "Filtering request options for: " + info.getComponentName()); + Slog.i(TAG, "Filtering request options for: " + info.getComponentName()); List<CredentialOption> filteredOptions = new ArrayList<>(); for (CredentialOption option : clientRequest.getCredentialOptions()) { if (providerCapabilities.contains(option.getType()) && isProviderAllowed(option, info.getComponentName()) && checkSystemProviderRequirement(option, info.isSystemProvider())) { - Slog.d(TAG, "Option of type: " + option.getType() + " meets all filtering" + Slog.i(TAG, "Option of type: " + option.getType() + " meets all filtering" + "conditions"); filteredOptions.add(option); } @@ -164,14 +163,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential .setCredentialOptions( filteredOptions).build(); } - Slog.d(TAG, "No options filtered"); + Slog.i(TAG, "No options filtered"); return null; } private static boolean isProviderAllowed(CredentialOption option, ComponentName componentName) { if (!option.getAllowedProviders().isEmpty() && !option.getAllowedProviders().contains( componentName)) { - Slog.d(TAG, "Provider allow list specified but does not contain this provider"); + Slog.i(TAG, "Provider allow list specified but does not contain this provider"); return false; } return true; @@ -180,7 +179,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential private static boolean checkSystemProviderRequirement(CredentialOption option, boolean isSystemProvider) { if (option.isSystemProviderRequired() && !isSystemProvider) { - Slog.d(TAG, "System provider required, but this service is not a system provider"); + Slog.i(TAG, "System provider required, but this service is not a system provider"); return false; } return true; @@ -208,7 +207,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** Called when the provider response has been updated by an external source. */ @Override // Callback from the remote provider public void onProviderResponseSuccess(@Nullable BeginGetCredentialResponse response) { - Slog.d(TAG, "Remote provider responded with a valid response: " + mComponentName); + Slog.i(TAG, "Remote provider responded with a valid response: " + mComponentName); onSetInitialRemoteResponse(response); } @@ -245,14 +244,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @Override // Selection call from the request provider protected void onUiEntrySelected(String entryType, String entryKey, ProviderPendingIntentResponse providerPendingIntentResponse) { - Slog.d(TAG, "onUiEntrySelected with entryType: " + entryType + ", and entryKey: " + Slog.i(TAG, "onUiEntrySelected with entryType: " + entryType + ", and entryKey: " + entryKey); switch (entryType) { case CREDENTIAL_ENTRY_KEY: CredentialEntry credentialEntry = mProviderResponseDataHandler .getCredentialEntry(entryKey); if (credentialEntry == null) { - Slog.w(TAG, "Unexpected credential entry key"); + Slog.i(TAG, "Unexpected credential entry key"); invokeCallbackOnInternalInvalidState(); return; } @@ -261,7 +260,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential case ACTION_ENTRY_KEY: Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey); if (actionEntry == null) { - Slog.w(TAG, "Unexpected action entry key"); + Slog.i(TAG, "Unexpected action entry key"); invokeCallbackOnInternalInvalidState(); return; } @@ -271,21 +270,21 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential Action authenticationEntry = mProviderResponseDataHandler .getAuthenticationAction(entryKey); if (authenticationEntry == null) { - Slog.w(TAG, "Unexpected authenticationEntry key"); + Slog.i(TAG, "Unexpected authenticationEntry key"); invokeCallbackOnInternalInvalidState(); return; } boolean additionalContentReceived = onAuthenticationEntrySelected(providerPendingIntentResponse); if (additionalContentReceived) { - Slog.d(TAG, "Additional content received - removing authentication entry"); + Slog.i(TAG, "Additional content received - removing authentication entry"); mProviderResponseDataHandler.removeAuthenticationAction(entryKey); if (!mProviderResponseDataHandler.isEmptyResponse()) { updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED, /*source=*/ CredentialsSource.AUTH_ENTRY); } } else { - Slog.d(TAG, "Additional content not received from authentication entry"); + Slog.i(TAG, "Additional content not received from authentication entry"); mProviderResponseDataHandler .updateAuthEntryWithNoCredentialsReceived(entryKey); updateStatusAndInvokeCallback(Status.NO_CREDENTIALS_FROM_AUTH_ENTRY, @@ -296,12 +295,12 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) { onRemoteEntrySelected(providerPendingIntentResponse); } else { - Slog.d(TAG, "Unexpected remote entry key"); + Slog.i(TAG, "Unexpected remote entry key"); invokeCallbackOnInternalInvalidState(); } break; default: - Slog.w(TAG, "Unsupported entry type selected"); + Slog.i(TAG, "Unsupported entry type selected"); invokeCallbackOnInternalInvalidState(); } } @@ -323,13 +322,12 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException { if (!ProviderSession.isUiInvokingStatus(getStatus())) { - Slog.d(TAG, "No data for UI from: " + mComponentName.flattenToString()); + Slog.i(TAG, "No data for UI from: " + mComponentName.flattenToString()); return null; } if (mProviderResponse != null && !mProviderResponseDataHandler.isEmptyResponse()) { return mProviderResponseDataHandler.toGetCredentialProviderData(); } - Slog.d(TAG, "In prepareUiData response null"); return null; } @@ -382,7 +380,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential getCredentialResponse); return; } - Slog.d(TAG, "Pending intent response contains no credential, or error " + Slog.i(TAG, "Pending intent response contains no credential, or error " + "for a credential entry"); invokeCallbackOnInternalInvalidState(); } @@ -413,11 +411,9 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential */ private boolean onAuthenticationEntrySelected( @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) { - Log.i(TAG, "onAuthenticationEntrySelected"); // Authentication entry is expected to have a BeginGetCredentialResponse instance. If it // does not have it, we remove the authentication entry and do not add any more content. if (providerPendingIntentResponse == null) { - Log.i(TAG, "providerPendingIntentResponse is null"); // Nothing received. This is equivalent to no content received. return false; } @@ -462,7 +458,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** Returns true if either an exception or a response is found. */ private void onActionEntrySelected(ProviderPendingIntentResponse providerPendingIntentResponse) { - Slog.d(TAG, "onActionEntrySelected"); + Slog.i(TAG, "onActionEntrySelected"); onCredentialEntrySelected(providerPendingIntentResponse); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java index c10f5640c466..b0b72bcf67cc 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java @@ -171,11 +171,11 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption @Override protected ProviderData prepareUiData() { if (!ProviderSession.isUiInvokingStatus(getStatus())) { - Slog.d(TAG, "No date for UI coming from: " + mComponentName.flattenToString()); + Slog.i(TAG, "No date for UI coming from: " + mComponentName.flattenToString()); return null; } if (mProviderResponse == null) { - Slog.w(TAG, "In prepareUiData but response is null. This is strange."); + Slog.w(TAG, "response is null when preparing ui data. This is strange."); return null; } return new GetCredentialProviderData.Builder( @@ -196,13 +196,13 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption case CREDENTIAL_ENTRY_KEY: CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey); if (credentialEntry == null) { - Slog.w(TAG, "Unexpected credential entry key"); + Slog.i(TAG, "Unexpected credential entry key"); return; } onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse); break; default: - Slog.w(TAG, "Unsupported entry type selected"); + Slog.i(TAG, "Unsupported entry type selected"); } } @@ -256,6 +256,7 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption @Override protected void invokeSession() { + startCandidateMetrics(); mProviderResponse = mCredentialDescriptionRegistry .getFilteredResultForProvider(mCredentialProviderPackageName, mElementKeys); @@ -266,7 +267,7 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption .collect(Collectors.toList()); updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED, /*source=*/ CredentialsSource.REGISTRY); - // TODO(use metric later) + mProviderSessionMetric.collectCandidateEntryMetrics(mCredentialEntries); } @Nullable @@ -279,7 +280,7 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption GetCredentialException exception = PendingIntentResultHandler .extractGetCredentialException(pendingIntentResponse.getResultData()); if (exception != null) { - Slog.d(TAG, "Pending intent contains provider exception"); + Slog.i(TAG, "Pending intent contains provider exception"); return exception; } } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index d02a8c1ee510..73fdc1ce2635 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -268,12 +268,9 @@ public abstract class ProviderSession<T, R> /*pId=*/-1, appInfo.uid) == PackageManager.PERMISSION_GRANTED) { return true; } - } catch (SecurityException e) { + } catch (SecurityException | PackageManager.NameNotFoundException e) { Slog.e(TAG, "Error getting info for " + mComponentName.flattenToString(), e); return false; - } catch (PackageManager.NameNotFoundException e) { - Slog.i(TAG, "Error getting info for " + mComponentName.flattenToString(), e); - return false; } return false; } diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java index 0ad73c945284..f5e3b86213a1 100644 --- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -292,13 +292,13 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr callback.onProviderResponseSuccess(result); } else { if (error instanceof TimeoutException) { - Slog.d(TAG, "Remote provider response timed tuo for: " + mComponentName); + Slog.i(TAG, "Remote provider response timed tuo for: " + mComponentName); dispatchCancellationSignal(cancellationSink.get()); callback.onProviderResponseFailure( CredentialProviderErrors.ERROR_TIMEOUT, null); } else if (error instanceof CancellationException) { - Slog.d(TAG, "Cancellation exception for remote provider: " + mComponentName); + Slog.i(TAG, "Cancellation exception for remote provider: " + mComponentName); dispatchCancellationSignal(cancellationSink.get()); callback.onProviderResponseFailure( CredentialProviderErrors.ERROR_TASK_CANCELED, diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index 15a30e427688..7caa921eacda 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -201,7 +201,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential } protected void finishSession(boolean propagateCancellation) { - Slog.d(TAG, "finishing session with propagateCancellation " + propagateCancellation); + Slog.i(TAG, "finishing session with propagateCancellation " + propagateCancellation); if (propagateCancellation) { mProviders.values().forEach(ProviderSession::cancelProviderRemoteSession); } @@ -265,7 +265,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential @NonNull protected ArrayList<ProviderData> getProviderDataForUi() { - Slog.d(TAG, "In getProviderDataAndInitiateUi providers size: " + mProviders.size()); + Slog.i(TAG, "For ui, provider data size: " + mProviders.size()); ArrayList<ProviderData> providerDataList = new ArrayList<>(); mRequestSessionMetric.logCandidatePhaseMetrics(mProviders); diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java index b99f28d07f75..1930a4859e87 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java @@ -27,7 +27,7 @@ import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INI import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_UNKNOWN; import android.credentials.ui.RequestInfo; -import android.util.Log; +import android.util.Slog; import java.util.AbstractMap; import java.util.Map; @@ -79,7 +79,7 @@ CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_IS_ENABLED_CREDENT */ public static int getMetricCodeFromRequestInfo(String stringKey) { if (!sRequestInfoToMetric.containsKey(stringKey)) { - Log.w(TAG, "Attempted to use an unsupported string key request info"); + Slog.i(TAG, "Attempted to use an unsupported string key request info"); return UNKNOWN.mInnerMetricCode; } return sRequestInfoToMetric.get(stringKey); diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java index 0e1e03897bf1..07af6549411e 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java @@ -27,8 +27,6 @@ package com.android.server.credentials.metrics; * though collection will begin in the candidate phase when the user begins browsing options. */ public class CandidateBrowsingPhaseMetric { - - private static final String TAG = "CandidateBrowsingPhaseMetric"; // The session id associated with the API Call this candidate provider is a part of, default -1 private int mSessionId = -1; // The EntryEnum that was pressed, defaults to -1 diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java index 721d3d782653..3ea9b1ce86f8 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java @@ -16,21 +16,18 @@ package com.android.server.credentials.metrics; -import android.util.IntArray; -import android.util.Log; +import android.util.Slog; import com.android.server.credentials.MetricUtilities; +import com.android.server.credentials.metrics.shared.ResponseCollective; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; /** * The central candidate provider metric object that mimics our defined metric setup. * Some types are redundant across these metric collectors, but that has debug use-cases as * these data-types are available at different moments of the flow (and typically, one can feed * into the next). - * TODO(b/270403549) - iterate on this in V3+ */ public class CandidatePhaseMetric { @@ -56,26 +53,12 @@ public class CandidatePhaseMetric { private int mProviderQueryStatus = -1; // Indicates if an exception was thrown by this provider, false by default private boolean mHasException = false; - // Indicates the number of total entries available. We can also locally store the entries, but - // cannot emit them in the current split form. TODO(b/271135048) - possibly readjust candidate - // entries. Also, it may be okay to remove this and instead aggregate from inner counts. - // Defaults to -1 - private int mNumEntriesTotal = -1; - // The count of action entries from this provider, defaults to -1 - private int mActionEntryCount = -1; - // The count of credential entries from this provider, defaults to -1 - private int mCredentialEntryCount = -1; - // The *type-count* of the credential entries, defaults to -1 - private int mCredentialEntryTypeCount = -1; - // The count of remote entries from this provider, defaults to -1 - private int mRemoteEntryCount = -1; - // The count of authentication entries from this provider, defaults to -1 - private int mAuthenticationEntryCount = -1; - // Gathered to pass on to chosen provider when required - private final IntArray mAvailableEntries = new IntArray(); - // The *framework only* exception held by this provider, empty string by default private String mFrameworkException = ""; + // Stores the response credential information, as well as the response entry information which + // by default, contains empty info + private ResponseCollective mResponseCollective = new ResponseCollective(Map.of(), Map.of()); + public CandidatePhaseMetric() { } @@ -129,7 +112,7 @@ public class CandidatePhaseMetric { */ public int getTimestampFromReferenceStartMicroseconds(long specificTimestamp) { if (specificTimestamp < mServiceBeganTimeNanoseconds) { - Log.i(TAG, "The timestamp is before service started, falling back to default int"); + Slog.i(TAG, "The timestamp is before service started, falling back to default int"); return MetricUtilities.DEFAULT_INT_32; } return (int) ((specificTimestamp @@ -186,88 +169,13 @@ public class CandidatePhaseMetric { return mHasException; } - /* -------------- Number of Entries ---------------- */ - - public void setNumEntriesTotal(int numEntriesTotal) { - mNumEntriesTotal = numEntriesTotal; - } - - public int getNumEntriesTotal() { - return mNumEntriesTotal; - } - - /* -------------- Count of Action Entries ---------------- */ - - public void setActionEntryCount(int actionEntryCount) { - mActionEntryCount = actionEntryCount; - } - - public int getActionEntryCount() { - return mActionEntryCount; - } - - /* -------------- Count of Credential Entries ---------------- */ - - public void setCredentialEntryCount(int credentialEntryCount) { - mCredentialEntryCount = credentialEntryCount; - } - - public int getCredentialEntryCount() { - return mCredentialEntryCount; - } - - /* -------------- Count of Credential Entry Types ---------------- */ - - public void setCredentialEntryTypeCount(int credentialEntryTypeCount) { - mCredentialEntryTypeCount = credentialEntryTypeCount; - } - - public int getCredentialEntryTypeCount() { - return mCredentialEntryTypeCount; + /* -------------- The Entries and Responses Gathered ---------------- */ + public void setResponseCollective(ResponseCollective responseCollective) { + mResponseCollective = responseCollective; } - /* -------------- Count of Remote Entries ---------------- */ - - public void setRemoteEntryCount(int remoteEntryCount) { - mRemoteEntryCount = remoteEntryCount; - } - - public int getRemoteEntryCount() { - return mRemoteEntryCount; - } - - /* -------------- Count of Authentication Entries ---------------- */ - - public void setAuthenticationEntryCount(int authenticationEntryCount) { - mAuthenticationEntryCount = authenticationEntryCount; - } - - public int getAuthenticationEntryCount() { - return mAuthenticationEntryCount; - } - - /* -------------- The Entries Gathered ---------------- */ - - /** - * Allows adding an entry record to this metric collector, which can then be propagated to - * the final phase to retain information on the data available to the candidate. - * - * @param e the entry enum collected by the candidate provider associated with this metric - * collector - */ - public void addEntry(EntryEnum e) { - mAvailableEntries.add(e.getMetricCode()); - } - - /** - * Returns a safely copied list of the entries captured by this metric collector associated - * with a particular candidate provider. - * - * @return the full collection of entries encountered by the candidate provider associated with - * this metric - */ - public List<Integer> getAvailableEntries() { - return Arrays.stream(mAvailableEntries.toArray()).boxed().collect(Collectors.toList()); + public ResponseCollective getResponseCollective() { + return mResponseCollective; } /* ------ Framework Exception for this Candidate ------ */ diff --git a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java index c80cc24fa455..93a82906aa50 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java @@ -16,12 +16,12 @@ package com.android.server.credentials.metrics; -import android.util.Log; +import android.util.Slog; import com.android.server.credentials.MetricUtilities; +import com.android.server.credentials.metrics.shared.ResponseCollective; -import java.util.ArrayList; -import java.util.List; +import java.util.Map; /** * The central chosen provider metric object that mimics our defined metric setup. This is used @@ -29,11 +29,8 @@ import java.util.List; * Some types are redundant across these metric collectors, but that has debug use-cases as * these data-types are available at different moments of the flow (and typically, one can feed * into the next). - * TODO(b/270403549) - iterate on this in V3+ */ public class ChosenProviderFinalPhaseMetric { - - // TODO(b/270403549) - applies elsewhere, likely removed or replaced w/ some hashed/count index private static final String TAG = "ChosenFinalPhaseMetric"; // The session id associated with this API call, used to unite split emits private int mSessionId = -1; @@ -69,21 +66,10 @@ public class ChosenProviderFinalPhaseMetric { private int mChosenProviderStatus = -1; // Indicates if an exception was thrown by this provider, false by default private boolean mHasException = false; - // Indicates the number of total entries available, defaults to -1. Not presently emitted, but - // left as a utility - private int mNumEntriesTotal = -1; - // The count of action entries from this provider, defaults to -1 - private int mActionEntryCount = -1; - // The count of credential entries from this provider, defaults to -1 - private int mCredentialEntryCount = -1; - // The *type-count* of the credential entries, defaults to -1 - private int mCredentialEntryTypeCount = -1; - // The count of remote entries from this provider, defaults to -1 - private int mRemoteEntryCount = -1; - // The count of authentication entries from this provider, defaults to -1 - private int mAuthenticationEntryCount = -1; - // Gathered to pass on to chosen provider when required - private List<Integer> mAvailableEntries = new ArrayList<>(); + + // Stores the response credential information, as well as the response entry information which + // by default, contains empty info + private ResponseCollective mResponseCollective = new ResponseCollective(Map.of(), Map.of()); public ChosenProviderFinalPhaseMetric() { @@ -230,7 +216,7 @@ public class ChosenProviderFinalPhaseMetric { */ public int getTimestampFromReferenceStartMicroseconds(long specificTimestamp) { if (specificTimestamp < mServiceBeganTimeNanoseconds) { - Log.i(TAG, "The timestamp is before service started, falling back to default int"); + Slog.i(TAG, "The timestamp is before service started, falling back to default int"); return MetricUtilities.DEFAULT_INT_32; } return (int) ((specificTimestamp @@ -267,87 +253,6 @@ public class ChosenProviderFinalPhaseMetric { return mUiReturned; } - /* -------------- Number of Entries ---------------- */ - - public void setNumEntriesTotal(int numEntriesTotal) { - mNumEntriesTotal = numEntriesTotal; - } - - public int getNumEntriesTotal() { - return mNumEntriesTotal; - } - - /* -------------- Count of Action Entries ---------------- */ - - public void setActionEntryCount(int actionEntryCount) { - mActionEntryCount = actionEntryCount; - } - - public int getActionEntryCount() { - return mActionEntryCount; - } - - /* -------------- Count of Credential Entries ---------------- */ - - public void setCredentialEntryCount(int credentialEntryCount) { - mCredentialEntryCount = credentialEntryCount; - } - - public int getCredentialEntryCount() { - return mCredentialEntryCount; - } - - /* -------------- Count of Credential Entry Types ---------------- */ - - public void setCredentialEntryTypeCount(int credentialEntryTypeCount) { - mCredentialEntryTypeCount = credentialEntryTypeCount; - } - - public int getCredentialEntryTypeCount() { - return mCredentialEntryTypeCount; - } - - /* -------------- Count of Remote Entries ---------------- */ - - public void setRemoteEntryCount(int remoteEntryCount) { - mRemoteEntryCount = remoteEntryCount; - } - - public int getRemoteEntryCount() { - return mRemoteEntryCount; - } - - /* -------------- Count of Authentication Entries ---------------- */ - - public void setAuthenticationEntryCount(int authenticationEntryCount) { - mAuthenticationEntryCount = authenticationEntryCount; - } - - public int getAuthenticationEntryCount() { - return mAuthenticationEntryCount; - } - - /* -------------- The Entries Gathered ---------------- */ - - /** - * Sets the collected list of entries from the candidate phase to be retrievable in the - * chosen phase in a semantically correct way. - */ - public void setAvailableEntries(List<Integer> entries) { - mAvailableEntries = new ArrayList<>(entries); // no alias copy - } - - /** - * Returns a list of the entries captured by this metric collector associated - * with a particular chosen provider. - * - * @return the full collection of entries encountered by the chosen provider during the - * candidate phase. - */ - public List<Integer> getAvailableEntries() { - return new ArrayList<>(mAvailableEntries); // no alias copy - } - /* -------------- Has Exception ---------------- */ public void setHasException(boolean hasException) { @@ -357,4 +262,14 @@ public class ChosenProviderFinalPhaseMetric { public boolean isHasException() { return mHasException; } + + /* -------------- The Entries and Responses Gathered ---------------- */ + + public void setResponseCollective(ResponseCollective responseCollective) { + mResponseCollective = responseCollective; + } + + public ResponseCollective getResponseCollective() { + return mResponseCollective; + } } 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 b9125ddf1145..530f01cbdfc5 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java +++ b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java @@ -26,7 +26,7 @@ import static com.android.server.credentials.ProviderGetSession.AUTHENTICATION_A import static com.android.server.credentials.ProviderGetSession.CREDENTIAL_ENTRY_KEY; import static com.android.server.credentials.ProviderGetSession.REMOTE_ENTRY_KEY; -import android.util.Log; +import android.util.Slog; import java.util.AbstractMap; import java.util.Map; @@ -77,7 +77,7 @@ public enum EntryEnum { */ public static int getMetricCodeFromString(String stringKey) { if (!sKeyToEntryCode.containsKey(stringKey)) { - Log.w(TAG, "Attempted to use an unsupported string key entry type"); + Slog.i(TAG, "Attempted to use an unsupported string key entry type"); return UNKNOWN.mInnerMetricCode; } return sKeyToEntryCode.get(stringKey); diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java index 0ecd9cc79e48..060e56ce965b 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java @@ -16,8 +16,6 @@ package com.android.server.credentials.metrics; -import android.util.Log; - import java.util.LinkedHashMap; import java.util.Map; @@ -26,7 +24,6 @@ import java.util.Map; * Some types are redundant across these metric collectors, but that has debug use-cases as * these data-types are available at different moments of the flow (and typically, one can feed * into the next). - * TODO(b/270403549) - iterate on this in V3+ */ public class InitialPhaseMetric { private static final String TAG = "InitialPhaseMetric"; @@ -47,10 +44,9 @@ public class InitialPhaseMetric { private long mCredentialServiceBeginQueryTimeNanoseconds = -1; // Indicates if the origin was specified when making this API request - // TODO(b/271135048) - Emit once metrics approved private boolean mOriginSpecified = false; - // Stores the deduped request information, particularly {"req":5}. + // Stores the deduped request information, particularly {"req":5} private Map<String, Integer> mRequestCounts = new LinkedHashMap<>(); @@ -140,26 +136,20 @@ public class InitialPhaseMetric { } /** - * Reruns the unique, deduped, request classtypes for logging. + * Returns the unique, deduped, request classtypes for logging. * @return a string array for deduped classtypes */ public String[] getUniqueRequestStrings() { - if (mRequestCounts.isEmpty()) { - Log.w(TAG, "There are no unique string request types collected"); - } String[] result = new String[mRequestCounts.keySet().size()]; mRequestCounts.keySet().toArray(result); return result; } /** - * Reruns the unique, deduped, request classtype counts for logging. + * Returns the unique, deduped, request classtype counts for logging. * @return a string array for deduped classtype counts */ public int[] getUniqueRequestCounts() { - if (mRequestCounts.isEmpty()) { - Log.w(TAG, "There are no unique string request type counts collected"); - } return mRequestCounts.values().stream().mapToInt(Integer::intValue).toArray(); } } 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 9a88255ce973..f011b554fe53 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java @@ -16,15 +16,21 @@ package com.android.server.credentials.metrics; +import static com.android.server.credentials.MetricUtilities.DELTA_CUT; +import static com.android.server.credentials.MetricUtilities.generateMetricKey; + import android.annotation.NonNull; import android.service.credentials.BeginCreateCredentialResponse; import android.service.credentials.BeginGetCredentialResponse; import android.service.credentials.CredentialEntry; -import android.util.Log; +import android.util.Slog; import com.android.server.credentials.MetricUtilities; +import com.android.server.credentials.metrics.shared.ResponseCollective; -import java.util.stream.Collectors; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** * Provides contextual metric collection for objects generated from @@ -68,7 +74,7 @@ public class ProviderSessionMetric { try { mCandidatePhasePerProviderMetric.setFrameworkException(exceptionType); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during candidate exception metric logging: " + e); } } @@ -97,7 +103,7 @@ public class ProviderSessionMetric { .getMetricCode()); } } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during candidate update metric logging: " + e); } } @@ -118,7 +124,7 @@ public class ProviderSessionMetric { initMetric.getCredentialServiceStartedTimeNanoseconds()); mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime()); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during candidate setup metric logging: " + e); } } @@ -138,59 +144,79 @@ public class ProviderSessionMetric { beginCreateCredentialResponseCollectionCandidateEntryMetrics( (BeginCreateCredentialResponse) response); } else { - Log.i(TAG, "Your response type is unsupported for metric logging"); + Slog.i(TAG, "Your response type is unsupported for metric logging"); } - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during candidate entry metric logging: " + e); } } + /** + * Once entries are received from the registry, this helps collect their info for metric + * purposes. + * + * @param entries contains matching entries from the Credential Registry. + */ + public void collectCandidateEntryMetrics(List<CredentialEntry> entries) { + int numCredEntries = entries.size(); + int numRemoteEntry = MetricUtilities.ZERO; + int numActionEntries = MetricUtilities.ZERO; + int numAuthEntries = MetricUtilities.ZERO; + Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); + Map<String, Integer> responseCounts = new LinkedHashMap<>(); + entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry); + entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCredEntries); + entryCounts.put(EntryEnum.ACTION_ENTRY, numActionEntries); + entryCounts.put(EntryEnum.AUTHENTICATION_ENTRY, numAuthEntries); + + entries.forEach(entry -> { + String entryKey = generateMetricKey(entry.getType(), DELTA_CUT); + responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1); + }); + + ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); + mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); + } + private void beginCreateCredentialResponseCollectionCandidateEntryMetrics( BeginCreateCredentialResponse response) { + Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); var createEntries = response.getCreateEntries(); - int numRemoteEntry = MetricUtilities.ZERO; - if (response.getRemoteCreateEntry() != null) { - numRemoteEntry = MetricUtilities.UNIT; - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.REMOTE_ENTRY); - } - int numCreateEntries = - createEntries == null ? MetricUtilities.ZERO : createEntries.size(); - if (numCreateEntries > MetricUtilities.ZERO) { - createEntries.forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.CREDENTIAL_ENTRY)); - } - mCandidatePhasePerProviderMetric.setNumEntriesTotal(numCreateEntries + numRemoteEntry); - mCandidatePhasePerProviderMetric.setRemoteEntryCount(numRemoteEntry); - mCandidatePhasePerProviderMetric.setCredentialEntryCount(numCreateEntries); - mCandidatePhasePerProviderMetric.setCredentialEntryTypeCount(MetricUtilities.UNIT); + 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 + + ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); + mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); } private void beginGetCredentialResponseCollectionCandidateEntryMetrics( BeginGetCredentialResponse response) { + Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); + Map<String, Integer> responseCounts = new LinkedHashMap<>(); int numCredEntries = response.getCredentialEntries().size(); int numActionEntries = response.getActions().size(); int numAuthEntries = response.getAuthenticationActions().size(); - int numRemoteEntry = MetricUtilities.ZERO; - if (response.getRemoteCredentialEntry() != null) { - numRemoteEntry = MetricUtilities.UNIT; - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.REMOTE_ENTRY); - } - response.getCredentialEntries().forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.CREDENTIAL_ENTRY)); - response.getActions().forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.ACTION_ENTRY)); - response.getAuthenticationActions().forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.AUTHENTICATION_ENTRY)); - mCandidatePhasePerProviderMetric.setNumEntriesTotal(numCredEntries + numAuthEntries - + numActionEntries + numRemoteEntry); - mCandidatePhasePerProviderMetric.setCredentialEntryCount(numCredEntries); - int numTypes = (response.getCredentialEntries().stream() - .map(CredentialEntry::getType).collect( - Collectors.toSet())).size(); // Dedupe type strings - mCandidatePhasePerProviderMetric.setCredentialEntryTypeCount(numTypes); - mCandidatePhasePerProviderMetric.setActionEntryCount(numActionEntries); - mCandidatePhasePerProviderMetric.setAuthenticationEntryCount(numAuthEntries); - mCandidatePhasePerProviderMetric.setRemoteEntryCount(numRemoteEntry); + int numRemoteEntry = response.getRemoteCredentialEntry() != null ? MetricUtilities.ZERO : + MetricUtilities.UNIT; + entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry); + entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCredEntries); + entryCounts.put(EntryEnum.ACTION_ENTRY, numActionEntries); + entryCounts.put(EntryEnum.AUTHENTICATION_ENTRY, numAuthEntries); + + response.getCredentialEntries().forEach(entry -> { + String entryKey = generateMetricKey(entry.getType(), DELTA_CUT); + responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1); + }); + + 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 547c09a625f6..4624e0b3701a 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -24,7 +24,7 @@ import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPh import android.credentials.GetCredentialRequest; import android.credentials.ui.UserSelectionDialogResult; import android.os.IBinder; -import android.util.Log; +import android.util.Slog; import com.android.server.credentials.ProviderSession; @@ -90,7 +90,7 @@ public class RequestSessionMetric { mInitialPhaseMetric.setCallerUid(mCallingUid); mInitialPhaseMetric.setApiName(metricCode); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error collecting initial metrics: " + e); } } @@ -103,7 +103,7 @@ public class RequestSessionMetric { try { mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error collecting ui end time metric: " + e); } } @@ -116,7 +116,7 @@ public class RequestSessionMetric { try { mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(uiCallStartTime); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error collecting ui start metric: " + e); } } @@ -132,7 +132,7 @@ public class RequestSessionMetric { mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(uiEndTimestamp); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error collecting ui response metric: " + e); } } @@ -146,7 +146,7 @@ public class RequestSessionMetric { try { mChosenProviderFinalPhaseMetric.setChosenProviderStatus(status); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error setting chosen provider status metric: " + e); } } @@ -159,7 +159,7 @@ public class RequestSessionMetric { try { mInitialPhaseMetric.setOriginSpecified(origin); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error collecting create flow metric: " + e); } } @@ -175,7 +175,7 @@ public class RequestSessionMetric { uniqueRequestCounts.put(optionKey, uniqueRequestCounts.get(optionKey) + 1); }); } catch (Exception e) { - Log.w(TAG, "Unexpected error during get request metric logging: " + e); + Slog.i(TAG, "Unexpected error during get request metric logging: " + e); } return uniqueRequestCounts; } @@ -190,7 +190,7 @@ public class RequestSessionMetric { mInitialPhaseMetric.setOriginSpecified(request.getOrigin() != null); mInitialPhaseMetric.setRequestCounts(getRequestCountMap(request)); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error collecting get flow metric: " + e); } } @@ -213,7 +213,7 @@ public class RequestSessionMetric { browsingPhaseMetric.setProviderUid(selectedProviderPhaseMetric.getCandidateUid()); mCandidateBrowsingPhaseMetric.add(browsingPhaseMetric); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error collecting browsing metric: " + e); } } @@ -226,7 +226,7 @@ public class RequestSessionMetric { try { mChosenProviderFinalPhaseMetric.setHasException(exceptionBitFinalPhase); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error setting final exception metric: " + e); } } @@ -244,7 +244,7 @@ public class RequestSessionMetric { mChosenProviderFinalPhaseMetric.setChosenProviderStatus( finalStatus.getMetricCode()); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during metric logging: " + e); } } @@ -272,24 +272,11 @@ public class RequestSessionMetric { candidatePhaseMetric.getStartQueryTimeNanoseconds()); mChosenProviderFinalPhaseMetric.setQueryEndTimeNanoseconds(candidatePhaseMetric .getQueryFinishTimeNanoseconds()); - - mChosenProviderFinalPhaseMetric.setNumEntriesTotal(candidatePhaseMetric - .getNumEntriesTotal()); - mChosenProviderFinalPhaseMetric.setCredentialEntryCount(candidatePhaseMetric - .getCredentialEntryCount()); - mChosenProviderFinalPhaseMetric.setCredentialEntryTypeCount( - candidatePhaseMetric.getCredentialEntryTypeCount()); - mChosenProviderFinalPhaseMetric.setActionEntryCount(candidatePhaseMetric - .getActionEntryCount()); - mChosenProviderFinalPhaseMetric.setRemoteEntryCount(candidatePhaseMetric - .getRemoteEntryCount()); - mChosenProviderFinalPhaseMetric.setAuthenticationEntryCount( - candidatePhaseMetric.getAuthenticationEntryCount()); - mChosenProviderFinalPhaseMetric.setAvailableEntries(candidatePhaseMetric - .getAvailableEntries()); + mChosenProviderFinalPhaseMetric.setResponseCollective( + candidatePhaseMetric.getResponseCollective()); mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime()); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during metric candidate to final transfer: " + e); } } @@ -312,7 +299,7 @@ public class RequestSessionMetric { /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); } } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during final metric failure emit: " + e); } } @@ -326,7 +313,7 @@ public class RequestSessionMetric { try { logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during candidate metric emit: " + e); } } @@ -337,15 +324,11 @@ public class RequestSessionMetric { */ public void logApiCalledAtFinish(int apiStatus) { try { - // TODO (b/270403549) - this browsing phase object is fine but also have a new emit - // For the returned types by authentication entries - i.e. a CandidatePhase During - // Browse - // Possibly think of adding in more atoms for other APIs as well. logApiCalledFinalPhase(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, apiStatus, ++mSequenceCounter); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during final 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 new file mode 100644 index 000000000000..fd785c2f4dfc --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java @@ -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.server.credentials.metrics.shared; + +import android.annotation.NonNull; + +import com.android.server.credentials.metrics.EntryEnum; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Some data is directly shared between the + * {@link com.android.server.credentials.metrics.CandidatePhaseMetric} and the + * {@link com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric}. This + * aims to create an abstraction that holds that information, to avoid duplication. + * + * This class should be immutable and threadsafe once generated. + */ +public class ResponseCollective { + /* + Abstract Function (responseCounts, entryCounts) -> A 'ResponseCollective' containing information + about a chosen or candidate providers available responses, be they entries or credentials. + + RepInvariant: mResponseCounts and mEntryCounts are always initialized + + Threadsafe and Immutability: Once generated, the maps remain unchangeable. The object is + threadsafe and immutable, and safe from external changes. This is threadsafe because it is + immutable after creation and only allows reads, not writes. + */ + + private static final String TAG = "ResponseCollective"; + + // Stores the deduped credential response information, eg {"response":5} for this provider + private final Map<String, Integer> mResponseCounts; + // Stores the deduped entry information, eg {ENTRY_ENUM:5} for this provider + private final Map<EntryEnum, Integer> mEntryCounts; + + public ResponseCollective(@NonNull Map<String, Integer> responseCounts, + @NonNull Map<EntryEnum, Integer> entryCounts) { + mResponseCounts = responseCounts == null ? new LinkedHashMap<>() : + new LinkedHashMap<>(responseCounts); + mEntryCounts = entryCounts == null ? new LinkedHashMap<>() : + new LinkedHashMap<>(entryCounts); + } + + /** + * Returns the unique, deduped, response classtypes for logging associated with this provider. + * + * @return a string array for deduped classtypes + */ + public String[] getUniqueResponseStrings() { + String[] result = new String[mResponseCounts.keySet().size()]; + mResponseCounts.keySet().toArray(result); + return result; + } + + /** + * Returns the unique, deduped, response classtype counts for logging associated with this + * provider. + * @return a string array for deduped classtype counts + */ + public int[] getUniqueResponseCounts() { + return mResponseCounts.values().stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Returns the unique, deduped, entry types for logging associated with this provider. + * @return an int array for deduped entries + */ + public int[] getUniqueEntries() { + return mEntryCounts.keySet().stream().mapToInt(Enum::ordinal).toArray(); + } + + /** + * Returns the unique, deduped, entry classtype counts for logging associated with this + * provider. + * @return a string array for deduped classtype counts + */ + public int[] getUniqueEntryCounts() { + return mEntryCounts.values().stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Given a specific {@link EntryEnum}, this provides us with the count of that entry within + * this particular provider. + * @param e the entry enum with which we want to know the count of + * @return a count of this particular entry enum stored by this provider + */ + public int getCountForEntry(EntryEnum e) { + return mEntryCounts.get(e); + } + + /** + * Indicates the total number of existing entries for this provider. + * @return a count of the total number of entries for this provider + */ + public int getNumEntriesTotal() { + return mEntryCounts.values().stream().mapToInt(Integer::intValue).sum(); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b1d613109e09..a8a1c0354cf4 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -593,8 +593,8 @@ public final class SystemServer implements Dumpable { * Spawn a thread that monitors for fd leaks. */ private static void spawnFdLeakCheckThread() { - final int enableThreshold = SystemProperties.getInt(SYSPROP_FDTRACK_ENABLE_THRESHOLD, 1024); - final int abortThreshold = SystemProperties.getInt(SYSPROP_FDTRACK_ABORT_THRESHOLD, 2048); + final int enableThreshold = SystemProperties.getInt(SYSPROP_FDTRACK_ENABLE_THRESHOLD, 1600); + final int abortThreshold = SystemProperties.getInt(SYSPROP_FDTRACK_ABORT_THRESHOLD, 3000); final int checkInterval = SystemProperties.getInt(SYSPROP_FDTRACK_INTERVAL, 120); new Thread(() -> { diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java index f05b1d47ac0b..475966ea00b8 100644 --- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java +++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java @@ -20,9 +20,11 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.os.Handler; import android.os.IBinder.DeathRecipient; import android.os.Looper; @@ -53,7 +55,8 @@ public final class ProfcollectForwardingService extends SystemService { public static final String LOG_TAG = "ProfcollectForwardingService"; private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); - + private static final String INTENT_UPLOAD_PROFILES = + "com.android.server.profcollect.UPLOAD_PROFILES"; private static final long BG_PROCESS_PERIOD = TimeUnit.HOURS.toMillis(4); // every 4 hours. private IProfCollectd mIProfcollect; @@ -66,6 +69,16 @@ public final class ProfcollectForwardingService extends SystemService { } }; + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() == INTENT_UPLOAD_PROFILES) { + Log.d(LOG_TAG, "Received broadcast to pack and upload reports"); + packAndUploadReport(); + } + } + }; + public ProfcollectForwardingService(Context context) { super(context); @@ -73,6 +86,10 @@ public final class ProfcollectForwardingService extends SystemService { throw new AssertionError("only one service instance allowed"); } sSelfService = this; + + final IntentFilter filter = new IntentFilter(); + filter.addAction(INTENT_UPLOAD_PROFILES); + context.registerReceiver(mBroadcastReceiver, filter); } /** @@ -296,7 +313,7 @@ public final class ProfcollectForwardingService extends SystemService { } if (status == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) { - packProfileReport(); + packAndUploadReport(); } } @@ -307,7 +324,7 @@ public final class ProfcollectForwardingService extends SystemService { }); } - private void packProfileReport() { + private void packAndUploadReport() { if (mIProfcollect == null) { return; } diff --git a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java index 3a47b476a131..2b57c5927678 100644 --- a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java @@ -154,6 +154,7 @@ public class DeviceIdleControllerTest { // Freeze time for testing. long nowElapsed; boolean useMotionSensor = true; + boolean isLocationPrefetchEnabled = true; InjectorForTest(Context ctx) { super(ctx); @@ -223,6 +224,11 @@ public class DeviceIdleControllerTest { } @Override + boolean isLocationPrefetchEnabled() { + return isLocationPrefetchEnabled; + } + + @Override PowerManager getPowerManager() { return mPowerManager; } @@ -991,6 +997,43 @@ public class DeviceIdleControllerTest { } @Test + public void testStepIdleStateLocked_ValidStates_LocationPrefetchDisabled() { + mInjector.locationManager = mLocationManager; + mInjector.isLocationPrefetchEnabled = false; + cleanupDeviceIdleController(); + setupDeviceIdleController(); + doReturn(mock(LocationProvider.class)).when(mLocationManager).getProvider(anyString()); + // Make sure the controller doesn't think there's a wake-from-idle alarm coming soon. + setAlarmSoon(false); + // Set state to INACTIVE. + mDeviceIdleController.becomeActiveLocked("testing", 0); + setChargingOn(false); + setScreenOn(false); + verifyStateConditions(STATE_INACTIVE); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE_PENDING); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_SENSING); + + mDeviceIdleController.stepIdleStateLocked("testing"); + // Prefetch location is off, so SENSING should go straight through to IDLE. + verifyStateConditions(STATE_IDLE); + + // Should just alternate between IDLE and IDLE_MAINTENANCE now. + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE_MAINTENANCE); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE_MAINTENANCE); + } + + @Test public void testStepIdleStateLocked_ValidStates_WithLocationManager_NoProviders() { // Make sure the controller doesn't think there's a wake-from-idle alarm coming soon. setAlarmSoon(false); @@ -1024,6 +1067,46 @@ public class DeviceIdleControllerTest { } @Test + public void testStepIdleStateLocked_ValidStates_WithLocationManager_MissingProviders() { + mInjector.locationManager = mLocationManager; + doReturn(null).when(mLocationManager) + .getProvider(eq(LocationManager.FUSED_PROVIDER)); + doReturn(null).when(mLocationManager) + .getProvider(eq(LocationManager.GPS_PROVIDER)); + doReturn(mock(LocationProvider.class)).when(mLocationManager) + .getProvider(eq(LocationManager.NETWORK_PROVIDER)); + // Make sure the controller doesn't think there's a wake-from-idle alarm coming soon. + setAlarmSoon(false); + // Set state to INACTIVE. + mDeviceIdleController.becomeActiveLocked("testing", 0); + setChargingOn(false); + setScreenOn(false); + verifyStateConditions(STATE_INACTIVE); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE_PENDING); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_SENSING); + + mDeviceIdleController.stepIdleStateLocked("testing"); + // Location manager exists, but the required providers don't exist, + // so SENSING should go straight through to IDLE. + verifyStateConditions(STATE_IDLE); + + // Should just alternate between IDLE and IDLE_MAINTENANCE now. + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE_MAINTENANCE); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE); + + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE_MAINTENANCE); + } + + @Test public void testStepIdleStateLocked_ValidStates_WithLocationManager_WithProviders() { mInjector.locationManager = mLocationManager; doReturn(mock(LocationProvider.class)).when(mLocationManager).getProvider(anyString()); diff --git a/services/tests/mockingservicestests/src/com/android/server/display/RefreshRateSettingsUtilsTest.java b/services/tests/mockingservicestests/src/com/android/server/display/RefreshRateSettingsUtilsTest.java deleted file mode 100644 index 17fba9f43707..000000000000 --- a/services/tests/mockingservicestests/src/com/android/server/display/RefreshRateSettingsUtilsTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.display; - -import static com.android.internal.display.RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.when; - -import android.hardware.display.DisplayManager; -import android.provider.Settings; -import android.testing.TestableContext; -import android.view.Display; - -import androidx.test.filters.SmallTest; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.display.RefreshRateSettingsUtils; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class RefreshRateSettingsUtilsTest { - - @Rule - public final TestableContext mContext = new TestableContext( - InstrumentationRegistry.getInstrumentation().getContext()); - - @Mock - private DisplayManager mDisplayManagerMock; - @Mock - private Display mDisplayMock; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mContext.addMockSystemService(DisplayManager.class, mDisplayManagerMock); - - Display.Mode[] modes = new Display.Mode[] { - new Display.Mode(/* modeId= */ 0, /* width= */ 800, /* height= */ 600, - /* refreshRate= */ 60), - new Display.Mode(/* modeId= */ 0, /* width= */ 800, /* height= */ 600, - /* refreshRate= */ 120), - new Display.Mode(/* modeId= */ 0, /* width= */ 800, /* height= */ 600, - /* refreshRate= */ 90) - }; - - when(mDisplayManagerMock.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(mDisplayMock); - when(mDisplayMock.getSupportedModes()).thenReturn(modes); - } - - @Test - public void testFindHighestRefreshRateForDefaultDisplay() { - when(mDisplayManagerMock.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(null); - assertEquals(DEFAULT_REFRESH_RATE, - RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay(mContext), - /* delta= */ 0); - - when(mDisplayManagerMock.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(mDisplayMock); - assertEquals(120, - RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay(mContext), - /* delta= */ 0); - } - - @Test - public void testGetMinRefreshRate() { - Settings.System.putInt(mContext.getContentResolver(), - Settings.System.FORCE_PEAK_REFRESH_RATE, -1); - assertEquals(0, RefreshRateSettingsUtils.getMinRefreshRate(mContext), /* delta= */ 0); - - Settings.System.putInt(mContext.getContentResolver(), - Settings.System.FORCE_PEAK_REFRESH_RATE, 0); - assertEquals(0, RefreshRateSettingsUtils.getMinRefreshRate(mContext), /* delta= */ 0); - - Settings.System.putInt(mContext.getContentResolver(), - Settings.System.FORCE_PEAK_REFRESH_RATE, 1); - assertEquals(120, RefreshRateSettingsUtils.getMinRefreshRate(mContext), /* delta= */ 0); - } - - @Test - public void testGetPeakRefreshRate() { - float defaultPeakRefreshRate = 100; - - Settings.System.putInt(mContext.getContentResolver(), Settings.System.SMOOTH_DISPLAY, -1); - assertEquals(defaultPeakRefreshRate, - RefreshRateSettingsUtils.getPeakRefreshRate(mContext, defaultPeakRefreshRate), - /* delta= */ 0); - - Settings.System.putInt(mContext.getContentResolver(), Settings.System.SMOOTH_DISPLAY, 0); - assertEquals(DEFAULT_REFRESH_RATE, - RefreshRateSettingsUtils.getPeakRefreshRate(mContext, defaultPeakRefreshRate), - /* delta= */ 0); - - Settings.System.putInt(mContext.getContentResolver(), Settings.System.SMOOTH_DISPLAY, 1); - assertEquals(120, - RefreshRateSettingsUtils.getPeakRefreshRate(mContext, defaultPeakRefreshRate), - /* delta= */ 0); - } -} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorListTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorListTest.java new file mode 100644 index 000000000000..3d80916bd63e --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorListTest.java @@ -0,0 +1,66 @@ +/* + * 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.biometrics.sensors; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.app.IActivityManager; +import android.app.SynchronousUserSwitchObserver; +import android.os.RemoteException; +import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import com.android.server.biometrics.sensors.face.aidl.Sensor; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@Presubmit +@SmallTest +public class SensorListTest { + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Mock + Sensor mSensor; + @Mock + IActivityManager mActivityManager; + @Mock + SynchronousUserSwitchObserver mUserSwitchObserver; + + SensorList<Sensor> mSensorList; + + @Before + public void setUp() throws RemoteException { + mSensorList = new SensorList<>(mActivityManager); + } + + @Test + public void testAddingSensor() throws RemoteException { + mSensorList.addSensor(0, mSensor, UserHandle.USER_NULL, mUserSwitchObserver); + + verify(mUserSwitchObserver).onUserSwitching(UserHandle.USER_SYSTEM); + verify(mActivityManager).registerUserSwitchObserver(eq(mUserSwitchObserver), anyString()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java index 41f743367aeb..31a58cd67d3e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.face.aidl; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -30,6 +32,7 @@ import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.ISession; import android.hardware.biometrics.face.SensorProps; import android.os.RemoteException; +import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; @@ -38,6 +41,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.HalClientMonitor; @@ -98,16 +102,34 @@ public class FaceProviderTest { mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext); } + @Test + public void testAddingSensors() { + waitForIdle(); + + for (SensorProps prop : mSensorProps) { + final BiometricScheduler scheduler = + mFaceProvider.mFaceSensors.get(prop.commonProps.sensorId) + .getScheduler(); + BaseClientMonitor currentClient = scheduler.getCurrentClient(); + + assertThat(currentClient).isInstanceOf(FaceInternalCleanupClient.class); + assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId); + assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM); + } + } + @SuppressWarnings("rawtypes") @Test public void halServiceDied_resetsAllSchedulers() { + waitForIdle(); + assertEquals(mSensorProps.length, mFaceProvider.getSensorProperties().size()); // Schedule N operations on each sensor final int numFakeOperations = 10; for (SensorProps prop : mSensorProps) { final BiometricScheduler scheduler = - mFaceProvider.mSensors.get(prop.commonProps.sensorId).getScheduler(); + mFaceProvider.mFaceSensors.get(prop.commonProps.sensorId).getScheduler(); for (int i = 0; i < numFakeOperations; i++) { final HalClientMonitor testMonitor = mock(HalClientMonitor.class); when(testMonitor.getFreshDaemon()).thenReturn(new Object()); @@ -119,8 +141,8 @@ public class FaceProviderTest { // The right amount of pending and current operations are scheduled for (SensorProps prop : mSensorProps) { final BiometricScheduler scheduler = - mFaceProvider.mSensors.get(prop.commonProps.sensorId).getScheduler(); - assertEquals(numFakeOperations - 1, scheduler.getCurrentPendingCount()); + mFaceProvider.mFaceSensors.get(prop.commonProps.sensorId).getScheduler(); + assertEquals(numFakeOperations, scheduler.getCurrentPendingCount()); assertNotNull(scheduler.getCurrentClient()); } @@ -132,7 +154,7 @@ public class FaceProviderTest { // No pending operations, no current operation. for (SensorProps prop : mSensorProps) { final BiometricScheduler scheduler = - mFaceProvider.mSensors.get(prop.commonProps.sensorId).getScheduler(); + mFaceProvider.mFaceSensors.get(prop.commonProps.sensorId).getScheduler(); assertNull(scheduler.getCurrentClient()); assertEquals(0, scheduler.getCurrentPendingCount()); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java index 9c9d3f894d95..25bd9bcf8d5c 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java @@ -29,9 +29,9 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.common.CommonProps; -import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.ISession; import android.hardware.biometrics.face.SensorProps; +import android.hardware.face.FaceSensorPropertiesInternal; import android.os.Handler; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -42,7 +42,6 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.BiometricScheduler; -import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; @@ -82,17 +81,13 @@ public class SensorTest { @Mock private AuthSessionCoordinator mAuthSessionCoordinator; @Mock - private IFace mDaemon; - @Mock - private BiometricStateCallback mBiometricStateCallback; + FaceProvider mFaceProvider; private final TestLooper mLooper = new TestLooper(); private final LockoutCache mLockoutCache = new LockoutCache(); private UserAwareBiometricScheduler mScheduler; private Sensor.HalSessionCallback mHalCallback; - private FaceProvider mFaceProvider; - private SensorProps[] mSensorProps; @Before public void setUp() { @@ -113,16 +108,6 @@ public class SensorTest { TAG, mScheduler, SENSOR_ID, USER_ID, mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator, mHalSessionCallback); - - final SensorProps sensor1 = new SensorProps(); - sensor1.commonProps = new CommonProps(); - sensor1.commonProps.sensorId = 0; - final SensorProps sensor2 = new SensorProps(); - sensor2.commonProps = new CommonProps(); - sensor2.commonProps.sensorId = 1; - mSensorProps = new SensorProps[]{sensor1, sensor2}; - mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback, - mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext); } @Test @@ -154,14 +139,26 @@ public class SensorTest { @Test public void onBinderDied_noErrorOnNullClient() { + mLooper.dispatchAll(); + + final SensorProps sensorProps = new SensorProps(); + sensorProps.commonProps = new CommonProps(); + sensorProps.commonProps.sensorId = 1; + final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( + sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength, + sensorProps.commonProps.maxEnrollmentsPerUser, null, + sensorProps.sensorType, sensorProps.supportsDetectInteraction, + sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */); + final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null, + internalProp, mLockoutResetDispatcher, mBiometricContext); + mScheduler.reset(); + assertNull(mScheduler.getCurrentClient()); - mFaceProvider.binderDied(); - for (int i = 0; i < mFaceProvider.mSensors.size(); i++) { - final Sensor sensor = mFaceProvider.mSensors.valueAt(i); - assertNull(sensor.getSessionForUser(USER_ID)); - } + sensor.onBinderDied(); + + assertNull(sensor.getSessionForUser(USER_ID)); } private void verifyNotLocked() { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java index c6ddf27f96b2..9c01de6f0461 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -33,6 +35,7 @@ import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.SensorLocation; import android.hardware.biometrics.fingerprint.SensorProps; import android.os.RemoteException; +import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; @@ -41,6 +44,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.HalClientMonitor; @@ -111,16 +115,38 @@ public class FingerprintProviderTest { mGestureAvailabilityDispatcher, mBiometricContext); } + @Test + public void testAddingSensors() { + mFingerprintProvider = new TestableFingerprintProvider(mDaemon, mContext, + mBiometricStateCallback, mSensorProps, TAG, mLockoutResetDispatcher, + mGestureAvailabilityDispatcher, mBiometricContext); + + waitForIdle(); + + for (SensorProps prop : mSensorProps) { + final BiometricScheduler scheduler = + mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId) + .getScheduler(); + BaseClientMonitor currentClient = scheduler.getCurrentClient(); + + assertThat(currentClient).isInstanceOf(FingerprintInternalCleanupClient.class); + assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId); + assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM); + } + } + @SuppressWarnings("rawtypes") @Test public void halServiceDied_resetsAllSchedulers() { + waitForIdle(); assertEquals(mSensorProps.length, mFingerprintProvider.getSensorProperties().size()); // Schedule N operations on each sensor final int numFakeOperations = 10; for (SensorProps prop : mSensorProps) { final BiometricScheduler scheduler = - mFingerprintProvider.mSensors.get(prop.commonProps.sensorId).getScheduler(); + mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId) + .getScheduler(); for (int i = 0; i < numFakeOperations; i++) { final HalClientMonitor testMonitor = mock(HalClientMonitor.class); when(testMonitor.getFreshDaemon()).thenReturn(new Object()); @@ -132,8 +158,9 @@ public class FingerprintProviderTest { // The right amount of pending and current operations are scheduled for (SensorProps prop : mSensorProps) { final BiometricScheduler scheduler = - mFingerprintProvider.mSensors.get(prop.commonProps.sensorId).getScheduler(); - assertEquals(numFakeOperations - 1, scheduler.getCurrentPendingCount()); + mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId) + .getScheduler(); + assertEquals(numFakeOperations, scheduler.getCurrentPendingCount()); assertNotNull(scheduler.getCurrentClient()); } @@ -145,7 +172,8 @@ public class FingerprintProviderTest { // No pending operations, no current operation. for (SensorProps prop : mSensorProps) { final BiometricScheduler scheduler = - mFingerprintProvider.mSensors.get(prop.commonProps.sensorId).getScheduler(); + mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId) + .getScheduler(); assertNull(scheduler.getCurrentClient()); assertEquals(0, scheduler.getCurrentPendingCount()); } diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java new file mode 100644 index 000000000000..eec026ccfc8a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java @@ -0,0 +1,138 @@ +/* + * 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 static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.Presubmit; +import android.testing.AndroidTestingRunner; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.companion.transport.CompanionTransportManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Presubmit +@RunWith(AndroidTestingRunner.class) +public class CrossDeviceSyncControllerTest { + + private CrossDeviceSyncController mCrossDeviceSyncController; + @Mock + private CompanionTransportManager mMockCompanionTransportManager; + @Mock + private CrossDeviceCall mMockCrossDeviceCall; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mCrossDeviceSyncController = new CrossDeviceSyncController( + InstrumentationRegistry.getInstrumentation().getContext(), + mMockCompanionTransportManager); + } + + @Test + public void processTelecomDataFromSync_createCallUpdateMessage_emptyCallsAndRequests() { + final byte[] data = mCrossDeviceSyncController.createCallUpdateMessage(new HashSet<>(), + InstrumentationRegistry.getInstrumentation().getContext().getUserId()); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Unexpectedly found a call").that( + callMetadataSyncData.getCalls()).isEmpty(); + assertWithMessage("Unexpectedly found a request").that( + callMetadataSyncData.getRequests()).isEmpty(); + } + + @Test + public void processTelecomDataFromSync_createEmptyMessage_emptyCallsAndRequests() { + final byte[] data = CrossDeviceSyncController.createEmptyMessage(); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Unexpectedly found a call").that( + callMetadataSyncData.getCalls()).isEmpty(); + assertWithMessage("Unexpectedly found a request").that( + callMetadataSyncData.getRequests()).isEmpty(); + } + + @Test + public void processTelecomDataFromSync_createCallUpdateMessage_hasCalls() { + when(mMockCrossDeviceCall.getId()).thenReturn(5L); + final String callerId = "Firstname Lastname"; + when(mMockCrossDeviceCall.getReadableCallerId(anyBoolean())).thenReturn(callerId); + final String appName = "AppName"; + when(mMockCrossDeviceCall.getCallingAppName()).thenReturn(appName); + final String appIcon = "ABCD"; + when(mMockCrossDeviceCall.getCallingAppIcon()).thenReturn(appIcon.getBytes()); + when(mMockCrossDeviceCall.getStatus()).thenReturn(android.companion.Telecom.Call.RINGING); + final Set<Integer> controls = Set.of( + android.companion.Telecom.Call.ACCEPT, + android.companion.Telecom.Call.REJECT, + android.companion.Telecom.Call.SILENCE); + when(mMockCrossDeviceCall.getControls()).thenReturn(controls); + final byte[] data = mCrossDeviceSyncController.createCallUpdateMessage( + new HashSet<>(List.of(mMockCrossDeviceCall)), + InstrumentationRegistry.getInstrumentation().getContext().getUserId()); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Wrong number of active calls").that( + callMetadataSyncData.getCalls()).hasSize(1); + final CallMetadataSyncData.Call call = + callMetadataSyncData.getCalls().stream().findAny().orElseThrow(); + assertWithMessage("Wrong id").that(call.getId()).isEqualTo(5L); + assertWithMessage("Wrong app icon").that(new String(call.getAppIcon())).isEqualTo(appIcon); + assertWithMessage("Wrong app name").that(call.getAppName()).isEqualTo(appName); + assertWithMessage("Wrong caller id").that(call.getCallerId()).isEqualTo(callerId); + assertWithMessage("Wrong status").that(call.getStatus()) + .isEqualTo(android.companion.Telecom.Call.RINGING); + assertWithMessage("Wrong controls").that(call.getControls()).isEqualTo(controls); + assertWithMessage("Unexpectedly has requests").that( + callMetadataSyncData.getRequests()).isEmpty(); + } + + @Test + public void processTelecomDataFromMessage_createCallControlMessage_hasCallControlRequest() { + final byte[] data = CrossDeviceSyncController.createCallControlMessage( + /* callId= */ 5L, /* status= */ android.companion.Telecom.Call.ACCEPT); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Wrong number of requests").that( + callMetadataSyncData.getRequests()).hasSize(1); + final CallMetadataSyncData.Call call = + callMetadataSyncData.getRequests().stream().findAny().orElseThrow(); + assertWithMessage("Wrong id").that(call.getId()).isEqualTo(5L); + assertWithMessage("Wrong app icon").that(call.getAppIcon()).isNull(); + assertWithMessage("Wrong app name").that(call.getAppName()).isNull(); + assertWithMessage("Wrong caller id").that(call.getCallerId()).isNull(); + assertWithMessage("Wrong status").that(call.getStatus()) + .isEqualTo(android.companion.Telecom.Call.UNKNOWN_STATUS); + assertWithMessage("Wrong controls").that(call.getControls()) + .isEqualTo(Set.of(android.companion.Telecom.Call.ACCEPT)); + assertWithMessage("Unexpectedly has active calls").that( + callMetadataSyncData.getCalls()).isEmpty(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index e49225216b6b..b2a3a57cccf8 100644 --- a/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -84,7 +84,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.internal.display.BrightnessSynchronizer; -import com.android.internal.display.RefreshRateSettingsUtils; import com.android.internal.os.BackgroundThread; import com.android.internal.util.Preconditions; import com.android.internal.util.test.FakeSettingsProvider; @@ -159,9 +158,6 @@ public class DisplayModeDirectorTest { LocalServices.addService(SensorManagerInternal.class, mSensorManagerInternalMock); LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); - - clearSmoothDisplaySetting(); - clearForcePeakRefreshRateSetting(); } private DisplayModeDirector createDirectorFromRefreshRateArray( @@ -922,6 +918,7 @@ public class DisplayModeDirectorTest { public void testLockFpsForLowZone() throws Exception { DisplayModeDirector director = createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0); + setPeakRefreshRate(90); director.getSettingsObserver().setDefaultRefreshRate(90); director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); @@ -929,7 +926,6 @@ public class DisplayModeDirectorTest { config.setRefreshRateInLowZone(90); config.setLowDisplayBrightnessThresholds(new int[] { 10 }); config.setLowAmbientBrightnessThresholds(new int[] { 20 }); - config.setDefaultPeakRefreshRate(90); Sensor lightSensor = createLightSensor(); SensorManager sensorManager = createMockSensorManager(lightSensor); @@ -980,6 +976,7 @@ public class DisplayModeDirectorTest { public void testLockFpsForHighZone() throws Exception { DisplayModeDirector director = createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0); + setPeakRefreshRate(90 /*fps*/); director.getSettingsObserver().setDefaultRefreshRate(90); director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); @@ -987,7 +984,6 @@ public class DisplayModeDirectorTest { config.setRefreshRateInHighZone(60); config.setHighDisplayBrightnessThresholds(new int[] { 255 }); config.setHighAmbientBrightnessThresholds(new int[] { 8000 }); - config.setDefaultPeakRefreshRate(90); Sensor lightSensor = createLightSensor(); SensorManager sensorManager = createMockSensorManager(lightSensor); @@ -1035,123 +1031,16 @@ public class DisplayModeDirectorTest { } @Test - public void testSmoothDisplay() { - float defaultRefreshRate = 60; - int defaultPeakRefreshRate = 100; - DisplayModeDirector director = - createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0); - director.getSettingsObserver().setDefaultRefreshRate(defaultRefreshRate); - director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); - - final FakeDeviceConfig config = mInjector.getDeviceConfig(); - config.setDefaultPeakRefreshRate(defaultPeakRefreshRate); - - Sensor lightSensor = createLightSensor(); - SensorManager sensorManager = createMockSensorManager(lightSensor); - director.start(sensorManager); - - // Default value of the setting - - Vote vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - defaultPeakRefreshRate); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - Float.POSITIVE_INFINITY); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - defaultRefreshRate); - - setSmoothDisplayEnabled(false); - - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - Float.POSITIVE_INFINITY); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - defaultRefreshRate); - - setSmoothDisplayEnabled(true); - - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay(mContext)); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - Float.POSITIVE_INFINITY); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - defaultRefreshRate); - } - - @Test - public void testForcePeakRefreshRate() { - float defaultRefreshRate = 60; - DisplayModeDirector director = - createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0); - director.getSettingsObserver().setDefaultRefreshRate(defaultRefreshRate); - director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); - - Sensor lightSensor = createLightSensor(); - SensorManager sensorManager = createMockSensorManager(lightSensor); - director.start(sensorManager); - - setForcePeakRefreshRateEnabled(false); - setSmoothDisplayEnabled(false); - - Vote vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, - /* frameRateHigh= */ Float.POSITIVE_INFINITY); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - defaultRefreshRate); - - setForcePeakRefreshRateEnabled(true); - - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay(mContext)); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ - RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay(mContext), - /* frameRateHigh= */ Float.POSITIVE_INFINITY); - vote = director.getVote(Display.DEFAULT_DISPLAY, - Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE); - assertVoteForRenderFrameRateRange(vote, /* frameRateLow= */ 0, /* frameRateHigh= */ - defaultRefreshRate); - } - - @Test public void testSensorRegistration() { // First, configure brightness zones or DMD won't register for sensor data. final FakeDeviceConfig config = mInjector.getDeviceConfig(); config.setRefreshRateInHighZone(60); config.setHighDisplayBrightnessThresholds(new int[] { 255 }); config.setHighAmbientBrightnessThresholds(new int[] { 8000 }); - config.setDefaultPeakRefreshRate(90); DisplayModeDirector director = createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0); + setPeakRefreshRate(90 /*fps*/); director.getSettingsObserver().setDefaultRefreshRate(90); director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); @@ -2527,10 +2416,10 @@ public class DisplayModeDirectorTest { config.setRefreshRateInHighZone(60); config.setHighDisplayBrightnessThresholds(new int[] { 255 }); config.setHighAmbientBrightnessThresholds(new int[] { 8000 }); - config.setDefaultPeakRefreshRate(90); DisplayModeDirector director = createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0); + setPeakRefreshRate(90 /*fps*/); director.getSettingsObserver().setDefaultRefreshRate(90); director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); @@ -2828,30 +2717,10 @@ public class DisplayModeDirectorTest { listener.onDisplayChanged(DISPLAY_ID); } - private void setSmoothDisplayEnabled(boolean enabled) { - Settings.System.putInt(mContext.getContentResolver(), Settings.System.SMOOTH_DISPLAY, - enabled ? 1 : 0); - mInjector.notifySmoothDisplaySettingChanged(); - waitForIdleSync(); - } - - private void clearSmoothDisplaySetting() { - Settings.System.putInt(mContext.getContentResolver(), Settings.System.SMOOTH_DISPLAY, -1); - mInjector.notifySmoothDisplaySettingChanged(); - waitForIdleSync(); - } - - private void setForcePeakRefreshRateEnabled(boolean enabled) { - Settings.System.putInt(mContext.getContentResolver(), - Settings.System.FORCE_PEAK_REFRESH_RATE, enabled ? 1 : 0); - mInjector.notifyForcePeakRefreshRateSettingChanged(); - waitForIdleSync(); - } - - private void clearForcePeakRefreshRateSetting() { - Settings.System.putInt(mContext.getContentResolver(), - Settings.System.FORCE_PEAK_REFRESH_RATE, -1); - mInjector.notifySmoothDisplaySettingChanged(); + private void setPeakRefreshRate(float fps) { + Settings.System.putFloat(mContext.getContentResolver(), Settings.System.PEAK_REFRESH_RATE, + fps); + mInjector.notifyPeakRefreshRateChanged(); waitForIdleSync(); } @@ -2900,8 +2769,7 @@ public class DisplayModeDirectorTest { private final Display mDisplay; private boolean mDisplayInfoValid = true; private ContentObserver mBrightnessObserver; - private ContentObserver mSmoothDisplaySettingObserver; - private ContentObserver mForcePeakRefreshRateSettingObserver; + private ContentObserver mPeakRefreshRateObserver; FakesInjector() { mDeviceConfig = new FakeDeviceConfig(); @@ -2918,15 +2786,9 @@ public class DisplayModeDirectorTest { } @Override - public void registerSmoothDisplayObserver(@NonNull ContentResolver cr, - @NonNull ContentObserver observer) { - mSmoothDisplaySettingObserver = observer; - } - - @Override - public void registerForcePeakRefreshRateObserver(@NonNull ContentResolver cr, + public void registerPeakRefreshRateObserver(@NonNull ContentResolver cr, @NonNull ContentObserver observer) { - mForcePeakRefreshRateSettingObserver = observer; + mPeakRefreshRateObserver = observer; } @Override @@ -2976,17 +2838,10 @@ public class DisplayModeDirectorTest { ApplicationProvider.getApplicationContext().getResources()); } - void notifySmoothDisplaySettingChanged() { - if (mSmoothDisplaySettingObserver != null) { - mSmoothDisplaySettingObserver.dispatchChange(false /*selfChange*/, - SMOOTH_DISPLAY_URI); - } - } - - void notifyForcePeakRefreshRateSettingChanged() { - if (mForcePeakRefreshRateSettingObserver != null) { - mForcePeakRefreshRateSettingObserver.dispatchChange(false /*selfChange*/, - FORCE_PEAK_REFRESH_RATE_URI); + void notifyPeakRefreshRateChanged() { + if (mPeakRefreshRateObserver != null) { + mPeakRefreshRateObserver.dispatchChange(false /*selfChange*/, + PEAK_REFRESH_RATE_URI); } } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java index a446e109c921..c53a7a708cfd 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java @@ -18,6 +18,7 @@ package com.android.server.hdmi; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_DESTINATION; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_PARAMETER; +import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_PARAMETER_LONG; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_PARAMETER_SHORT; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_SOURCE; import static com.android.server.hdmi.HdmiCecMessageValidator.OK; @@ -145,11 +146,12 @@ public class HdmiCecMessageValidatorTest { @Test public void isValid_systemAudioModeStatus() { assertMessageValidity("40:7E:00").isEqualTo(OK); - assertMessageValidity("40:7E:01:01").isEqualTo(OK); + assertMessageValidity("40:7E:01").isEqualTo(OK); assertMessageValidity("0F:7E:00").isEqualTo(ERROR_DESTINATION); assertMessageValidity("F0:7E").isEqualTo(ERROR_SOURCE); assertMessageValidity("40:7E").isEqualTo(ERROR_PARAMETER_SHORT); + assertMessageValidity("40:7E:01:1F:28").isEqualTo(ERROR_PARAMETER_LONG); assertMessageValidity("40:7E:02").isEqualTo(ERROR_PARAMETER); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index 4dd5e94541e4..fd6eb9286651 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -761,6 +761,13 @@ public class HdmiControlServiceTest { assertThat(mHdmiControlServiceSpy.handleCecCommand(message)) .isEqualTo(Constants.ABORT_INVALID_OPERAND); + + // Validating ERROR_PARAMETER_LONG will generate ABORT_INVALID_OPERAND. + // Taken from HdmiCecMessageValidatorTest#isValid_systemAudioModeStatus + HdmiCecMessage systemAudioModeStatus = HdmiUtils.buildMessage("40:7E:01:1F:28"); + + assertThat(mHdmiControlServiceSpy.handleCecCommand(systemAudioModeStatus)) + .isEqualTo(Constants.ABORT_INVALID_OPERAND); } @Test diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java index d07831dd7929..fd65807a1976 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java @@ -20,16 +20,25 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import android.platform.test.annotations.Presubmit; + import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.inputmethod.SoftInputShowHideReason; + import org.junit.Test; import org.junit.runner.RunWith; +import java.io.PrintWriter; +import java.io.StringWriter; + +@Presubmit @SmallTest @RunWith(AndroidJUnit4.class) public class InputMethodManagerServiceTests { @@ -87,4 +96,25 @@ public class InputMethodManagerServiceTests { InputMethodManagerService.computeImeDisplayIdForTarget( SYSTEM_DECORATION_SUPPORT_DISPLAY_ID, sChecker)); } + + @Test + public void testSoftInputShowHideHistoryDump_withNulls_doesntThrow() { + var writer = new StringWriter(); + var history = new InputMethodManagerService.SoftInputShowHideHistory(); + history.addEntry(new InputMethodManagerService.SoftInputShowHideHistory.Entry( + null, + null, + null, + SOFT_INPUT_STATE_UNSPECIFIED, + SoftInputShowHideReason.SHOW_SOFT_INPUT, + false, + null, + null, + null, + null)); + + history.dump(new PrintWriter(writer), "" /* prefix */); + + // Asserts that dump doesn't throw an NPE. + } } diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index e65f8cf2a199..7c1845fcd54e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -201,7 +201,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50; assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); - mService.saveBaseStateLocked(); + mService.saveBaseState(); dumpBaseStateFile(); diff --git a/services/tests/servicestests/utils/com/android/server/testutils/OWNERS b/services/tests/servicestests/utils/com/android/server/testutils/OWNERS new file mode 100644 index 000000000000..bdacf7f37d3f --- /dev/null +++ b/services/tests/servicestests/utils/com/android/server/testutils/OWNERS @@ -0,0 +1 @@ +per-file *Transaction.java = file:/services/core/java/com/android/server/wm/OWNERS
\ No newline at end of file diff --git a/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java index 31546e85f8a4..34e8ff2ec5c4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java +++ b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.wm; +package com.android.server.testutils; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,6 +30,8 @@ import android.view.InputWindowHandle; import android.view.Surface; import android.view.SurfaceControl; +import com.android.server.testutils.StubTransaction; + import java.util.HashSet; import java.util.concurrent.Executor; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java index 397e3c1f55a2..539f329cae98 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java @@ -36,6 +36,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.Manifest; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; @@ -47,7 +48,6 @@ import android.util.Pair; import androidx.test.runner.AndroidJUnit4; import com.android.server.UiServiceTestCase; -import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -67,7 +67,7 @@ import java.util.Set; public class PermissionHelperTest extends UiServiceTestCase { @Mock - private PermissionManagerServiceInternal mPmi; + private Context mContext; @Mock private IPackageManager mPackageManager; @Mock @@ -80,7 +80,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager); + mPermissionHelper = new PermissionHelper(mContext, mPackageManager, mPermManager); PackageInfo testPkgInfo = new PackageInfo(); testPkgInfo.requestedPermissions = new String[]{ Manifest.permission.POST_NOTIFICATIONS }; when(mPackageManager.getPackageInfo(anyString(), anyLong(), anyInt())) @@ -89,12 +89,12 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testHasPermission() throws Exception { - when(mPmi.checkUidPermission(anyInt(), anyString())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_GRANTED); assertThat(mPermissionHelper.hasPermission(1)).isTrue(); - when(mPmi.checkUidPermission(anyInt(), anyString())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_DENIED); assertThat(mPermissionHelper.hasPermission(1)).isFalse(); @@ -241,7 +241,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_grantUserSet() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_DENIED); mPermissionHelper.setNotificationPermission("pkg", 10, true, true); @@ -255,7 +255,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_pkgPerm_grantedByDefaultPermSet_allUserSet() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_DENIED); when(mPermManager.getPermissionFlags(anyString(), eq(Manifest.permission.POST_NOTIFICATIONS), @@ -273,7 +273,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_revokeUserSet() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_GRANTED); mPermissionHelper.setNotificationPermission("pkg", 10, false, true); @@ -287,7 +287,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_grantNotUserSet() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_DENIED); mPermissionHelper.setNotificationPermission("pkg", 10, true, false); @@ -300,7 +300,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_revokeNotUserSet() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_GRANTED); mPermissionHelper.setNotificationPermission("pkg", 10, false, false); @@ -340,7 +340,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_alreadyGrantedNotRegranted() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_GRANTED); mPermissionHelper.setNotificationPermission("pkg", 10, true, false); @@ -350,7 +350,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_alreadyRevokedNotRerevoked() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_DENIED); mPermissionHelper.setNotificationPermission("pkg", 10, false, false); @@ -360,16 +360,19 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testSetNotificationPermission_doesntRequestNotChanged() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + int testUid = -1; + when(mContext.checkPermission(anyString(), anyInt(), anyInt())) .thenReturn(PERMISSION_GRANTED); + when(mPackageManager.getPackageUid(anyString(), anyInt(), anyInt())) + .thenReturn(testUid); PackageInfo testPkgInfo = new PackageInfo(); testPkgInfo.requestedPermissions = new String[]{ Manifest.permission.RECORD_AUDIO }; when(mPackageManager.getPackageInfo(anyString(), anyLong(), anyInt())) .thenReturn(testPkgInfo); mPermissionHelper.setNotificationPermission("pkg", 10, false, false); - verify(mPmi, never()).checkPermission( - eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10)); + verify(mContext, never()).checkPermission( + eq(Manifest.permission.POST_NOTIFICATIONS), eq(-1), eq(testUid)); verify(mPermManager, never()).revokeRuntimePermission( eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString()); } diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java new file mode 100644 index 000000000000..f8a068c7f121 --- /dev/null +++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java @@ -0,0 +1,188 @@ +/* + * 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.soundtrigger_middleware; + +import static com.android.internal.util.LatencyTracker.ACTION_SHOW_VOICE_INTERACTION; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; + +import android.Manifest; +import android.app.ActivityThread; +import android.media.permission.Identity; +import android.media.permission.IdentityContext; +import android.media.soundtrigger.PhraseRecognitionEvent; +import android.media.soundtrigger.PhraseRecognitionExtra; +import android.media.soundtrigger.RecognitionEvent; +import android.media.soundtrigger.RecognitionStatus; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.os.BatteryStatsInternal; +import android.os.Process; +import android.os.RemoteException; + +import androidx.test.filters.FlakyTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.util.FakeLatencyTracker; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +@RunWith(JUnit4.class) +public class SoundTriggerMiddlewareLoggingLatencyTest { + + private FakeLatencyTracker mLatencyTracker; + @Mock + private BatteryStatsInternal mBatteryStatsInternal; + @Mock + private ISoundTriggerMiddlewareInternal mDelegateMiddleware; + @Mock + private ISoundTriggerCallback mISoundTriggerCallback; + @Mock + private ISoundTriggerModule mSoundTriggerModule; + private SoundTriggerMiddlewareLogging mSoundTriggerMiddlewareLogging; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .adoptShellPermissionIdentity(Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.READ_DEVICE_CONFIG); + + Identity identity = new Identity(); + identity.uid = Process.myUid(); + identity.pid = Process.myPid(); + identity.packageName = ActivityThread.currentOpPackageName(); + IdentityContext.create(identity); + + mLatencyTracker = FakeLatencyTracker.create(); + mLatencyTracker.forceEnabled(ACTION_SHOW_VOICE_INTERACTION, -1); + mSoundTriggerMiddlewareLogging = new SoundTriggerMiddlewareLogging(mLatencyTracker, + () -> mBatteryStatsInternal, + mDelegateMiddleware); + } + + @After + public void tearDown() { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .dropShellPermissionIdentity(); + } + + @Test + @FlakyTest(bugId = 275113847) + public void testSetUpAndTearDown() { + } + + @Test + @FlakyTest(bugId = 275113847) + public void testOnPhraseRecognitionStartsLatencyTrackerWithSuccessfulPhraseIdTrigger() + throws RemoteException { + ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( + ISoundTriggerCallback.class); + mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); + verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); + + triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), + RecognitionStatus.SUCCESS, Optional.of(100) /* keyphraseId */); + + assertThat(mLatencyTracker.getActiveActionStartTime( + ACTION_SHOW_VOICE_INTERACTION)).isGreaterThan(-1); + } + + @Test + @FlakyTest(bugId = 275113847) + public void testOnPhraseRecognitionRestartsActiveSession() throws RemoteException { + ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( + ISoundTriggerCallback.class); + mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); + verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); + + triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), + RecognitionStatus.SUCCESS, Optional.of(100) /* keyphraseId */); + long firstTriggerSessionStartTime = mLatencyTracker.getActiveActionStartTime( + ACTION_SHOW_VOICE_INTERACTION); + triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), + RecognitionStatus.SUCCESS, Optional.of(100) /* keyphraseId */); + assertThat(mLatencyTracker.getActiveActionStartTime( + ACTION_SHOW_VOICE_INTERACTION)).isGreaterThan(-1); + assertThat(mLatencyTracker.getActiveActionStartTime( + ACTION_SHOW_VOICE_INTERACTION)).isNotEqualTo(firstTriggerSessionStartTime); + } + + @Test + @FlakyTest(bugId = 275113847) + public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNonSuccessEvent() + throws RemoteException { + ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( + ISoundTriggerCallback.class); + mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); + verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); + + triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), + RecognitionStatus.ABORTED, Optional.of(100) /* keyphraseId */); + + assertThat( + mLatencyTracker.getActiveActionStartTime(ACTION_SHOW_VOICE_INTERACTION)).isEqualTo( + -1); + } + + @Test + @FlakyTest(bugId = 275113847) + public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNoKeyphraseId() + throws RemoteException { + ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( + ISoundTriggerCallback.class); + mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); + verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); + + triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), + RecognitionStatus.SUCCESS, Optional.empty() /* keyphraseId */); + + assertThat( + mLatencyTracker.getActiveActionStartTime(ACTION_SHOW_VOICE_INTERACTION)).isEqualTo( + -1); + } + + private void triggerPhraseRecognitionEvent(ISoundTriggerCallback callback, + @RecognitionStatus int triggerEventStatus, Optional<Integer> optionalKeyphraseId) + throws RemoteException { + // trigger a phrase recognition to start a latency tracker session + PhraseRecognitionEvent successEventWithKeyphraseId = new PhraseRecognitionEvent(); + successEventWithKeyphraseId.common = new RecognitionEvent(); + successEventWithKeyphraseId.common.status = triggerEventStatus; + if (optionalKeyphraseId.isPresent()) { + PhraseRecognitionExtra recognitionExtra = new PhraseRecognitionExtra(); + recognitionExtra.id = optionalKeyphraseId.get(); + successEventWithKeyphraseId.phraseExtras = + new PhraseRecognitionExtra[]{recognitionExtra}; + } + callback.onPhraseRecognition(0 /* modelHandle */, successEventWithKeyphraseId, + 0 /* captureSession */); + } +} diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java index eb117d128383..f92e0dbe94e0 100644 --- a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java +++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java @@ -18,186 +18,27 @@ package com.android.server.soundtrigger_middleware; import static com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareLogging.ServiceEvent; import static com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareLogging.SessionEvent; -import static com.android.internal.util.LatencyTracker.ACTION_SHOW_VOICE_INTERACTION; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.verify; - -import android.Manifest; -import android.app.ActivityThread; -import android.media.permission.Identity; -import android.media.permission.IdentityContext; -import android.media.soundtrigger.PhraseRecognitionEvent; -import android.media.soundtrigger.PhraseRecognitionExtra; -import android.media.soundtrigger.RecognitionEvent; -import android.media.soundtrigger.RecognitionStatus; -import android.media.soundtrigger_middleware.ISoundTriggerCallback; -import android.media.soundtrigger_middleware.ISoundTriggerModule; -import android.os.BatteryStatsInternal; -import android.os.Process; -import android.os.RemoteException; - -import androidx.test.filters.FlakyTest; -import androidx.test.platform.app.InstrumentationRegistry; - -import com.android.internal.util.FakeLatencyTracker; - -import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; @RunWith(JUnit4.class) public class SoundTriggerMiddlewareLoggingTest { private static final ServiceEvent.Type SERVICE_TYPE = ServiceEvent.Type.ATTACH; private static final SessionEvent.Type SESSION_TYPE = SessionEvent.Type.LOAD_MODEL; - private FakeLatencyTracker mLatencyTracker; - @Mock - private BatteryStatsInternal mBatteryStatsInternal; - @Mock - private ISoundTriggerMiddlewareInternal mDelegateMiddleware; - @Mock - private ISoundTriggerCallback mISoundTriggerCallback; - @Mock - private ISoundTriggerModule mSoundTriggerModule; - private SoundTriggerMiddlewareLogging mSoundTriggerMiddlewareLogging; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - InstrumentationRegistry.getInstrumentation().getUiAutomation() - .adoptShellPermissionIdentity(Manifest.permission.WRITE_DEVICE_CONFIG, - Manifest.permission.READ_DEVICE_CONFIG); - - Identity identity = new Identity(); - identity.uid = Process.myUid(); - identity.pid = Process.myPid(); - identity.packageName = ActivityThread.currentOpPackageName(); - IdentityContext.create(identity); - - mLatencyTracker = FakeLatencyTracker.create(); - mLatencyTracker.forceEnabled(ACTION_SHOW_VOICE_INTERACTION, -1); - mSoundTriggerMiddlewareLogging = new SoundTriggerMiddlewareLogging(mLatencyTracker, - () -> mBatteryStatsInternal, - mDelegateMiddleware); - } - - @After - public void tearDown() { - InstrumentationRegistry.getInstrumentation().getUiAutomation() - .dropShellPermissionIdentity(); - } - - @Test - @FlakyTest(bugId = 275113847) - public void testSetUpAndTearDown() { - } - - @Test - @FlakyTest(bugId = 275113847) - public void testOnPhraseRecognitionStartsLatencyTrackerWithSuccessfulPhraseIdTrigger() - throws RemoteException { - ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( - ISoundTriggerCallback.class); - mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); - verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); - - triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), - RecognitionStatus.SUCCESS, Optional.of(100) /* keyphraseId */); - - assertThat(mLatencyTracker.getActiveActionStartTime( - ACTION_SHOW_VOICE_INTERACTION)).isGreaterThan(-1); - } - - @Test - @FlakyTest(bugId = 275113847) - public void testOnPhraseRecognitionRestartsActiveSession() throws RemoteException { - ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( - ISoundTriggerCallback.class); - mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); - verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); - - triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), - RecognitionStatus.SUCCESS, Optional.of(100) /* keyphraseId */); - long firstTriggerSessionStartTime = mLatencyTracker.getActiveActionStartTime( - ACTION_SHOW_VOICE_INTERACTION); - triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), - RecognitionStatus.SUCCESS, Optional.of(100) /* keyphraseId */); - assertThat(mLatencyTracker.getActiveActionStartTime( - ACTION_SHOW_VOICE_INTERACTION)).isGreaterThan(-1); - assertThat(mLatencyTracker.getActiveActionStartTime( - ACTION_SHOW_VOICE_INTERACTION)).isNotEqualTo(firstTriggerSessionStartTime); - } - - @Test - @FlakyTest(bugId = 275113847) - public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNonSuccessEvent() - throws RemoteException { - ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( - ISoundTriggerCallback.class); - mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); - verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); - - triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), - RecognitionStatus.ABORTED, Optional.of(100) /* keyphraseId */); - - assertThat( - mLatencyTracker.getActiveActionStartTime(ACTION_SHOW_VOICE_INTERACTION)).isEqualTo( - -1); - } - - @Test - @FlakyTest(bugId = 275113847) - public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNoKeyphraseId() - throws RemoteException { - ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( - ISoundTriggerCallback.class); - mSoundTriggerMiddlewareLogging.attach(0, mISoundTriggerCallback); - verify(mDelegateMiddleware).attach(anyInt(), soundTriggerCallbackCaptor.capture()); - - triggerPhraseRecognitionEvent(soundTriggerCallbackCaptor.getValue(), - RecognitionStatus.SUCCESS, Optional.empty() /* keyphraseId */); - - assertThat( - mLatencyTracker.getActiveActionStartTime(ACTION_SHOW_VOICE_INTERACTION)).isEqualTo( - -1); - } - - private void triggerPhraseRecognitionEvent(ISoundTriggerCallback callback, - @RecognitionStatus int triggerEventStatus, Optional<Integer> optionalKeyphraseId) - throws RemoteException { - // trigger a phrase recognition to start a latency tracker session - PhraseRecognitionEvent successEventWithKeyphraseId = new PhraseRecognitionEvent(); - successEventWithKeyphraseId.common = new RecognitionEvent(); - successEventWithKeyphraseId.common.status = triggerEventStatus; - if (optionalKeyphraseId.isPresent()) { - PhraseRecognitionExtra recognitionExtra = new PhraseRecognitionExtra(); - recognitionExtra.id = optionalKeyphraseId.get(); - successEventWithKeyphraseId.phraseExtras = - new PhraseRecognitionExtra[]{recognitionExtra}; - } - callback.onPhraseRecognition(0 /* modelHandle */, successEventWithKeyphraseId, - 0 /* captureSession */); - } - @Test public void serviceEventException_getStringContainsInfo() { String packageName = "com.android.test"; Exception exception = new Exception("test"); Object param1 = new Object(); Object param2 = new Object(); - final var event = ServiceEvent.createForException( - SERVICE_TYPE, packageName, exception, param1, param2); + final var event = + ServiceEvent.createForException( + SERVICE_TYPE, packageName, exception, param1, param2); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SERVICE_TYPE.name()); assertThat(stringRep).contains(packageName); @@ -211,8 +52,7 @@ public class SoundTriggerMiddlewareLoggingTest { public void serviceEventExceptionNoArgs_getStringContainsInfo() { String packageName = "com.android.test"; Exception exception = new Exception("test"); - final var event = ServiceEvent.createForException( - SERVICE_TYPE, packageName, exception); + final var event = ServiceEvent.createForException(SERVICE_TYPE, packageName, exception); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SERVICE_TYPE.name()); assertThat(stringRep).contains(packageName); @@ -226,8 +66,8 @@ public class SoundTriggerMiddlewareLoggingTest { Object param1 = new Object(); Object param2 = new Object(); Object retValue = new Object(); - final var event = ServiceEvent.createForReturn( - SERVICE_TYPE, packageName, retValue, param1, param2); + final var event = + ServiceEvent.createForReturn(SERVICE_TYPE, packageName, retValue, param1, param2); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SERVICE_TYPE.name()); assertThat(stringRep).contains(packageName); @@ -241,8 +81,7 @@ public class SoundTriggerMiddlewareLoggingTest { public void serviceEventReturnNoArgs_getStringContainsInfo() { String packageName = "com.android.test"; Object retValue = new Object(); - final var event = ServiceEvent.createForReturn( - SERVICE_TYPE, packageName, retValue); + final var event = ServiceEvent.createForReturn(SERVICE_TYPE, packageName, retValue); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SERVICE_TYPE.name()); assertThat(stringRep).contains(packageName); @@ -255,8 +94,7 @@ public class SoundTriggerMiddlewareLoggingTest { Object param1 = new Object(); Object param2 = new Object(); Exception exception = new Exception("test"); - final var event = SessionEvent.createForException( - SESSION_TYPE, exception, param1, param2); + final var event = SessionEvent.createForException(SESSION_TYPE, exception, param1, param2); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SESSION_TYPE.name()); assertThat(stringRep).contains(exception.toString()); @@ -268,8 +106,7 @@ public class SoundTriggerMiddlewareLoggingTest { @Test public void sessionEventExceptionNoArgs_getStringContainsInfo() { Exception exception = new Exception("test"); - final var event = SessionEvent.createForException( - SESSION_TYPE, exception); + final var event = SessionEvent.createForException(SESSION_TYPE, exception); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SESSION_TYPE.name()); assertThat(stringRep).contains(exception.toString()); @@ -281,8 +118,7 @@ public class SoundTriggerMiddlewareLoggingTest { Object param1 = new Object(); Object param2 = new Object(); Object retValue = new Object(); - final var event = SessionEvent.createForReturn( - SESSION_TYPE, retValue, param1, param2); + final var event = SessionEvent.createForReturn(SESSION_TYPE, retValue, param1, param2); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SESSION_TYPE.name()); assertThat(stringRep).contains(retValue.toString()); @@ -294,8 +130,7 @@ public class SoundTriggerMiddlewareLoggingTest { @Test public void sessionEventReturnNoArgs_getStringContainsInfo() { Object retValue = new Object(); - final var event = SessionEvent.createForReturn( - SESSION_TYPE, retValue); + final var event = SessionEvent.createForReturn(SESSION_TYPE, retValue); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SESSION_TYPE.name()); assertThat(stringRep).contains(retValue.toString()); @@ -306,8 +141,7 @@ public class SoundTriggerMiddlewareLoggingTest { public void sessionEventVoid_getStringContainsInfo() { Object param1 = new Object(); Object param2 = new Object(); - final var event = SessionEvent.createForVoid( - SESSION_TYPE, param1, param2); + final var event = SessionEvent.createForVoid(SESSION_TYPE, param1, param2); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SESSION_TYPE.name()); assertThat(stringRep).contains(param1.toString()); @@ -317,8 +151,7 @@ public class SoundTriggerMiddlewareLoggingTest { @Test public void sessionEventVoidNoArgs_getStringContainsInfo() { - final var event = SessionEvent.createForVoid( - SESSION_TYPE); + final var event = SessionEvent.createForVoid(SESSION_TYPE); final var stringRep = event.eventToString(); assertThat(stringRep).contains(SESSION_TYPE.name()); assertThat(stringRep).ignoringCase().doesNotContain("error"); diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index 5282585e9757..f235d153c658 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -36,6 +36,7 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import com.android.server.wm.SurfaceAnimator.AnimationType; +import com.android.server.testutils.StubTransaction; import org.junit.Before; import org.junit.Test; diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java index d400a4c9daca..d2494ff3fc9a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java @@ -37,6 +37,8 @@ import android.view.SurfaceControl; import androidx.test.filters.SmallTest; +import com.android.server.testutils.StubTransaction; + import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java index e30206ee0a64..c8fc6b892bc8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java @@ -30,6 +30,8 @@ import android.window.SurfaceSyncGroup; import androidx.test.filters.SmallTest; +import com.android.server.testutils.StubTransaction; + import org.junit.Before; import org.junit.Test; diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index ddd630ee8635..a3a36841d807 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java @@ -42,6 +42,7 @@ import android.view.SurfaceControl; import androidx.test.filters.SmallTest; +import com.android.server.testutils.StubTransaction; import com.android.server.testutils.TestHandler; import org.junit.Before; diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index 013c6d50d4e7..7edfd9a3ecb3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -88,6 +88,7 @@ import com.android.server.pm.UserManagerService; import com.android.server.policy.PermissionPolicyInternal; import com.android.server.policy.WindowManagerPolicy; import com.android.server.statusbar.StatusBarManagerInternal; +import com.android.server.testutils.StubTransaction; import com.android.server.uri.UriGrantsManagerInternal; import org.junit.rules.TestRule; diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java index e2f1334c7f8c..608d7c9707f9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java @@ -38,6 +38,8 @@ import android.view.animation.ClipRectAnimation; import androidx.test.filters.SmallTest; +import com.android.server.testutils.StubTransaction; + import org.junit.Test; /** diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerThumbnailTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerThumbnailTest.java index 2ae117214dd3..849072e133ae 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerThumbnailTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerThumbnailTest.java @@ -28,6 +28,8 @@ import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; +import com.android.server.testutils.StubTransaction; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 460a603d51a2..ee1afcf318fa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -110,6 +110,8 @@ import android.window.TaskFragmentOrganizer; import androidx.test.filters.SmallTest; +import com.android.server.testutils.StubTransaction; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index a98429ad4902..ef1359449b9b 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -2539,10 +2539,20 @@ public class UsageStatsService extends SystemService implements } @Override - public void reportChooserSelection(String packageName, int userId, String contentType, - String[] annotations, String action) { + public void reportChooserSelection(@NonNull String packageName, int userId, + @NonNull String contentType, String[] annotations, @NonNull String action) { if (packageName == null) { - Slog.w(TAG, "Event report user selecting a null package"); + throw new IllegalArgumentException("Package selection must not be null."); + } + if (contentType == null) { + throw new IllegalArgumentException("Content type for selection must not be null."); + } + if (action == null) { + throw new IllegalArgumentException("Selection action must not be null."); + } + // Verify if this package exists before reporting an event for it. + if (mPackageManagerInternal.getPackageUid(packageName, 0, userId) < 0) { + Slog.w(TAG, "Event report user selecting an invalid package"); return; } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 486945d9159e..edaaf3fbbccc 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -913,8 +913,10 @@ final class HotwordDetectionConnection { } // Handle case where all hotword detector sessions are destroyed with only the visual // detector session left - if (mDetectorSessions.size() == 1 - && mDetectorSessions.get(0) instanceof VisualQueryDetectorSession) { + boolean allHotwordDetectionServiceSessionsRemoved = mDetectorSessions.size() == 0 + || (mDetectorSessions.size() == 1 && mDetectorSessions.get(0) + instanceof VisualQueryDetectorSession); + if (allHotwordDetectionServiceSessionsRemoved) { unbindHotwordDetectionService(); } } diff --git a/services/wallpapereffectsgeneration/java/com/android/server/wallpapereffectsgeneration/RemoteWallpaperEffectsGenerationService.java b/services/wallpapereffectsgeneration/java/com/android/server/wallpapereffectsgeneration/RemoteWallpaperEffectsGenerationService.java index c228dafcee8e..61f34c2570ff 100644 --- a/services/wallpapereffectsgeneration/java/com/android/server/wallpapereffectsgeneration/RemoteWallpaperEffectsGenerationService.java +++ b/services/wallpapereffectsgeneration/java/com/android/server/wallpapereffectsgeneration/RemoteWallpaperEffectsGenerationService.java @@ -40,6 +40,8 @@ public class RemoteWallpaperEffectsGenerationService extends private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS; + private static final long TIMEOUT_IDLE_BIND_MILLIS = 120 * DateUtils.SECOND_IN_MILLIS; + private final RemoteWallpaperEffectsGenerationServiceCallback mCallback; public RemoteWallpaperEffectsGenerationService(Context context, @@ -62,7 +64,7 @@ public class RemoteWallpaperEffectsGenerationService extends @Override protected long getTimeoutIdleBindMillis() { - return PERMANENT_BOUND_TIMEOUT_MS; + return TIMEOUT_IDLE_BIND_MILLIS; } @Override diff --git a/telephony/java/android/telephony/DisconnectCause.java b/telephony/java/android/telephony/DisconnectCause.java index 2704418935d9..6997f3c79bc3 100644 --- a/telephony/java/android/telephony/DisconnectCause.java +++ b/telephony/java/android/telephony/DisconnectCause.java @@ -360,6 +360,11 @@ public final class DisconnectCause { */ public static final int INCOMING_AUTO_REJECTED = 81; + /** + * Indicates that the call was unable to be made because the satellite modem is enabled. + * @hide + */ + public static final int SATELLITE_ENABLED = 82; //********************************************************************************************* // When adding a disconnect type: @@ -379,168 +384,170 @@ public final class DisconnectCause { @UnsupportedAppUsage public static @NonNull String toString(int cause) { switch (cause) { - case NOT_DISCONNECTED: - return "NOT_DISCONNECTED"; - case INCOMING_MISSED: - return "INCOMING_MISSED"; - case NORMAL: - return "NORMAL"; - case LOCAL: - return "LOCAL"; - case BUSY: - return "BUSY"; - case CONGESTION: - return "CONGESTION"; - case INVALID_NUMBER: - return "INVALID_NUMBER"; - case NUMBER_UNREACHABLE: - return "NUMBER_UNREACHABLE"; - case SERVER_UNREACHABLE: - return "SERVER_UNREACHABLE"; - case INVALID_CREDENTIALS: - return "INVALID_CREDENTIALS"; - case OUT_OF_NETWORK: - return "OUT_OF_NETWORK"; - case SERVER_ERROR: - return "SERVER_ERROR"; - case TIMED_OUT: - return "TIMED_OUT"; - case LOST_SIGNAL: - return "LOST_SIGNAL"; - case LIMIT_EXCEEDED: - return "LIMIT_EXCEEDED"; - case INCOMING_REJECTED: - return "INCOMING_REJECTED"; - case POWER_OFF: - return "POWER_OFF"; - case OUT_OF_SERVICE: - return "OUT_OF_SERVICE"; - case ICC_ERROR: - return "ICC_ERROR"; - case CALL_BARRED: - return "CALL_BARRED"; - case FDN_BLOCKED: - return "FDN_BLOCKED"; - case CS_RESTRICTED: - return "CS_RESTRICTED"; - case CS_RESTRICTED_NORMAL: - return "CS_RESTRICTED_NORMAL"; - case CS_RESTRICTED_EMERGENCY: - return "CS_RESTRICTED_EMERGENCY"; - case UNOBTAINABLE_NUMBER: - return "UNOBTAINABLE_NUMBER"; - case CDMA_LOCKED_UNTIL_POWER_CYCLE: - return "CDMA_LOCKED_UNTIL_POWER_CYCLE"; - case CDMA_DROP: - return "CDMA_DROP"; - case CDMA_INTERCEPT: - return "CDMA_INTERCEPT"; - case CDMA_REORDER: - return "CDMA_REORDER"; - case CDMA_SO_REJECT: - return "CDMA_SO_REJECT"; - case CDMA_RETRY_ORDER: - return "CDMA_RETRY_ORDER"; - case CDMA_ACCESS_FAILURE: - return "CDMA_ACCESS_FAILURE"; - case CDMA_PREEMPTED: - return "CDMA_PREEMPTED"; - case CDMA_NOT_EMERGENCY: - return "CDMA_NOT_EMERGENCY"; - case CDMA_ACCESS_BLOCKED: - return "CDMA_ACCESS_BLOCKED"; - case EMERGENCY_ONLY: - return "EMERGENCY_ONLY"; - case NO_PHONE_NUMBER_SUPPLIED: - return "NO_PHONE_NUMBER_SUPPLIED"; - case DIALED_MMI: - return "DIALED_MMI"; - case VOICEMAIL_NUMBER_MISSING: - return "VOICEMAIL_NUMBER_MISSING"; - case CDMA_CALL_LOST: - return "CDMA_CALL_LOST"; - case EXITED_ECM: - return "EXITED_ECM"; - case DIAL_MODIFIED_TO_USSD: - return "DIAL_MODIFIED_TO_USSD"; - case DIAL_MODIFIED_TO_SS: - return "DIAL_MODIFIED_TO_SS"; - case DIAL_MODIFIED_TO_DIAL: - return "DIAL_MODIFIED_TO_DIAL"; - case DIAL_MODIFIED_TO_DIAL_VIDEO: - return "DIAL_MODIFIED_TO_DIAL_VIDEO"; - case DIAL_VIDEO_MODIFIED_TO_SS: - return "DIAL_VIDEO_MODIFIED_TO_SS"; - case DIAL_VIDEO_MODIFIED_TO_USSD: - return "DIAL_VIDEO_MODIFIED_TO_USSD"; - case DIAL_VIDEO_MODIFIED_TO_DIAL: - return "DIAL_VIDEO_MODIFIED_TO_DIAL"; - case DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO: - return "DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO"; - case ERROR_UNSPECIFIED: - return "ERROR_UNSPECIFIED"; - case OUTGOING_FAILURE: - return "OUTGOING_FAILURE"; - case OUTGOING_CANCELED: - return "OUTGOING_CANCELED"; - case IMS_MERGED_SUCCESSFULLY: - return "IMS_MERGED_SUCCESSFULLY"; - case CDMA_ALREADY_ACTIVATED: - return "CDMA_ALREADY_ACTIVATED"; - case VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED: - return "VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED"; - case CALL_PULLED: - return "CALL_PULLED"; - case ANSWERED_ELSEWHERE: - return "ANSWERED_ELSEWHERE"; - case MAXIMUM_NUMBER_OF_CALLS_REACHED: - return "MAXIMUM_NUMER_OF_CALLS_REACHED"; - case DATA_DISABLED: - return "DATA_DISABLED"; - case DATA_LIMIT_REACHED: - return "DATA_LIMIT_REACHED"; - case DIALED_CALL_FORWARDING_WHILE_ROAMING: - return "DIALED_CALL_FORWARDING_WHILE_ROAMING"; - case IMEI_NOT_ACCEPTED: - return "IMEI_NOT_ACCEPTED"; - case WIFI_LOST: - return "WIFI_LOST"; - case IMS_ACCESS_BLOCKED: - return "IMS_ACCESS_BLOCKED"; - case LOW_BATTERY: - return "LOW_BATTERY"; - case DIAL_LOW_BATTERY: - return "DIAL_LOW_BATTERY"; - case EMERGENCY_TEMP_FAILURE: - return "EMERGENCY_TEMP_FAILURE"; - case EMERGENCY_PERM_FAILURE: - return "EMERGENCY_PERM_FAILURE"; - case NORMAL_UNSPECIFIED: - return "NORMAL_UNSPECIFIED"; - case IMS_SIP_ALTERNATE_EMERGENCY_CALL: - return "IMS_SIP_ALTERNATE_EMERGENCY_CALL"; - case ALREADY_DIALING: - return "ALREADY_DIALING"; - case CANT_CALL_WHILE_RINGING: - return "CANT_CALL_WHILE_RINGING"; - case CALLING_DISABLED: - return "CALLING_DISABLED"; - case TOO_MANY_ONGOING_CALLS: - return "TOO_MANY_ONGOING_CALLS"; - case OTASP_PROVISIONING_IN_PROCESS: - return "OTASP_PROVISIONING_IN_PROCESS"; - case MEDIA_TIMEOUT: - return "MEDIA_TIMEOUT"; - case EMERGENCY_CALL_OVER_WFC_NOT_AVAILABLE: - return "EMERGENCY_CALL_OVER_WFC_NOT_AVAILABLE"; - case WFC_SERVICE_NOT_AVAILABLE_IN_THIS_LOCATION: - return "WFC_SERVICE_NOT_AVAILABLE_IN_THIS_LOCATION"; - case OUTGOING_EMERGENCY_CALL_PLACED: - return "OUTGOING_EMERGENCY_CALL_PLACED"; + case NOT_DISCONNECTED: + return "NOT_DISCONNECTED"; + case INCOMING_MISSED: + return "INCOMING_MISSED"; + case NORMAL: + return "NORMAL"; + case LOCAL: + return "LOCAL"; + case BUSY: + return "BUSY"; + case CONGESTION: + return "CONGESTION"; + case INVALID_NUMBER: + return "INVALID_NUMBER"; + case NUMBER_UNREACHABLE: + return "NUMBER_UNREACHABLE"; + case SERVER_UNREACHABLE: + return "SERVER_UNREACHABLE"; + case INVALID_CREDENTIALS: + return "INVALID_CREDENTIALS"; + case OUT_OF_NETWORK: + return "OUT_OF_NETWORK"; + case SERVER_ERROR: + return "SERVER_ERROR"; + case TIMED_OUT: + return "TIMED_OUT"; + case LOST_SIGNAL: + return "LOST_SIGNAL"; + case LIMIT_EXCEEDED: + return "LIMIT_EXCEEDED"; + case INCOMING_REJECTED: + return "INCOMING_REJECTED"; + case POWER_OFF: + return "POWER_OFF"; + case OUT_OF_SERVICE: + return "OUT_OF_SERVICE"; + case ICC_ERROR: + return "ICC_ERROR"; + case CALL_BARRED: + return "CALL_BARRED"; + case FDN_BLOCKED: + return "FDN_BLOCKED"; + case CS_RESTRICTED: + return "CS_RESTRICTED"; + case CS_RESTRICTED_NORMAL: + return "CS_RESTRICTED_NORMAL"; + case CS_RESTRICTED_EMERGENCY: + return "CS_RESTRICTED_EMERGENCY"; + case UNOBTAINABLE_NUMBER: + return "UNOBTAINABLE_NUMBER"; + case CDMA_LOCKED_UNTIL_POWER_CYCLE: + return "CDMA_LOCKED_UNTIL_POWER_CYCLE"; + case CDMA_DROP: + return "CDMA_DROP"; + case CDMA_INTERCEPT: + return "CDMA_INTERCEPT"; + case CDMA_REORDER: + return "CDMA_REORDER"; + case CDMA_SO_REJECT: + return "CDMA_SO_REJECT"; + case CDMA_RETRY_ORDER: + return "CDMA_RETRY_ORDER"; + case CDMA_ACCESS_FAILURE: + return "CDMA_ACCESS_FAILURE"; + case CDMA_PREEMPTED: + return "CDMA_PREEMPTED"; + case CDMA_NOT_EMERGENCY: + return "CDMA_NOT_EMERGENCY"; + case CDMA_ACCESS_BLOCKED: + return "CDMA_ACCESS_BLOCKED"; + case EMERGENCY_ONLY: + return "EMERGENCY_ONLY"; + case NO_PHONE_NUMBER_SUPPLIED: + return "NO_PHONE_NUMBER_SUPPLIED"; + case DIALED_MMI: + return "DIALED_MMI"; + case VOICEMAIL_NUMBER_MISSING: + return "VOICEMAIL_NUMBER_MISSING"; + case CDMA_CALL_LOST: + return "CDMA_CALL_LOST"; + case EXITED_ECM: + return "EXITED_ECM"; + case DIAL_MODIFIED_TO_USSD: + return "DIAL_MODIFIED_TO_USSD"; + case DIAL_MODIFIED_TO_SS: + return "DIAL_MODIFIED_TO_SS"; + case DIAL_MODIFIED_TO_DIAL: + return "DIAL_MODIFIED_TO_DIAL"; + case DIAL_MODIFIED_TO_DIAL_VIDEO: + return "DIAL_MODIFIED_TO_DIAL_VIDEO"; + case DIAL_VIDEO_MODIFIED_TO_SS: + return "DIAL_VIDEO_MODIFIED_TO_SS"; + case DIAL_VIDEO_MODIFIED_TO_USSD: + return "DIAL_VIDEO_MODIFIED_TO_USSD"; + case DIAL_VIDEO_MODIFIED_TO_DIAL: + return "DIAL_VIDEO_MODIFIED_TO_DIAL"; + case DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO: + return "DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO"; + case ERROR_UNSPECIFIED: + return "ERROR_UNSPECIFIED"; + case OUTGOING_FAILURE: + return "OUTGOING_FAILURE"; + case OUTGOING_CANCELED: + return "OUTGOING_CANCELED"; + case IMS_MERGED_SUCCESSFULLY: + return "IMS_MERGED_SUCCESSFULLY"; + case CDMA_ALREADY_ACTIVATED: + return "CDMA_ALREADY_ACTIVATED"; + case VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED: + return "VIDEO_CALL_NOT_ALLOWED_WHILE_TTY_ENABLED"; + case CALL_PULLED: + return "CALL_PULLED"; + case ANSWERED_ELSEWHERE: + return "ANSWERED_ELSEWHERE"; + case MAXIMUM_NUMBER_OF_CALLS_REACHED: + return "MAXIMUM_NUMER_OF_CALLS_REACHED"; + case DATA_DISABLED: + return "DATA_DISABLED"; + case DATA_LIMIT_REACHED: + return "DATA_LIMIT_REACHED"; + case DIALED_CALL_FORWARDING_WHILE_ROAMING: + return "DIALED_CALL_FORWARDING_WHILE_ROAMING"; + case IMEI_NOT_ACCEPTED: + return "IMEI_NOT_ACCEPTED"; + case WIFI_LOST: + return "WIFI_LOST"; + case IMS_ACCESS_BLOCKED: + return "IMS_ACCESS_BLOCKED"; + case LOW_BATTERY: + return "LOW_BATTERY"; + case DIAL_LOW_BATTERY: + return "DIAL_LOW_BATTERY"; + case EMERGENCY_TEMP_FAILURE: + return "EMERGENCY_TEMP_FAILURE"; + case EMERGENCY_PERM_FAILURE: + return "EMERGENCY_PERM_FAILURE"; + case NORMAL_UNSPECIFIED: + return "NORMAL_UNSPECIFIED"; + case IMS_SIP_ALTERNATE_EMERGENCY_CALL: + return "IMS_SIP_ALTERNATE_EMERGENCY_CALL"; + case ALREADY_DIALING: + return "ALREADY_DIALING"; + case CANT_CALL_WHILE_RINGING: + return "CANT_CALL_WHILE_RINGING"; + case CALLING_DISABLED: + return "CALLING_DISABLED"; + case TOO_MANY_ONGOING_CALLS: + return "TOO_MANY_ONGOING_CALLS"; + case OTASP_PROVISIONING_IN_PROCESS: + return "OTASP_PROVISIONING_IN_PROCESS"; + case MEDIA_TIMEOUT: + return "MEDIA_TIMEOUT"; + case EMERGENCY_CALL_OVER_WFC_NOT_AVAILABLE: + return "EMERGENCY_CALL_OVER_WFC_NOT_AVAILABLE"; + case WFC_SERVICE_NOT_AVAILABLE_IN_THIS_LOCATION: + return "WFC_SERVICE_NOT_AVAILABLE_IN_THIS_LOCATION"; + case OUTGOING_EMERGENCY_CALL_PLACED: + return "OUTGOING_EMERGENCY_CALL_PLACED"; case INCOMING_AUTO_REJECTED: return "INCOMING_AUTO_REJECTED"; - default: - return "INVALID: " + cause; + case SATELLITE_ENABLED: + return "SATELLITE_ENABLED"; + default: + return "INVALID: " + cause; } } } diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index c5830b8249c8..431e9e628962 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -1503,6 +1503,31 @@ public class SatelliteManager { } } + /** + * Inform whether the device is aligned with the satellite for demo mode. + * + * @param isAligned {@true} Device is aligned with the satellite for demo mode + * {@false} Device is not aligned with the satellite for demo mode + * + * @throws SecurityException if the caller doesn't have required permission. + * @throws IllegalStateException if the Telephony process is not currently available. + */ + @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) + + public void onDeviceAlignedWithSatellite(boolean isAligned) { + try { + ITelephony telephony = getITelephony(); + if (telephony != null) { + telephony.onDeviceAlignedWithSatellite(mSubId, isAligned); + } else { + throw new IllegalStateException("telephony service is null."); + } + } catch (RemoteException ex) { + loge("informDeviceAlignedToSatellite() RemoteException:" + ex); + ex.rethrowFromSystemServer(); + } + } + private static ITelephony getITelephony() { ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer .getTelephonyServiceManager() diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 18e4c37f3e23..21aad73ce005 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2982,6 +2982,16 @@ interface ITelephony { void requestTimeForNextSatelliteVisibility(int subId, in ResultReceiver receiver); /** + * Inform whether the device is aligned with the satellite within in margin for demo mode. + * + * @param isAligned {@true} Device is aligned with the satellite for demo mode + * {@false} Device is not aligned with the satellite for demo mode + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.SATELLITE_COMMUNICATION)") + void onDeviceAlignedWithSatellite(int subId, in boolean isAligned); + + /** * This API can be used by only CTS to update satellite vendor service package name. * * @param servicePackageName The package name of the satellite vendor service. @@ -3018,4 +3028,13 @@ interface ITelephony { * {@code false} otherwise. */ boolean setSatellitePointingUiClassName(in String packageName, in String className); + + /** + * This API can be used by only CTS to update the timeout duration in milliseconds whether + * the device is aligned with the satellite for demo mode + * + * @param timeoutMillis The timeout duration in millisecond. + * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise. + */ + boolean setSatelliteDeviceAlignedTimeoutDuration(long timeoutMillis); } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt index a3fb73bad25f..496165ab5b09 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt @@ -18,6 +18,11 @@ package com.android.server.wm.flicker.ime import android.platform.test.annotations.Presubmit import android.tools.common.Rotation +import android.platform.test.annotations.Postsubmit +import android.tools.common.Timestamp +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.common.flicker.subject.exceptions.ExceptionMessageBuilder +import android.tools.common.flicker.subject.exceptions.InvalidPropertyException import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest @@ -36,7 +41,7 @@ import org.junit.runners.Parameterized /** * Test IME window layer will become visible when switching from the fixed orientation activity * (e.g. Launcher activity). To run this test: `atest - * FlickerTests:OpenImeWindowFromFixedOrientationAppTest` + * FlickerTests:ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest` */ @RequiresDevice @RunWith(Parameterized::class) @@ -77,6 +82,49 @@ open class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: Fl flicker.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp) } + @Postsubmit + @Test + fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() { + // Check if the snapshot appeared during the trace + var imeSnapshotRemovedTimestamp: Timestamp? = null + + val layerTrace = flicker.reader.readLayersTrace() + val layerTraceEntries = layerTrace?.entries?.toList() ?: emptyList() + + layerTraceEntries.zipWithNext { prev, next -> + val prevSnapshotLayerVisible = + ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(prev.visibleLayers) + val nextSnapshotLayerVisible = + ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(next.visibleLayers) + + if (imeSnapshotRemovedTimestamp == null && + (prevSnapshotLayerVisible && !nextSnapshotLayerVisible)) { + imeSnapshotRemovedTimestamp = next.timestamp + } + } + + // if so, make an assertion + imeSnapshotRemovedTimestamp?.let { timestamp -> + val stateAfterSnapshot = layerTrace?.getEntryAt(timestamp) + ?: error("State not found for $timestamp") + + val imeLayers = ComponentNameMatcher.IME + .filterLayers(stateAfterSnapshot.visibleLayers.toList()) + + require(imeLayers.isNotEmpty()) { "IME layer not found" } + if (imeLayers.any { it.color.a != 1.0f }) { + val errorMsgBuilder = ExceptionMessageBuilder() + .setTimestamp(timestamp) + .forInvalidProperty("IME layer alpha") + .setExpected("is 1.0") + .setActual("not 1.0") + .addExtraDescription("Filter", + ComponentNameMatcher.IME.toLayerIdentifier()) + throw InvalidPropertyException(errorMsgBuilder) + } + } + } + companion object { /** * Creates the test configurations. diff --git a/tests/SilkFX/res/layout/gainmap_image.xml b/tests/SilkFX/res/layout/gainmap_image.xml index 89bbb709d0bf..b0ed9147585e 100644 --- a/tests/SilkFX/res/layout/gainmap_image.xml +++ b/tests/SilkFX/res/layout/gainmap_image.xml @@ -34,7 +34,7 @@ android:layout_width="wrap_content" android:layout_weight="1" android:layout_height="wrap_content" - android:text="SDR original" /> + android:text="SDR" /> <RadioButton android:id="@+id/output_gainmap" android:layout_width="wrap_content" @@ -46,13 +46,34 @@ android:layout_width="wrap_content" android:layout_weight="1" android:layout_height="wrap_content" - android:text="HDR (sdr+gainmap)" /> + android:text="HDR" /> + + <RadioButton android:id="@+id/output_hdr_test" + android:layout_width="wrap_content" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="HDR (test)" /> </RadioGroup> - <Spinner - android:id="@+id/image_selection" + <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Spinner + android:id="@+id/image_selection" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" /> + + <Button + android:id="@+id/gainmap_metadata" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Gainmap Metadata..." /> + + </LinearLayout> <TextView android:id="@+id/error_msg" @@ -67,4 +88,4 @@ </LinearLayout> -</com.android.test.silkfx.hdr.GainmapImage>
\ No newline at end of file +</com.android.test.silkfx.hdr.GainmapImage> diff --git a/tests/SilkFX/res/layout/gainmap_metadata.xml b/tests/SilkFX/res/layout/gainmap_metadata.xml new file mode 100644 index 000000000000..0dabaca457f0 --- /dev/null +++ b/tests/SilkFX/res/layout/gainmap_metadata.xml @@ -0,0 +1,264 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <LinearLayout + android:layout_width="350dp" + android:layout_height="300dp" + android:layout_centerHorizontal="true" + android:padding="8dp" + android:orientation="vertical" + android:background="#444444"> + + <TextView + android:id="@+id/gainmap_metadata_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:text="Metadata for "HDR (test)" (values in linear space):" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> + + <TextView + android:id="@+id/gainmap_metadata_gainmapmin_text" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Gain Map Min:" /> + + <TextView + android:id="@+id/gainmap_metadata_gainmapmin_val" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ems="4" + android:text="TODO" /> + + <SeekBar + android:id="@+id/gainmap_metadata_gainmapmin" + android:min="0" + android:max="100" + android:layout_width="150dp" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> + + <TextView + android:id="@+id/gainmap_metadata_gainmapmax_text" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Gain Map Max:" /> + + <TextView + android:id="@+id/gainmap_metadata_gainmapmax_val" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ems="4" + android:text="TODO" /> + + <SeekBar + android:id="@+id/gainmap_metadata_gainmapmax" + android:min="0" + android:max="100" + android:layout_width="150dp" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> + + <TextView + android:id="@+id/gainmap_metadata_capacitymin_text" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Capacity Min:" /> + + <TextView + android:id="@+id/gainmap_metadata_capacitymin_val" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ems="4" + android:text="TODO" /> + + <SeekBar + android:id="@+id/gainmap_metadata_capacitymin" + android:min="0" + android:max="100" + android:layout_width="150dp" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> + + <TextView + android:id="@+id/gainmap_metadata_capacitymax_text" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Capacity Max:" /> + + <TextView + android:id="@+id/gainmap_metadata_capacitymax_val" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ems="4" + android:text="TODO" /> + + <SeekBar + android:id="@+id/gainmap_metadata_capacitymax" + android:min="0" + android:max="100" + android:layout_width="150dp" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> + + <TextView + android:id="@+id/gainmap_metadata_gamma_text" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Gamma:" /> + + <TextView + android:id="@+id/gainmap_metadata_gamma_val" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ems="4" + android:text="TODO" /> + + <SeekBar + android:id="@+id/gainmap_metadata_gamma" + android:min="0" + android:max="100" + android:layout_width="150dp" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> + + <TextView + android:id="@+id/gainmap_metadata_offsetsdr_text" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Offset SDR:" /> + + <TextView + android:id="@+id/gainmap_metadata_offsetsdr_val" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ems="4" + android:text="TODO" /> + + <SeekBar + android:id="@+id/gainmap_metadata_offsetsdr" + android:min="0" + android:max="100" + android:layout_width="150dp" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> + + <TextView + android:id="@+id/gainmap_metadata_offsethdr_text" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Offset HDR:" /> + + <TextView + android:id="@+id/gainmap_metadata_offsethdr_val" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ems="4" + android:text="TODO" /> + + <SeekBar + android:id="@+id/gainmap_metadata_offsethdr" + android:min="0" + android:max="100" + android:layout_width="150dp" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/gainmap_metadata_reset" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Reset" /> + + <Button + android:id="@+id/gainmap_metadata_done" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="Done" /> + + </LinearLayout> + + </LinearLayout> + +</RelativeLayout> diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt index 78bc4c45fa4f..7cf69b7780d9 100644 --- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt +++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt @@ -27,6 +27,7 @@ import android.util.AttributeSet import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter +import android.widget.Button import android.widget.FrameLayout import android.widget.RadioGroup import android.widget.Spinner @@ -44,6 +45,7 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context private var gainmap: Gainmap? = null private var gainmapVisualizer: Bitmap? = null private lateinit var imageView: SubsamplingScaleImageView + private lateinit var gainmapMetadataEditor: GainmapMetadataEditor init { gainmapImages = context.assets.list("gainmaps")!! @@ -58,6 +60,7 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context super.onFinishInflate() imageView = findViewById(R.id.image)!! + gainmapMetadataEditor = GainmapMetadataEditor(this, imageView) findViewById<RadioGroup>(R.id.output_mode)!!.also { it.check(outputMode) @@ -92,6 +95,10 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context } } + findViewById<Button>(R.id.gainmap_metadata)!!.setOnClickListener { + gainmapMetadataEditor.openEditor() + } + setImage(0) imageView.apply { @@ -132,6 +139,7 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context findViewById<RadioGroup>(R.id.output_mode)!!.visibility = View.VISIBLE gainmap = bitmap!!.gainmap + gainmapMetadataEditor.setGainmap(gainmap) val map = gainmap!!.gainmapContents if (map.config != Bitmap.Config.ALPHA_8) { gainmapVisualizer = map @@ -175,7 +183,15 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context imageView.setImage(ImageSource.cachedBitmap(when (outputMode) { R.id.output_hdr -> { - bitmap!!.gainmap = gainmap; bitmap!! + gainmapMetadataEditor.useOriginalMetadata() + bitmap!!.gainmap = gainmap + bitmap!! + } + + R.id.output_hdr_test -> { + gainmapMetadataEditor.useEditMetadata() + bitmap!!.gainmap = gainmap + bitmap!! } R.id.output_sdr -> { @@ -186,4 +202,4 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context else -> throw IllegalStateException() })) } -}
\ No newline at end of file +} diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt new file mode 100644 index 000000000000..8a653045c97b --- /dev/null +++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt @@ -0,0 +1,284 @@ +/* + * 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.test.silkfx.hdr + +import android.graphics.Gainmap +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.PopupWindow +import android.widget.SeekBar +import android.widget.TextView +import com.android.test.silkfx.R + +data class GainmapMetadata( + var ratioMin: Float, + var ratioMax: Float, + var capacityMin: Float, + var capacityMax: Float, + var gamma: Float, + var offsetSdr: Float, + var offsetHdr: Float +) + +class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { + private var gainmap: Gainmap? = null + private var showingEdits = false + + private var metadataPopup: PopupWindow? = null + + private var originalMetadata: GainmapMetadata = GainmapMetadata( + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f) + private var currentMetadata: GainmapMetadata = originalMetadata.copy() + + private val maxProgress = 100.0f + + private val minRatioMin = .001f + private val maxRatioMin = 1.0f + private val minRatioMax = 1.0f + private val maxRatioMax = 16.0f + private val minCapacityMin = 1.0f + private val maxCapacityMin = maxRatioMax + private val minCapacityMax = 1.001f + private val maxCapacityMax = maxRatioMax + private val minGamma = 0.1f + private val maxGamma = 3.0f + // Min and max offsets are 0.0 and 1.0 respectively + + fun setGainmap(newGainmap: Gainmap?) { + gainmap = newGainmap + originalMetadata = GainmapMetadata(gainmap!!.getRatioMin()[0], + gainmap!!.getRatioMax()[0], gainmap!!.getMinDisplayRatioForHdrTransition(), + gainmap!!.getDisplayRatioForFullHdr(), gainmap!!.getGamma()[0], + gainmap!!.getEpsilonSdr()[0], gainmap!!.getEpsilonHdr()[0]) + currentMetadata = originalMetadata.copy() + } + + fun useOriginalMetadata() { + showingEdits = false + applyMetadata(originalMetadata) + } + + fun useEditMetadata() { + showingEdits = true + applyMetadata(currentMetadata) + } + + fun closeEditor() { + metadataPopup?.let { + it.dismiss() + metadataPopup = null + } + } + + fun openEditor() { + if (metadataPopup != null) return + + val view = LayoutInflater.from(parent.getContext()).inflate(R.layout.gainmap_metadata, null) + + metadataPopup = PopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + metadataPopup!!.showAtLocation(view, Gravity.CENTER, 0, 0) + + (view.getParent() as ViewGroup).removeView(view) + parent.addView(view) + + view.findViewById<Button>(R.id.gainmap_metadata_done)!!.setOnClickListener { + closeEditor() + } + + view.findViewById<Button>(R.id.gainmap_metadata_reset)!!.setOnClickListener { + resetGainmapMetadata() + } + + updateMetadataUi() + + val gainmapMinSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_gainmapmin) + val gainmapMaxSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_gainmapmax) + val capacityMinSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_capacitymin) + val capacityMaxSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_capacitymax) + val gammaSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_gamma) + val offsetSdrSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_offsetsdr) + val offsetHdrSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_offsethdr) + arrayOf(gainmapMinSeek, gainmapMaxSeek, capacityMinSeek, capacityMaxSeek, gammaSeek, + offsetSdrSeek, offsetHdrSeek).forEach { + it.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{ + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (!fromUser) return + val normalized = progress.toFloat() / maxProgress + when (seekBar) { + gainmapMinSeek -> updateGainmapMin(normalized) + gainmapMaxSeek -> updateGainmapMax(normalized) + capacityMinSeek -> updateCapacityMin(normalized) + capacityMaxSeek -> updateCapacityMax(normalized) + gammaSeek -> updateGamma(normalized) + offsetSdrSeek -> updateOffsetSdr(normalized) + offsetHdrSeek -> updateOffsetHdr(normalized) + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) {} + }) + } + } + + private fun updateMetadataUi() { + val gainmapMinSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_gainmapmin) + val gainmapMaxSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_gainmapmax) + val capacityMinSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_capacitymin) + val capacityMaxSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_capacitymax) + val gammaSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_gamma) + val offsetSdrSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_offsetsdr) + val offsetHdrSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_offsethdr) + + gainmapMinSeek.setProgress( + ((currentMetadata.ratioMin - minRatioMin) / maxRatioMin * maxProgress).toInt()) + gainmapMaxSeek.setProgress( + ((currentMetadata.ratioMax - minRatioMax) / maxRatioMax * maxProgress).toInt()) + capacityMinSeek.setProgress( + ((currentMetadata.capacityMin - minCapacityMin) / maxCapacityMin * maxProgress).toInt()) + capacityMaxSeek.setProgress( + ((currentMetadata.capacityMax - minCapacityMax) / maxCapacityMax * maxProgress).toInt()) + gammaSeek.setProgress( + ((currentMetadata.gamma - minGamma) / maxGamma * maxProgress).toInt()) + // Log base 3 via: log_b(x) = log_y(x) / log_y(b) + offsetSdrSeek.setProgress( + ((1.0 - Math.log(currentMetadata.offsetSdr.toDouble() / Math.log(3.0)) / -11.0) + .toFloat() * maxProgress).toInt()) + offsetHdrSeek.setProgress( + ((1.0 - Math.log(currentMetadata.offsetHdr.toDouble() / Math.log(3.0)) / -11.0) + .toFloat() * maxProgress).toInt()) + + parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val)!!.setText( + "%.3f".format(currentMetadata.ratioMin)) + parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val)!!.setText( + "%.3f".format(currentMetadata.ratioMax)) + parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymin_val)!!.setText( + "%.3f".format(currentMetadata.capacityMin)) + parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymax_val)!!.setText( + "%.3f".format(currentMetadata.capacityMax)) + parent.findViewById<TextView>(R.id.gainmap_metadata_gamma_val)!!.setText( + "%.3f".format(currentMetadata.gamma)) + parent.findViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val)!!.setText( + "%.5f".format(currentMetadata.offsetSdr)) + parent.findViewById<TextView>(R.id.gainmap_metadata_offsethdr_val)!!.setText( + "%.5f".format(currentMetadata.offsetHdr)) + } + + private fun resetGainmapMetadata() { + currentMetadata = originalMetadata.copy() + applyMetadata(currentMetadata) + updateMetadataUi() + } + + private fun applyMetadata(newMetadata: GainmapMetadata) { + gainmap!!.setRatioMin(newMetadata.ratioMin, newMetadata.ratioMin, newMetadata.ratioMin) + gainmap!!.setRatioMax(newMetadata.ratioMax, newMetadata.ratioMax, newMetadata.ratioMax) + gainmap!!.setMinDisplayRatioForHdrTransition(newMetadata.capacityMin) + gainmap!!.setDisplayRatioForFullHdr(newMetadata.capacityMax) + gainmap!!.setGamma(newMetadata.gamma, newMetadata.gamma, newMetadata.gamma) + gainmap!!.setEpsilonSdr(newMetadata.offsetSdr, newMetadata.offsetSdr, newMetadata.offsetSdr) + gainmap!!.setEpsilonHdr(newMetadata.offsetHdr, newMetadata.offsetHdr, newMetadata.offsetHdr) + renderView.invalidate() + } + + private fun updateGainmapMin(normalized: Float) { + val newValue = minRatioMin + normalized * (maxRatioMin - minRatioMin) + parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val)!!.setText( + "%.3f".format(newValue)) + currentMetadata.ratioMin = newValue + if (showingEdits) { + gainmap!!.setRatioMin(newValue, newValue, newValue) + renderView.invalidate() + } + } + + private fun updateGainmapMax(normalized: Float) { + val newValue = minRatioMax + normalized * (maxRatioMax - minRatioMax) + parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val)!!.setText( + "%.3f".format(newValue)) + currentMetadata.ratioMax = newValue + if (showingEdits) { + gainmap!!.setRatioMax(newValue, newValue, newValue) + renderView.invalidate() + } + } + + private fun updateCapacityMin(normalized: Float) { + val newValue = minCapacityMin + normalized * (maxCapacityMin - minCapacityMin) + parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymin_val)!!.setText( + "%.3f".format(newValue)) + currentMetadata.capacityMin = newValue + if (showingEdits) { + gainmap!!.setMinDisplayRatioForHdrTransition(newValue) + renderView.invalidate() + } + } + + private fun updateCapacityMax(normalized: Float) { + val newValue = minCapacityMax + normalized * (maxCapacityMax - minCapacityMax) + parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymax_val)!!.setText( + "%.3f".format(newValue)) + currentMetadata.capacityMax = newValue + if (showingEdits) { + gainmap!!.setDisplayRatioForFullHdr(newValue) + renderView.invalidate() + } + } + + private fun updateGamma(normalized: Float) { + val newValue = minGamma + normalized * (maxGamma - minGamma) + parent.findViewById<TextView>(R.id.gainmap_metadata_gamma_val)!!.setText( + "%.3f".format(newValue)) + currentMetadata.gamma = newValue + if (showingEdits) { + gainmap!!.setGamma(newValue, newValue, newValue) + renderView.invalidate() + } + } + + private fun updateOffsetSdr(normalized: Float) { + var newValue = 0.0f + if (normalized > 0.0f ) { + newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat() + } + parent.findViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val)!!.setText( + "%.5f".format(newValue)) + currentMetadata.offsetSdr = newValue + if (showingEdits) { + gainmap!!.setEpsilonSdr(newValue, newValue, newValue) + renderView.invalidate() + } + } + + private fun updateOffsetHdr(normalized: Float) { + var newValue = 0.0f + if (normalized > 0.0f ) { + newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat() + } + parent.findViewById<TextView>(R.id.gainmap_metadata_offsethdr_val)!!.setText( + "%.5f".format(newValue)) + currentMetadata.offsetHdr = newValue + if (showingEdits) { + gainmap!!.setEpsilonHdr(newValue, newValue, newValue) + renderView.invalidate() + } + } +} |