diff options
508 files changed, 13289 insertions, 4341 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 41c58ef67e65..1571fddb012f 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -34,7 +34,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded; import static android.window.ConfigurationHelper.isDifferentDisplay; import static android.window.ConfigurationHelper.shouldUpdateResources; -import static android.window.ConfigurationHelper.shouldUpdateWindowMetricsBounds; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL; @@ -6118,11 +6117,6 @@ public final class ActivityThread extends ClientTransactionHandler public static boolean shouldReportChange(@Nullable Configuration currentConfig, @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets sizeBuckets, int handledConfigChanges, boolean alwaysReportChange) { - // Always report changes in window configuration bounds - if (shouldUpdateWindowMetricsBounds(currentConfig, newConfig)) { - return true; - } - final int publicDiff = currentConfig.diffPublicOnly(newConfig); // Don't report the change if there's no public diff between current and new config. if (publicDiff == 0) { diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index d8cedb8fa5d2..7ee1332ea76a 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -26,12 +26,14 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ShortcutInfo; import android.media.AudioAttributes; +import android.media.RingtoneManager; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.text.TextUtils; +import android.util.Log; import android.util.proto.ProtoOutputStream; import com.android.internal.util.Preconditions; @@ -54,6 +56,7 @@ import java.util.Objects; * A representation of settings that apply to a collection of similarly themed notifications. */ public final class NotificationChannel implements Parcelable { + private static final String TAG = "NotificationChannel"; /** * The id of the default channel for an app. This id is reserved by the system. All @@ -959,8 +962,11 @@ public final class NotificationChannel implements Parcelable { setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY)); Uri sound = safeUri(parser, ATT_SOUND); - setSound(forRestore ? restoreSoundUri(context, sound, pkgInstalled) : sound, - safeAudioAttributes(parser)); + + final AudioAttributes audioAttributes = safeAudioAttributes(parser); + final int usage = audioAttributes.getUsage(); + setSound(forRestore ? restoreSoundUri(context, sound, pkgInstalled, usage) : sound, + audioAttributes); enableLights(safeBool(parser, ATT_LIGHTS, false)); setLightColor(safeInt(parser, ATT_LIGHT_COLOR, DEFAULT_LIGHT_COLOR)); @@ -1010,18 +1016,34 @@ public final class NotificationChannel implements Parcelable { if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return uri; } - return contentResolver.canonicalize(uri); } @Nullable - private Uri getUncanonicalizedSoundUri(ContentResolver contentResolver, @NonNull Uri uri) { + private Uri getUncanonicalizedSoundUri( + ContentResolver contentResolver, @NonNull Uri uri, int usage) { if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri) || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme()) || ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return uri; } - return contentResolver.uncanonicalize(uri); + int ringtoneType = 0; + + // Consistent with UI(SoundPreferenceController.handlePreferenceTreeClick). + if (AudioAttributes.USAGE_ALARM == usage) { + ringtoneType = RingtoneManager.TYPE_ALARM; + } else if (AudioAttributes.USAGE_NOTIFICATION_RINGTONE == usage) { + ringtoneType = RingtoneManager.TYPE_RINGTONE; + } else { + ringtoneType = RingtoneManager.TYPE_NOTIFICATION; + } + try { + return RingtoneManager.getRingtoneUriForRestore( + contentResolver, uri.toString(), ringtoneType); + } catch (Exception e) { + Log.e(TAG, "Failed to uncanonicalized sound uri for " + uri + " " + e); + return Settings.System.DEFAULT_NOTIFICATION_URI; + } } /** @@ -1033,7 +1055,8 @@ public final class NotificationChannel implements Parcelable { * @hide */ @Nullable - public Uri restoreSoundUri(Context context, @Nullable Uri uri, boolean pkgInstalled) { + public Uri restoreSoundUri( + Context context, @Nullable Uri uri, boolean pkgInstalled, int usage) { if (uri == null || Uri.EMPTY.equals(uri)) { return null; } @@ -1060,7 +1083,7 @@ public final class NotificationChannel implements Parcelable { } } mSoundRestored = true; - return getUncanonicalizedSoundUri(contentResolver, canonicalizedUri); + return getUncanonicalizedSoundUri(contentResolver, canonicalizedUri, usage); } /** diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java index 1ebf56550e03..a87187b8affb 100644 --- a/core/java/android/app/SharedPreferencesImpl.java +++ b/core/java/android/app/SharedPreferencesImpl.java @@ -55,6 +55,11 @@ import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; final class SharedPreferencesImpl implements SharedPreferences { private static final String TAG = "SharedPreferencesImpl"; @@ -119,6 +124,10 @@ final class SharedPreferencesImpl implements SharedPreferences { private final ExponentiallyBucketedHistogram mSyncTimes = new ExponentiallyBucketedHistogram(16); private int mNumSync = 0; + private static final ThreadPoolExecutor sLoadExecutor = new ThreadPoolExecutor(0, 1, 10L, + TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), + new SharedPreferencesThreadFactory()); + @UnsupportedAppUsage SharedPreferencesImpl(File file, int mode) { mFile = file; @@ -135,11 +144,10 @@ final class SharedPreferencesImpl implements SharedPreferences { synchronized (mLock) { mLoaded = false; } - new Thread("SharedPreferencesImpl-load") { - public void run() { - loadFromDisk(); - } - }.start(); + + sLoadExecutor.execute(() -> { + loadFromDisk(); + }); } private void loadFromDisk() { @@ -874,4 +882,14 @@ final class SharedPreferencesImpl implements SharedPreferences { } mcr.setDiskWriteResult(false, false); } + + + private static final class SharedPreferencesThreadFactory implements ThreadFactory { + @Override + public Thread newThread(Runnable runnable) { + Thread thread = Executors.defaultThreadFactory().newThread(runnable); + thread.setName("SharedPreferences"); + return thread; + } + } } diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 2b5175ca6659..634089b73618 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -248,6 +248,13 @@ public class TaskInfo { public boolean topActivityEligibleForUserAspectRatioButton; /** + * Whether the user has forced the activity to be fullscreen through the user aspect ratio + * settings. + * @hide + */ + public boolean isUserFullscreenOverrideEnabled; + + /** * Hint about the letterbox state of the top activity. * @hide */ @@ -543,7 +550,8 @@ public class TaskInfo { && isSleeping == that.isSleeping && Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId) && parentTaskId == that.parentTaskId - && Objects.equals(topActivity, that.topActivity); + && Objects.equals(topActivity, that.topActivity) + && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled; } /** @@ -574,7 +582,8 @@ public class TaskInfo { && (!hasCompatUI() || configuration.getLayoutDirection() == that.configuration.getLayoutDirection()) && (!hasCompatUI() || configuration.uiMode == that.configuration.uiMode) - && (!hasCompatUI() || isVisible == that.isVisible); + && (!hasCompatUI() || isVisible == that.isVisible) + && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled; } /** @@ -630,6 +639,7 @@ public class TaskInfo { topActivityLetterboxHorizontalPosition = source.readInt(); topActivityLetterboxWidth = source.readInt(); topActivityLetterboxHeight = source.readInt(); + isUserFullscreenOverrideEnabled = source.readBoolean(); } /** @@ -686,6 +696,7 @@ public class TaskInfo { dest.writeInt(topActivityLetterboxHorizontalPosition); dest.writeInt(topActivityLetterboxWidth); dest.writeInt(topActivityLetterboxHeight); + dest.writeBoolean(isUserFullscreenOverrideEnabled); } @Override @@ -732,6 +743,7 @@ public class TaskInfo { + topActivityLetterboxHorizontalPosition + " topActivityLetterboxWidth=" + topActivityLetterboxWidth + " topActivityLetterboxHeight=" + topActivityLetterboxHeight + + " isUserFullscreenOverrideEnabled=" + isUserFullscreenOverrideEnabled + " locusId=" + mTopActivityLocusId + " displayAreaFeatureId=" + displayAreaFeatureId + " cameraCompatControlState=" diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING index 3ffbe1d1c71b..2ea6513c4d77 100644 --- a/core/java/android/content/pm/TEST_MAPPING +++ b/core/java/android/content/pm/TEST_MAPPING @@ -101,20 +101,6 @@ ], "presubmit-large":[ { - "name":"CtsContentTestCases", - "options":[ - { - "exclude-annotation":"androidx.test.filters.FlakyTest" - }, - { - "exclude-annotation":"org.junit.Ignore" - }, - { - "include-filter":"android.content.pm.cts" - } - ] - }, - { "name":"CtsUsesNativeLibraryTest", "options":[ { @@ -156,6 +142,20 @@ ], "postsubmit":[ { + "name":"CtsContentTestCases", + "options":[ + { + "exclude-annotation":"androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation":"org.junit.Ignore" + }, + { + "include-filter":"android.content.pm.cts" + } + ] + }, + { "name":"CtsAppSecurityHostTestCases", "options":[ { diff --git a/core/java/android/credentials/CreateCredentialRequest.java b/core/java/android/credentials/CreateCredentialRequest.java index fc3dc796d3ed..946b5f329c0b 100644 --- a/core/java/android/credentials/CreateCredentialRequest.java +++ b/core/java/android/credentials/CreateCredentialRequest.java @@ -261,7 +261,10 @@ public final class CreateCredentialRequest implements Parcelable { /** * @param type the type of the credential to be stored - * @param credentialData the full credential creation request data + * @param credentialData the full credential creation request data, which must at minimum + * contain the required fields observed at the + * {@link androidx.credentials.CreateCredentialRequest} Bundle conversion static methods, + * because they are required for properly displaying the system credential selector UI * @param candidateQueryData the partial request data that will be sent to the provider * during the initial creation candidate query stage */ diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index c2fe0800812f..c80124c4c2ec 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -31,6 +31,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Point; import android.hardware.CameraExtensionSessionStats; +import android.hardware.CameraIdRemapping; import android.hardware.CameraStatus; import android.hardware.ICameraService; import android.hardware.ICameraServiceListener; @@ -1730,6 +1731,17 @@ public final class CameraManager { } /** + * Remaps Camera Ids in the CameraService. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) + public void remapCameraIds(@NonNull CameraIdRemapping cameraIdRemapping) + throws CameraAccessException, SecurityException, IllegalArgumentException { + CameraManagerGlobal.get().remapCameraIds(cameraIdRemapping); + } + + /** * Reports {@link CameraExtensionSessionStats} to the {@link ICameraService} to be logged for * currently active session. Validation is done downstream. * @@ -1802,6 +1814,13 @@ public final class CameraManager { private final Object mLock = new Object(); + /** + * The active CameraIdRemapping. This will be used to refresh the cameraIdRemapping state + * in the CameraService every time we connect to it, including when the CameraService + * Binder dies and we reconnect to it. + */ + @Nullable private CameraIdRemapping mActiveCameraIdRemapping; + // Access only through getCameraService to deal with binder death private ICameraService mCameraService; private boolean mHasOpenCloseListenerPermission = false; @@ -1944,6 +1963,41 @@ public final class CameraManager { } catch (RemoteException e) { // Camera service died in all probability } + + if (mActiveCameraIdRemapping != null) { + try { + cameraService.remapCameraIds(mActiveCameraIdRemapping); + } catch (ServiceSpecificException e) { + // Unexpected failure, ignore and continue. + Log.e(TAG, "Unable to remap camera Ids in the camera service"); + } catch (RemoteException e) { + // Camera service died in all probability + } + } + } + + /** Updates the cameraIdRemapping state in the CameraService. */ + public void remapCameraIds(@NonNull CameraIdRemapping cameraIdRemapping) + throws CameraAccessException, SecurityException { + synchronized (mLock) { + ICameraService cameraService = getCameraService(); + if (cameraService == null) { + throw new CameraAccessException( + CameraAccessException.CAMERA_DISCONNECTED, + "Camera service is currently unavailable."); + } + + try { + cameraService.remapCameraIds(cameraIdRemapping); + mActiveCameraIdRemapping = cameraIdRemapping; + } catch (ServiceSpecificException e) { + throwAsPublicException(e); + } catch (RemoteException e) { + throw new CameraAccessException( + CameraAccessException.CAMERA_DISCONNECTED, + "Camera service is currently unavailable."); + } + } } private String[] extractCameraIdListLocked() { diff --git a/core/java/android/hardware/usb/UsbConfiguration.java b/core/java/android/hardware/usb/UsbConfiguration.java index 66269cb772f8..b25f47b11532 100644 --- a/core/java/android/hardware/usb/UsbConfiguration.java +++ b/core/java/android/hardware/usb/UsbConfiguration.java @@ -172,7 +172,8 @@ public class UsbConfiguration implements Parcelable { String name = in.readString(); int attributes = in.readInt(); int maxPower = in.readInt(); - Parcelable[] interfaces = in.readParcelableArray(UsbInterface.class.getClassLoader()); + Parcelable[] interfaces = in.readParcelableArray( + UsbInterface.class.getClassLoader(), UsbInterface.class); UsbConfiguration configuration = new UsbConfiguration(id, name, attributes, maxPower); configuration.setInterfaces(interfaces); return configuration; diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index ab58306ba5ab..4cfec99dff01 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -208,6 +208,7 @@ public class HandwritingInitiator { candidateView.getHandwritingDelegatorCallback().run(); mState.mHasPreparedHandwritingDelegation = true; } else { + mState.mPendingConnectedView = new WeakReference<>(candidateView); requestFocusWithoutReveal(candidateView); } } @@ -269,8 +270,9 @@ public class HandwritingInitiator { mShowHoverIconForConnectedView = false; return; } - if (mState != null && mState.mShouldInitHandwriting) { - tryStartHandwriting(); + if (mState != null && mState.mPendingConnectedView != null + && mState.mPendingConnectedView.get() == view) { + startHandwriting(view); } } } @@ -295,40 +297,6 @@ public class HandwritingInitiator { } } - /** - * Try to initiate handwriting. For this method to successfully send startHandwriting signal, - * the following 3 conditions should meet: - * a) The stylus movement exceeds the touchSlop. - * b) A View has built InputConnection with IME. - * c) The stylus event lands into the connected View's boundary. - * This method will immediately fail without any side effect if condition a or b is not met. - * However, if both condition a and b are met but the condition c is not met, it will reset the - * internal states. And HandwritingInitiator won't attempt to call startHandwriting until the - * next ACTION_DOWN. - */ - private void tryStartHandwriting() { - if (!mState.mExceedHandwritingSlop) { - return; - } - final View connectedView = getConnectedView(); - if (connectedView == null) { - return; - } - - if (!connectedView.isAutoHandwritingEnabled()) { - clearConnectedView(); - return; - } - - final Rect handwritingArea = getViewHandwritingArea(connectedView); - if (isInHandwritingArea( - handwritingArea, mState.mStylusDownX, mState.mStylusDownY, connectedView)) { - startHandwriting(connectedView); - } else { - mState.mShouldInitHandwriting = false; - } - } - /** Starts a stylus handwriting session for the view. */ @VisibleForTesting public void startHandwriting(@NonNull View view) { @@ -631,6 +599,7 @@ public class HandwritingInitiator { private boolean mHasInitiatedHandwriting; private boolean mHasPreparedHandwritingDelegation; + /** * Whether the current ongoing stylus MotionEvent sequence already exceeds the * handwriting slop. @@ -639,6 +608,12 @@ public class HandwritingInitiator { */ private boolean mExceedHandwritingSlop; + /** + * A view which has requested focus and is pending input connection creation. When an input + * connection is created for the view, a handwriting session should be started for the view. + */ + private WeakReference<View> mPendingConnectedView = null; + /** The pointer id of the stylus pointer that is being tracked. */ private final int mStylusPointerId; /** The time stamp when the stylus pointer goes down. */ diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index c11f4975149d..e67367639514 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -47,6 +47,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.gui.DropInputMode; +import android.gui.StalledTransactionInfo; import android.hardware.DataSpace; import android.hardware.HardwareBuffer; import android.hardware.OverlayProperties; @@ -292,6 +293,7 @@ public final class SurfaceControl implements Parcelable { long nativeObject, long nativeTpc, TrustedPresentationThresholds thresholds); private static native void nativeClearTrustedPresentationCallback(long transactionObj, long nativeObject); + private static native StalledTransactionInfo nativeGetStalledTransactionInfo(int pid); /** * Transforms that can be applied to buffers as they are displayed to a window. @@ -4363,4 +4365,11 @@ public final class SurfaceControl implements Parcelable { callback.accept(fence); } + /** + * @hide + */ + public static StalledTransactionInfo getStalledTransactionInfo(int pid) { + return nativeGetStalledTransactionInfo(pid); + } + } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b369c450cb73..6edf0e24d5ab 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1808,6 +1808,9 @@ public final class ViewRootImpl implements ViewParent, // Request to update light center. mAttachInfo.mNeedsUpdateLightCenter = true; } + if ((changes & WindowManager.LayoutParams.COLOR_MODE_CHANGED) != 0) { + invalidate(); + } if (mWindowAttributes.packageName == null) { mWindowAttributes.packageName = mBasePackageName; } @@ -5513,6 +5516,7 @@ public final class ViewRootImpl implements ViewParent, if (desiredRatio != mDesiredHdrSdrRatio) { mDesiredHdrSdrRatio = desiredRatio; updateRenderHdrSdrRatio(); + invalidate(); if (mDesiredHdrSdrRatio < 1.01f) { mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioChangedListener); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 65677cd7c3fd..9195509312e8 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -913,7 +913,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Add CTS tests. String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; @@ -983,7 +982,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Make this public API. String PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS = "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS"; @@ -1018,7 +1016,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Add CTS tests. String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"; /** @@ -1056,7 +1053,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Add CTS tests. String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION = "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"; @@ -1102,7 +1098,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Add CTS tests. String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH = "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"; @@ -1151,7 +1146,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Add CTS tests. String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"; @@ -1189,7 +1183,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Add CTS tests. String PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE"; @@ -1233,7 +1226,6 @@ public interface WindowManager extends ViewManager { * </application> * </pre> */ - // TODO(b/263984287): Add CTS tests. String PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"; @@ -1300,6 +1292,102 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"; /** + * Application level + * {@link android.content.pm.PackageManager.Property PackageManager.Property} + * tag that (when set to false) informs the system the app has opted out of the + * user-facing aspect ratio compatibility override. + * + * <p>The compatibility override enables device users to set the app's aspect + * ratio or force the app to fill the display regardless of the aspect + * ratio or orientation specified in the app manifest. + * + * <p>The aspect ratio compatibility override is exposed to users in device + * settings. A menu in device settings lists all apps that have not opted out of + * the compatibility override. Users select apps from the menu and set the + * app aspect ratio on a per-app basis. Typically, the menu is available + * only on large screen devices. + * + * <p>When users apply the aspect ratio override, the minimum aspect ratio + * specified in the app manifest is overridden. If users choose a + * full-screen aspect ratio, the orientation of the activity is forced to + * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_USER}; + * see {@link #PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE} to + * disable the full-screen option only. + * + * <p>The user override is intended to improve the app experience on devices + * that have the ignore orientation request display setting enabled by OEMs + * (enables compatibility mode for fixed orientation on Android 12 (API + * level 31) or higher; see + * <a href="https://developer.android.com/guide/topics/large-screens/large-screen-app-compatibility"> + * Large screen app compatibility</a> + * for more details). + * + * <p>To opt out of the user aspect ratio compatibility override, add this property + * to your app manifest and set the value to {@code false}. Your app will be excluded + * from the list of apps in device settings, and users will not be able to override + * the app's aspect ratio. + * + * <p>Not setting this property at all, or setting this property to {@code true} has no effect. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE" + * android:value="false"/> + * </application> + * </pre> + * @hide + */ + // TODO(b/294227289): Make this public API + String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = + "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"; + + /** + * Application level + * {@link android.content.pm.PackageManager.Property PackageManager.Property} + * tag that (when set to false) informs the system the app has opted out of the + * full-screen option of the user aspect ratio compatibility override settings. (For + * background information about the user aspect ratio compatibility override, see + * {@link #PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE}.) + * + * <p>When users apply the full-screen compatibility override, the orientation + * of the activity is forced to {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_USER}. + * + * <p>The user override is intended to improve the app experience on devices + * that have the ignore orientation request display setting enabled by OEMs + * (enables compatibility mode for fixed orientation on Android 12 (API + * level 31) or higher; see + * <a href="https://developer.android.com/guide/topics/large-screens/large-screen-app-compatibility"> + * Large screen app compatibility</a> + * for more details). + * + * <p>To opt out of the full-screen option of the user aspect ratio compatibility + * override, add this property to your app manifest and set the value to {@code false}. + * Your app will have full-screen option removed from the list of user aspect ratio + * override options in device settings, and users will not be able to apply + * full-screen override to your app. + * + * <p><b>Note:</b> If {@link #PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE} is + * {@code false}, this property has no effect. + * + * <p>Not setting this property at all, or setting this property to {@code true} has no effect. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE" + * android:value="false"/> + * </application> + * </pre> + * @hide + */ + // TODO(b/294227289): Make this public API + String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = + "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 1d6778b8a4a9..55b2251ac196 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -1498,6 +1498,11 @@ public class HorizontalScrollView extends FrameLayout { * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. */ private int consumeFlingInStretch(int unconsumed) { + int scrollX = getScrollX(); + if (scrollX < 0 || scrollX > getScrollRange()) { + // We've overscrolled, so don't stretch + return unconsumed; + } if (unconsumed > 0 && mEdgeGlowLeft != null && mEdgeGlowLeft.getDistance() != 0f) { int size = getWidth(); float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index a2f95fa9df45..fea3b78f27b4 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -337,7 +337,7 @@ public class RemoteViews implements Parcelable, Filter { * * @hide */ - private static final int MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 2000; + private static final int MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 5000; /** * Application that hosts the remote views. @@ -4820,7 +4820,7 @@ public class RemoteViews implements Parcelable, Filter { public static boolean isAdapterConversionEnabled() { return AppGlobals.getIntCoreSetting( SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION, - SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION_DEFAULT ? 1 : 0) == 1; + SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION_DEFAULT ? 1 : 0) != 0; } /** diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java index d4f4d19e9bad..a250a867b9de 100644 --- a/core/java/android/widget/RemoteViewsService.java +++ b/core/java/android/widget/RemoteViewsService.java @@ -47,7 +47,7 @@ public abstract class RemoteViewsService extends Service { * * @hide */ - private static final int MAX_NUM_ENTRY = 25; + private static final int MAX_NUM_ENTRY = 10; /** * An interface for an adapter between a remote collection view (ListView, GridView, etc) and diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index cb5dbe6c5618..e591e9ee78eb 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -1545,6 +1545,11 @@ public class ScrollView extends FrameLayout { * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. */ private int consumeFlingInStretch(int unconsumed) { + int scrollY = getScrollY(); + if (scrollY < 0 || scrollY > getScrollRange()) { + // We've overscrolled, so don't stretch + return unconsumed; + } if (unconsumed > 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) { int size = getHeight(); float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size; diff --git a/core/java/android/window/ConfigurationHelper.java b/core/java/android/window/ConfigurationHelper.java index 269ce083d205..e32adcf23a3b 100644 --- a/core/java/android/window/ConfigurationHelper.java +++ b/core/java/android/window/ConfigurationHelper.java @@ -106,7 +106,7 @@ public class ConfigurationHelper { * @see WindowManager#getCurrentWindowMetrics() * @see WindowManager#getMaximumWindowMetrics() */ - public static boolean shouldUpdateWindowMetricsBounds(@NonNull Configuration currentConfig, + private static boolean shouldUpdateWindowMetricsBounds(@NonNull Configuration currentConfig, @NonNull Configuration newConfig) { final Rect currentBounds = currentConfig.windowConfiguration.getBounds(); final Rect newBounds = newConfig.windowConfiguration.getBounds(); diff --git a/core/java/com/android/internal/app/AssistUtils.java b/core/java/com/android/internal/app/AssistUtils.java index 0ea80144a798..4261a0f14767 100644 --- a/core/java/com/android/internal/app/AssistUtils.java +++ b/core/java/com/android/internal/app/AssistUtils.java @@ -234,6 +234,23 @@ public class AssistUtils { } /** + * Allows subscription to {@link android.service.voice.VisualQueryDetectionService} service + * status. + * + * @param listener to receive visual service start/stop events. + */ + public void subscribeVisualQueryRecognitionStatus(IVisualQueryRecognitionStatusListener + listener) { + try { + if (mVoiceInteractionManagerService != null) { + mVoiceInteractionManagerService.subscribeVisualQueryRecognitionStatus(listener); + } + } catch (RemoteException e) { + Log.w(TAG, "Failed to register visual query detection start listener", e); + } + } + + /** * Enables visual detection service. * * @param listener to receive visual attention gained/lost events. diff --git a/core/java/com/android/internal/app/IVisualQueryRecognitionStatusListener.aidl b/core/java/com/android/internal/app/IVisualQueryRecognitionStatusListener.aidl new file mode 100644 index 000000000000..cc49a7539cf0 --- /dev/null +++ b/core/java/com/android/internal/app/IVisualQueryRecognitionStatusListener.aidl @@ -0,0 +1,32 @@ +/* + * 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.app; + + + oneway interface IVisualQueryRecognitionStatusListener { + /** + * Called when {@link VisualQueryDetectionService#onStartDetection} is scheduled from the system + * server via {@link VoiceInteractionManagerService#StartPerceiving}. + */ + void onStartPerceiving(); + + /** + * Called when {@link VisualQueryDetectionService#onStopDetection} is scheduled from the system + * server via {@link VoiceInteractionManagerService#StopPerceiving}. + */ + void onStopPerceiving(); + }
\ No newline at end of file diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 24d5afc42d8f..314ed69cb885 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -40,6 +40,7 @@ import com.android.internal.app.IVoiceInteractionSessionShowCallback; import com.android.internal.app.IVoiceInteractionSoundTriggerSession; import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.internal.app.IVisualQueryRecognitionStatusListener; interface IVoiceInteractionManagerService { void showSession(in Bundle sessionArgs, int flags, String attributionTag); @@ -325,6 +326,9 @@ interface IVoiceInteractionManagerService { void shutdownHotwordDetectionService(); @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE") + void subscribeVisualQueryRecognitionStatus(in IVisualQueryRecognitionStatusListener listener); + + @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE") void enableVisualQueryDetection(in IVisualQueryDetectionAttentionListener Listener); @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE") diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index 43d263bc8a6d..b3b06037ec24 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -390,12 +390,17 @@ public class LocaleStore { public static Set<LocaleInfo> transformImeLanguageTagToLocaleInfo( List<InputMethodSubtype> list) { Set<LocaleInfo> imeLocales = new HashSet<>(); + Set<String> languageTagSet = new HashSet<>(); for (InputMethodSubtype subtype : list) { - Locale locale = Locale.forLanguageTag(subtype.getLanguageTag()); - LocaleInfo cacheInfo = getLocaleInfo(locale, sLocaleCache); - LocaleInfo localeInfo = new LocaleInfo(cacheInfo); - localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE; - imeLocales.add(localeInfo); + String languageTag = subtype.getLanguageTag(); + if (!languageTagSet.contains(languageTag)) { + languageTagSet.add(languageTag); + Locale locale = Locale.forLanguageTag(languageTag); + LocaleInfo cacheInfo = getLocaleInfo(locale, sLocaleCache); + LocaleInfo localeInfo = new LocaleInfo(cacheInfo); + localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE; + imeLocales.add(localeInfo); + } } return imeLocales; } diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index 0ba271f253fb..3aa554a759de 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -534,7 +534,14 @@ public final class SystemUiDeviceConfigFlags { /** * (boolean) Whether to enable the adapter conversion in RemoteViews */ - public static final String REMOTEVIEWS_ADAPTER_CONVERSION = "remoteviews_adapter_conversion"; + public static final String REMOTEVIEWS_ADAPTER_CONVERSION = + "CursorControlFeature__remoteviews_adapter_conversion"; + + /** + * The key name used in app core settings for {@link #REMOTEVIEWS_ADAPTER_CONVERSION} + */ + public static final String KEY_REMOTEVIEWS_ADAPTER_CONVERSION = + "systemui__remoteviews_adapter_conversion"; /** * Default value for whether the adapter conversion is enabled or not. This is set for diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 635adcad0e7b..7dda91d7b25e 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -383,7 +383,11 @@ public class ConversationLayout extends FrameLayout updateContentEndPaddings(); } - @RemotableViewMethod + /** + * Set conversation data + * @param extras Bundle contains conversation data + */ + @RemotableViewMethod(asyncImpl = "setDataAsync") public void setData(Bundle extras) { Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); List<Notification.MessagingStyle.Message> newMessages @@ -393,8 +397,7 @@ public class ConversationLayout extends FrameLayout = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); // mUser now set (would be nice to avoid the side effect but WHATEVER) - setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, android.app.Person.class)); - + final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); // Append remote input history to newMessages (again, side effect is lame but WHATEVS) RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, android.app.RemoteInputHistoryItem.class); @@ -402,11 +405,30 @@ public class ConversationLayout extends FrameLayout boolean showSpinner = extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); + int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); + + // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding + // if they exist + final List<MessagingMessage> newMessagingMessages = + createMessages(newMessages, false /* isHistoric */); + final List<MessagingMessage> newHistoricMessagingMessages = + createMessages(newHistoricMessages, true /* isHistoric */); // bind it, baby - bind(newMessages, newHistoricMessages, showSpinner); + bindViews(user, showSpinner, unreadCount, + newMessagingMessages, + newHistoricMessagingMessages); + } - int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); - setUnreadCount(unreadCount); + /** + * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}. + * This should be called on a background thread, and returns a Runnable which is then must be + * called on the main thread to complete the operation and set text. + * @param extras Bundle contains conversation data + * @hide + */ + @NonNull + public Runnable setDataAsync(Bundle extras) { + return () -> setData(extras); } @Override @@ -436,15 +458,17 @@ public class ConversationLayout extends FrameLayout } } - private void bind(List<Notification.MessagingStyle.Message> newMessages, - List<Notification.MessagingStyle.Message> newHistoricMessages, - boolean showSpinner) { - // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding - // if they exist - List<MessagingMessage> historicMessages = createMessages(newHistoricMessages, - true /* isHistoric */); - List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */); + private void bindViews(Person user, + boolean showSpinner, int unreadCount, List<MessagingMessage> newMessagingMessages, + List<MessagingMessage> newHistoricMessagingMessages) { + setUser(user); + setUnreadCount(unreadCount); + bind(showSpinner, newMessagingMessages, newHistoricMessagingMessages); + } + + private void bind(boolean showSpinner, List<MessagingMessage> messages, + List<MessagingMessage> historicMessages) { // Copy our groups, before they get clobbered ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index 9d142f689b98..8345c5cc9ef9 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -156,7 +156,11 @@ public class MessagingLayout extends FrameLayout mConversationTitle = conversationTitle; } - @RemotableViewMethod + /** + * Set Messaging data + * @param extras Bundle contains messaging data + */ + @RemotableViewMethod(asyncImpl = "setDataAsync") public void setData(Bundle extras) { Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); List<Notification.MessagingStyle.Message> newMessages @@ -168,9 +172,28 @@ public class MessagingLayout extends FrameLayout RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, android.app.RemoteInputHistoryItem.class); addRemoteInputHistoryToMessages(newMessages, history); + + final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); boolean showSpinner = extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); - bind(newMessages, newHistoricMessages, showSpinner); + + final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages, + true /* isHistoric */); + final List<MessagingMessage> newMessagingMessages = + createMessages(newMessages, false /* isHistoric */); + bindViews(user, showSpinner, historicMessagingMessages, newMessagingMessages); + } + + /** + * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}. + * This should be called on a background thread, and returns a Runnable which is then must be + * called on the main thread to complete the operation and set text. + * @param extras Bundle contains messaging data + * @hide + */ + @NonNull + public Runnable setDataAsync(Bundle extras) { + return () -> setData(extras); } @Override @@ -195,14 +218,15 @@ public class MessagingLayout extends FrameLayout } } - private void bind(List<Notification.MessagingStyle.Message> newMessages, - List<Notification.MessagingStyle.Message> newHistoricMessages, - boolean showSpinner) { - - List<MessagingMessage> historicMessages = createMessages(newHistoricMessages, - true /* isHistoric */); - List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */); + private void bindViews(Person user, boolean showSpinner, + List<MessagingMessage> historicMessagingMessages, + List<MessagingMessage> newMessagingMessages) { + setUser(user); + bind(showSpinner, historicMessagingMessages, newMessagingMessages); + } + private void bind(boolean showSpinner, List<MessagingMessage> historicMessages, + List<MessagingMessage> messages) { ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); addMessagesToGroups(historicMessages, messages, showSpinner); diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 424925328dfd..dbe03386a9cf 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -257,6 +257,14 @@ static struct { jmethodID onTrustedPresentationChanged; } gTrustedPresentationCallbackClassInfo; +static struct { + jclass clazz; + jmethodID ctor; + jfieldID layerName; + jfieldID bufferId; + jfieldID frameNumber; +} gStalledTransactionInfoClassInfo; + constexpr ui::Dataspace pickDataspaceFromColorMode(const ui::ColorMode colorMode) { switch (colorMode) { case ui::ColorMode::DISPLAY_P3: @@ -2032,6 +2040,29 @@ static jlong getNativeTrustedPresentationCallbackFinalizer(JNIEnv* env, jclass c return static_cast<jlong>(reinterpret_cast<uintptr_t>(&destroyNativeTpc)); } +static jobject nativeGetStalledTransactionInfo(JNIEnv* env, jclass clazz, jint pid) { + std::optional<gui::StalledTransactionInfo> stalledTransactionInfo = + SurfaceComposerClient::getStalledTransactionInfo(pid); + if (!stalledTransactionInfo) { + return nullptr; + } + + jobject jStalledTransactionInfo = env->NewObject(gStalledTransactionInfoClassInfo.clazz, + gStalledTransactionInfoClassInfo.ctor); + if (!jStalledTransactionInfo) { + jniThrowException(env, "java/lang/OutOfMemoryError", nullptr); + return nullptr; + } + + env->SetObjectField(jStalledTransactionInfo, gStalledTransactionInfoClassInfo.layerName, + env->NewStringUTF(String8{stalledTransactionInfo->layerName})); + env->SetLongField(jStalledTransactionInfo, gStalledTransactionInfoClassInfo.bufferId, + static_cast<jlong>(stalledTransactionInfo->bufferId)); + env->SetLongField(jStalledTransactionInfo, gStalledTransactionInfoClassInfo.frameNumber, + static_cast<jlong>(stalledTransactionInfo->frameNumber)); + return jStalledTransactionInfo; +} + // ---------------------------------------------------------------------------- SurfaceControl* android_view_SurfaceControl_getNativeSurfaceControl(JNIEnv* env, @@ -2281,6 +2312,8 @@ static const JNINativeMethod sSurfaceControlMethods[] = { {"nativeCreateTpc", "(Landroid/view/SurfaceControl$TrustedPresentationCallback;)J", (void*)nativeCreateTpc}, {"getNativeTrustedPresentationCallbackFinalizer", "()J", (void*)getNativeTrustedPresentationCallbackFinalizer }, + {"nativeGetStalledTransactionInfo", "(I)Landroid/gui/StalledTransactionInfo;", + (void*) nativeGetStalledTransactionInfo }, // clang-format on }; @@ -2524,6 +2557,18 @@ int register_android_view_SurfaceControl(JNIEnv* env) gTrustedPresentationCallbackClassInfo.onTrustedPresentationChanged = GetMethodIDOrDie(env, trustedPresentationCallbackClazz, "onTrustedPresentationChanged", "(Z)V"); + + jclass stalledTransactionInfoClazz = FindClassOrDie(env, "android/gui/StalledTransactionInfo"); + gStalledTransactionInfoClassInfo.clazz = MakeGlobalRefOrDie(env, stalledTransactionInfoClazz); + gStalledTransactionInfoClassInfo.ctor = + GetMethodIDOrDie(env, stalledTransactionInfoClazz, "<init>", "()V"); + gStalledTransactionInfoClassInfo.layerName = + GetFieldIDOrDie(env, stalledTransactionInfoClazz, "layerName", "Ljava/lang/String;"); + gStalledTransactionInfoClassInfo.bufferId = + GetFieldIDOrDie(env, stalledTransactionInfoClazz, "bufferId", "J"); + gStalledTransactionInfoClassInfo.frameNumber = + GetFieldIDOrDie(env, stalledTransactionInfoClazz, "frameNumber", "J"); + return err; } diff --git a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp index a95b6e37f5de..76f5c107c970 100644 --- a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp +++ b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp @@ -127,16 +127,17 @@ static void native_writeToParcel(JNIEnv *env, jobject self, jlong nativePtr, job } } -static void throwReadRE(JNIEnv *env, binder_status_t status) { +static void throwReadException(JNIEnv *env, binder_status_t status) { ALOGE("Could not read LongArrayMultiStateCounter from Parcel, status = %d", status); - jniThrowRuntimeException(env, "Could not read LongArrayMultiStateCounter from Parcel"); + jniThrowException(env, "android.os.BadParcelableException", + "Could not read LongArrayMultiStateCounter from Parcel"); } #define THROW_AND_RETURN_ON_READ_ERROR(expr) \ { \ binder_status_t status = expr; \ if (status != STATUS_OK) { \ - throwReadRE(env, status); \ + throwReadException(env, status); \ return 0L; \ } \ } @@ -147,6 +148,11 @@ static jlong native_initFromParcel(JNIEnv *env, jclass theClass, jobject jParcel int32_t stateCount; THROW_AND_RETURN_ON_READ_ERROR(AParcel_readInt32(parcel.get(), &stateCount)); + if (stateCount < 0 || stateCount > 0xEFFF) { + throwReadException(env, STATUS_INVALID_OPERATION); + return 0L; + } + int32_t arrayLength; THROW_AND_RETURN_ON_READ_ERROR(AParcel_readInt32(parcel.get(), &arrayLength)); diff --git a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp index 1712b3a8512b..ddf7a67e00ce 100644 --- a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp +++ b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp @@ -131,16 +131,17 @@ static void native_writeToParcel(JNIEnv *env, jobject self, jlong nativePtr, job } } -static void throwReadRE(JNIEnv *env, binder_status_t status) { +static void throwReadException(JNIEnv *env, binder_status_t status) { ALOGE("Could not read LongMultiStateCounter from Parcel, status = %d", status); - jniThrowRuntimeException(env, "Could not read LongMultiStateCounter from Parcel"); + jniThrowException(env, "android.os.BadParcelableException", + "Could not read LongMultiStateCounter from Parcel"); } #define THROW_AND_RETURN_ON_READ_ERROR(expr) \ { \ binder_status_t status = expr; \ if (status != STATUS_OK) { \ - throwReadRE(env, status); \ + throwReadException(env, status); \ return 0L; \ } \ } @@ -151,6 +152,11 @@ static jlong native_initFromParcel(JNIEnv *env, jclass theClass, jobject jParcel int32_t stateCount; THROW_AND_RETURN_ON_READ_ERROR(AParcel_readInt32(parcel.get(), &stateCount)); + if (stateCount < 0 || stateCount > 0xEFFF) { + throwReadException(env, STATUS_INVALID_OPERATION); + return 0L; + } + auto counter = std::make_unique<battery::LongMultiStateCounter>(stateCount, 0); for (battery::state_t state = 0; state < stateCount; state++) { diff --git a/core/proto/android/server/windowmanagertransitiontrace.proto b/core/proto/android/server/windowmanagertransitiontrace.proto index a950a79d94fb..34ccb482fb14 100644 --- a/core/proto/android/server/windowmanagertransitiontrace.proto +++ b/core/proto/android/server/windowmanagertransitiontrace.proto @@ -56,6 +56,7 @@ message Transition { repeated Target targets = 8; optional int32 flags = 9; optional int64 abort_time_ns = 10; + optional int64 starting_window_remove_time_ns = 11; } message Target { diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 130648c74bb1..cbc6c2a176cc 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5336,6 +5336,12 @@ <!-- Default value for performant auth feature. --> <bool name="config_performantAuthDefault">false</bool> + <!-- Threshold for false rejection rate (FRR) of biometric authentication. Applies for both + fingerprint and face. If a dual-modality device only enrolled a single biometric and + experiences high FRR (above threshold), system notification will be sent to encourage user + to enroll the other eligible biometric. --> + <fraction name="config_biometricNotificationFrrThreshold">30%</fraction> + <!-- The component name for the default profile supervisor, which can be set as a profile owner even after user setup is complete. The defined component should be used for supervision purposes only. The component must be part of a system app. --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index f795bd7cc3fd..33c18c29dc74 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1831,6 +1831,8 @@ <string name="fingerprint_error_not_match">Fingerprint not recognized</string> <!-- Message shown when UDFPS fails to match --> <string name="fingerprint_udfps_error_not_match">Fingerprint not recognized</string> + <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] --> + <string name="fingerprint_dialog_use_fingerprint_instead">Can\u2019t recognize face. Use fingerprint instead.</string> <!-- Accessibility message announced when a fingerprint has been authenticated [CHAR LIMIT=NONE] --> <string name="fingerprint_authenticated">Fingerprint authenticated</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3610eaddff42..8de13048e736 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2581,6 +2581,9 @@ <java-symbol type="string" name="biometric_error_device_not_secured" /> <java-symbol type="string" name="biometric_error_generic" /> + <!-- Biometric FRR config --> + <java-symbol type="fraction" name="config_biometricNotificationFrrThreshold" /> + <!-- Device credential strings for BiometricManager --> <java-symbol type="string" name="screen_lock_app_setting_name" /> <java-symbol type="string" name="screen_lock_dialog_default_subtitle" /> @@ -2594,6 +2597,7 @@ <java-symbol type="string" name="fingerprint_error_vendor_unknown" /> <java-symbol type="string" name="fingerprint_error_not_match" /> <java-symbol type="string" name="fingerprint_udfps_error_not_match" /> + <java-symbol type="string" name="fingerprint_dialog_use_fingerprint_instead" /> <java-symbol type="string" name="fingerprint_acquired_partial" /> <java-symbol type="string" name="fingerprint_acquired_insufficient" /> <java-symbol type="string" name="fingerprint_acquired_imager_dirty" /> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index 129de649a4b5..31755efb88ed 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -231,6 +231,28 @@ </intent-filter> </activity> + <activity android:name="android.widget.HorizontalScrollViewActivity" + android:label="HorizontalScrollViewActivity" + android:screenOrientation="portrait" + android:exported="true" + android:theme="@android:style/Theme.Material.Light"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> + </intent-filter> + </activity> + + <activity android:name="android.widget.ScrollViewActivity" + android:label="ScrollViewActivity" + android:screenOrientation="portrait" + android:exported="true" + android:theme="@android:style/Theme.Material.Light"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> + </intent-filter> + </activity> + <activity android:name="android.widget.DatePickerActivity" android:label="DatePickerActivity" android:screenOrientation="portrait" diff --git a/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml new file mode 100644 index 000000000000..866e1a95c3f5 --- /dev/null +++ b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml @@ -0,0 +1,118 @@ +<?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 + --> + +<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/horizontal_scroll_view"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + </LinearLayout> +</HorizontalScrollView> diff --git a/core/tests/coretests/res/layout/activity_scroll_view.xml b/core/tests/coretests/res/layout/activity_scroll_view.xml new file mode 100644 index 000000000000..61fabf8ee437 --- /dev/null +++ b/core/tests/coretests/res/layout/activity_scroll_view.xml @@ -0,0 +1,118 @@ +<?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 + --> + +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/scroll_view"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + </LinearLayout> +</ScrollView> diff --git a/core/tests/coretests/src/android/app/NotificationChannelTest.java b/core/tests/coretests/src/android/app/NotificationChannelTest.java index 647bfe84231d..d8305f054d11 100644 --- a/core/tests/coretests/src/android/app/NotificationChannelTest.java +++ b/core/tests/coretests/src/android/app/NotificationChannelTest.java @@ -16,19 +16,52 @@ package android.app; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.AttributionSource; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.Context; +import android.content.IContentProvider; +import android.content.pm.ApplicationInfo; +import android.database.MatrixCursor; +import android.media.AudioAttributes; import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; import android.os.Parcel; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.provider.MediaStore.Audio.AudioColumns; +import android.test.mock.MockContentResolver; +import android.util.Xml; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; + import com.google.common.base.Strings; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; @RunWith(AndroidJUnit4.class) @@ -36,6 +69,88 @@ import java.lang.reflect.Field; public class NotificationChannelTest { private final String CLASS = "android.app.NotificationChannel"; + Context mContext; + ContentProvider mContentProvider; + IContentProvider mIContentProvider; + + @Before + public void setUp() throws Exception { + mContext = mock(Context.class); + when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo()); + MockContentResolver mContentResolver = new MockContentResolver(mContext); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + mContentProvider = mock(ContentProvider.class); + mIContentProvider = mock(IContentProvider.class); + when(mContentProvider.getIContentProvider()).thenReturn(mIContentProvider); + doAnswer( + invocation -> { + AttributionSource attributionSource = invocation.getArgument(0); + Uri uri = invocation.getArgument(1); + RemoteCallback cb = invocation.getArgument(2); + IContentProvider mock = (IContentProvider) (invocation.getMock()); + AsyncTask.SERIAL_EXECUTOR.execute( + () -> { + final Bundle bundle = new Bundle(); + try { + bundle.putParcelable( + ContentResolver.REMOTE_CALLBACK_RESULT, + mock.canonicalize(attributionSource, uri)); + } catch (RemoteException e) { + /* consume */ + } + cb.sendResult(bundle); + }); + return null; + }) + .when(mIContentProvider) + .canonicalizeAsync(any(), any(), any()); + doAnswer( + invocation -> { + AttributionSource attributionSource = invocation.getArgument(0); + Uri uri = invocation.getArgument(1); + RemoteCallback cb = invocation.getArgument(2); + IContentProvider mock = (IContentProvider) (invocation.getMock()); + AsyncTask.SERIAL_EXECUTOR.execute( + () -> { + final Bundle bundle = new Bundle(); + try { + bundle.putParcelable( + ContentResolver.REMOTE_CALLBACK_RESULT, + mock.uncanonicalize(attributionSource, uri)); + } catch (RemoteException e) { + /* consume */ + } + cb.sendResult(bundle); + }); + return null; + }) + .when(mIContentProvider) + .uncanonicalizeAsync(any(), any(), any()); + doAnswer( + invocation -> { + Uri uri = invocation.getArgument(0); + RemoteCallback cb = invocation.getArgument(1); + IContentProvider mock = (IContentProvider) (invocation.getMock()); + AsyncTask.SERIAL_EXECUTOR.execute( + () -> { + final Bundle bundle = new Bundle(); + try { + bundle.putString( + ContentResolver.REMOTE_CALLBACK_RESULT, + mock.getType(uri)); + } catch (RemoteException e) { + /* consume */ + } + cb.sendResult(bundle); + }); + return null; + }) + .when(mIContentProvider) + .getTypeAsync(any(), any()); + + mContentResolver.addProvider("media", mContentProvider); + } + @Test public void testLongStringFields() { NotificationChannel channel = new NotificationChannel("id", "name", 3); @@ -103,4 +218,139 @@ public class NotificationChannelTest { assertEquals(NotificationChannel.MAX_TEXT_LENGTH, fromParcel.getSound().toString().length()); } + + @Test + public void testRestoreSoundUri_customLookup() throws Exception { + Uri uriToBeRestoredUncanonicalized = Uri.parse("content://media/1"); + Uri uriToBeRestoredCanonicalized = Uri.parse("content://media/1?title=Song&canonical=1"); + Uri uriAfterRestoredUncanonicalized = Uri.parse("content://media/100"); + Uri uriAfterRestoredCanonicalized = Uri.parse("content://media/100?title=Song&canonical=1"); + + NotificationChannel channel = new NotificationChannel("id", "name", 3); + + MatrixCursor cursor = new MatrixCursor(new String[] {"_id"}); + cursor.addRow(new Object[] {100L}); + + when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredUncanonicalized))) + .thenReturn(uriToBeRestoredCanonicalized); + + // Mock the failure of regular uncanonicalize. + when(mIContentProvider.uncanonicalize(any(), eq(uriToBeRestoredCanonicalized))) + .thenReturn(null); + + // Mock the custom lookup in RingtoneManager.getRingtoneUriForRestore. + when(mIContentProvider.query(any(), any(), any(), any(), any())).thenReturn(cursor); + + // Mock the canonicalize in RingtoneManager.getRingtoneUriForRestore. + when(mIContentProvider.canonicalize(any(), eq(uriAfterRestoredUncanonicalized))) + .thenReturn(uriAfterRestoredCanonicalized); + + assertThat( + channel.restoreSoundUri( + mContext, + uriToBeRestoredUncanonicalized, + true, + AudioAttributes.USAGE_NOTIFICATION)) + .isEqualTo(uriAfterRestoredCanonicalized); + } + + @Test + public void testWriteXmlForBackup_customLookup_notificationUsage() throws Exception { + testWriteXmlForBackup_customLookup( + AudioAttributes.USAGE_NOTIFICATION, AudioColumns.IS_NOTIFICATION); + } + + @Test + public void testWriteXmlForBackup_customLookup_alarmUsage() throws Exception { + testWriteXmlForBackup_customLookup(AudioAttributes.USAGE_ALARM, AudioColumns.IS_ALARM); + } + + @Test + public void testWriteXmlForBackup_customLookup_ringtoneUsage() throws Exception { + testWriteXmlForBackup_customLookup( + AudioAttributes.USAGE_NOTIFICATION_RINGTONE, AudioColumns.IS_RINGTONE); + } + + @Test + public void testWriteXmlForBackup_customLookup_unknownUsage() throws Exception { + testWriteXmlForBackup_customLookup( + AudioAttributes.USAGE_UNKNOWN, AudioColumns.IS_NOTIFICATION); + } + + private void testWriteXmlForBackup_customLookup(int usage, String customQuerySelection) + throws Exception { + Uri uriToBeRestoredUncanonicalized = Uri.parse("content://media/1"); + Uri uriToBeRestoredCanonicalized = Uri.parse("content://media/1?title=Song&canonical=1"); + Uri uriAfterRestoredUncanonicalized = Uri.parse("content://media/100"); + Uri uriAfterRestoredCanonicalized = Uri.parse("content://media/100?title=Song&canonical=1"); + + AudioAttributes mAudioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + .setUsage(usage) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .build(); + + NotificationChannel channel = new NotificationChannel("id", "name", 3); + channel.setSound(uriToBeRestoredCanonicalized, mAudioAttributes); + + TypedXmlSerializer serializer = Xml.newFastSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + + // mock the canonicalize in writeXmlForBackup -> getSoundForBackup + when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredUncanonicalized))) + .thenReturn(uriToBeRestoredCanonicalized); + when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredCanonicalized))) + .thenReturn(uriToBeRestoredCanonicalized); + + channel.writeXmlForBackup(serializer, mContext); + serializer.endDocument(); + serializer.flush(); + + TypedXmlPullParser parser = Xml.newFastPullParser(); + byte[] byteArray = baos.toByteArray(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(byteArray)), null); + parser.nextTag(); + + NotificationChannel targetChannel = new NotificationChannel("id", "name", 3); + + MatrixCursor cursor = new MatrixCursor(new String[] {"_id"}); + cursor.addRow(new Object[] {100L}); + + when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredCanonicalized))) + .thenReturn(uriToBeRestoredCanonicalized); + + // Mock the failure of regular uncanonicalize. + when(mIContentProvider.uncanonicalize(any(), eq(uriToBeRestoredCanonicalized))) + .thenReturn(null); + + Bundle expectedBundle = + ContentResolver.createSqlQueryBundle( + customQuerySelection + "=1 AND title=?", new String[] {"Song"}, null); + + // Mock the custom lookup in RingtoneManager.getRingtoneUriForRestore. + when(mIContentProvider.query( + any(), + any(), + any(), + // any(), + argThat( + queryBundle -> { + return queryBundle != null + && expectedBundle + .toString() + .equals(queryBundle.toString()); + }), + any())) + .thenReturn(cursor); + + // Mock the canonicalize in RingtoneManager.getRingtoneUriForRestore. + when(mIContentProvider.canonicalize(any(), eq(uriAfterRestoredUncanonicalized))) + .thenReturn(uriAfterRestoredCanonicalized); + + targetChannel.populateFromXmlForRestore(parser, true, mContext); + assertThat(targetChannel.getSound()).isEqualTo(uriAfterRestoredCanonicalized); + } } diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index 48577416b3d0..f9a714841d32 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -451,8 +451,10 @@ public class ActivityThreadTest { final Rect bounds = activity.getWindowManager().getCurrentWindowMetrics().getBounds(); assertEquals(activityConfigPortrait.windowConfiguration.getBounds(), bounds); - // Ensure changes in window configuration bounds are reported - assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); + // Ensure that Activity#onConfigurationChanged() not be called because the changes in + // WindowConfiguration shouldn't be reported, and we only apply the latest Configuration + // update in transaction. + assertEquals(numOfConfig, activity.mNumOfConfigChanges); } @Test diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index 8028b14d53b6..f1eef7551dd5 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -221,8 +221,10 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_startHandwriting_inputConnectionBuilt_stylusMoveInExtendedHWArea() { + // The stylus down point is between mTestView1 and mTestView2, but it is within the + // extended handwriting area of both views. It is closer to mTestView1. final int x1 = sHwArea1.right + HW_BOUNDS_OFFSETS_RIGHT_PX / 2; - final int y1 = sHwArea1.bottom + HW_BOUNDS_OFFSETS_BOTTOM_PX / 2; + final int y1 = sHwArea1.bottom + (sHwArea2.top - sHwArea1.bottom) / 3; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); mHandwritingInitiator.onTouchEvent(stylusEvent1); @@ -231,10 +233,14 @@ public class HandwritingInitiatorTest { MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); mHandwritingInitiator.onTouchEvent(stylusEvent2); - // InputConnection is created after stylus movement. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + // First create InputConnection for mTestView2 and verify that handwriting is not started. + mHandwritingInitiator.onInputConnectionCreated(mTestView2); + verify(mHandwritingInitiator, never()).startHandwriting(mTestView2); - verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); + // Next create InputConnection for mTextView1. Handwriting is started for this view since + // the stylus down point is closest to this view. + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + verify(mHandwritingInitiator).startHandwriting(mTestView1); } @Test diff --git a/core/tests/coretests/src/android/widget/HorizontalScrollViewActivity.java b/core/tests/coretests/src/android/widget/HorizontalScrollViewActivity.java new file mode 100644 index 000000000000..21013545008c --- /dev/null +++ b/core/tests/coretests/src/android/widget/HorizontalScrollViewActivity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.app.Activity; +import android.os.Bundle; + +import com.android.frameworks.coretests.R; + +/** + * An activity for testing the TextView widget. + */ +public class HorizontalScrollViewActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_horizontal_scroll_view); + } +} diff --git a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java new file mode 100644 index 000000000000..86f26e59e370 --- /dev/null +++ b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java @@ -0,0 +1,71 @@ +/* + * 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 android.widget; + +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.Presubmit; +import android.util.PollingCheck; + +import androidx.test.filters.MediumTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.coretests.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@MediumTest +@Presubmit +public class HorizontalScrollViewFunctionalTest { + private HorizontalScrollViewActivity mActivity; + private HorizontalScrollView mHorizontalScrollView; + @Rule + public ActivityTestRule<HorizontalScrollViewActivity> mActivityRule = new ActivityTestRule<>( + HorizontalScrollViewActivity.class); + + @Before + public void setUp() throws Exception { + mActivity = mActivityRule.getActivity(); + mHorizontalScrollView = mActivity.findViewById(R.id.horizontal_scroll_view); + } + + @Test + public void testScrollAfterFlingTop() { + mHorizontalScrollView.scrollTo(100, 0); + mHorizontalScrollView.fling(-10000); + PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowLeft.getDistance() > 0); + PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowLeft.getDistance() == 0f); + assertEquals(0, mHorizontalScrollView.getScrollX()); + } + + @Test + public void testScrollAfterFlingBottom() { + int childWidth = mHorizontalScrollView.getChildAt(0).getWidth(); + int maxScroll = childWidth - mHorizontalScrollView.getWidth(); + mHorizontalScrollView.scrollTo(maxScroll - 100, 0); + mHorizontalScrollView.fling(10000); + PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowRight.getDistance() > 0); + PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowRight.getDistance() == 0f); + assertEquals(maxScroll, mHorizontalScrollView.getScrollX()); + } +} + diff --git a/core/tests/coretests/src/android/widget/ScrollViewActivity.java b/core/tests/coretests/src/android/widget/ScrollViewActivity.java new file mode 100644 index 000000000000..899d63163aa4 --- /dev/null +++ b/core/tests/coretests/src/android/widget/ScrollViewActivity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.app.Activity; +import android.os.Bundle; + +import com.android.frameworks.coretests.R; + +/** + * An activity for testing the TextView widget. + */ +public class ScrollViewActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_scroll_view); + } +} diff --git a/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java new file mode 100644 index 000000000000..a49bb6af13d2 --- /dev/null +++ b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java @@ -0,0 +1,71 @@ +/* + * 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 android.widget; + +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.Presubmit; +import android.util.PollingCheck; + +import androidx.test.filters.MediumTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.coretests.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@MediumTest +@Presubmit +public class ScrollViewFunctionalTest { + private ScrollViewActivity mActivity; + private ScrollView mScrollView; + @Rule + public ActivityTestRule<ScrollViewActivity> mActivityRule = new ActivityTestRule<>( + ScrollViewActivity.class); + + @Before + public void setUp() throws Exception { + mActivity = mActivityRule.getActivity(); + mScrollView = mActivity.findViewById(R.id.scroll_view); + } + + @Test + public void testScrollAfterFlingTop() { + mScrollView.scrollTo(0, 100); + mScrollView.fling(-10000); + PollingCheck.waitFor(() -> mScrollView.mEdgeGlowTop.getDistance() > 0); + PollingCheck.waitFor(() -> mScrollView.mEdgeGlowTop.getDistance() == 0f); + assertEquals(0, mScrollView.getScrollY()); + } + + @Test + public void testScrollAfterFlingBottom() { + int childHeight = mScrollView.getChildAt(0).getHeight(); + int maxScroll = childHeight - mScrollView.getHeight(); + mScrollView.scrollTo(0, maxScroll - 100); + mScrollView.fling(10000); + PollingCheck.waitFor(() -> mScrollView.mEdgeGlowBottom.getDistance() > 0); + PollingCheck.waitFor(() -> mScrollView.mEdgeGlowBottom.getDistance() == 0f); + assertEquals(maxScroll, mScrollView.getScrollY()); + } +} + diff --git a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java index 516dee7dc9aa..faccf1ad19a1 100644 --- a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java +++ b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import android.os.BadParcelableException; import android.os.Parcel; import androidx.test.filters.SmallTest; @@ -163,6 +164,45 @@ public class LongArrayMultiStateCounterTest { } @Test + public void createFromBadBundle() { + Parcel data = Parcel.obtain(); + int bundleLenPos = data.dataPosition(); + data.writeInt(0); + data.writeInt(0x4C444E42); // BaseBundle.BUNDLE_MAGIC + + int bundleStart = data.dataPosition(); + + data.writeInt(1); + data.writeString("key"); + data.writeInt(4); + int lazyValueLenPos = data.dataPosition(); + data.writeInt(0); + int lazyValueStart = data.dataPosition(); + data.writeString("com.android.internal.os.LongArrayMultiStateCounter"); + + // Invalid int16 value + data.writeInt(0x10000); // stateCount + data.writeInt(10); // arrayLength + for (int i = 0; i < 0x10000; ++i) { + data.writeLong(0); + } + + backPatchLength(data, lazyValueLenPos, lazyValueStart); + backPatchLength(data, bundleLenPos, bundleStart); + data.setDataPosition(0); + + assertThrows(BadParcelableException.class, + () -> data.readBundle().getParcelable("key", LongArrayMultiStateCounter.class)); + } + + private static void backPatchLength(Parcel parcel, int lengthPos, int startPos) { + int endPos = parcel.dataPosition(); + parcel.setDataPosition(lengthPos); + parcel.writeInt(endPos - startPos); + parcel.setDataPosition(endPos); + } + + @Test public void combineValues() { long[] values = new long[] {0, 1, 2, 3, 42}; LongArrayMultiStateCounter.LongArrayContainer container = diff --git a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java index fc86ebe1c10e..341375357902 100644 --- a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java +++ b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import android.os.BadParcelableException; import android.os.Parcel; import androidx.test.filters.SmallTest; @@ -210,4 +211,42 @@ public class LongMultiStateCounterTest { assertThrows(RuntimeException.class, () -> LongMultiStateCounter.CREATOR.createFromParcel(parcel)); } + + @Test + public void createFromBadBundle() { + Parcel data = Parcel.obtain(); + int bundleLenPos = data.dataPosition(); + data.writeInt(0); + data.writeInt(0x4C444E42); // BaseBundle.BUNDLE_MAGIC + + int bundleStart = data.dataPosition(); + + data.writeInt(1); + data.writeString("key"); + data.writeInt(4); + int lazyValueLenPos = data.dataPosition(); + data.writeInt(0); + int lazyValueStart = data.dataPosition(); + data.writeString("com.android.internal.os.LongMultiStateCounter"); + + // Invalid int16 value + data.writeInt(0x10000); // stateCount + for (int i = 0; i < 0x10000; ++i) { + data.writeLong(0); + } + + backPatchLength(data, lazyValueLenPos, lazyValueStart); + backPatchLength(data, bundleLenPos, bundleStart); + data.setDataPosition(0); + + assertThrows(BadParcelableException.class, + () -> data.readBundle().getParcelable("key", LongMultiStateCounter.class)); + } + + private static void backPatchLength(Parcel parcel, int lengthPos, int startPos) { + int endPos = parcel.dataPosition(); + parcel.setDataPosition(lengthPos); + parcel.writeInt(endPos - startPos); + parcel.setDataPosition(endPos); + } } diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml index e278c529d7a3..dcc96861bc31 100644 --- a/data/etc/com.android.settings.xml +++ b/data/etc/com.android.settings.xml @@ -37,6 +37,7 @@ <permission name="android.permission.MANAGE_USER_OEM_UNLOCK_STATE" /> <permission name="android.permission.MASTER_CLEAR"/> <permission name="android.permission.MEDIA_CONTENT_CONTROL"/> + <permission name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" /> <permission name="android.permission.MODIFY_PHONE_STATE"/> <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <permission name="android.permission.MOVE_PACKAGE"/> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 94e23e735d06..71e9263915ee 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -427,12 +427,6 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/BackNavigationController.java" }, - "-1715268616": { - "message": "Last window, removing starting window %s", - "level": "VERBOSE", - "group": "WM_DEBUG_STARTING_WINDOW", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "-1710206702": { "message": "Display id=%d is frozen while keyguard locked, return %d", "level": "VERBOSE", @@ -691,6 +685,12 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, + "-1449515133": { + "message": "Content Recording: stopping active projection for display %d", + "level": "ERROR", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-1443029505": { "message": "SAFE MODE ENABLED (menu=%d s=%d dpad=%d trackball=%d)", "level": "INFO", @@ -1279,6 +1279,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-921346089": { + "message": "Content Recording: Unable to tell MediaProjectionManagerService to stop the active projection for display %d: %s", + "level": "ERROR", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-917215012": { "message": "%s: caller %d is using old GET_TASKS but privileged; allowing", "level": "WARN", @@ -2227,12 +2233,6 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "-88873335": { - "message": "Content Recording: Unable to tell MediaProjectionManagerService to stop the active projection: %s", - "level": "ERROR", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/ContentRecorder.java" - }, "-87705714": { "message": "findFocusedWindow: focusedApp=null using new focus @ %s", "level": "VERBOSE", @@ -4057,12 +4057,6 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, - "1671994402": { - "message": "Nulling last startingData", - "level": "VERBOSE", - "group": "WM_DEBUG_STARTING_WINDOW", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "1674747211": { "message": "%s forcing orientation to %d for display id=%d", "level": "VERBOSE", @@ -4243,12 +4237,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1853793312": { - "message": "Notify removed startingWindow %s", - "level": "VERBOSE", - "group": "WM_DEBUG_STARTING_WINDOW", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "1856783490": { "message": "resumeTopActivity: Restarting %s", "level": "DEBUG", diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index f95f3ffb4df3..bcbf72857544 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -242,9 +242,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + // Abort if no space to split. + final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes( + task.getTaskProperties(), splitPinRule, + splitPinRule.getDefaultSplitAttributes(), + getActivitiesMinDimensionsPair(primaryContainer.getTopNonFinishingActivity(), + topContainer.getTopNonFinishingActivity())); + if (!SplitPresenter.shouldShowSplit(calculatedSplitAttributes)) { + Log.w(TAG, "No space to split, abort pinning top ActivityStack."); + return false; + } + // Registers a Split final SplitPinContainer splitPinContainer = new SplitPinContainer(primaryContainer, - topContainer, splitPinRule, splitPinRule.getDefaultSplitAttributes()); + topContainer, splitPinRule, calculatedSplitAttributes); task.addSplitContainer(splitPinContainer); // Updates the Split @@ -263,7 +274,33 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void unpinTopActivityStack(int taskId){ - // TODO + synchronized (mLock) { + final TaskContainer task = getTaskContainer(taskId); + if (task == null) { + Log.e(TAG, "Cannot find the task to unpin, id: " + taskId); + return; + } + + final SplitPinContainer splitPinContainer = task.getSplitPinContainer(); + if (splitPinContainer == null) { + Log.e(TAG, "No ActivityStack is pinned."); + return; + } + + // Remove the SplitPinContainer from the task. + final TaskFragmentContainer containerToUnpin = + splitPinContainer.getSecondaryContainer(); + task.removeSplitPinContainer(); + + // Resets the isolated navigation and updates the container. + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + mPresenter.setTaskFragmentIsolatedNavigation(wct, + containerToUnpin.getTaskFragmentToken(), false /* isolated */); + updateContainer(wct, containerToUnpin); + transactionRecord.apply(false /* shouldApplyIndependently */); + updateCallbackIfNecessary(); + } } @Override @@ -831,7 +868,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return true; } - if (!isOnReparent && getContainerWithActivity(activity) == null + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (!isOnReparent && container == null && getTaskFragmentTokenFromActivityClientRecord(activity) != null) { // We can't find the new launched activity in any recorded container, but it is // currently placed in an embedded TaskFragment. This can happen in two cases: @@ -843,11 +881,21 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return true; } - final TaskFragmentContainer container = getContainerWithActivity(activity); - if (!isOnReparent && container != null - && container.getTaskContainer().getTopNonFinishingTaskFragmentContainer() + // Skip resolving if the activity is on a pinned TaskFragmentContainer. + // TODO(b/243518738): skip resolving for overlay container. + if (container != null) { + final TaskContainer taskContainer = container.getTaskContainer(); + if (taskContainer.isTaskFragmentContainerPinned(container)) { + return true; + } + } + + final TaskContainer taskContainer = container != null ? container.getTaskContainer() : null; + if (!isOnReparent && taskContainer != null + && taskContainer.getTopNonFinishingTaskFragmentContainer(false /* includePin */) != container) { - // Do not resolve if the launched activity is not the top-most container in the Task. + // Do not resolve if the launched activity is not the top-most container (excludes + // the pinned container) in the Task. return true; } @@ -1244,6 +1292,19 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { + // Skip resolving if started from pinned TaskFragmentContainer. + // TODO(b/243518738): skip resolving for overlay container. + if (launchingActivity != null) { + final TaskFragmentContainer taskFragmentContainer = getContainerWithActivity( + launchingActivity); + final TaskContainer taskContainer = + taskFragmentContainer != null ? taskFragmentContainer.getTaskContainer() : null; + if (taskContainer != null && taskContainer.isTaskFragmentContainerPinned( + taskFragmentContainer)) { + return null; + } + } + /* * We will check the following to see if there is any embedding rule matched: * 1. Whether the new activity intent should always expand. @@ -1584,6 +1645,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } + // If the secondary container is pinned, it should not be removed. + final SplitContainer activeContainer = + getActiveSplitForContainer(existingSplitContainer.getSecondaryContainer()); + if (activeContainer instanceof SplitPinContainer) { + return; + } + existingSplitContainer.getSecondaryContainer().finish( false /* shouldFinishDependent */, mPresenter, wct, this); } @@ -1625,12 +1693,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // background. return; } - final SplitContainer splitContainer = getActiveSplitForContainer(container); - if (splitContainer instanceof SplitPinContainer - && updateSplitContainerIfNeeded(splitContainer, wct, null /* splitAttributes */)) { - // A SplitPinContainer exists and is updated. - return; - } + if (launchPlaceholderIfNecessary(wct, container)) { // Placeholder was launched, the positions will be updated when the activity is added // to the secondary container. @@ -1643,6 +1706,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // If the info is not available yet the task fragment will be expanded when it's ready return; } + final SplitContainer splitContainer = getActiveSplitForContainer(container); if (splitContainer == null) { return; } @@ -1826,6 +1890,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Don't launch placeholder for primary split container. return false; } + if (splitContainer instanceof SplitPinContainer) { + // Don't launch placeholder if pinned + return false; + } return true; } @@ -2072,8 +2140,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ + @VisibleForTesting @GuardedBy("mLock") - private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { + boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { continue; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 969e3ed5b9b6..463c8ceaf992 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -179,8 +179,16 @@ class TaskContainer { @Nullable TaskFragmentContainer getTopNonFinishingTaskFragmentContainer() { + return getTopNonFinishingTaskFragmentContainer(true /* includePin */); + } + + @Nullable + TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin) { for (int i = mContainers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = mContainers.get(i); + if (!includePin && isTaskFragmentContainerPinned(container)) { + continue; + } if (!container.isFinished()) { return container; } @@ -257,8 +265,25 @@ class TaskContainer { } void removeSplitPinContainer() { + if (mSplitPinContainer == null) { + return; + } + + final TaskFragmentContainer primaryContainer = mSplitPinContainer.getPrimaryContainer(); + final TaskFragmentContainer secondaryContainer = mSplitPinContainer.getSecondaryContainer(); mSplitContainers.remove(mSplitPinContainer); mSplitPinContainer = null; + + // Remove the other SplitContainers that contains the unpinned container (unless it + // is the current top-most split-pair), since the state are no longer valid. + final List<SplitContainer> splitsToRemove = new ArrayList<>(); + for (SplitContainer splitContainer : mSplitContainers) { + if (splitContainer.getSecondaryContainer().equals(secondaryContainer) + && !splitContainer.getPrimaryContainer().equals(primaryContainer)) { + splitsToRemove.add(splitContainer); + } + } + removeSplitContainers(splitsToRemove); } @Nullable @@ -266,6 +291,11 @@ class TaskContainer { return mSplitPinContainer; } + boolean isTaskFragmentContainerPinned(@NonNull TaskFragmentContainer taskFragmentContainer) { + return mSplitPinContainer != null + && mSplitPinContainer.getSecondaryContainer() == taskFragmentContainer; + } + void addTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) { mContainers.add(taskFragmentContainer); onTaskFragmentContainerUpdated(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 9af1fe916279..b2ffad7a74e4 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -595,6 +595,18 @@ public class SplitControllerTest { } @Test + public void testResolveStartActivityIntent_skipIfPinned() { + final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity); + final TaskContainer taskContainer = container.getTaskContainer(); + spyOn(taskContainer); + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + doReturn(true).when(taskContainer).isTaskFragmentContainerPinned(container); + assertNull(mSplitController.resolveStartActivityIntent(mTransaction, TASK_ID, intent, + mActivity)); + } + + @Test public void testPlaceActivityInTopContainer() { mSplitController.placeActivityInTopContainer(mTransaction, mActivity); @@ -1044,6 +1056,29 @@ public class SplitControllerTest { } @Test + public void testResolveActivityToContainer_skipIfNonTopOrPinned() { + final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity); + final Activity pinnedActivity = createMockActivity(); + final TaskFragmentContainer topContainer = mSplitController.newContainer(pinnedActivity, + TASK_ID); + final TaskContainer taskContainer = container.getTaskContainer(); + spyOn(taskContainer); + doReturn(container).when(taskContainer).getTopNonFinishingTaskFragmentContainer(false); + doReturn(true).when(taskContainer).isTaskFragmentContainerPinned(topContainer); + + // No need to handle when the new launched activity is in a pinned TaskFragment. + assertTrue(mSplitController.resolveActivityToContainer(mTransaction, pinnedActivity, + false /* isOnReparent */)); + verify(mSplitController, never()).shouldExpand(any(), any()); + + // Should proceed to resolve if the new launched activity is in the next top TaskFragment + // (e.g. the top-most TaskFragment is pinned) + mSplitController.resolveActivityToContainer(mTransaction, mActivity, + false /* isOnReparent */); + verify(mSplitController).shouldExpand(any(), any()); + } + + @Test public void testGetPlaceholderOptions() { // Setup to make sure a transaction record is started. mTransactionManager.startNewTransaction(); diff --git a/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml new file mode 100644 index 000000000000..6e4752c9d27d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml @@ -0,0 +1,34 @@ +<?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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> + <path + android:fillColor="@color/compat_controls_background" + android:strokeAlpha="0.8" + android:fillAlpha="0.8" + android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/> + <group + android:translateX="12" + android:translateY="12"> + <path + android:fillColor="@color/compat_controls_text" + android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml new file mode 100644 index 000000000000..141a1ce60b8e --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml @@ -0,0 +1,20 @@ +<?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. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/compat_background_ripple"> + <item android:drawable="@drawable/user_aspect_ratio_settings_button"/> +</ripple>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml index dfaeeeb81c07..257fe1544bbb 100644 --- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml @@ -55,7 +55,7 @@ <include android:id="@+id/size_compat_hint" android:visibility="gone" - android:layout_width="@dimen/size_compat_hint_width" + android:layout_width="@dimen/compat_hint_width" android:layout_height="wrap_content" layout="@layout/compat_mode_hint"/> diff --git a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml new file mode 100644 index 000000000000..433d8546ece0 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml @@ -0,0 +1,41 @@ +<?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. + --> +<com.android.wm.shell.compatui.UserAspectRatioSettingsLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="bottom|end"> + + <include android:id="@+id/user_aspect_ratio_settings_hint" + android:visibility="gone" + android:layout_width="@dimen/compat_hint_width" + android:layout_height="wrap_content" + layout="@layout/compat_mode_hint"/> + + <ImageButton + android:id="@+id/user_aspect_ratio_settings_button" + android:visibility="gone" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/compat_button_margin" + android:layout_marginBottom="@dimen/compat_button_margin" + android:src="@drawable/user_aspect_ratio_settings_button_ripple" + android:background="@android:color/transparent" + android:contentDescription="@string/user_aspect_ratio_settings_button_description"/> + +</com.android.wm.shell.compatui.UserAspectRatioSettingsLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 64fed1cacca9..0502a99d2a45 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -260,8 +260,8 @@ + compat_button_margin - compat_hint_corner_radius - compat_hint_point_width / 2). --> <dimen name="compat_hint_padding_end">7dp</dimen> - <!-- The width of the size compat hint. --> - <dimen name="size_compat_hint_width">188dp</dimen> + <!-- The width of the compat hint. --> + <dimen name="compat_hint_width">188dp</dimen> <!-- The width of the camera compat hint. --> <dimen name="camera_compat_hint_width">143dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index b192fdf245e2..8cbc3d016b01 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -173,7 +173,13 @@ <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> - <string name="restart_button_description">Tap to restart this app for a better view.</string> + <string name="restart_button_description">Tap to restart this app for a better view</string> + + <!-- Tooltip text of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] --> + <string name="user_aspect_ratio_settings_button_hint">Change this app\'s aspect ratio in Settings</string> + + <!-- Content description of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] --> + <string name="user_aspect_ratio_settings_button_description">Change aspect ratio</string> <!-- Description of the camera compat button for applying stretched issues treatment in the hint for compatibility control. [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java index edefe9e3ab06..74a243d34642 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java @@ -371,7 +371,15 @@ class CrossActivityAnimation { @Override public void onBackCancelled() { - mProgressAnimator.onBackCancelled(CrossActivityAnimation.this::finishAnimation); + mProgressAnimator.onBackCancelled(() -> { + // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring, + // and if we release all animation leash first, the leavingProgressSpring won't + // able to update the animation anymore, which cause flicker. + // Here should force update the closing animation target to the final stage before + // release it. + setLeavingProgress(0); + finishAnimation(); + }); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 99c8acdb3116..a70cf001a88b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -25,7 +25,6 @@ import static android.view.View.VISIBLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; -import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED; @@ -37,6 +36,7 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES; import android.annotation.BinderThread; @@ -85,6 +85,7 @@ import androidx.annotation.MainThread; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; @@ -1004,9 +1005,7 @@ public class BubbleController implements ConfigurationChangeListener, } private void onNotificationPanelExpandedChanged(boolean expanded) { - if (DEBUG_BUBBLE_GESTURE) { - Log.d(TAG, "onNotificationPanelExpandedChanged: expanded=" + expanded); - } + ProtoLog.d(WM_SHELL_BUBBLES, "onNotificationPanelExpandedChanged: expanded=%b", expanded); if (mStackView != null && mStackView.isExpanded()) { if (expanded) { mStackView.stopMonitoringSwipeUpGesture(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java index dce6b56261ff..250e010f4d69 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java @@ -47,7 +47,6 @@ public class BubbleDebugConfig { static final boolean DEBUG_USER_EDUCATION = false; static final boolean DEBUG_POSITIONER = false; public static final boolean DEBUG_COLLAPSE_ANIMATOR = false; - static final boolean DEBUG_BUBBLE_GESTURE = false; public static boolean DEBUG_EXPANDED_VIEW_DRAGGING = false; private static final boolean FORCE_SHOW_USER_EDUCATION = false; 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 f58b121ae04f..da5974fe3dc2 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 @@ -21,11 +21,11 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; -import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -75,6 +75,7 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; @@ -2024,9 +2025,7 @@ public class BubbleStackView extends FrameLayout * Monitor for swipe up gesture that is used to collapse expanded view */ void startMonitoringSwipeUpGesture() { - if (DEBUG_BUBBLE_GESTURE) { - Log.d(TAG, "startMonitoringSwipeUpGesture"); - } + ProtoLog.d(WM_SHELL_BUBBLES, "startMonitoringSwipeUpGesture"); stopMonitoringSwipeUpGestureInternal(); if (isGestureNavEnabled()) { @@ -2046,9 +2045,7 @@ public class BubbleStackView extends FrameLayout * Stop monitoring for swipe up gesture */ void stopMonitoringSwipeUpGesture() { - if (DEBUG_BUBBLE_GESTURE) { - Log.d(TAG, "stopMonitoringSwipeUpGesture"); - } + ProtoLog.d(WM_SHELL_BUBBLES, "stopMonitoringSwipeUpGesture"); stopMonitoringSwipeUpGestureInternal(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java index 3a3a378e00d3..137568458e3c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java @@ -18,10 +18,10 @@ package com.android.wm.shell.bubbles; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.content.Context; import android.hardware.input.InputManager; -import android.util.Log; import android.view.Choreographer; import android.view.InputChannel; import android.view.InputEventReceiver; @@ -29,6 +29,7 @@ import android.view.InputMonitor; import androidx.annotation.Nullable; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; /** @@ -58,9 +59,7 @@ class BubblesNavBarGestureTracker { * @param listener listener that is notified of touch events */ void start(MotionEventListener listener) { - if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) { - Log.d(TAG, "start monitoring bubbles swipe up gesture"); - } + ProtoLog.d(WM_SHELL_BUBBLES, "start monitoring bubbles swipe up gesture"); stopInternal(); @@ -76,9 +75,7 @@ class BubblesNavBarGestureTracker { } void stop() { - if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) { - Log.d(TAG, "stop monitoring bubbles swipe up gesture"); - } + ProtoLog.d(WM_SHELL_BUBBLES, "stop monitoring bubbles swipe up gesture"); stopInternal(); } @@ -94,9 +91,7 @@ class BubblesNavBarGestureTracker { } private void onInterceptTouch() { - if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) { - Log.d(TAG, "intercept touch event"); - } + ProtoLog.d(WM_SHELL_BUBBLES, "intercept touch event"); if (mInputMonitor != null) { mInputMonitor.pilferPointers(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java index 844526ca0f35..b7107f09b17f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java @@ -16,19 +16,20 @@ package com.android.wm.shell.bubbles; -import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.content.Context; import android.graphics.PointF; -import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import androidx.annotation.Nullable; +import com.android.internal.protolog.common.ProtoLog; + /** * Handles {@link MotionEvent}s for bubbles that begin in the nav bar area */ @@ -112,10 +113,8 @@ class BubblesNavBarMotionEventHandler { private boolean isInGestureRegion(MotionEvent ev) { // Only handles touch events beginning in navigation bar system gesture zone if (mPositioner.getNavBarGestureZone().contains((int) ev.getX(), (int) ev.getY())) { - if (DEBUG_BUBBLE_GESTURE) { - Log.d(TAG, "handling touch y=" + ev.getY() - + " navBarGestureZone=" + mPositioner.getNavBarGestureZone()); - } + ProtoLog.d(WM_SHELL_BUBBLES, "handling touch x=%d y=%d navBarGestureZone=%s", + (int) ev.getX(), (int) ev.getY(), mPositioner.getNavBarGestureZone()); return true; } return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt index e95e8e5cdaea..1b41f793311d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt @@ -41,9 +41,9 @@ class ManageEducationView constructor(context: Context, positioner: BubblePositi private val ANIMATE_DURATION: Long = 200 private val positioner: BubblePositioner = positioner - private val manageView by lazy { findViewById<ViewGroup>(R.id.manage_education_view) } - private val manageButton by lazy { findViewById<Button>(R.id.manage_button) } - private val gotItButton by lazy { findViewById<Button>(R.id.got_it) } + private val manageView by lazy { requireViewById<ViewGroup>(R.id.manage_education_view) } + private val manageButton by lazy { requireViewById<Button>(R.id.manage_button) } + private val gotItButton by lazy { requireViewById<Button>(R.id.got_it) } private var isHiding = false private var realManageButtonRect = Rect() @@ -122,7 +122,7 @@ class ManageEducationView constructor(context: Context, positioner: BubblePositi manageButton .setOnClickListener { hide() - expandedView.findViewById<View>(R.id.manage_button).performClick() + expandedView.requireViewById<View>(R.id.manage_button).performClick() } gotItButton.setOnClickListener { hide() } setOnClickListener { hide() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt index d0598cd28582..5e3a077a3716 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -48,9 +48,9 @@ class StackEducationView constructor( private val positioner: BubblePositioner = positioner private val controller: BubbleController = controller - private val view by lazy { findViewById<View>(R.id.stack_education_layout) } - private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) } - private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) } + private val view by lazy { requireViewById<View>(R.id.stack_education_layout) } + private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) } + private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) } var isHiding = false private set diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt new file mode 100644 index 000000000000..a141ff951684 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.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.common.pip + +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.PackageManager +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.pip.PipUtils + +class PipAppOpsListener( + private val mContext: Context, + private val mCallback: Callback, + private val mMainExecutor: ShellExecutor +) { + private val mAppOpsManager: AppOpsManager = checkNotNull( + mContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager) + private val mAppOpsChangedListener = AppOpsManager.OnOpChangedListener { _, packageName -> + try { + // Dismiss the PiP once the user disables the app ops setting for that package + val topPipActivityInfo = PipUtils.getTopPipActivity(mContext) + val componentName = topPipActivityInfo.first ?: return@OnOpChangedListener + val userId = topPipActivityInfo.second + val appInfo = mContext.packageManager + .getApplicationInfoAsUser(packageName, 0, userId) + if (appInfo.packageName == componentName.packageName && + mAppOpsManager.checkOpNoThrow( + AppOpsManager.OP_PICTURE_IN_PICTURE, appInfo.uid, + packageName + ) != AppOpsManager.MODE_ALLOWED + ) { + mMainExecutor.execute { mCallback.dismissPip() } + } + } catch (e: PackageManager.NameNotFoundException) { + // Unregister the listener if the package can't be found + unregisterAppOpsListener() + } + } + + fun onActivityPinned(packageName: String) { + // Register for changes to the app ops setting for this package while it is in PiP + registerAppOpsListener(packageName) + } + + fun onActivityUnpinned() { + // Unregister for changes to the previously PiP'ed package + unregisterAppOpsListener() + } + + private fun registerAppOpsListener(packageName: String) { + mAppOpsManager.startWatchingMode( + AppOpsManager.OP_PICTURE_IN_PICTURE, packageName, + mAppOpsChangedListener + ) + } + + private fun unregisterAppOpsListener() { + mAppOpsManager.stopWatchingMode(mAppOpsChangedListener) + } + + /** Callback for PipAppOpsListener to request changes to the PIP window. */ + interface Callback { + /** Dismisses the PIP window. */ + fun dismissPip() + } +}
\ No newline at end of file diff --git a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index a065e2b78d80..1901e0bbe700 100644 --- a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.internal.policy; +package com.android.wm.shell.common.split; import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_LEFT; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 2dbc4445d606..0b0c6937553b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -53,7 +53,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.DividerSnapAlgorithm; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; diff --git a/core/java/com/android/internal/policy/DockedDividerUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DockedDividerUtils.java index b61b9dea2554..f25dfeafb32c 100644 --- a/core/java/com/android/internal/policy/DockedDividerUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DockedDividerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.internal.policy; +package com.android.wm.shell.common.split; import static android.view.WindowManager.DOCKED_BOTTOM; import static android.view.WindowManager.DOCKED_INVALID; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index d3fada37a685..5d7e532d63d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -25,8 +25,8 @@ import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE; -import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; -import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; +import static com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; +import static com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -58,8 +58,6 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.DividerSnapAlgorithm; -import com.android.internal.policy.DockedDividerUtils; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 62b0799618ac..54f89846ac85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -16,12 +16,17 @@ package com.android.wm.shell.compatui; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.hardware.display.DisplayManager; +import android.provider.Settings; import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -41,7 +46,6 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -104,6 +108,13 @@ public class CompatUIController implements OnDisplaysChangedListener, private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>(); /** + * The active user aspect ratio settings button layout if there is one (there can be at most + * one active). + */ + @Nullable + private UserAspectRatioSettingsWindowManager mUserAspectRatioSettingsLayout; + + /** * The active Letterbox Education layout if there is one (there can be at most one active). * * <p>An active layout is a layout that is eligible to be shown for the associated task but @@ -121,38 +132,51 @@ public class CompatUIController implements OnDisplaysChangedListener, /** Avoid creating display context frequently for non-default display. */ private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); + @NonNull private final Context mContext; + @NonNull private final ShellController mShellController; + @NonNull private final DisplayController mDisplayController; + @NonNull private final DisplayInsetsController mDisplayInsetsController; + @NonNull private final DisplayImeController mImeController; + @NonNull private final SyncTransactionQueue mSyncQueue; + @NonNull private final ShellExecutor mMainExecutor; + @NonNull private final Lazy<Transitions> mTransitionsLazy; + @NonNull private final DockStateReader mDockStateReader; + @NonNull private final CompatUIConfiguration mCompatUIConfiguration; // Only show each hint once automatically in the process life. + @NonNull private final CompatUIHintsState mCompatUIHintsState; + @NonNull private final CompatUIShellCommandHandler mCompatUIShellCommandHandler; - private CompatUICallback mCallback; + @Nullable + private CompatUICallback mCompatUICallback; // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't // be shown. private boolean mKeyguardShowing; - public CompatUIController(Context context, - ShellInit shellInit, - ShellController shellController, - DisplayController displayController, - DisplayInsetsController displayInsetsController, - DisplayImeController imeController, - SyncTransactionQueue syncQueue, - ShellExecutor mainExecutor, - Lazy<Transitions> transitionsLazy, - DockStateReader dockStateReader, - CompatUIConfiguration compatUIConfiguration, - CompatUIShellCommandHandler compatUIShellCommandHandler) { + public CompatUIController(@NonNull Context context, + @NonNull ShellInit shellInit, + @NonNull ShellController shellController, + @NonNull DisplayController displayController, + @NonNull DisplayInsetsController displayInsetsController, + @NonNull DisplayImeController imeController, + @NonNull SyncTransactionQueue syncQueue, + @NonNull ShellExecutor mainExecutor, + @NonNull Lazy<Transitions> transitionsLazy, + @NonNull DockStateReader dockStateReader, + @NonNull CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -175,9 +199,9 @@ public class CompatUIController implements OnDisplaysChangedListener, mCompatUIShellCommandHandler.onInit(); } - /** Sets the callback for UI interactions. */ - public void setCompatUICallback(CompatUICallback callback) { - mCallback = callback; + /** Sets the callback for Compat UI interactions. */ + public void setCompatUICallback(@NonNull CompatUICallback compatUiCallback) { + mCompatUICallback = compatUiCallback; } /** @@ -187,7 +211,7 @@ public class CompatUIController implements OnDisplaysChangedListener, * @param taskInfo {@link TaskInfo} task the activity is in. * @param taskListener listener to handle the Task Surface placement. */ - public void onCompatInfoChanged(TaskInfo taskInfo, + public void onCompatInfoChanged(@NonNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener) { if (taskInfo != null && !taskInfo.topActivityInSizeCompat) { mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); @@ -203,6 +227,16 @@ public class CompatUIController implements OnDisplaysChangedListener, createOrUpdateRestartDialogLayout(taskInfo, taskListener); if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { createOrUpdateReachabilityEduLayout(taskInfo, taskListener); + // The user aspect ratio button should not be handled when a new TaskInfo is + // sent because of a double tap or when in multi-window mode. + if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + return; + } + if (!taskInfo.isFromLetterboxDoubleTap) { + createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); + } } } @@ -280,8 +314,8 @@ public class CompatUIController implements OnDisplaysChangedListener, return mDisplaysWithIme.contains(displayId); } - private void createOrUpdateCompatLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateCompatLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId); if (layout != null) { if (layout.needsToBeRecreated(taskInfo, taskListener)) { @@ -314,7 +348,7 @@ public class CompatUIController implements OnDisplaysChangedListener, CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { return new CompatUIWindowManager(context, - taskInfo, mSyncQueue, mCallback, taskListener, + taskInfo, mSyncQueue, mCompatUICallback, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState, mCompatUIConfiguration, this::onRestartButtonClicked); } @@ -328,12 +362,12 @@ public class CompatUIController implements OnDisplaysChangedListener, mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId); onCompatInfoChanged(taskInfoState.first, taskInfoState.second); } else { - mCallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId); + mCompatUICallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId); } } - private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { if (mActiveLetterboxEduLayout != null) { if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) { mActiveLetterboxEduLayout.release(); @@ -342,6 +376,7 @@ public class CompatUIController implements OnDisplaysChangedListener, if (!mActiveLetterboxEduLayout.updateCompatInfo(taskInfo, taskListener, showOnDisplay(mActiveLetterboxEduLayout.getDisplayId()))) { // The layout is no longer eligible to be shown, clear active layout. + mActiveLetterboxEduLayout.release(); mActiveLetterboxEduLayout = null; } return; @@ -371,19 +406,13 @@ public class CompatUIController implements OnDisplaysChangedListener, ShellTaskOrganizer.TaskListener taskListener) { return new LetterboxEduWindowManager(context, taskInfo, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), - mTransitionsLazy.get(), this::onLetterboxEduDismissed, mDockStateReader, - mCompatUIConfiguration); + mTransitionsLazy.get(), + stateInfo -> createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second), + mDockStateReader, mCompatUIConfiguration); } - private void onLetterboxEduDismissed( - Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { - mActiveLetterboxEduLayout = null; - // We need to update the UI - createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second); - } - - private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { RestartDialogWindowManager layout = mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId); if (layout != null) { @@ -428,7 +457,7 @@ public class CompatUIController implements OnDisplaysChangedListener, private void onRestartDialogCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId); - mCallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId); + mCompatUICallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId); } private void onRestartDialogDismissCallback( @@ -437,8 +466,8 @@ public class CompatUIController implements OnDisplaysChangedListener, onCompatInfoChanged(stateInfo.first, stateInfo.second); } - private void createOrUpdateReachabilityEduLayout(TaskInfo taskInfo, - ShellTaskOrganizer.TaskListener taskListener) { + private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { if (mActiveReachabilityEduLayout != null) { if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) { mActiveReachabilityEduLayout.release(); @@ -448,6 +477,7 @@ public class CompatUIController implements OnDisplaysChangedListener, if (!mActiveReachabilityEduLayout.updateCompatInfo(taskInfo, taskListener, showOnDisplay(mActiveReachabilityEduLayout.getDisplayId()))) { // The layout is no longer eligible to be shown, remove from active layouts. + mActiveReachabilityEduLayout.release(); mActiveReachabilityEduLayout = null; } return; @@ -478,14 +508,67 @@ public class CompatUIController implements OnDisplaysChangedListener, ShellTaskOrganizer.TaskListener taskListener) { return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), - mCompatUIConfiguration, mMainExecutor); + mCompatUIConfiguration, mMainExecutor, this::onInitialReachabilityEduDismissed); } + private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo, + @NonNull ShellTaskOrganizer.TaskListener taskListener) { + // We need to update the UI otherwise it will not be shown until the user relaunches the app + createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); + } + + private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { + if (mUserAspectRatioSettingsLayout != null) { + if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + } else { + // UI already exists, update the UI layout. + if (!mUserAspectRatioSettingsLayout.updateCompatInfo(taskInfo, taskListener, + showOnDisplay(mUserAspectRatioSettingsLayout.getDisplayId()))) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + } + return; + } + } + + // Create a new UI layout. + final Context context = getOrCreateDisplayContext(taskInfo.displayId); + if (context == null) { + return; + } + final UserAspectRatioSettingsWindowManager newLayout = + createUserAspectRatioSettingsWindowManager(context, taskInfo, taskListener); + if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { + // The new layout is eligible to be shown, add it the active layouts. + mUserAspectRatioSettingsLayout = newLayout; + } + } + + @VisibleForTesting + @NonNull + UserAspectRatioSettingsWindowManager createUserAspectRatioSettingsWindowManager( + @NonNull Context context, @NonNull TaskInfo taskInfo, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { + return new UserAspectRatioSettingsWindowManager(context, taskInfo, mSyncQueue, + taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), + mCompatUIHintsState, this::launchUserAspectRatioSettings, mMainExecutor); + } + + private void launchUserAspectRatioSettings( + @NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) { + final Intent intent = new Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivity(intent); + } private void removeLayouts(int taskId) { - final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); - if (layout != null) { - layout.release(); + final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId); + if (compatLayout != null) { + compatLayout.release(); mActiveCompatLayouts.remove(taskId); } @@ -506,6 +589,12 @@ public class CompatUIController implements OnDisplaysChangedListener, mActiveReachabilityEduLayout.release(); mActiveReachabilityEduLayout = null; } + + if (mUserAspectRatioSettingsLayout != null + && mUserAspectRatioSettingsLayout.getTaskId() == taskId) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + } } private Context getOrCreateDisplayContext(int displayId) { @@ -561,6 +650,10 @@ public class CompatUIController implements OnDisplaysChangedListener, if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) { callback.accept(mActiveReachabilityEduLayout); } + if (mUserAspectRatioSettingsLayout != null && condition.test( + mUserAspectRatioSettingsLayout)) { + callback.accept(mUserAspectRatioSettingsLayout); + } } /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ @@ -595,4 +688,14 @@ public class CompatUIController implements OnDisplaysChangedListener, insetsChanged(insetsState); } } + + /** + * A class holding the state of the compat UI hints, which is shared between all compat UI + * window managers. + */ + static class CompatUIHintsState { + boolean mHasShownSizeCompatHint; + boolean mHasShownCameraCompatHint; + boolean mHasShownUserAspectRatioSettingsButtonHint; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 065806df3dc8..ce3c5093fdd4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -38,6 +38,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUICallback; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import java.util.function.Consumer; @@ -235,15 +236,4 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN && mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED; } - - /** - * A class holding the state of the compat UI hints, which is shared between all compat UI - * window managers. - */ - static class CompatUIHintsState { - @VisibleForTesting - boolean mHasShownSizeCompatHint; - @VisibleForTesting - boolean mHasShownCameraCompatHint; - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java index 95bb1fe1c986..9de3f9dec34e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -36,6 +36,8 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import java.util.function.BiConsumer; + /** * Window manager for the reachability education */ @@ -73,6 +75,8 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { // we need to animate them. private boolean mHasLetterboxSizeChanged; + private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback; + @Nullable @VisibleForTesting ReachabilityEduLayout mLayout; @@ -80,7 +84,8 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { ReachabilityEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, - CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor) { + CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor, + BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onDismissCallback) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled; mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition; @@ -89,6 +94,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight; mCompatUIConfiguration = compatUIConfiguration; mMainExecutor = mainExecutor; + mOnDismissCallback = onDismissCallback; } @Override @@ -217,13 +223,17 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { return; } final TaskInfo lastTaskInfo = getLastTaskInfo(); + final boolean hasSeenHorizontalReachabilityEdu = + mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo); + final boolean hasSeenVerticalReachabilityEdu = + mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo); final boolean eligibleForDisplayHorizontalEducation = mForceUpdate - || !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo) + || !hasSeenHorizontalReachabilityEdu || (mHasUserDoubleTapped && (mLetterboxHorizontalPosition == REACHABILITY_LEFT_OR_UP_POSITION || mLetterboxHorizontalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION)); final boolean eligibleForDisplayVerticalEducation = mForceUpdate - || !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo) + || !hasSeenVerticalReachabilityEdu || (mHasUserDoubleTapped && (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION || mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION)); @@ -239,6 +249,14 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { if (!mHasLetterboxSizeChanged) { updateHideTime(); mMainExecutor.executeDelayed(this::hideReachability, DISAPPEAR_DELAY_MS); + // If reachability education has been seen for the first time, trigger callback to + // display aspect ratio settings button once reachability education disappears + if (hasShownHorizontalReachabilityEduFirstTime(hasSeenHorizontalReachabilityEdu) + || hasShownVerticalReachabilityEduFirstTime( + hasSeenVerticalReachabilityEdu)) { + mMainExecutor.executeDelayed(this::triggerOnDismissCallback, + DISAPPEAR_DELAY_MS); + } } mHasUserDoubleTapped = false; } else { @@ -246,6 +264,38 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { } } + /** + * Compares the value of + * {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} before and after the + * layout is shown. Horizontal reachability education is considered seen for the first time if + * prior to viewing the layout, + * {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} is {@code false} + * but becomes {@code true} once the current layout is shown. + */ + private boolean hasShownHorizontalReachabilityEduFirstTime( + boolean previouslyShownHorizontalReachabilityEducation) { + return !previouslyShownHorizontalReachabilityEducation + && mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(getLastTaskInfo()); + } + + /** + * Compares the value of + * {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} before and after the + * layout is shown. Horizontal reachability education is considered seen for the first time if + * prior to viewing the layout, + * {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} is {@code false} + * but becomes {@code true} once the current layout is shown. + */ + private boolean hasShownVerticalReachabilityEduFirstTime( + boolean previouslyShownVerticalReachabilityEducation) { + return !previouslyShownVerticalReachabilityEducation + && mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(getLastTaskInfo()); + } + + private void triggerOnDismissCallback() { + mOnDismissCallback.accept(getLastTaskInfo(), getTaskListener()); + } + private void hideReachability() { if (mLayout == null || !shouldHideEducation()) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java new file mode 100644 index 000000000000..5eeb3b650074 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java @@ -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.compatui; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.IdRes; +import android.annotation.NonNull; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.wm.shell.R; + +/** + * Layout for the user aspect ratio button which opens the app list page in settings + * and allows users to change apps aspect ratio. + */ +public class UserAspectRatioSettingsLayout extends LinearLayout { + + private static final float ALPHA_FULL_TRANSPARENT = 0f; + + private static final float ALPHA_FULL_OPAQUE = 1f; + + private static final long VISIBILITY_ANIMATION_DURATION_MS = 50; + + private static final String ALPHA_PROPERTY_NAME = "alpha"; + + private UserAspectRatioSettingsWindowManager mWindowManager; + + public UserAspectRatioSettingsLayout(Context context) { + this(context, null); + } + + public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + void inject(@NonNull UserAspectRatioSettingsWindowManager windowManager) { + mWindowManager = windowManager; + } + + void setUserAspectRatioSettingsHintVisibility(boolean show) { + setViewVisibility(R.id.user_aspect_ratio_settings_hint, show); + } + + void setUserAspectRatioButtonVisibility(boolean show) { + setViewVisibility(R.id.user_aspect_ratio_settings_button, show); + // Hint should never be visible without button. + if (!show) { + setUserAspectRatioSettingsHintVisibility(/* show= */ false); + } + } + + private void setViewVisibility(@IdRes int resId, boolean show) { + final View view = findViewById(resId); + int visibility = show ? View.VISIBLE : View.GONE; + if (view.getVisibility() == visibility) { + return; + } + if (show) { + showItem(view); + } else { + view.setVisibility(visibility); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + // Need to relayout after changes like hiding / showing a hint since they affect size. + // Doing this directly in setUserAspectRatioButtonVisibility can result in flaky animation. + mWindowManager.relayout(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + final ImageButton userAspectRatioButton = + findViewById(R.id.user_aspect_ratio_settings_button); + userAspectRatioButton.setOnClickListener( + view -> mWindowManager.onUserAspectRatioSettingsButtonClicked()); + userAspectRatioButton.setOnLongClickListener(view -> { + mWindowManager.onUserAspectRatioSettingsButtonLongClicked(); + return true; + }); + + final LinearLayout sizeCompatHint = findViewById(R.id.user_aspect_ratio_settings_hint); + ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text)) + .setText(R.string.user_aspect_ratio_settings_button_hint); + sizeCompatHint.setOnClickListener( + view -> setUserAspectRatioSettingsHintVisibility(/* show= */ false)); + } + + private void showItem(@NonNull View view) { + view.setVisibility(View.VISIBLE); + final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME, + ALPHA_FULL_TRANSPARENT, ALPHA_FULL_OPAQUE); + fadeIn.setDuration(VISIBILITY_ANIMATION_DURATION_MS); + fadeIn.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.VISIBLE); + } + }); + fadeIn.start(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java new file mode 100644 index 000000000000..bd53dc7390c8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java @@ -0,0 +1,211 @@ +/* + * 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.compatui; + +import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; + +import java.util.function.BiConsumer; + +/** + * Window manager for the user aspect ratio settings button which allows users to go to + * app settings and change apps aspect ratio. + */ +class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract { + + private static final long SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 500L; + + private static final long HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 4000L; + + private long mNextButtonHideTimeMs = -1L; + + private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnButtonClicked; + + private final ShellExecutor mShellExecutor; + + @VisibleForTesting + @NonNull + final CompatUIHintsState mCompatUIHintsState; + + @Nullable + private UserAspectRatioSettingsLayout mLayout; + + // Remember the last reported states in case visibility changes due to keyguard or IME updates. + @VisibleForTesting + boolean mHasUserAspectRatioSettingsButton; + + UserAspectRatioSettingsWindowManager(@NonNull Context context, @NonNull TaskInfo taskInfo, + @NonNull SyncTransactionQueue syncQueue, + @Nullable ShellTaskOrganizer.TaskListener taskListener, + @NonNull DisplayLayout displayLayout, @NonNull CompatUIHintsState compatUIHintsState, + @NonNull BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onButtonClicked, + @NonNull ShellExecutor shellExecutor) { + super(context, taskInfo, syncQueue, taskListener, displayLayout); + mShellExecutor = shellExecutor; + mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo); + mCompatUIHintsState = compatUIHintsState; + mOnButtonClicked = onButtonClicked; + } + + @Override + protected int getZOrder() { + return TASK_CHILD_LAYER_COMPAT_UI + 1; + } + + @Override + protected @Nullable View getLayout() { + return mLayout; + } + + @Override + protected void removeLayout() { + mLayout = null; + } + + @Override + protected boolean eligibleToShowLayout() { + return mHasUserAspectRatioSettingsButton; + } + + @Override + protected View createLayout() { + mLayout = inflateLayout(); + mLayout.inject(this); + + updateVisibilityOfViews(); + + return mLayout; + } + + @VisibleForTesting + UserAspectRatioSettingsLayout inflateLayout() { + return (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate( + R.layout.user_aspect_ratio_settings_layout, null); + } + + @Override + public boolean updateCompatInfo(@NonNull TaskInfo taskInfo, + @NonNull ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { + final boolean prevHasUserAspectRatioSettingsButton = mHasUserAspectRatioSettingsButton; + mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo); + + if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { + return false; + } + + if (prevHasUserAspectRatioSettingsButton != mHasUserAspectRatioSettingsButton) { + updateVisibilityOfViews(); + } + return true; + } + + /** Called when the user aspect ratio settings button is clicked. */ + void onUserAspectRatioSettingsButtonClicked() { + mOnButtonClicked.accept(getLastTaskInfo(), getTaskListener()); + } + + /** Called when the user aspect ratio settings button is long clicked. */ + void onUserAspectRatioSettingsButtonLongClicked() { + if (mLayout == null) { + return; + } + mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true); + mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + mShellExecutor.executeDelayed(this::hideUserAspectRatioButton, + HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + } + + @Override + @VisibleForTesting + public void updateSurfacePosition() { + if (mLayout == null) { + return; + } + // Position of the button in the container coordinate. + final Rect taskBounds = getTaskBounds(); + final Rect taskStableBounds = getTaskStableBounds(); + final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? taskStableBounds.left - taskBounds.left + : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth(); + final int positionY = taskStableBounds.bottom - taskBounds.top + - mLayout.getMeasuredHeight(); + updateSurfacePosition(positionX, positionY); + } + + @VisibleForTesting + void updateVisibilityOfViews() { + if (mHasUserAspectRatioSettingsButton) { + mShellExecutor.executeDelayed(this::showUserAspectRatioButton, + SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + mShellExecutor.executeDelayed(this::hideUserAspectRatioButton, + HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS); + } else { + mShellExecutor.removeCallbacks(this::showUserAspectRatioButton); + mShellExecutor.execute(this::hideUserAspectRatioButton); + } + } + + private void showUserAspectRatioButton() { + if (mLayout == null) { + return; + } + mLayout.setUserAspectRatioButtonVisibility(true); + // Only show by default for the first time. + if (!mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint) { + mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true); + mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true; + } + } + + private void hideUserAspectRatioButton() { + if (mLayout == null || !isHideDelayReached(mNextButtonHideTimeMs)) { + return; + } + mLayout.setUserAspectRatioButtonVisibility(false); + } + + private boolean isHideDelayReached(long nextHideTime) { + return SystemClock.uptimeMillis() >= nextHideTime; + } + + private long updateHideTime(long hideDelay) { + return SystemClock.uptimeMillis() + hideDelay; + } + + private boolean getHasUserAspectRatioSettingsButton(@NonNull TaskInfo taskInfo) { + return taskInfo.topActivityEligibleForUserAspectRatioButton + && (taskInfo.topActivityBoundsLetterboxed + || taskInfo.isUserFullscreenOverrideEnabled); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 422e3b0216c8..1a84f4b41b57 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -44,6 +44,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; +import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -73,7 +74,6 @@ import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedController; -import com.android.wm.shell.pip.Pip; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; @@ -122,6 +122,12 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static FloatingContentCoordinator provideFloatingContentCoordinator() { + return new FloatingContentCoordinator(); + } + + @WMSingleton + @Provides static DisplayController provideDisplayController(Context context, IWindowManager wmService, ShellInit shellInit, @@ -797,7 +803,6 @@ public abstract class WMShellBaseModule { ShellTaskOrganizer shellTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, - Optional<Pip> pipOptional, FullscreenTaskListener fullscreenTaskListener, Optional<UnfoldAnimationController> unfoldAnimationController, Optional<UnfoldTransitionHandler> unfoldTransitionHandler, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 881c8f5552b6..065e7b09a05d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -325,12 +325,13 @@ public abstract class WMShellModule { Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel, + Optional<DesktopTasksController> desktopTasksController, @ShellMainThread ShellExecutor mainExecutor) { return new SplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, - windowDecorViewModel, mainExecutor); + windowDecorViewModel, desktopTasksController, mainExecutor); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 54be90197e47..9bf973f523bf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -31,13 +31,13 @@ import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; +import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipDisplayLayoutState; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java index f29b3a35128d..e8fae2490bc5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java @@ -21,7 +21,6 @@ import android.content.pm.PackageManager; import android.os.Handler; import com.android.internal.logging.UiEventLogger; -import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.pip.PipMediaController; @@ -37,12 +36,6 @@ import dagger.Provides; */ @Module public abstract class Pip1SharedModule { - @WMSingleton - @Provides - static FloatingContentCoordinator provideFloatingContentCoordinator() { - return new FloatingContentCoordinator(); - } - // Needs handler for registering broadcast receivers @WMSingleton @Provides diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java index 52c6d2008a9d..80ffbb0968f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java @@ -29,12 +29,12 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.LegacySizeSpecSource; +import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index db6c258e84c2..5b24d7a60c4e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -535,6 +535,11 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll } @Override + public void onDesktopSplitSelectAnimComplete(RunningTaskInfo taskInfo) { + + } + + @Override public void stashDesktopApps(int displayId) throws RemoteException { // Stashing of desktop apps not needed. Apps always launch on desktop } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index b15fd912e32d..1d46e755ec9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.Context @@ -33,7 +34,6 @@ import android.os.IBinder import android.os.SystemProperties import android.util.DisplayMetrics.DENSITY_DEFAULT import android.view.SurfaceControl -import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN @@ -56,6 +56,7 @@ import com.android.wm.shell.common.annotations.ExternalThread import com.android.wm.shell.common.annotations.ShellMainThread import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit @@ -105,6 +106,9 @@ class DesktopTasksController( get() = context.resources.getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_transition_area_height) + // This is public to avoid cyclic dependency; it is set by SplitScreenController + lateinit var splitScreenController: SplitScreenController + init { desktopMode = DesktopModeImpl() if (DesktopModeStatus.isProto2Enabled()) { @@ -262,6 +266,19 @@ class DesktopTasksController( } } + /** + * Perform needed cleanup transaction once animation is complete. Bounds need to be set + * here instead of initial wct to both avoid flicker and to have task bounds to use for + * the staging animation. + * + * @param taskInfo task entering split that requires a bounds update + */ + fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { + val wct = WindowContainerTransaction() + wct.setBounds(taskInfo.token, Rect()) + shellTaskOrganizer.applyTransaction(wct) + } + /** Move a task with given `taskId` to fullscreen */ fun moveToFullscreen(taskId: Int) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) } @@ -296,7 +313,7 @@ class DesktopTasksController( task.taskId ) val wct = WindowContainerTransaction() - wct.setBounds(task.token, null) + wct.setBounds(task.token, Rect()) if (Transitions.ENABLE_SHELL_TRANSITIONS) { enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(wct, @@ -533,6 +550,7 @@ class DesktopTasksController( ) // Check if we should skip handling this transition var reason = "" + val triggerTask = request.triggerTask val shouldHandleRequest = when { // Only handle open or to front transitions @@ -541,19 +559,19 @@ class DesktopTasksController( false } // Only handle when it is a task transition - request.triggerTask == null -> { + triggerTask == null -> { reason = "triggerTask is null" false } // Only handle standard type tasks - request.triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> { - reason = "activityType not handled (${request.triggerTask.activityType})" + triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> { + reason = "activityType not handled (${triggerTask.activityType})" false } // Only handle fullscreen or freeform tasks - request.triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN && - request.triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> { - reason = "windowingMode not handled (${request.triggerTask.windowingMode})" + triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN && + triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> { + reason = "windowingMode not handled (${triggerTask.windowingMode})" false } // Otherwise process it @@ -569,17 +587,17 @@ class DesktopTasksController( return null } - val task: RunningTaskInfo = request.triggerTask - - val result = when { - // If display has tasks stashed, handle as stashed launch - desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) - // Check if fullscreen task should be updated - task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task) - // Check if freeform task should be updated - task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) - else -> { - null + val result = triggerTask?.let { task -> + when { + // If display has tasks stashed, handle as stashed launch + desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) + // Check if fullscreen task should be updated + task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task) + // Check if freeform task should be updated + task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) + else -> { + null + } } } KtProtoLog.v( @@ -686,13 +704,43 @@ class DesktopTasksController( WINDOWING_MODE_FULLSCREEN } wct.setWindowingMode(taskInfo.token, targetWindowingMode) - wct.setBounds(taskInfo.token, null) + wct.setBounds(taskInfo.token, Rect()) if (isDesktopDensityOverrideSet()) { - wct.setDensityDpi(taskInfo.token, getFullscreenDensityDpi()) + wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } } - private fun getFullscreenDensityDpi(): Int { + /** + * Adds split screen changes to a transaction. Note that bounds are not reset here due to + * animation; see {@link onDesktopSplitSelectAnimComplete} + */ + private fun addMoveToSplitChanges( + wct: WindowContainerTransaction, + taskInfo: RunningTaskInfo + ) { + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) + // The task's density may have been overridden in freeform; revert it here as we don't + // want it overridden in multi-window. + wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) + } + + /** + * Requests a task be transitioned from desktop to split select. Applies needed windowing + * changes if this transition is enabled. + */ + fun requestSplit( + taskInfo: RunningTaskInfo + ) { + val windowingMode = taskInfo.windowingMode + if (windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM + ) { + val wct = WindowContainerTransaction() + addMoveToSplitChanges(wct, taskInfo) + splitScreenController.requestEnterSplitSelect(taskInfo, wct) + } + } + + private fun getDefaultDensityDpi(): Int { return context.resources.displayMetrics.densityDpi } @@ -969,6 +1017,13 @@ class DesktopTasksController( return result[0] } + override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "onDesktopSplitSelectAnimComplete" + ) { c -> c.onDesktopSplitSelectAnimComplete(taskInfo) } + } + override fun setTaskListener(listener: IDesktopTaskListener?) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index ee3a080e7318..47edfd455f5a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode; +import android.app.ActivityManager.RunningTaskInfo; import com.android.wm.shell.desktopmode.IDesktopTaskListener; /** @@ -38,6 +39,9 @@ interface IDesktopMode { /** Get count of visible desktop tasks on the given display */ int getVisibleTaskCount(int displayId); + /** Perform cleanup transactions after the animation to split select is complete */ + oneway void onDesktopSplitSelectAnimComplete(in RunningTaskInfo taskInfo); + /** Set listener that will receive callbacks about updates to desktop tasks */ oneway void setTaskListener(IDesktopTaskListener listener); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index b9cb5c75aaf0..9debb25ff296 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -69,7 +69,7 @@ class ToggleResizeDesktopTaskTransitionHandler( ): Boolean { val change = findRelevantChange(info) val leash = change.leash - val taskId = change.taskInfo.taskId + val taskId = checkNotNull(change.taskInfo).taskId val startBounds = change.startAbsBounds val endBounds = change.endAbsBounds val windowDecor = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java deleted file mode 100644 index 48a3fc2460a2..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java +++ /dev/null @@ -1,94 +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 com.android.wm.shell.pip; - -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE; - -import android.app.AppOpsManager; -import android.app.AppOpsManager.OnOpChangedListener; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.util.Pair; - -import com.android.wm.shell.common.ShellExecutor; - -public class PipAppOpsListener { - private static final String TAG = PipAppOpsListener.class.getSimpleName(); - - private Context mContext; - private ShellExecutor mMainExecutor; - private AppOpsManager mAppOpsManager; - private Callback mCallback; - - private AppOpsManager.OnOpChangedListener mAppOpsChangedListener = new OnOpChangedListener() { - @Override - public void onOpChanged(String op, String packageName) { - try { - // Dismiss the PiP once the user disables the app ops setting for that package - final Pair<ComponentName, Integer> topPipActivityInfo = - PipUtils.getTopPipActivity(mContext); - if (topPipActivityInfo.first != null) { - final ApplicationInfo appInfo = mContext.getPackageManager() - .getApplicationInfoAsUser(packageName, 0, topPipActivityInfo.second); - if (appInfo.packageName.equals(topPipActivityInfo.first.getPackageName()) && - mAppOpsManager.checkOpNoThrow(OP_PICTURE_IN_PICTURE, appInfo.uid, - packageName) != MODE_ALLOWED) { - mMainExecutor.execute(() -> mCallback.dismissPip()); - } - } - } catch (NameNotFoundException e) { - // Unregister the listener if the package can't be found - unregisterAppOpsListener(); - } - } - }; - - public PipAppOpsListener(Context context, Callback callback, ShellExecutor mainExecutor) { - mContext = context; - mMainExecutor = mainExecutor; - mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - mCallback = callback; - } - - public void onActivityPinned(String packageName) { - // Register for changes to the app ops setting for this package while it is in PiP - registerAppOpsListener(packageName); - } - - public void onActivityUnpinned() { - // Unregister for changes to the previously PiP'ed package - unregisterAppOpsListener(); - } - - private void registerAppOpsListener(String packageName) { - mAppOpsManager.startWatchingMode(OP_PICTURE_IN_PICTURE, packageName, - mAppOpsChangedListener); - } - - private void unregisterAppOpsListener() { - mAppOpsManager.stopWatchingMode(mAppOpsChangedListener); - } - - /** Callback for PipAppOpsListener to request changes to the PIP window. */ - public interface Callback { - /** Dismisses the PIP window. */ - void dismissPip(); - } -} 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 f396f3fedf64..b872267947dc 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 @@ -75,6 +75,7 @@ import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.IPip; @@ -82,7 +83,6 @@ import com.android.wm.shell.pip.IPipAnimationListener; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipDisplayLayoutState; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index 43d3f36f1fe5..b251f6f55794 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -41,7 +41,7 @@ import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; -import com.android.wm.shell.pip.PipAppOpsListener; +import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 02eeb2ac4fd5..5e583c20061f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -47,10 +47,10 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 3af1b75f33b6..05e4af3302af 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -55,6 +55,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_BUBBLES(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + "Bubbles"), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index c414e708b28d..14304a3c0aac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -27,6 +27,7 @@ import android.view.RemoteAnimationTarget; import android.window.RemoteTransition; import com.android.wm.shell.splitscreen.ISplitScreenListener; +import com.android.wm.shell.splitscreen.ISplitSelectListener; /** * Interface that is exposed to remote callers to manipulate the splitscreen feature. @@ -44,6 +45,16 @@ interface ISplitScreen { oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2; /** + * Registers a split select listener. + */ + oneway void registerSplitSelectListener(in ISplitSelectListener listener) = 20; + + /** + * Unregisters a split select listener. + */ + oneway void unregisterSplitSelectListener(in ISplitSelectListener listener) = 21; + + /** * Removes a task from the side stage. */ oneway void removeFromSideStage(int taskId) = 4; @@ -148,4 +159,4 @@ interface ISplitScreen { */ RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; } -// Last id = 19
\ No newline at end of file +// Last id = 21
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitSelectListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitSelectListener.aidl new file mode 100644 index 000000000000..7171da5d885d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitSelectListener.aidl @@ -0,0 +1,29 @@ +/* + * 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.wm.shell.splitscreen; + +import android.app.ActivityManager.RunningTaskInfo; + +/** + * Listener interface that Launcher attaches to SystemUI to get split-select callbacks. + */ +interface ISplitSelectListener { + /** + * Called when a task requests to enter split select + */ + boolean onRequestSplitSelect(in RunningTaskInfo taskInfo); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 2f2bc77b804b..f20fe0b88e12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -18,6 +18,7 @@ package com.android.wm.shell.splitscreen; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.ActivityManager; import android.graphics.Rect; import com.android.wm.shell.common.annotations.ExternalThread; @@ -63,6 +64,13 @@ public interface SplitScreen { default void onSplitVisibilityChanged(boolean visible) {} } + /** Callback interface for listening to requests to enter split select */ + interface SplitSelectListener { + default boolean onRequestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo) { + return false; + } + } + /** Registers listener that gets split screen callback. */ void registerSplitScreenListener(@NonNull SplitScreenListener listener, @NonNull Executor executor); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 5fa26542ee07..210bf68f3d4f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -87,6 +87,7 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.common.split.SplitScreenUtils; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.DragAndDropPolicy; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -104,6 +105,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Optional; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; /** * Class manages split-screen multitasking mode and implements the main interface @@ -177,6 +179,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final Optional<RecentTasksController> mRecentTasksOptional; private final LaunchAdjacentController mLaunchAdjacentController; private final Optional<WindowDecorViewModel> mWindowDecorViewModel; + private final Optional<DesktopTasksController> mDesktopTasksController; private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler; private final String[] mAppsSupportMultiInstances; @@ -205,6 +208,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel, + Optional<DesktopTasksController> desktopTasksController, ShellExecutor mainExecutor) { mShellCommandHandler = shellCommandHandler; mShellController = shellController; @@ -223,6 +227,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mRecentTasksOptional = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; + mDesktopTasksController = desktopTasksController; mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic // override for this controller from the base module @@ -254,6 +259,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, RecentTasksController recentTasks, LaunchAdjacentController launchAdjacentController, WindowDecorViewModel windowDecorViewModel, + DesktopTasksController desktopTasksController, ShellExecutor mainExecutor, StageCoordinator stageCoordinator) { mShellCommandHandler = shellCommandHandler; @@ -273,6 +279,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mRecentTasksOptional = Optional.of(recentTasks); mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = Optional.of(windowDecorViewModel); + mDesktopTasksController = Optional.of(desktopTasksController); mStageCoordinator = stageCoordinator; mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); shellInit.addInitCallback(this::onInit, this); @@ -306,6 +313,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } mDragAndDropController.ifPresent(controller -> controller.setSplitScreenController(this)); mWindowDecorViewModel.ifPresent(viewModel -> viewModel.setSplitScreenController(this)); + mDesktopTasksController.ifPresent(controller -> controller.setSplitScreenController(this)); } protected StageCoordinator createStageCoordinator() { @@ -468,6 +476,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.unregisterSplitScreenListener(listener); } + /** Register a split select listener */ + public void registerSplitSelectListener(SplitScreen.SplitSelectListener listener) { + mStageCoordinator.registerSplitSelectListener(listener); + } + + /** Unregister a split select listener */ + public void unregisterSplitSelectListener(SplitScreen.SplitSelectListener listener) { + mStageCoordinator.unregisterSplitSelectListener(listener); + } + public void goToFullscreenFromSplit() { mStageCoordinator.goToFullscreenFromSplit(); } @@ -485,6 +503,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mStageCoordinator.getActivateSplitPosition(taskInfo); } + /** + * Move a task to split select + * @param taskInfo the task being moved to split select + * @param wct transaction to apply if this is a valid request + */ + public void requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, + WindowContainerTransaction wct) { + mStageCoordinator.requestEnterSplitSelect(taskInfo, wct); + } + public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { final int[] result = new int[1]; IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { @@ -1088,6 +1116,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private SplitScreenController mController; private final SingleInstanceRemoteListener<SplitScreenController, ISplitScreenListener> mListener; + private final SingleInstanceRemoteListener<SplitScreenController, + ISplitSelectListener> mSelectListener; private final SplitScreen.SplitScreenListener mSplitScreenListener = new SplitScreen.SplitScreenListener() { @Override @@ -1101,11 +1131,25 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } }; + private final SplitScreen.SplitSelectListener mSplitSelectListener = + new SplitScreen.SplitSelectListener() { + @Override + public boolean onRequestEnterSplitSelect( + ActivityManager.RunningTaskInfo taskInfo) { + AtomicBoolean result = new AtomicBoolean(false); + mSelectListener.call(l -> result.set(l.onRequestSplitSelect(taskInfo))); + return result.get(); + } + }; + public ISplitScreenImpl(SplitScreenController controller) { mController = controller; mListener = new SingleInstanceRemoteListener<>(controller, c -> c.registerSplitScreenListener(mSplitScreenListener), c -> c.unregisterSplitScreenListener(mSplitScreenListener)); + mSelectListener = new SingleInstanceRemoteListener<>(controller, + c -> c.registerSplitSelectListener(mSplitSelectListener), + c -> c.unregisterSplitSelectListener(mSplitSelectListener)); } /** @@ -1131,6 +1175,18 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void registerSplitSelectListener(ISplitSelectListener listener) { + executeRemoteCallWithTaskPermission(mController, "registerSplitSelectListener", + (controller) -> mSelectListener.register(listener)); + } + + @Override + public void unregisterSplitSelectListener(ISplitSelectListener listener) { + executeRemoteCallWithTaskPermission(mController, "unregisterSplitSelectListener", + (controller) -> mSelectListener.unregister()); + } + + @Override public void exitSplitScreen(int toTopTaskId) { executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", (controller) -> controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 99be5b8ee861..7dec12aac39b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -324,8 +324,10 @@ class SplitScreenTransitions { void startFullscreenTransition(WindowContainerTransaction wct, @Nullable RemoteTransition handler) { - mTransitions.startTransition(TRANSIT_OPEN, wct, - new OneShotRemoteHandler(mTransitions.getMainExecutor(), handler)); + OneShotRemoteHandler fullscreenHandler = + new OneShotRemoteHandler(mTransitions.getMainExecutor(), handler); + fullscreenHandler.setTransition(mTransitions + .startTransition(TRANSIT_OPEN, wct, fullscreenHandler)); } 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 3758b6890f48..697006868e5a 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 @@ -144,8 +144,10 @@ import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; /** * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and @@ -185,6 +187,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); + private final Set<SplitScreen.SplitSelectListener> mSelectListeners = new HashSet<>(); private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; @@ -462,6 +465,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mLogger; } + void requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, + WindowContainerTransaction wct) { + boolean enteredSplitSelect = false; + for (SplitScreen.SplitSelectListener listener : mSelectListeners) { + enteredSplitSelect |= listener.onRequestEnterSplitSelect(taskInfo); + } + if (enteredSplitSelect) mTaskOrganizer.applyTransaction(wct); + } + void startShortcut(String packageName, String shortcutId, @SplitPosition int position, Bundle options, UserHandle user) { final boolean isEnteringSplit = !isSplitActive(); @@ -1657,6 +1669,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mListeners.remove(listener); } + void registerSplitSelectListener(SplitScreen.SplitSelectListener listener) { + mSelectListeners.add(listener); + } + + void unregisterSplitSelectListener(SplitScreen.SplitSelectListener listener) { + mSelectListeners.remove(listener); + } + void sendStatusToListener(SplitScreen.SplitScreenListener listener) { listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java index a2301b133426..c10142588bde 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java @@ -87,7 +87,7 @@ public class TvSplitScreenController extends SplitScreenController { syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, Optional.empty(), - mainExecutor); + Optional.empty(), mainExecutor); mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index dc91a11dc64f..29be34347b22 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -385,8 +385,8 @@ public class SplashscreenContentDrawer { private static int estimateWindowBGColor(Drawable themeBGDrawable) { final DrawableColorTester themeBGTester = new DrawableColorTester( themeBGDrawable, DrawableColorTester.TRANSLUCENT_FILTER /* filterType */); - if (themeBGTester.passFilterRatio() != 1) { - // the window background is translucent, unable to draw + if (themeBGTester.passFilterRatio() < 0.5f) { + // more than half pixels of the window background is translucent, unable to draw Slog.w(TAG, "Window background is translucent, fill background with black color"); return getSystemBGColor(); } else { 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 2b19da2498a6..2be7a491fdf2 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 @@ -365,6 +365,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false)); mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId)); decoration.closeHandleMenu(); + } else if (id == R.id.split_screen_button) { + decoration.closeHandleMenu(); + mDesktopTasksController.ifPresent(c -> { + c.requestSplit(decoration.mTaskInfo); + }); } else if (id == R.id.collapse_menu_button) { decoration.closeHandleMenu(); } else if (id == R.id.select_button) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt index 672e57aab5ad..a9eb8829bd2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt @@ -23,14 +23,14 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( appIcon: Drawable ) : DesktopModeWindowDecorationViewHolder(rootView) { - private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption) - private val captionHandle: View = rootView.findViewById(R.id.caption_handle) - private val openMenuButton: View = rootView.findViewById(R.id.open_menu_button) - private val closeWindowButton: ImageButton = rootView.findViewById(R.id.close_window) - private val expandMenuButton: ImageButton = rootView.findViewById(R.id.expand_menu_button) - private val maximizeWindowButton: ImageButton = rootView.findViewById(R.id.maximize_window) - private val appNameTextView: TextView = rootView.findViewById(R.id.application_name) - private val appIconImageView: ImageView = rootView.findViewById(R.id.application_icon) + private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) + private val captionHandle: View = rootView.requireViewById(R.id.caption_handle) + private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button) + private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window) + private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button) + private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window) + private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name) + private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon) init { captionView.setOnTouchListener(onCaptionTouchListener) @@ -47,7 +47,9 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( override fun bindData(taskInfo: RunningTaskInfo) { val captionDrawable = captionView.background as GradientDrawable - captionDrawable.setColor(taskInfo.taskDescription.statusBarColor) + taskInfo.taskDescription?.statusBarColor?.let { + captionDrawable.setColor(it) + } closeWindowButton.imageTintList = ColorStateList.valueOf( getCaptionCloseButtonColor(taskInfo)) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt index 47a12a0cb71c..9374ac95e83d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt @@ -17,8 +17,8 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( onCaptionButtonClickListener: View.OnClickListener ) : DesktopModeWindowDecorationViewHolder(rootView) { - private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption) - private val captionHandle: ImageButton = rootView.findViewById(R.id.caption_handle) + private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) + private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) init { captionView.setOnTouchListener(onCaptionTouchListener) @@ -27,9 +27,10 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( } override fun bindData(taskInfo: RunningTaskInfo) { - val captionColor = taskInfo.taskDescription.statusBarColor - val captionDrawable = captionView.background as GradientDrawable - captionDrawable.setColor(captionColor) + taskInfo.taskDescription?.statusBarColor?.let { captionColor -> + val captionDrawable = captionView.background as GradientDrawable + captionDrawable.setColor(captionColor) + } captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt index d293cf73a0f7..49e8d15dcc02 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt @@ -25,11 +25,14 @@ internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { * with the caption background color. */ protected fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean { - return if (Color.alpha(taskInfo.taskDescription.statusBarColor) != 0 && - taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { - Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5 - } else { - taskInfo.taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 - } + return taskInfo.taskDescription + ?.let { taskDescription -> + if (Color.alpha(taskDescription.statusBarColor) != 0 && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { + Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5 + } else { + taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 + } + } ?: false } } diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index ad4d97f6fe40..c2f184a03380 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -43,6 +43,7 @@ android_test { "frameworks-base-testutils", "kotlinx-coroutines-android", "kotlinx-coroutines-core", + "mockito-kotlin2", "mockito-target-extended-minus-junit4", "truth-prebuilt", "testables", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt index 0e05e01a8da3..e35995775f76 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt @@ -29,11 +29,11 @@ import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before import org.junit.Test -import org.mockito.Mockito -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify class BubbleDataRepositoryTest : ShellTestCase() { @@ -124,7 +124,7 @@ class BubbleDataRepositoryTest : ShellTestCase() { private val testHandler = Handler(Looper.getMainLooper()) private val mainExecutor = HandlerExecutor(testHandler) - private val launcherApps = mock(LauncherApps::class.java) + private val launcherApps = mock<LauncherApps>() private val persistedBubbles = SparseArray<List<BubbleEntity>>() @@ -158,8 +158,7 @@ class BubbleDataRepositoryTest : ShellTestCase() { assertThat(persistedBubbles).isEqualTo(validEntitiesByUser) // No invalid users, so no persist to disk happened - verify(dataRepository, never()).persistToDisk( - any(SparseArray<List<BubbleEntity>>()::class.java)) + verify(dataRepository, never()).persistToDisk(any()) } @Test @@ -199,6 +198,4 @@ class BubbleDataRepositoryTest : ShellTestCase() { // Verify that persist to disk happened with the new valid entities list. verify(dataRepository).persistToDisk(validEntitiesByUser) } - - fun <T> any(type: Class<T>): T = Mockito.any<T>(type) }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 443cea245a4f..fe2da5dd19fc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -38,7 +38,6 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.internal.policy.DividerSnapAlgorithm; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index a6501f05475f..efc69ebd395c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -58,6 +58,8 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,8 +68,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import dagger.Lazy; - /** * Tests for {@link CompatUIController}. * @@ -82,21 +82,36 @@ public class CompatUIControllerTest extends ShellTestCase { private CompatUIController mController; private ShellInit mShellInit; - private @Mock ShellController mMockShellController; - private @Mock DisplayController mMockDisplayController; - private @Mock DisplayInsetsController mMockDisplayInsetsController; - private @Mock DisplayLayout mMockDisplayLayout; - private @Mock DisplayImeController mMockImeController; - private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; - private @Mock SyncTransactionQueue mMockSyncQueue; - private @Mock ShellExecutor mMockExecutor; - private @Mock Lazy<Transitions> mMockTransitionsLazy; - private @Mock CompatUIWindowManager mMockCompatLayout; - private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout; - private @Mock RestartDialogWindowManager mMockRestartDialogLayout; - private @Mock DockStateReader mDockStateReader; - private @Mock CompatUIConfiguration mCompatUIConfiguration; - private @Mock CompatUIShellCommandHandler mCompatUIShellCommandHandler; + @Mock + private ShellController mMockShellController; + @Mock + private DisplayController mMockDisplayController; + @Mock + private DisplayInsetsController mMockDisplayInsetsController; + @Mock + private DisplayLayout mMockDisplayLayout; + @Mock + private DisplayImeController mMockImeController; + @Mock + private ShellTaskOrganizer.TaskListener mMockTaskListener; + @Mock + private SyncTransactionQueue mMockSyncQueue; + @Mock + private ShellExecutor mMockExecutor; + @Mock + private Lazy<Transitions> mMockTransitionsLazy; + @Mock + private CompatUIWindowManager mMockCompatLayout; + @Mock + private LetterboxEduWindowManager mMockLetterboxEduLayout; + @Mock + private RestartDialogWindowManager mMockRestartDialogLayout; + @Mock + private DockStateReader mDockStateReader; + @Mock + private CompatUIConfiguration mCompatUIConfiguration; + @Mock + private CompatUIShellCommandHandler mCompatUIShellCommandHandler; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 5f294d53b662..3bce2b824e28 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -44,7 +44,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 78c3cbdaace6..4c837e635939 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -53,7 +53,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java index 973a99c269ea..a802f15a0a41 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java @@ -40,6 +40,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.function.BiConsumer; + /** * Tests for {@link ReachabilityEduWindowManager}. * @@ -57,6 +59,8 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { private CompatUIConfiguration mCompatUIConfiguration; @Mock private DisplayLayout mDisplayLayout; + @Mock + private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback; private TestShellExecutor mExecutor; private TaskInfo mTaskInfo; private ReachabilityEduWindowManager mWindowManager; @@ -104,6 +108,7 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) { return new ReachabilityEduWindowManager(mContext, taskInfo, mSyncTransactionQueue, - mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor); + mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor, + mOnDismissCallback); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java new file mode 100644 index 000000000000..1fee153877b5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java @@ -0,0 +1,154 @@ +/* + * 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.compatui; + +import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; +import android.content.ComponentName; +import android.testing.AndroidTestingRunner; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.SurfaceControlViewHost; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.BiConsumer; + +/** + * Tests for {@link UserAspectRatioSettingsLayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:UserAspectRatioSettingsLayoutTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { + + private static final int TASK_ID = 1; + + @Mock + private SyncTransactionQueue mSyncTransactionQueue; + @Mock + private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> + mOnUserAspectRatioSettingsButtonClicked; + @Mock + private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock + private SurfaceControlViewHost mViewHost; + @Captor + private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor; + @Captor + private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor; + + private UserAspectRatioSettingsWindowManager mWindowManager; + private UserAspectRatioSettingsLayout mLayout; + private TaskInfo mTaskInfo; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo, + mSyncTransactionQueue, mTaskListener, new DisplayLayout(), + new CompatUIController.CompatUIHintsState(), + mOnUserAspectRatioSettingsButtonClicked, new TestShellExecutor()); + + mLayout = (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate( + R.layout.user_aspect_ratio_settings_layout, null); + mLayout.inject(mWindowManager); + + spyOn(mWindowManager); + spyOn(mLayout); + doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); + doReturn(mLayout).when(mWindowManager).inflateLayout(); + } + + @Test + public void testOnClickForUserAspectRatioSettingsButton() { + final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button); + button.performClick(); + + verify(mWindowManager).onUserAspectRatioSettingsButtonClicked(); + verify(mOnUserAspectRatioSettingsButtonClicked).accept( + mUserAspectRationTaskInfoCaptor.capture(), + mUserAspectRatioTaskListenerCaptor.capture()); + final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = + new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(), + mUserAspectRatioTaskListenerCaptor.getValue()); + Assert.assertEquals(mTaskInfo, result.first); + Assert.assertEquals(mTaskListener, result.second); + } + + @Test + public void testOnLongClickForUserAspectRatioButton() { + doNothing().when(mWindowManager).onUserAspectRatioSettingsButtonLongClicked(); + + final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button); + button.performLongClick(); + + verify(mWindowManager).onUserAspectRatioSettingsButtonLongClicked(); + } + + @Test + public void testOnClickForUserAspectRatioSettingsHint() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ true); + final LinearLayout sizeCompatHint = mLayout.findViewById( + R.id.user_aspect_ratio_settings_hint); + sizeCompatHint.performClick(); + + verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ false); + } + + private static TaskInfo createTaskInfo(boolean hasSizeCompat, + @CameraCompatControlState int cameraCompatControlState) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = TASK_ID; + taskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java new file mode 100644 index 000000000000..b48538ca99ca --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -0,0 +1,350 @@ +/* + * 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.compatui; + +import static android.view.WindowInsets.Type.navigationBars; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.content.ComponentName; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.util.Pair; +import android.view.DisplayInfo; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * Tests for {@link UserAspectRatioSettingsWindowManager}. + * + * Build/Install/Run: + * atest WMShellUnitTests:UserAspectRatioSettingsWindowManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { + + private static final int TASK_ID = 1; + + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock + private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> + mOnUserAspectRatioSettingsButtonClicked; + @Mock private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock private UserAspectRatioSettingsLayout mLayout; + @Mock private SurfaceControlViewHost mViewHost; + @Captor + private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor; + @Captor + private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor; + + private final Set<String> mPackageNameCache = new HashSet<>(); + + private UserAspectRatioSettingsWindowManager mWindowManager; + private TaskInfo mTaskInfo; + + private TestShellExecutor mExecutor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mExecutor = new TestShellExecutor(); + mTaskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + false, /* topActivityBoundsLetterboxed */ true); + mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo, + mSyncTransactionQueue, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mOnUserAspectRatioSettingsButtonClicked, mExecutor); + spyOn(mWindowManager); + doReturn(mLayout).when(mWindowManager).inflateLayout(); + doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); + } + + @Test + public void testCreateUserAspectRatioButton() { + // Doesn't create layout if show is false. + mWindowManager.mHasUserAspectRatioSettingsButton = true; + assertTrue(mWindowManager.createLayout(/* canShow= */ false)); + + verify(mWindowManager, never()).inflateLayout(); + + // Doesn't create hint popup. + mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager).inflateLayout(); + mExecutor.flushAll(); + verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true); + verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + + // Creates hint popup. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + mWindowManager.release(); + mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = false; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager).inflateLayout(); + assertNotNull(mLayout); + mExecutor.flushAll(); + verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true); + verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + assertTrue(mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint); + + // Returns false and doesn't create layout if mHasUserAspectRatioSettingsButton is false. + clearInvocations(mWindowManager); + mWindowManager.release(); + mWindowManager.mHasUserAspectRatioSettingsButton = false; + assertFalse(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager, never()).inflateLayout(); + } + + @Test + public void testRelease() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + + mWindowManager.release(); + + verify(mViewHost).release(); + } + + @Test + public void testUpdateCompatInfo() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ true); + + // No diff + clearInvocations(mWindowManager); + TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + true, /* topActivityBoundsLetterboxed */ true); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true)); + + verify(mWindowManager, never()).updateSurfacePosition(); + verify(mWindowManager, never()).release(); + verify(mWindowManager, never()).createLayout(anyBoolean()); + + + // Change task listener, recreate button. + clearInvocations(mWindowManager); + final ShellTaskOrganizer.TaskListener newTaskListener = mock( + ShellTaskOrganizer.TaskListener.class); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mWindowManager).release(); + verify(mWindowManager).createLayout(/* canShow= */ true); + + // Change has eligibleForUserAspectRatioButton to false, dispose the component + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + false, /* topActivityBoundsLetterboxed */ true); + assertFalse( + mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + verify(mWindowManager).release(); + } + + @Test + public void testUpdateCompatInfoLayoutNotInflatedYet() { + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.createLayout(/* canShow= */ false); + + verify(mWindowManager, never()).inflateLayout(); + + // Change topActivityInSizeCompat to false and pass canShow true, layout shouldn't be + // inflated + clearInvocations(mWindowManager); + TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + false, /* topActivityBoundsLetterboxed */ true); + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager, never()).inflateLayout(); + + // Change topActivityInSizeCompat to true and pass canShow true, layout should be inflated. + clearInvocations(mWindowManager); + taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ + true, /* topActivityBoundsLetterboxed */ true); + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + } + + @Test + public void testUpdateDisplayLayout() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 1000; + displayInfo.logicalHeight = 2000; + final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false); + + mWindowManager.updateDisplayLayout(displayLayout1); + verify(mWindowManager).updateSurfacePosition(); + + // No update if the display bounds is the same. + clearInvocations(mWindowManager); + final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false); + mWindowManager.updateDisplayLayout(displayLayout2); + verify(mWindowManager, never()).updateSurfacePosition(); + } + + @Test + public void testUpdateDisplayLayoutInsets() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 1000; + displayInfo.logicalHeight = 2000; + final DisplayLayout displayLayout = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false); + + mWindowManager.updateDisplayLayout(displayLayout); + verify(mWindowManager).updateSurfacePosition(); + + // Update if the insets change on the existing display layout + clearInvocations(mWindowManager); + InsetsState insetsState = new InsetsState(); + insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000)); + InsetsSource insetsSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); + insetsSource.setFrame(0, 1800, 1000, 2000); + insetsState.addSource(insetsSource); + displayLayout.setInsets(mContext.getResources(), insetsState); + mWindowManager.updateDisplayLayout(displayLayout); + verify(mWindowManager).updateSurfacePosition(); + } + + @Test + public void testUpdateVisibility() { + // Create button if it is not created. + mWindowManager.removeLayout(); + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.updateVisibility(/* canShow= */ true); + + verify(mWindowManager).createLayout(/* canShow= */ true); + + // Hide button. + clearInvocations(mWindowManager); + doReturn(View.VISIBLE).when(mLayout).getVisibility(); + mWindowManager.updateVisibility(/* canShow= */ false); + + verify(mWindowManager, never()).createLayout(anyBoolean()); + verify(mLayout).setVisibility(View.GONE); + + // Show button. + doReturn(View.GONE).when(mLayout).getVisibility(); + mWindowManager.updateVisibility(/* canShow= */ true); + + verify(mWindowManager, never()).createLayout(anyBoolean()); + verify(mLayout).setVisibility(View.VISIBLE); + } + + @Test + public void testAttachToParentSurface() { + final SurfaceControl.Builder b = new SurfaceControl.Builder(); + mWindowManager.attachToParentSurface(b); + + verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b); + } + + @Test + public void testOnUserAspectRatioButtonClicked() { + mWindowManager.onUserAspectRatioSettingsButtonClicked(); + + verify(mOnUserAspectRatioSettingsButtonClicked).accept( + mUserAspectRationTaskInfoCaptor.capture(), + mUserAspectRatioTaskListenerCaptor.capture()); + final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = + new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(), + mUserAspectRatioTaskListenerCaptor.getValue()); + Assert.assertEquals(mTaskInfo, result.first); + Assert.assertEquals(mTaskListener, result.second); + } + + @Test + public void testOnUserAspectRatioButtonLongClicked_showHint() { + // Not create hint popup. + mWindowManager.mHasUserAspectRatioSettingsButton = true; + mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true; + mWindowManager.createLayout(/* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + + mWindowManager.onUserAspectRatioSettingsButtonLongClicked(); + + verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true); + } + + @Test + public void testWhenDockedStateHasChanged_needsToBeRecreated() { + ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); + newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK; + + Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener)); + } + + private static TaskInfo createTaskInfo(boolean eligibleForUserAspectRatioButton, + boolean topActivityBoundsLetterboxed) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = TASK_ID; + taskInfo.topActivityEligibleForUserAspectRatioButton = eligibleForUserAspectRatioButton; + taskInfo.topActivityBoundsLetterboxed = topActivityBoundsLetterboxed; + taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; + taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); + return taskInfo; + } +} 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 2cc28acd0b17..0ae290847644 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 @@ -55,9 +55,9 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipDisplayLayoutState; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index e8a1e91acd4d..568db919818c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -65,6 +65,7 @@ import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -106,6 +107,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock RecentTasksController mRecentTasks; @Mock LaunchAdjacentController mLaunchAdjacentController; @Mock WindowDecorViewModel mWindowDecorViewModel; + @Mock DesktopTasksController mDesktopTasksController; @Captor ArgumentCaptor<Intent> mIntentCaptor; private ShellController mShellController; @@ -122,7 +124,7 @@ public class SplitScreenControllerTests extends ShellTestCase { mRootTDAOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, - mMainExecutor, mStageCoordinator)); + mDesktopTasksController, mMainExecutor, mStageCoordinator)); } @Test diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index d2b21ae19162..43acdd51b07b 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -46,7 +46,9 @@ import android.os.ServiceManager; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.provider.BaseColumns; import android.provider.MediaStore; +import android.provider.MediaStore.Audio.AudioColumns; import android.provider.MediaStore.MediaColumns; import android.provider.Settings; import android.provider.Settings.System; @@ -507,6 +509,95 @@ public class RingtoneManager { return getUriFromCursor(mContext, mCursor); } + /** + * Gets the valid ringtone uri by a given uri string and ringtone type for the restore purpose. + * + * @param contentResolver ContentResolver to execute media query. + * @param value a canonicalized uri which refers to the ringtone. + * @param ringtoneType an integer representation of the kind of uri that is being restored, can + * be RingtoneManager.TYPE_RINGTONE, RingtoneManager.TYPE_NOTIFICATION, or + * RingtoneManager.TYPE_ALARM. + * @hide + */ + public static @Nullable Uri getRingtoneUriForRestore( + @NonNull ContentResolver contentResolver, @Nullable String value, int ringtoneType) + throws FileNotFoundException, IllegalArgumentException { + if (value == null) { + // Return a valid null. It means the null value is intended instead of a failure. + return null; + } + + Uri ringtoneUri; + final Uri canonicalUri = Uri.parse(value); + + // Try to get the media uri via the regular uncanonicalize method first. + ringtoneUri = contentResolver.uncanonicalize(canonicalUri); + if (ringtoneUri != null) { + // Canonicalize it to make the result contain the right metadata of the media asset. + ringtoneUri = contentResolver.canonicalize(ringtoneUri); + return ringtoneUri; + } + + // Query the media by title and ringtone type. + final String title = canonicalUri.getQueryParameter(AudioColumns.TITLE); + Uri baseUri = ContentUris.removeId(canonicalUri).buildUpon().clearQuery().build(); + String ringtoneTypeSelection = ""; + switch (ringtoneType) { + case RingtoneManager.TYPE_RINGTONE: + ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_RINGTONE; + break; + case RingtoneManager.TYPE_NOTIFICATION: + ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_NOTIFICATION; + break; + case RingtoneManager.TYPE_ALARM: + ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_ALARM; + break; + default: + throw new IllegalArgumentException("Unknown ringtone type: " + ringtoneType); + } + + final String selection = ringtoneTypeSelection + "=1 AND " + AudioColumns.TITLE + "=?"; + Cursor cursor = null; + try { + cursor = + contentResolver.query( + baseUri, + /* projection */ new String[] {BaseColumns._ID}, + /* selection */ selection, + /* selectionArgs */ new String[] {title}, + /* sortOrder */ null, + /* cancellationSignal */ null); + + } catch (IllegalArgumentException e) { + throw new FileNotFoundException("Volume not found for " + baseUri); + } + if (cursor == null) { + throw new FileNotFoundException("Missing cursor for " + baseUri); + } else if (cursor.getCount() == 0) { + FileUtils.closeQuietly(cursor); + throw new FileNotFoundException("No item found for " + baseUri); + } else if (cursor.getCount() > 1) { + // Find more than 1 result. + // We are not sure which one is the right ringtone file so just abandon this case. + FileUtils.closeQuietly(cursor); + throw new FileNotFoundException( + "Find multiple ringtone candidates by title+ringtone_type query: count: " + + cursor.getCount()); + } + if (cursor.moveToFirst()) { + ringtoneUri = ContentUris.withAppendedId(baseUri, cursor.getLong(0)); + FileUtils.closeQuietly(cursor); + } else { + FileUtils.closeQuietly(cursor); + throw new FileNotFoundException("Failed to read row from the result."); + } + + // Canonicalize it to make the result contain the right metadata of the media asset. + ringtoneUri = contentResolver.canonicalize(ringtoneUri); + Log.v(TAG, "Find a valid result: " + ringtoneUri); + return ringtoneUri; + } + private static Uri getUriFromCursor(Context context, Cursor cursor) { final Uri uri = ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)), cursor.getLong(ID_COLUMN_INDEX)); diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index fb72c7ba9db4..223b432c12af 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -83,6 +83,7 @@ public final class MediaProjection { try { mImpl.start(new MediaProjectionCallback()); } catch (RemoteException e) { + Log.e(TAG, "Content Recording: Failed to start media projection", e); throw new RuntimeException("Failed to start media projection", e); } mDisplayManager = displayManager; @@ -105,11 +106,18 @@ public final class MediaProjection { * @see #unregisterCallback */ public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) { - final Callback c = Objects.requireNonNull(callback); - if (handler == null) { - handler = new Handler(); + try { + final Callback c = Objects.requireNonNull(callback); + if (handler == null) { + handler = new Handler(); + } + mCallbacks.put(c, new CallbackRecord(c, handler)); + } catch (NullPointerException e) { + Log.e(TAG, "Content Recording: cannot register null Callback", e); + throw e; + } catch (RuntimeException e) { + Log.e(TAG, "Content Recording: failed to create new Handler to register Callback", e); } - mCallbacks.put(c, new CallbackRecord(c, handler)); } /** @@ -120,8 +128,13 @@ public final class MediaProjection { * @see #registerCallback */ public void unregisterCallback(@NonNull Callback callback) { - final Callback c = Objects.requireNonNull(callback); - mCallbacks.remove(c); + try { + final Callback c = Objects.requireNonNull(callback); + mCallbacks.remove(c); + } catch (NullPointerException e) { + Log.d(TAG, "Content Recording: cannot unregister null Callback", e); + throw e; + } } /** @@ -203,9 +216,11 @@ public final class MediaProjection { @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { if (shouldMediaProjectionRequireCallback()) { if (mCallbacks.isEmpty()) { - throw new IllegalStateException( + final IllegalStateException e = new IllegalStateException( "Must register a callback before starting capture, to manage resources in" + " response to MediaProjection states."); + Log.e(TAG, "Content Recording: no callback registered for virtual display", e); + throw e; } } final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, @@ -272,6 +287,7 @@ public final class MediaProjection { */ public void stop() { try { + Log.d(TAG, "Content Recording: stopping projection"); mImpl.stop(); } catch (RemoteException e) { Log.e(TAG, "Unable to stop projection", e); diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 5a68c53b8f68..9790d02025b7 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -256,6 +256,7 @@ public final class MediaProjectionManager { */ public void stopActiveProjection() { try { + Log.d(TAG, "Content Recording: stopping active projection"); mService.stopActiveProjection(); } catch (RemoteException e) { Log.e(TAG, "Unable to stop the currently active media projection", e); @@ -269,6 +270,7 @@ public final class MediaProjectionManager { */ public void addCallback(@NonNull Callback callback, @Nullable Handler handler) { if (callback == null) { + Log.w(TAG, "Content Recording: cannot add null callback"); throw new IllegalArgumentException("callback must not be null"); } CallbackDelegate delegate = new CallbackDelegate(callback, handler); @@ -286,6 +288,7 @@ public final class MediaProjectionManager { */ public void removeCallback(@NonNull Callback callback) { if (callback == null) { + Log.w(TAG, "ContentRecording: cannot remove null callback"); throw new IllegalArgumentException("callback must not be null"); } CallbackDelegate delegate = mCallbacks.remove(callback); diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java index 0c4cb8e4e8f6..74acf677918e 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java @@ -19,6 +19,7 @@ package com.android.printspooler.ui; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; +import android.app.ActivityOptions; import android.app.LoaderManager; import android.content.ComponentName; import android.content.Context; @@ -714,8 +715,13 @@ public final class SelectPrinterActivity extends Activity implements try { mPrinterForInfoIntent = printer; + Bundle options = ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .toBundle(); startIntentSenderForResult(printer.getInfoIntent().getIntentSender(), - INFO_INTENT_REQUEST_CODE, fillInIntent, 0, 0, 0); + INFO_INTENT_REQUEST_CODE, fillInIntent, 0, 0, 0, + options); } catch (SendIntentException e) { mPrinterForInfoIntent = null; Log.e(LOG_TAG, "Could not execute pending info intent: %s", e); diff --git a/packages/SettingsLib/Spa/spa/res/values/colors.xml b/packages/SettingsLib/Spa/spa/res/values/colors.xml new file mode 100644 index 000000000000..ca4a0b2bbed6 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/res/values/colors.xml @@ -0,0 +1,67 @@ +<?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. + --> + +<resources> + <color name="settingslib_protection_color">@android:color/white</color> + + <!-- Dynamic colors--> + <color name="settingslib_color_blue600">#1a73e8</color> + <color name="settingslib_color_blue400">#669df6</color> + <color name="settingslib_color_blue300">#8ab4f8</color> + <color name="settingslib_color_blue100">#d2e3fc</color> + <color name="settingslib_color_blue50">#e8f0fe</color> + <color name="settingslib_color_green600">#1e8e3e</color> + <color name="settingslib_color_green500">#34A853</color> + <color name="settingslib_color_green400">#5bb974</color> + <color name="settingslib_color_green100">#ceead6</color> + <color name="settingslib_color_green50">#e6f4ea</color> + <color name="settingslib_color_red600">#d93025</color> + <color name="settingslib_color_red500">#B3261E</color> + <color name="settingslib_color_red400">#ee675c</color> + <color name="settingslib_color_red100">#fad2cf</color> + <color name="settingslib_color_red50">#fce8e6</color> + <color name="settingslib_color_yellow600">#f9ab00</color> + <color name="settingslib_color_yellow400">#fcc934</color> + <color name="settingslib_color_yellow100">#feefc3</color> + <color name="settingslib_color_yellow50">#fef7e0</color> + <color name="settingslib_color_grey900">#202124</color> + <color name="settingslib_color_grey800">#3c4043</color> + <color name="settingslib_color_grey700">#5f6368</color> + <color name="settingslib_color_grey600">#80868b</color> + <color name="settingslib_color_grey500">#9AA0A6</color> + <color name="settingslib_color_grey400">#bdc1c6</color> + <color name="settingslib_color_grey300">#dadce0</color> + <color name="settingslib_color_grey200">#e8eaed</color> + <color name="settingslib_color_grey100">#f1f3f4</color> + <color name="settingslib_color_grey50">#f8f9fa</color> + <color name="settingslib_color_orange600">#e8710a</color> + <color name="settingslib_color_orange400">#fa903e</color> + <color name="settingslib_color_orange300">#fcad70</color> + <color name="settingslib_color_orange100">#fedfc8</color> + <color name="settingslib_color_pink600">#e52592</color> + <color name="settingslib_color_pink400">#ff63b8</color> + <color name="settingslib_color_pink300">#ff8bcb</color> + <color name="settingslib_color_pink100">#fdcfe8</color> + <color name="settingslib_color_purple600">#9334e6</color> + <color name="settingslib_color_purple400">#af5cf7</color> + <color name="settingslib_color_purple300">#c58af9</color> + <color name="settingslib_color_purple100">#e9d2fd</color> + <color name="settingslib_color_cyan600">#12b5c8</color> + <color name="settingslib_color_cyan400">#4ecde6</color> + <color name="settingslib_color_cyan300">#78d9ec</color> + <color name="settingslib_color_cyan100">#cbf0f8</color> +</resources> diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/illustration/Illustration.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/illustration/Illustration.kt index 7cc9bf7c9d50..6a2163c02b2f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/illustration/Illustration.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/illustration/Illustration.kt @@ -22,11 +22,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.ui.ImageBox import com.android.settingslib.spa.widget.ui.Lottie @@ -81,7 +81,7 @@ fun Illustration( maxHeight = SettingsDimension.illustrationMaxHeight, ) .clip(RoundedCornerShape(SettingsDimension.illustrationCornerRadius)) - .background(color = MaterialTheme.colorScheme.surface) + .background(color = Color.Transparent) when (resourceType) { ResourceType.LOTTIE -> { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt index 915c6e2456bc..5f7fe850fa8f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt @@ -16,15 +16,24 @@ package com.android.settingslib.spa.widget.ui +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.colorResource +import com.airbnb.lottie.LottieProperty import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition +import com.airbnb.lottie.compose.rememberLottieDynamicProperties +import com.airbnb.lottie.compose.rememberLottieDynamicProperty +import com.android.settingslib.spa.R @Composable fun Lottie( @@ -38,6 +47,34 @@ fun Lottie( } } +object LottieColorUtils { + private val DARK_TO_LIGHT_THEME_COLOR_MAP = mapOf( + ".grey600" to R.color.settingslib_color_grey400, + ".grey800" to R.color.settingslib_color_grey300, + ".grey900" to R.color.settingslib_color_grey50, + ".red400" to R.color.settingslib_color_red600, + ".black" to android.R.color.white, + ".blue400" to R.color.settingslib_color_blue600, + ".green400" to R.color.settingslib_color_green600, + ".green200" to R.color.settingslib_color_green500, + ".red200" to R.color.settingslib_color_red500, + ) + + @Composable + private fun getDefaultPropertiesList() = + DARK_TO_LIGHT_THEME_COLOR_MAP.map { (key, colorRes) -> + val color = colorResource(colorRes).toArgb() + rememberLottieDynamicProperty( + property = LottieProperty.COLOR_FILTER, + keyPath = arrayOf("**", key, "**") + ){ PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) } + } + + @Composable + fun getDefaultDynamicProperties() = + rememberLottieDynamicProperties(*getDefaultPropertiesList().toTypedArray()) +} + @Composable private fun BaseLottie(resId: Int) { val composition by rememberLottieComposition( @@ -47,8 +84,10 @@ private fun BaseLottie(resId: Int) { composition, iterations = LottieConstants.IterateForever, ) + val isLightMode = !isSystemInDarkTheme() LottieAnimation( composition = composition, + dynamicProperties = LottieColorUtils.getDefaultDynamicProperties().takeIf { isLightMode }, progress = { progress }, ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 2118d2cbf4b3..f03ff00828a6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1518,10 +1518,15 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> * list. */ public void switchMemberDeviceContent(CachedBluetoothDevice newMainDevice) { - // Backup from main device + // Remove the sub device from mMemberDevices first to prevent hash mismatch problem due + // to mDevice switch + removeMemberDevice(newMainDevice); + + // Backup from current main device final BluetoothDevice tmpDevice = mDevice; final short tmpRssi = mRssi; final boolean tmpJustDiscovered = mJustDiscovered; + // Set main device from sub device release(); mDevice = newMainDevice.mDevice; @@ -1535,6 +1540,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> newMainDevice.mRssi = tmpRssi; newMainDevice.mJustDiscovered = tmpJustDiscovered; newMainDevice.fillData(); + + // Add the sub device back into mMemberDevices with correct hash + addMemberDevice(newMainDevice); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index efba953e3c6b..111a2c1a07ea 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -186,6 +186,14 @@ public class HearingAidDeviceManager { if (cachedDevice.getHiSyncId() != hiSyncId) { continue; } + + // The remote device supports CSIP, the other ear should be processed as a member + // device. Ignore hiSyncId grouping from ASHA here. + if (cachedDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof CsipSetCoordinatorProfile)) { + continue; + } + if (firstMatchedIndex == -1) { // Found the first one firstMatchedIndex = i; diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java index 1251b0dc306a..9ab84d254ed6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java @@ -26,6 +26,7 @@ import static android.os.BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE; import static android.os.BatteryManager.EXTRA_PLUGGED; import static android.os.BatteryManager.EXTRA_PRESENT; import static android.os.BatteryManager.EXTRA_STATUS; +import static android.os.OsProtoEnums.BATTERY_PLUGGED_NONE; import android.content.Context; import android.content.Intent; @@ -40,6 +41,8 @@ import java.util.Optional; */ public class BatteryStatus { private static final int LOW_BATTERY_THRESHOLD = 20; + private static final int SEVERE_LOW_BATTERY_THRESHOLD = 10; + private static final int EXTREME_LOW_BATTERY_THRESHOLD = 3; private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000; public static final int CHARGING_UNKNOWN = -1; @@ -90,21 +93,7 @@ public class BatteryStatus { present = batteryChangedIntent.getBooleanExtra(EXTRA_PRESENT, true); this.incompatibleCharger = incompatibleCharger; - final int maxChargingMicroAmp = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_CURRENT, - -1); - int maxChargingMicroVolt = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_VOLTAGE, -1); - - if (maxChargingMicroVolt <= 0) { - maxChargingMicroVolt = DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT; - } - if (maxChargingMicroAmp > 0) { - // Calculating muW = muA * muV / (10^6 mu^2 / mu); splitting up the divisor - // to maintain precision equally on both factors. - maxChargingWattage = (maxChargingMicroAmp / 1000) - * (maxChargingMicroVolt / 1000); - } else { - maxChargingWattage = -1; - } + maxChargingWattage = calculateMaxChargingMicroWatt(batteryChangedIntent); } /** Determine whether the device is plugged. */ @@ -126,7 +115,7 @@ public class BatteryStatus { /** Determine whether the device is plugged in dock. */ public boolean isPluggedInDock() { - return plugged == BatteryManager.BATTERY_PLUGGED_DOCK; + return isPluggedInDock(plugged); } /** @@ -140,15 +129,15 @@ public class BatteryStatus { /** Whether battery is low and needs to be charged. */ public boolean isBatteryLow() { - return level < LOW_BATTERY_THRESHOLD; + return isLowBattery(level); } /** Whether battery defender is enabled. */ public boolean isBatteryDefender() { - return chargingStatus == CHARGING_POLICY_ADAPTIVE_LONGLIFE; + return isBatteryDefender(chargingStatus); } - /** Return current chargin speed is fast, slow or normal. */ + /** Return current charging speed is fast, slow or normal. */ public final int getChargingSpeed(Context context) { final int slowThreshold = context.getResources().getInteger( R.integer.config_chargingSlowlyThreshold); @@ -218,4 +207,126 @@ public class BatteryStatus { || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS || plugged == BatteryManager.BATTERY_PLUGGED_DOCK; } + + /** Determine whether the device is plugged in dock. */ + public static boolean isPluggedInDock(Intent batteryChangedIntent) { + return isPluggedInDock( + batteryChangedIntent.getIntExtra(EXTRA_PLUGGED, BATTERY_PLUGGED_NONE)); + } + + /** Determine whether the device is plugged in dock. */ + public static boolean isPluggedInDock(int plugged) { + return plugged == BatteryManager.BATTERY_PLUGGED_DOCK; + } + + /** + * Whether the battery is low or not. + * + * @param batteryChangedIntent the {@link ACTION_BATTERY_CHANGED} intent + * @return {@code true} if the battery level is less or equal to {@link LOW_BATTERY_THRESHOLD} + */ + public static boolean isLowBattery(Intent batteryChangedIntent) { + int level = getBatteryLevel(batteryChangedIntent); + return isLowBattery(level); + } + + /** + * Whether the battery is low or not. + * + * @param batteryLevel the battery level + * @return {@code true} if the battery level is less or equal to {@link LOW_BATTERY_THRESHOLD} + */ + public static boolean isLowBattery(int batteryLevel) { + return batteryLevel <= LOW_BATTERY_THRESHOLD; + } + + /** + * Whether the battery is severe low or not. + * + * @param batteryChangedIntent the ACTION_BATTERY_CHANGED intent + * @return {@code true} if the battery level is less or equal to {@link + * SEVERE_LOW_BATTERY_THRESHOLD} + */ + public static boolean isSevereLowBattery(Intent batteryChangedIntent) { + int level = getBatteryLevel(batteryChangedIntent); + return level <= SEVERE_LOW_BATTERY_THRESHOLD; + } + + /** + * Whether the battery is extreme low or not. + * + * @param batteryChangedIntent the ACTION_BATTERY_CHANGED intent + * @return {@code true} if the battery level is less or equal to {@link + * EXTREME_LOW_BATTERY_THRESHOLD} + */ + public static boolean isExtremeLowBattery(Intent batteryChangedIntent) { + int level = getBatteryLevel(batteryChangedIntent); + return level <= EXTREME_LOW_BATTERY_THRESHOLD; + } + + /** + * Whether the battery defender is enabled or not. + * + * @param batteryChangedIntent the ACTION_BATTERY_CHANGED intent + * @return {@code true} if the battery defender is enabled. It could be dock defend, dwell + * defend, or temp defend + */ + public static boolean isBatteryDefender(Intent batteryChangedIntent) { + int chargingStatus = + batteryChangedIntent.getIntExtra(EXTRA_CHARGING_STATUS, CHARGING_POLICY_DEFAULT); + return isBatteryDefender(chargingStatus); + } + + /** + * Whether the battery defender is enabled or not. + * + * @param chargingStatus for {@link EXTRA_CHARGING_STATUS} field in the ACTION_BATTERY_CHANGED + * intent + * @return {@code true} if the battery defender is enabled. It could be dock defend, dwell + * defend, or temp defend + */ + public static boolean isBatteryDefender(int chargingStatus) { + return chargingStatus == CHARGING_POLICY_ADAPTIVE_LONGLIFE; + } + + /** + * Gets the max charging current and max charging voltage form {@link + * Intent.ACTION_BATTERY_CHANGED} and calculates the charging speed based on the {@link + * R.integer.config_chargingSlowlyThreshold} and {@link R.integer.config_chargingFastThreshold}. + * + * @param context the application context + * @param batteryChangedIntent the intent from {@link Intent.ACTION_BATTERY_CHANGED} + * @return the charging speed. {@link CHARGING_REGULAR}, {@link CHARGING_FAST}, {@link + * CHARGING_SLOWLY} or {@link CHARGING_UNKNOWN} + */ + public static int getChargingSpeed(Context context, Intent batteryChangedIntent) { + final int maxChargingMicroWatt = calculateMaxChargingMicroWatt(batteryChangedIntent); + if (maxChargingMicroWatt <= 0) { + return CHARGING_UNKNOWN; + } else if (maxChargingMicroWatt + < context.getResources().getInteger(R.integer.config_chargingSlowlyThreshold)) { + return CHARGING_SLOWLY; + } else if (maxChargingMicroWatt + > context.getResources().getInteger(R.integer.config_chargingFastThreshold)) { + return CHARGING_FAST; + } else { + return CHARGING_REGULAR; + } + } + + private static int calculateMaxChargingMicroWatt(Intent batteryChangedIntent) { + final int maxChargingMicroAmp = + batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_CURRENT, -1); + int maxChargingMicroVolt = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_VOLTAGE, -1); + if (maxChargingMicroVolt <= 0) { + maxChargingMicroVolt = DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT; + } + + if (maxChargingMicroAmp > 0) { + // Calculating µW = mA * mV + return (int) Math.round(maxChargingMicroAmp * 0.001 * maxChargingMicroVolt * 0.001); + } else { + return -1; + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt index a03acc3a078c..73f6db6d464b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt +++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt @@ -325,7 +325,7 @@ open class ThemedBatteryDrawable(private val context: Context, frameColor: Int) return batteryLevel } - override fun onBoundsChange(bounds: Rect?) { + override fun onBoundsChange(bounds: Rect) { super.onBoundsChange(bounds) updateSize() } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index a9d15f3b4afe..ac93019f950c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -22,6 +22,7 @@ import static android.media.MediaRoute2Info.TYPE_DOCK; import static android.media.MediaRoute2Info.TYPE_GROUP; import static android.media.MediaRoute2Info.TYPE_HDMI; import static android.media.MediaRoute2Info.TYPE_HEARING_AID; +import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; import static android.media.MediaRoute2Info.TYPE_UNKNOWN; @@ -83,7 +84,8 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, MediaDeviceType.TYPE_CAST_DEVICE, - MediaDeviceType.TYPE_CAST_GROUP_DEVICE}) + MediaDeviceType.TYPE_CAST_GROUP_DEVICE, + MediaDeviceType.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER}) public @interface MediaDeviceType { int TYPE_UNKNOWN = 0; int TYPE_PHONE_DEVICE = 1; @@ -93,6 +95,7 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { int TYPE_BLUETOOTH_DEVICE = 5; int TYPE_CAST_DEVICE = 6; int TYPE_CAST_GROUP_DEVICE = 7; + int TYPE_REMOTE_AUDIO_VIDEO_RECEIVER = 8; } @Retention(RetentionPolicy.SOURCE) @@ -161,6 +164,9 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { case TYPE_BLE_HEADSET: mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; break; + case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER: + mType = MediaDeviceType.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; + break; case TYPE_UNKNOWN: case TYPE_REMOTE_TV: case TYPE_REMOTE_SPEAKER: diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/fuelgague/BatteryStatusTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/fuelgague/BatteryStatusTest.kt new file mode 100644 index 000000000000..6c0c1a73bd11 --- /dev/null +++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/fuelgague/BatteryStatusTest.kt @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.fuelgague + +import android.content.Context +import android.content.Intent +import android.os.BatteryManager +import android.os.BatteryManager.BATTERY_PLUGGED_AC +import android.os.BatteryManager.BATTERY_PLUGGED_DOCK +import android.os.BatteryManager.BATTERY_PLUGGED_USB +import android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS +import android.os.BatteryManager.BATTERY_STATUS_FULL +import android.os.BatteryManager.BATTERY_STATUS_UNKNOWN +import android.os.BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE +import android.os.BatteryManager.CHARGING_POLICY_DEFAULT +import android.os.BatteryManager.EXTRA_MAX_CHARGING_CURRENT +import android.os.BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE +import android.os.OsProtoEnums.BATTERY_PLUGGED_NONE +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.fuelgauge.BatteryStatus +import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_FAST +import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_REGULAR +import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_SLOWLY +import com.android.settingslib.fuelgauge.BatteryStatus.CHARGING_UNKNOWN +import com.android.settingslib.fuelgauge.BatteryStatus.isBatteryDefender +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import java.util.Optional +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Suite +import org.junit.runners.Suite.SuiteClasses + +@RunWith(Suite::class) +@SuiteClasses( + BatteryStatusTest.NonParameterizedTest::class, + BatteryStatusTest.IsPluggedInTest::class, + BatteryStatusTest.IsChargedTest::class, + BatteryStatusTest.GetChargingSpeedTest::class, + BatteryStatusTest.IsPluggedInDockTest::class, +) +open class BatteryStatusTest { + + @RunWith(AndroidJUnit4::class) + class NonParameterizedTest : BatteryStatusTest() { + @Test + fun isLowBattery_20Percent_returnsTrue() { + val level = 20 + val intent = createIntent(batteryLevel = level) + + assertWithMessage("failed by isLowBattery(Intent), level=$level") + .that(BatteryStatus.isLowBattery(intent)) + .isTrue() + assertWithMessage("failed by isLowBattery($level)") + .that(BatteryStatus.isLowBattery(level)) + .isTrue() + } + + @Test + fun isLowBattery_21Percent_returnsFalse() { + val level = 21 + val intent = createIntent(batteryLevel = level) + + assertWithMessage("failed by isLowBattery(intent), level=$level") + .that(BatteryStatus.isLowBattery(intent)) + .isFalse() + assertWithMessage("failed by isLowBattery($level)") + .that(BatteryStatus.isLowBattery(intent)) + .isFalse() + } + + @Test + fun isSevereLowBattery_10Percent_returnsTrue() { + val batteryChangedIntent = createIntent(batteryLevel = 10) + + assertThat(BatteryStatus.isSevereLowBattery(batteryChangedIntent)).isTrue() + } + + @Test + fun isSevereLowBattery_11Percent_returnFalse() { + val batteryChangedIntent = createIntent(batteryLevel = 11) + + assertThat(BatteryStatus.isSevereLowBattery(batteryChangedIntent)).isFalse() + } + + @Test + fun isExtremeLowBattery_3Percent_returnsTrue() { + val batteryChangedIntent = createIntent(batteryLevel = 3) + + assertThat(BatteryStatus.isExtremeLowBattery(batteryChangedIntent)).isTrue() + } + + @Test + fun isExtremeLowBattery_4Percent_returnsFalse() { + val batteryChangedIntent = createIntent(batteryLevel = 4) + + assertThat(BatteryStatus.isExtremeLowBattery(batteryChangedIntent)).isFalse() + } + + @Test + fun isBatteryDefender_chargingLongLife_returnsTrue() { + val chargingStatus = CHARGING_POLICY_ADAPTIVE_LONGLIFE + val batteryChangedIntent = createIntent(chargingStatus = chargingStatus) + + assertIsBatteryDefender(chargingStatus, batteryChangedIntent).isTrue() + } + + @Test + fun isBatteryDefender_nonChargingLongLife_returnsFalse() { + val chargingStatus = CHARGING_POLICY_DEFAULT + val batteryChangedIntent = createIntent(chargingStatus = chargingStatus) + + assertIsBatteryDefender(chargingStatus, batteryChangedIntent).isFalse() + } + + private fun assertIsBatteryDefender(chargingStatus: Int, batteryChangedIntent: Intent) = + object { + val assertions = + listOf( + "failed by isBatteryDefender(Intent), chargingStatus=$chargingStatus".let { + assertWithMessage(it).that(isBatteryDefender(batteryChangedIntent)) + }, + "failed by isBatteryDefender($chargingStatus)".let { + assertWithMessage(it).that(isBatteryDefender(chargingStatus)) + }, + ) + + fun isTrue() = assertions.forEach { it.isTrue() } + + fun isFalse() = assertions.forEach { it.isFalse() } + } + } + + @RunWith(Parameterized::class) + class IsPluggedInTest( + private val name: String, + private val plugged: Int, + val expected: Boolean + ) : BatteryStatusTest() { + + @Test + fun isPluggedIn_() { + val batteryChangedIntent = createIntent(plugged = plugged) + + assertWithMessage("failed by isPluggedIn(plugged=$plugged)") + .that(BatteryStatus.isPluggedIn(plugged)) + .isEqualTo(expected) + assertWithMessage("failed by isPlugged(Intent), which plugged=$plugged") + .that(BatteryStatus.isPluggedIn(batteryChangedIntent)) + .isEqualTo(expected) + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun parameters() = + arrayListOf( + arrayOf("withAC_returnsTrue", BATTERY_PLUGGED_AC, true), + arrayOf("withDock_returnsTrue", BATTERY_PLUGGED_DOCK, true), + arrayOf("withUSB_returnsTrue", BATTERY_PLUGGED_USB, true), + arrayOf("withWireless_returnsTrue", BATTERY_PLUGGED_WIRELESS, true), + arrayOf("pluggedNone_returnsTrue", BATTERY_PLUGGED_NONE, false), + ) + } + } + + @RunWith(Parameterized::class) + class IsPluggedInDockTest( + private val name: String, + private val plugged: Int, + val expected: Boolean + ) : BatteryStatusTest() { + + @Test + fun isPluggedDockIn_() { + val batteryChangedIntent = createIntent(plugged = plugged) + + assertWithMessage("failed by isPluggedInDock(plugged=$plugged)") + .that(BatteryStatus.isPluggedInDock(plugged)) + .isEqualTo(expected) + assertWithMessage("failed by isPluggedInDock(Intent), which plugged=$plugged") + .that(BatteryStatus.isPluggedInDock(batteryChangedIntent)) + .isEqualTo(expected) + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun parameters() = + arrayListOf( + arrayOf("withAC_returnsTrue", BATTERY_PLUGGED_AC, false), + arrayOf("withDock_returnsTrue", BATTERY_PLUGGED_DOCK, true), + arrayOf("withUSB_returnsTrue", BATTERY_PLUGGED_USB, false), + arrayOf("withWireless_returnsTrue", BATTERY_PLUGGED_WIRELESS, false), + arrayOf("pluggedNone_returnsTrue", BATTERY_PLUGGED_NONE, false), + ) + } + } + + @RunWith(Parameterized::class) + class IsChargedTest( + private val status: Int, + private val batteryLevel: Int, + private val expected: Boolean + ) : BatteryStatusTest() { + + @Test + fun isCharged_() { + val batteryChangedIntent = createIntent(batteryLevel = batteryLevel, status = status) + + assertWithMessage( + "failed by isCharged(Intent), status=$status, batteryLevel=$batteryLevel" + ) + .that(BatteryStatus.isCharged(batteryChangedIntent)) + .isEqualTo(expected) + assertWithMessage("failed by isCharged($status, $batteryLevel)") + .that(BatteryStatus.isCharged(status, batteryLevel)) + .isEqualTo(expected) + } + + companion object { + @Parameterized.Parameters(name = "status{0}_level{1}_returns-{2}") + @JvmStatic + fun parameters() = + arrayListOf( + arrayOf(BATTERY_STATUS_FULL, 99, true), + arrayOf(BATTERY_STATUS_UNKNOWN, 100, true), + arrayOf(BATTERY_STATUS_FULL, 100, true), + arrayOf(BATTERY_STATUS_UNKNOWN, 99, false), + ) + } + } + + @RunWith(Parameterized::class) + class GetChargingSpeedTest( + private val name: String, + private val maxChargingCurrent: Optional<Int>, + private val maxChargingVoltage: Optional<Int>, + private val expectedChargingSpeed: Int, + ) { + + val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun getChargingSpeed_() { + val batteryChangedIntent = + Intent(Intent.ACTION_BATTERY_CHANGED).apply { + maxChargingCurrent.ifPresent { putExtra(EXTRA_MAX_CHARGING_CURRENT, it) } + maxChargingVoltage.ifPresent { putExtra(EXTRA_MAX_CHARGING_VOLTAGE, it) } + } + + assertThat(BatteryStatus.getChargingSpeed(context, batteryChangedIntent)) + .isEqualTo(expectedChargingSpeed) + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun parameters() = + arrayListOf( + arrayOf( + "maxCurrent=n/a, maxVoltage=n/a -> UNKNOWN", + Optional.empty<Int>(), + Optional.empty<Int>(), + CHARGING_UNKNOWN + ), + arrayOf( + "maxCurrent=0, maxVoltage=9000000 -> UNKNOWN", + Optional.of(0), + Optional.of(0), + CHARGING_UNKNOWN + ), + arrayOf( + "maxCurrent=1500000, maxVoltage=5000000 -> CHARGING_REGULAR", + Optional.of(1500000), + Optional.of(5000000), + CHARGING_REGULAR + ), + arrayOf( + "maxCurrent=1000000, maxVoltage=5000000 -> CHARGING_REGULAR", + Optional.of(1000000), + Optional.of(5000000), + CHARGING_REGULAR + ), + arrayOf( + "maxCurrent=1500001, maxVoltage=5000000 -> CHARGING_FAST", + Optional.of(1501000), + Optional.of(5000000), + CHARGING_FAST + ), + arrayOf( + "maxCurrent=999999, maxVoltage=5000000 -> CHARGING_SLOWLY", + Optional.of(999999), + Optional.of(5000000), + CHARGING_SLOWLY + ), + ) + } + } + + protected fun createIntent( + batteryLevel: Int = 50, + chargingStatus: Int = CHARGING_POLICY_DEFAULT, + plugged: Int = BATTERY_PLUGGED_NONE, + status: Int = BatteryManager.BATTERY_STATUS_CHARGING, + ): Intent = + Intent(Intent.ACTION_BATTERY_CHANGED).apply { + putExtra(BatteryManager.EXTRA_STATUS, status) + putExtra(BatteryManager.EXTRA_LEVEL, batteryLevel) + putExtra(BatteryManager.EXTRA_SCALE, 100) + putExtra(BatteryManager.EXTRA_CHARGING_STATUS, chargingStatus) + putExtra(BatteryManager.EXTRA_PLUGGED, plugged) + } +} diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 9da1ab8ae69c..27a45dfc552e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -44,6 +44,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.LocalePicker; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; @@ -332,21 +333,30 @@ public class SettingsHelper { * @param value can be a canonicalized uri or "_silent" to indicate a silent (null) ringtone. */ private void setRingtone(String name, String value) { - // If it's null, don't change the default + Log.v(TAG, "Set ringtone for name: " + name + " value: " + value); + + // If it's null, don't change the default. if (value == null) return; - final Uri ringtoneUri; + final int ringtoneType = getRingtoneType(name); if (SILENT_RINGTONE.equals(value)) { - ringtoneUri = null; - } else { - Uri canonicalUri = Uri.parse(value); - ringtoneUri = mContext.getContentResolver().uncanonicalize(canonicalUri); - if (ringtoneUri == null) { - // Unrecognized or invalid Uri, don't restore - return; - } + // SILENT_RINGTONE is a special constant generated by onBackupValue in the source + // device. + RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, null); + return; + } + + Uri ringtoneUri = null; + try { + ringtoneUri = + RingtoneManager.getRingtoneUriForRestore( + mContext.getContentResolver(), value, ringtoneType); + } catch (FileNotFoundException | IllegalArgumentException e) { + Log.w(TAG, "Failed to resolve " + value + ": " + e); + // Unrecognized or invalid Uri, don't restore + return; } - final int ringtoneType = getRingtoneType(name); + Log.v(TAG, "setActualDefaultRingtoneUri type: " + ringtoneType + ", uri: " + ringtoneUri); RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, ringtoneUri); } diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java index bc81c4441af5..ef062dfd3ec3 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java @@ -26,15 +26,24 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.res.Resources; +import android.database.Cursor; +import android.database.MatrixCursor; import android.media.AudioManager; import android.net.Uri; +import android.os.Bundle; import android.os.LocaleList; +import android.provider.BaseColumns; +import android.provider.MediaStore; import android.provider.Settings; import android.telephony.TelephonyManager; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContentResolver; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -57,6 +66,13 @@ public class SettingsHelperTest { private static final String SETTING_VALUE = "setting_value"; private static final String SETTING_REAL_VALUE = "setting_real_value"; + private static final String DEFAULT_RINGTONE_VALUE = + "content://media/internal/audio/media/10?title=DefaultRingtone&canonical=1"; + private static final String DEFAULT_NOTIFICATION_VALUE = + "content://media/internal/audio/media/20?title=DefaultNotification&canonical=1"; + private static final String DEFAULT_ALARM_VALUE = + "content://media/internal/audio/media/30?title=DefaultAlarm&canonical=1"; + private SettingsHelper mSettingsHelper; @Mock private Context mContext; @@ -74,6 +90,7 @@ public class SettingsHelperTest { mTelephonyManager); when(mContext.getResources()).thenReturn(mResources); when(mContext.getApplicationContext()).thenReturn(mContext); + when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo()); when(mContext.getContentResolver()).thenReturn(getContentResolver()); mSettingsHelper = spy(new SettingsHelper(mContext)); @@ -338,6 +355,377 @@ public class SettingsHelperTest { } @Test + public void testRestoreValue_customRingtone_regularUncanonicalize_Success() { + final String sourceRingtoneValue = + "content://media/internal/audio/media/1?title=Song&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://media/internal/audio/media/100"; + final String newRingtoneValueCanonicalized = + "content://media/internal/audio/media/100?title=Song&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(sourceRingtoneValue)); + return Uri.parse(newRingtoneValueUncanonicalized); + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(DEFAULT_RINGTONE_VALUE); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_useCustomLookup_success() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/1?title=Song&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://0@media/external/audio/media/100"; + final String newRingtoneValueCanonicalized = + "content://0@media/external/audio/media/100?title=Song&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {100L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + assertThat(uri) + .isEqualTo(Uri.parse("content://0@media/external/audio/media")); + assertThat(projection).isEqualTo(new String[] {"_id"}); + assertThat(selection).isEqualTo("is_ringtone=1 AND title=?"); + assertThat(selectionArgs).isEqualTo(new String[] {"Song"}); + return cursor; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_notificationSound_useCustomLookup_success() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/2?title=notificationPing&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://0@media/external/audio/media/200"; + final String newRingtoneValueCanonicalized = + "content://0@media/external/audio/media/200?title=notificationPing&canonicalize=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {200L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + assertThat(uri) + .isEqualTo(Uri.parse("content://0@media/external/audio/media")); + assertThat(projection).isEqualTo(new String[] {"_id"}); + assertThat(selection).isEqualTo("is_notification=1 AND title=?"); + assertThat(selectionArgs).isEqualTo(new String[] {"notificationPing"}); + return cursor; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.NOTIFICATION_SOUND, + sourceRingtoneValue, + 0); + + assertThat( + Settings.System.getString( + mMockContentResolver, Settings.System.NOTIFICATION_SOUND)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_alarmSound_useCustomLookup_success() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/3?title=alarmSound&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://0@media/external/audio/media/300"; + final String newRingtoneValueCanonicalized = + "content://0@media/external/audio/media/300?title=alarmSound&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {300L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + assertThat(uri) + .isEqualTo(Uri.parse("content://0@media/external/audio/media")); + assertThat(projection).isEqualTo(new String[] {"_id"}); + assertThat(selection).isEqualTo("is_alarm=1 AND title=?"); + assertThat(selectionArgs).isEqualTo(new String[] {"alarmSound"}); + return cursor; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.ALARM_ALERT, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.ALARM_ALERT)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_useCustomLookup_multipleResults_notRestore() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/1?title=Song&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + // This is to mock the case that there are multiple results by querying title + + // ringtone_type. + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {100L}); + cursor.addRow(new Object[] {110L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(DEFAULT_RINGTONE_VALUE); + } + + @Test + public void testRestoreValue_customRingtone_restoreSilentValue() { + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + "_silent", + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(null); + } + + public static class MockSettingsProvider extends MockContentProvider { + ContentResolver mBaseContentResolver; + + public MockSettingsProvider(Context context, ContentResolver baseContentResolver) { + super(context); + this.mBaseContentResolver = baseContentResolver; + } + + @Override + public Bundle call(String method, String request, Bundle args) { + return mBaseContentResolver.call(Settings.AUTHORITY, method, request, args); + } + } + + @Test public void restoreValue_autoRotation_deviceStateAutoRotationDisabled_restoresValue() { when(mResources.getStringArray(R.array.config_perDeviceStateRotationLockDefaults)) .thenReturn(new String[]{}); @@ -400,4 +788,20 @@ public class SettingsHelperTest { Settings.Global.putString(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, null); Settings.Global.putString(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, null); } + + private void resetRingtoneSettingsToDefault(ContentResolver contentResolver) { + Settings.System.putString( + contentResolver, Settings.System.RINGTONE, DEFAULT_RINGTONE_VALUE); + Settings.System.putString( + contentResolver, Settings.System.NOTIFICATION_SOUND, DEFAULT_NOTIFICATION_VALUE); + Settings.System.putString( + contentResolver, Settings.System.ALARM_ALERT, DEFAULT_ALARM_VALUE); + + assertThat(Settings.System.getString(contentResolver, Settings.System.RINGTONE)) + .isEqualTo(DEFAULT_RINGTONE_VALUE); + assertThat(Settings.System.getString(contentResolver, Settings.System.NOTIFICATION_SOUND)) + .isEqualTo(DEFAULT_NOTIFICATION_VALUE); + assertThat(Settings.System.getString(contentResolver, Settings.System.ALARM_ALERT)) + .isEqualTo(DEFAULT_ALARM_VALUE); + } } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 851f976d5246..07f7ecd112e4 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -409,6 +409,7 @@ android_library { "mockito-target-extended-minus-junit4", "androidx.test.ext.junit", "androidx.test.ext.truth", + "kotlin-test", ], libs: [ "android.test.runner", diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 37b1ee543e46..187d0739b734 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -249,7 +249,7 @@ constructor( // intent is to launch a dialog from another dialog. val animatedParent = openedDialogs.firstOrNull { - it.dialog.window.decorView.viewRootImpl == controller.viewRoot + it.dialog.window?.decorView?.viewRootImpl == controller.viewRoot } val controller = animatedParent?.dialogContentWithBackground?.let { @@ -336,7 +336,7 @@ constructor( ): ActivityLaunchAnimator.Controller? { val animatedDialog = openedDialogs.firstOrNull { - it.dialog.window.decorView.viewRootImpl == view.viewRootImpl + it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl } ?: return null return createActivityLaunchController(animatedDialog, cujType) @@ -417,7 +417,7 @@ constructor( animatedDialog.prepareForStackDismiss() // Remove the dim. - dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) } override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { @@ -783,7 +783,7 @@ private class AnimatedDialog( } // Show the background dim. - dialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) startAnimation( isLaunching = true, @@ -863,7 +863,7 @@ private class AnimatedDialog( isLaunching = false, onLaunchAnimationStart = { // Remove the dim background as soon as we start the animation. - dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) }, onLaunchAnimationEnd = { val dialogContentWithBackground = this.dialogContentWithBackground!! diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index 1a03ede98d12..6c4b695ed709 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -206,8 +206,9 @@ constructor( return } - backgroundView = FrameLayout(launchContainer.context) - launchContainerOverlay.add(backgroundView) + backgroundView = FrameLayout(launchContainer.context).also { + launchContainerOverlay.add(it) + } // We wrap the ghosted view background and use it to draw the expandable background. Its // alpha will be set to 0 as soon as we start drawing the expanding background. @@ -319,7 +320,7 @@ constructor( backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha GhostView.removeGhost(ghostedView) - launchContainerOverlay.remove(backgroundView) + backgroundView?.let { launchContainerOverlay.remove(it) } if (ghostedView is LaunchableView) { // Restore the ghosted view visibility. diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt index 142fd21d4a16..d6eba2e7064d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt @@ -283,7 +283,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In animator.addListener( object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { + override fun onAnimationStart(animation: Animator, isReverse: Boolean) { if (DEBUG) { Log.d(TAG, "Animation started") } @@ -295,7 +295,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In launchContainerOverlay.add(windowBackgroundLayer) } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { if (DEBUG) { Log.d(TAG, "Animation ended") } 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 b555fa583588..8dc74951d332 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -42,7 +42,9 @@ interface TypefaceVariantCache { return baseTypeface } - val axes = FontVariationAxis.fromFontVariationSettings(fVar).toMutableList() + val axes = FontVariationAxis.fromFontVariationSettings(fVar) + ?.toMutableList() + ?: mutableListOf() axes.removeIf { !baseTypeface.isSupportedAxes(it.getOpenTypeTagValue()) } if (axes.isEmpty()) { return baseTypeface @@ -120,8 +122,8 @@ class TextAnimator( } addListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) = textInterpolator.rebase() - override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase() + override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase() + override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase() } ) } @@ -302,11 +304,11 @@ class TextAnimator( if (onAnimationEnd != null) { val listener = object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { onAnimationEnd.run() animator.removeListener(this) } - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { animator.removeListener(this) } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt index 38b99cc5f5ee..bd3706e1aff3 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt @@ -1046,7 +1046,7 @@ class ViewHierarchyAnimator { } } - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { cancelled = true } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt index c3f44f8b1069..f7ebe2fc6d34 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt @@ -37,7 +37,14 @@ sealed class Key(val name: String, val identity: Any) { } /** Key for a scene. */ -class SceneKey(name: String, identity: Any = Object()) : Key(name, identity) { +class SceneKey( + name: String, + identity: Any = Object(), +) : Key(name, identity) { + + /** The unique [ElementKey] identifying this scene's root element. */ + val rootElementKey = ElementKey(name, identity) + override fun toString(): String { return "SceneKey(name=$name)" } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt index 9752f53fbd49..f4e39023edfe 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -35,7 +35,7 @@ import com.android.compose.ui.util.fastMap /** The transitions configuration of a [SceneTransitionLayout]. */ class SceneTransitions( - val transitionSpecs: List<TransitionSpec>, + private val transitionSpecs: List<TransitionSpec>, ) { private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>() diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt index afd49b4fde09..48d5638e8b4e 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -75,7 +75,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { } } -private class TransitionBuilderImpl : TransitionBuilder { +internal class TransitionBuilderImpl : TransitionBuilder { val transformations = mutableListOf<Transformation>() override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index b9baa7930b4e..81b9eb067e7f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -24,7 +24,7 @@ import android.content.DialogInterface import androidx.compose.animation.Crossfade import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween -import androidx.compose.foundation.background +import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,6 +46,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.systemui.R import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel @@ -63,6 +64,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +object Bouncer { + object Elements { + val Background = ElementKey("BouncerBackground") + val Content = ElementKey("BouncerContent") + } +} + /** The bouncer scene displays authentication challenges like PIN, password, or pattern. */ @SysUISingleton class BouncerScene @@ -88,7 +96,7 @@ constructor( } @Composable -private fun BouncerScene( +private fun SceneScope.BouncerScene( viewModel: BouncerViewModel, dialogFactory: BouncerSceneDialogFactory, modifier: Modifier = Modifier, @@ -97,84 +105,90 @@ private fun BouncerScene( val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState() val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState() var dialog: Dialog? by remember { mutableStateOf(null) } + val backgroundColor = MaterialTheme.colorScheme.surface - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(60.dp), - modifier = - modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - .padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp) - ) { - Crossfade( - targetState = message, - label = "Bouncer message", - animationSpec = if (message.isUpdateAnimated) tween() else snap(), - ) { message -> - Text( - text = message.text, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyLarge, - ) + Box(modifier) { + Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) { + drawRect(color = backgroundColor) } - Box(Modifier.weight(1f)) { - when (val nonNullViewModel = authMethodViewModel) { - is PinBouncerViewModel -> - PinBouncer( - viewModel = nonNullViewModel, - modifier = Modifier.align(Alignment.Center), - ) - is PasswordBouncerViewModel -> - PasswordBouncer( - viewModel = nonNullViewModel, - modifier = Modifier.align(Alignment.Center), - ) - is PatternBouncerViewModel -> - PatternBouncer( - viewModel = nonNullViewModel, - modifier = - Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false) - .align(Alignment.BottomCenter), - ) - else -> Unit + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(60.dp), + modifier = + Modifier.element(Bouncer.Elements.Content) + .fillMaxSize() + .padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp) + ) { + Crossfade( + targetState = message, + label = "Bouncer message", + animationSpec = if (message.isUpdateAnimated) tween() else snap(), + ) { message -> + Text( + text = message.text, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) } - } - Button( - onClick = viewModel::onEmergencyServicesButtonClicked, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - ), - ) { - Text( - text = stringResource(com.android.internal.R.string.lockscreen_emergency_call), - style = MaterialTheme.typography.bodyMedium, - ) - } + Box(Modifier.weight(1f)) { + when (val nonNullViewModel = authMethodViewModel) { + is PinBouncerViewModel -> + PinBouncer( + viewModel = nonNullViewModel, + modifier = Modifier.align(Alignment.Center), + ) + is PasswordBouncerViewModel -> + PasswordBouncer( + viewModel = nonNullViewModel, + modifier = Modifier.align(Alignment.Center), + ) + is PatternBouncerViewModel -> + PatternBouncer( + viewModel = nonNullViewModel, + modifier = + Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false) + .align(Alignment.BottomCenter), + ) + else -> Unit + } + } + + Button( + onClick = viewModel::onEmergencyServicesButtonClicked, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + ), + ) { + Text( + text = stringResource(com.android.internal.R.string.lockscreen_emergency_call), + style = MaterialTheme.typography.bodyMedium, + ) + } - if (dialogMessage != null) { - if (dialog == null) { - dialog = - dialogFactory().apply { - setMessage(dialogMessage) - setButton( - DialogInterface.BUTTON_NEUTRAL, - context.getString(R.string.ok), - ) { _, _ -> - viewModel.onThrottlingDialogDismissed() + if (dialogMessage != null) { + if (dialog == null) { + dialog = + dialogFactory().apply { + setMessage(dialogMessage) + setButton( + DialogInterface.BUTTON_NEUTRAL, + context.getString(R.string.ok), + ) { _, _ -> + viewModel.onThrottlingDialogDismissed() + } + setCancelable(false) + setCanceledOnTouchOutside(false) + show() } - setCancelable(false) - setCanceledOnTouchOutside(false) - show() - } + } + } else { + dialog?.dismiss() + dialog = null } - } else { - dialog?.dismiss() - dialog = null } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 38b751c9445d..889c026a0568 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -31,9 +31,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope + +object Notifications { + object Elements { + val Notifications = ElementKey("Notifications") + } +} @Composable -fun Notifications( +fun SceneScope.Notifications( modifier: Modifier = Modifier, ) { // TODO(b/272779828): implement. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt index 1bb341c76e69..c84a5e91ca50 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt @@ -31,15 +31,27 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneScope + +object QuickSettings { + object Elements { + // TODO RENAME + val Content = ElementKey("QuickSettingsContent") + val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid") + val FooterActions = ElementKey("QuickSettingsFooterActions") + } +} @Composable -fun QuickSettings( +fun SceneScope.QuickSettings( modifier: Modifier = Modifier, ) { // TODO(b/272780058): implement. Column( modifier = modifier + .element(QuickSettings.Elements.Content) .fillMaxWidth() .defaultMinSize(minHeight = 300.dp) .clip(RoundedCornerShape(32.dp)) @@ -47,15 +59,19 @@ fun QuickSettings( .padding(16.dp), ) { Text( - text = "Quick settings", - modifier = Modifier.align(Alignment.CenterHorizontally), + text = "Quick settings grid", + modifier = + Modifier.element(QuickSettings.Elements.CollapsedGrid) + .align(Alignment.CenterHorizontally), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onPrimary, ) Spacer(modifier = Modifier.weight(1f)) Text( text = "QS footer actions", - modifier = Modifier.align(Alignment.CenterHorizontally), + modifier = + Modifier.element(QuickSettings.Elements.FooterActions) + .align(Alignment.CenterHorizontally), style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onPrimary, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 29763c2e329d..e5cd4397166e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -16,19 +16,17 @@ package com.android.systemui.qs.ui.composable -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.footer.ui.compose.QuickSettings import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey @@ -69,23 +67,18 @@ constructor( } @Composable -private fun QuickSettingsScene( +private fun SceneScope.QuickSettingsScene( viewModel: QuickSettingsSceneViewModel, modifier: Modifier = Modifier, ) { // TODO(b/280887232): implement the real UI. - Box(modifier = modifier) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.align(Alignment.Center) - ) { - Text("Quick settings", style = MaterialTheme.typography.headlineMedium) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") } - } - } + Box( + modifier + .fillMaxSize() + .clickable(onClick = { viewModel.onContentClicked() }) + .padding(horizontal = 16.dp, vertical = 48.dp) + ) { + QuickSettings(modifier = Modifier.fillMaxHeight()) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index f91baf298347..c865070b2c91 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -31,7 +31,6 @@ import com.android.compose.animation.scene.SceneTransitionLayoutState import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction import com.android.compose.animation.scene.observableTransitionState -import com.android.compose.animation.scene.transitions import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey @@ -78,7 +77,7 @@ fun SceneContainer( SceneTransitionLayout( currentScene = currentSceneKey.toTransitionSceneKey(), onChangeScene = viewModel::onSceneChanged, - transitions = transitions {}, + transitions = SceneContainerTransitions, state = state, modifier = modifier.fillMaxSize(), ) { @@ -98,7 +97,9 @@ fun SceneContainer( ) { with(composableScene) { this@scene.Content( - modifier = Modifier.fillMaxSize(), + modifier = + Modifier.element(sceneKey.toTransitionSceneKey().rootElementKey) + .fillMaxSize(), ) } } @@ -129,14 +130,6 @@ private fun toTransitionModels( } // TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout. -private fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey { - return SceneTransitionSceneKey( - name = toString(), - identity = this, - ) -} - -// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout. private fun SceneTransitionSceneKey.toModel(): SceneModel { return SceneModel(key = identity as SceneKey) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt new file mode 100644 index 000000000000..404bf81ee387 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -0,0 +1,34 @@ +package com.android.systemui.scene.ui.composable + +import com.android.compose.animation.scene.transitions +import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition +import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition +import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition +import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition +import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition +import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsTransition +import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition +import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettingsTransition + +/** + * Comprehensive definition of all transitions between scenes in [SceneContainer]. + * + * Transitions are automatically reversible, so define only one transition per scene pair. By + * convention, use the more common transition direction when defining the pair order, e.g. + * Lockscreen to Bouncer rather than Bouncer to Lockscreen. + * + * The actual transition DSL must be placed in a separate file under the package + * [com.android.systemui.scene.ui.composable.transitions]. + * + * Please keep the list sorted alphabetically. + */ +val SceneContainerTransitions = transitions { + from(Bouncer, to = Gone) { bouncerToGoneTransition() } + from(Gone, to = Shade) { goneToShadeTransition() } + from(Gone, to = QuickSettings) { goneToQuickSettingsTransition() } + from(Lockscreen, to = Bouncer) { lockscreenToBouncerTransition() } + from(Lockscreen, to = Shade) { lockscreenToShadeTransition() } + from(Lockscreen, to = QuickSettings) { lockscreenToQuickSettingsTransition() } + from(Lockscreen, to = Gone) { lockscreenToGoneTransition() } + from(Shade, to = QuickSettings) { shadeToQuickSettingsTransition() } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt new file mode 100644 index 000000000000..8d0d7050dcca --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt @@ -0,0 +1,15 @@ +package com.android.systemui.scene.ui.composable + +import com.android.compose.animation.scene.SceneKey as SceneTransitionSceneKey +import com.android.systemui.scene.shared.model.SceneKey + +val Lockscreen = SceneKey.Lockscreen.toTransitionSceneKey() +val Bouncer = SceneKey.Bouncer.toTransitionSceneKey() +val Shade = SceneKey.Shade.toTransitionSceneKey() +val QuickSettings = SceneKey.QuickSettings.toTransitionSceneKey() +val Gone = SceneKey.Gone.toTransitionSceneKey() + +// TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout. +fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey { + return SceneTransitionSceneKey(name = toString(), identity = this) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromBouncerToGoneTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromBouncerToGoneTransition.kt new file mode 100644 index 000000000000..1a9facea7518 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromBouncerToGoneTransition.kt @@ -0,0 +1,11 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.scene.ui.composable.Bouncer + +fun TransitionBuilder.bouncerToGoneTransition() { + spec = tween(durationMillis = 500) + + fade(Bouncer.rootElementKey) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt new file mode 100644 index 000000000000..38712b01ae44 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt @@ -0,0 +1,11 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.scene.ui.composable.QuickSettings + +fun TransitionBuilder.goneToQuickSettingsTransition() { + spec = tween(durationMillis = 500) + + fade(QuickSettings.rootElementKey) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt new file mode 100644 index 000000000000..1d57c1a377e5 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt @@ -0,0 +1,11 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.scene.ui.composable.Shade + +fun TransitionBuilder.goneToShadeTransition() { + spec = tween(durationMillis = 500) + + fade(Shade.rootElementKey) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt new file mode 100644 index 000000000000..1fee8741fe48 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt @@ -0,0 +1,14 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.bouncer.ui.composable.Bouncer + +fun TransitionBuilder.lockscreenToBouncerTransition() { + spec = tween(durationMillis = 500) + + translate(Bouncer.Elements.Content, y = 300.dp) + fractionRange(end = 0.5f) { fade(Bouncer.Elements.Background) } + fractionRange(start = 0.5f) { fade(Bouncer.Elements.Content) } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToGoneTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToGoneTransition.kt new file mode 100644 index 000000000000..da6306dc656d --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToGoneTransition.kt @@ -0,0 +1,11 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.scene.ui.composable.Lockscreen + +fun TransitionBuilder.lockscreenToGoneTransition() { + spec = tween(durationMillis = 500) + + fade(Lockscreen.rootElementKey) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt new file mode 100644 index 000000000000..9a8a3e2048d1 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt @@ -0,0 +1,11 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.scene.ui.composable.QuickSettings + +fun TransitionBuilder.lockscreenToQuickSettingsTransition() { + spec = tween(durationMillis = 500) + + fade(QuickSettings.rootElementKey) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt new file mode 100644 index 000000000000..7ecfb62c4f62 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt @@ -0,0 +1,24 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.notifications.ui.composable.Notifications +import com.android.systemui.qs.footer.ui.compose.QuickSettings +import com.android.systemui.shade.ui.composable.Shade + +fun TransitionBuilder.lockscreenToShadeTransition() { + spec = tween(durationMillis = 500) + + punchHole(Shade.Elements.QuickSettings, bounds = Shade.Elements.Scrim, Shade.Shapes.Scrim) + translate(Shade.Elements.Scrim, Edge.Top, startsOutsideLayoutBounds = false) + fractionRange(end = 0.5f) { + fade(Shade.Elements.ScrimBackground) + translate( + QuickSettings.Elements.CollapsedGrid, + Edge.Top, + startsOutsideLayoutBounds = false, + ) + } + fractionRange(start = 0.5f) { fade(Notifications.Elements.Notifications) } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt new file mode 100644 index 000000000000..6c7964b30989 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt @@ -0,0 +1,15 @@ +package com.android.systemui.scene.ui.composable.transitions + +import androidx.compose.animation.core.tween +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.notifications.ui.composable.Notifications +import com.android.systemui.qs.footer.ui.compose.QuickSettings + +fun TransitionBuilder.shadeToQuickSettingsTransition() { + spec = tween(durationMillis = 500) + + translate(Notifications.Elements.Notifications, Edge.Bottom) + fade(Notifications.Elements.Notifications) + timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index ff1cb5f1afa3..f985aa2a2aa0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -16,16 +16,22 @@ package com.android.systemui.shade.ui.composable +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -44,6 +50,26 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +object Shade { + object Elements { + val QuickSettings = ElementKey("ShadeQuickSettings") + val Scrim = ElementKey("ShadeScrim") + val ScrimBackground = ElementKey("ShadeScrimBackground") + } + + object Dimensions { + val ScrimCornerSize = 32.dp + } + + object Shapes { + val Scrim = + RoundedCornerShape( + topStart = Dimensions.ScrimCornerSize, + topEnd = Dimensions.ScrimCornerSize, + ) + } +} + /** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */ @SysUISingleton class ShadeScene @@ -79,20 +105,28 @@ constructor( } @Composable -private fun ShadeScene( +private fun SceneScope.ShadeScene( viewModel: ShadeSceneViewModel, modifier: Modifier = Modifier, ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = - modifier - .fillMaxSize() - .clickable(onClick = { viewModel.onContentClicked() }) - .padding(horizontal = 16.dp, vertical = 48.dp) - ) { - QuickSettings(modifier = Modifier.height(160.dp)) - Notifications(modifier = Modifier.weight(1f)) + Box(modifier.element(Shade.Elements.Scrim)) { + Spacer( + modifier = + Modifier.element(Shade.Elements.ScrimBackground) + .fillMaxSize() + .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim) + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = + Modifier.fillMaxSize() + .clickable(onClick = { viewModel.onContentClicked() }) + .padding(horizontal = 16.dp, vertical = 48.dp) + ) { + QuickSettings(modifier = Modifier.height(160.dp)) + Notifications(modifier = Modifier.weight(1f)) + } } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java index cf7d2c57923c..3d9645a3d983 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java @@ -58,9 +58,26 @@ public interface VolumeDialogController { void userActivity(); void getState(); - boolean areCaptionsEnabled(); - void setCaptionsEnabled(boolean isEnabled); - + /** + * Get Captions enabled state + * + * @param checkForSwitchState set true when we'd like to switch captions enabled state after + * getting the latest captions state. + */ + void getCaptionsEnabledState(boolean checkForSwitchState); + + /** + * Set Captions enabled state + * + * @param enabled the captions enabled state we'd like to update. + */ + void setCaptionsEnabledState(boolean enabled); + + /** + * Get Captions component state + * + * @param fromTooltip if it's triggered from tooltip. + */ void getCaptionsComponentState(boolean fromTooltip); @ProvidesInterface(version = StreamState.VERSION) @@ -192,7 +209,22 @@ public interface VolumeDialogController { void onScreenOff(); void onShowSafetyWarning(int flags); void onAccessibilityModeChanged(Boolean showA11yStream); + + /** + * Callback function for captions component state changed event + * + * @param isComponentEnabled the lateset captions component state. + * @param fromTooltip if it's triggered from tooltip. + */ void onCaptionComponentStateChanged(Boolean isComponentEnabled, Boolean fromTooltip); + + /** + * Callback function for captions enabled state changed event + * + * @param isEnabled the lateset captions enabled state. + * @param checkBeforeSwitch intend to switch captions enabled state after the callback. + */ + void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch); // requires version 2 void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs); } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt index f83fa33caa04..affb76b79d2e 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt @@ -57,7 +57,7 @@ constructor( private fun readIntFromBundle(extras: Bundle, key: String): Int? = try { - extras.getString(key).toInt() + extras.getString(key)?.toInt() } catch (e: Exception) { null } diff --git a/packages/SystemUI/res-keyguard/layout-land/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout-land/keyguard_pin_view.xml new file mode 100644 index 000000000000..cd7ab986844c --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout-land/keyguard_pin_view.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +** +** Copyright 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. +*/ +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include layout="@layout/keyguard_pin_view_landscape" /> + +</FrameLayout> diff --git a/packages/SystemUI/res-keyguard/layout-sw600dp-land/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout-sw600dp-land/keyguard_pin_view.xml new file mode 100644 index 000000000000..80cc8c06680a --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout-sw600dp-land/keyguard_pin_view.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +** +** Copyright 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. +*/ +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include layout="@layout/keyguard_pin_view_portrait" /> + +</FrameLayout> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view_landscape.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view_landscape.xml new file mode 100644 index 000000000000..e00742d80017 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view_landscape.xml @@ -0,0 +1,231 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +** +** Copyright 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. +*/ +--> + +<com.android.keyguard.KeyguardPINView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/keyguard_pin_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center_horizontal|bottom" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="horizontal"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="2" + android:clipChildren="false" + android:clipToPadding="false" + android:layoutDirection="ltr" + android:orientation="vertical"> + + <include layout="@layout/keyguard_bouncer_message_area" /> + + <com.android.systemui.bouncer.ui.BouncerMessageView + android:id="@+id/bouncer_message_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + androidprv:layout_constraintBottom_toTopOf="@+id/row0" + androidprv:layout_constraintTop_toTopOf="parent" + androidprv:layout_constraintVertical_chainStyle="packed" /> + + <!-- Set this to be just above key1. It would be better to introduce a barrier above + key1/key2/key3, then place this View above that. Sadly, that doesn't work (the Barrier + drops to the bottom of the page, and key1/2/3 all shoot up to the top-left). In any + case, the Flow should ensure that key1/2/3 all have the same top, so this should be + fine. --> + <com.android.keyguard.AlphaOptimizedRelativeLayout + android:id="@+id/row0" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="@dimen/num_pad_entry_row_margin_bottom" + androidprv:layout_constraintBottom_toTopOf="@+id/keyguard_selector_fade_container" + androidprv:layout_constraintTop_toBottomOf="@+id/bouncer_message_view" + tools:layout_editor_absoluteX="-16dp"> + + <com.android.keyguard.PasswordTextView + android:id="@+id/pinEntry" + style="@style/Widget.TextView.Password" + android:layout_width="@dimen/keyguard_security_width" + android:layout_height="@dimen/keyguard_password_height" + android:layout_centerHorizontal="true" + android:layout_marginRight="72dp" + android:contentDescription="@string/keyguard_accessibility_pin_area" + androidprv:scaledTextSize="@integer/scaled_password_text_size" /> + </com.android.keyguard.AlphaOptimizedRelativeLayout> + + <include + android:id="@+id/keyguard_selector_fade_container" + layout="@layout/keyguard_eca" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom|center_horizontal" + android:layout_marginBottom="@dimen/keyguard_eca_bottom_margin" + android:layout_marginTop="@dimen/keyguard_eca_top_margin" + android:gravity="center_horizontal" + android:orientation="vertical" + androidprv:layout_constraintBottom_toBottomOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/pin_container" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="3" + android:clipChildren="false" + android:clipToPadding="false" + android:layoutDirection="ltr" + android:orientation="vertical"> + + <!-- Guideline used to place the top row of keys relative to the screen height. This will be + updated in KeyguardPINView to reduce the height of the PIN pad. --> + <androidx.constraintlayout.widget.Guideline + android:id="@+id/pin_pad_top_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + androidprv:layout_constraintGuide_percent="0" /> + + <com.android.keyguard.KeyguardPinFlowView + android:id="@+id/flow1" + android:layout_width="0dp" + android:layout_height="0dp" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="horizontal" + + androidprv:constraint_referenced_ids="key1,key2,key3,key4,key5,key6,key7,key8,key9,delete_button,key0,key_enter" + + androidprv:flow_horizontalGap="@dimen/num_pad_key_margin_end" + + androidprv:flow_horizontalStyle="packed" + androidprv:flow_maxElementsWrap="3" + + androidprv:flow_verticalBias="0.5" + androidprv:flow_verticalGap="@dimen/num_pad_entry_row_margin_bottom" + androidprv:flow_verticalStyle="packed" + + androidprv:flow_wrapMode="aligned" + androidprv:layout_constraintBottom_toBottomOf="parent" + androidprv:layout_constraintEnd_toEndOf="parent" + androidprv:layout_constraintStart_toStartOf="parent" + androidprv:layout_constraintTop_toBottomOf="@id/pin_pad_top_guideline" /> + + <com.android.keyguard.NumPadButton + android:id="@+id/delete_button" + style="@style/NumPadKey.Delete" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key0" + android:contentDescription="@string/keyboardview_keycode_delete" /> + + <com.android.keyguard.NumPadButton + android:id="@+id/key_enter" + style="@style/NumPadKey.Enter" + android:layout_width="0dp" + android:layout_height="0dp" + android:contentDescription="@string/keyboardview_keycode_enter" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key1" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key2" + androidprv:digit="1" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key2" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key3" + androidprv:digit="2" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key3" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key4" + androidprv:digit="3" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key4" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key5" + androidprv:digit="4" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key5" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key6" + androidprv:digit="5" + androidprv:textView="@+id/pinEntry" /> + + + <com.android.keyguard.NumPadKey + android:id="@+id/key6" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key7" + androidprv:digit="6" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key7" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key8" + androidprv:digit="7" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key8" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key9" + androidprv:digit="8" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key9" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/delete_button" + androidprv:digit="9" + androidprv:textView="@+id/pinEntry" /> + + <com.android.keyguard.NumPadKey + android:id="@+id/key0" + android:layout_width="0dp" + android:layout_height="0dp" + android:accessibilityTraversalBefore="@id/key_enter" + androidprv:digit="0" + androidprv:textView="@+id/pinEntry" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</com.android.keyguard.KeyguardPINView> diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 36f7b96a267c..66c57fc2a9ac 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -22,6 +22,42 @@ android:layout_width="match_parent" android:outlineProvider="none" > + <LinearLayout + android:id="@id/keyguard_indication_area" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/keyguard_indication_margin_bottom" + android:layout_gravity="bottom|center_horizontal" + android:orientation="vertical"> + + <com.android.systemui.statusbar.phone.KeyguardIndicationTextView + android:id="@id/keyguard_indication_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:paddingStart="@dimen/keyguard_indication_text_padding" + android:paddingEnd="@dimen/keyguard_indication_text_padding" + android:textAppearance="@style/TextAppearance.Keyguard.BottomArea" + android:accessibilityLiveRegion="polite"/> + + <com.android.systemui.statusbar.phone.KeyguardIndicationTextView + android:id="@id/keyguard_indication_text_bottom" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:minHeight="@dimen/keyguard_indication_text_min_height" + android:layout_gravity="center_horizontal" + android:paddingStart="@dimen/keyguard_indication_text_padding" + android:paddingEnd="@dimen/keyguard_indication_text_padding" + android:textAppearance="@style/TextAppearance.Keyguard.BottomArea" + android:maxLines="2" + android:ellipsize="end" + android:alpha=".8" + android:accessibilityLiveRegion="polite" + android:visibility="gone"/> + + </LinearLayout> + <com.android.systemui.animation.view.LaunchableImageView android:id="@+id/start_button" android:layout_height="@dimen/keyguard_affordance_fixed_height" diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 7e892f7e4800..d85e0122f2b4 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -21,9 +21,11 @@ <dimen name="status_bar_header_height_keyguard">@dimen/status_bar_height</dimen> <!-- padding for container with status icons and battery --> - <dimen name="status_bar_icons_padding_end">12dp</dimen> + <dimen name="status_bar_icons_padding_end">4dp</dimen> <!-- start padding is smaller to account for status icon margins coming from drawable itself --> - <dimen name="status_bar_icons_padding_start">11dp</dimen> + <dimen name="status_bar_icons_padding_start">3dp</dimen> + <dimen name="status_bar_icons_padding_bottom">2dp</dimen> + <dimen name="status_bar_icons_padding_top">2dp</dimen> <dimen name="status_bar_padding_end">0dp</dimen> @@ -78,8 +80,8 @@ <dimen name="large_screen_shade_header_height">42dp</dimen> <!-- start padding is smaller to account for status icon margins coming from drawable itself --> - <dimen name="shade_header_system_icons_padding_start">11dp</dimen> - <dimen name="shade_header_system_icons_padding_end">12dp</dimen> + <dimen name="shade_header_system_icons_padding_start">3dp</dimen> + <dimen name="shade_header_system_icons_padding_end">4dp</dimen> <!-- Lockscreen shade transition values --> <dimen name="lockscreen_shade_transition_by_tap_distance">200dp</dimen> diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml index d74eca6e127d..dc1f0e4b6069 100644 --- a/packages/SystemUI/res/values-sw720dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp/dimens.xml @@ -16,9 +16,6 @@ */ --> <resources> - <!-- it's a bit smaller on 720dp to account for status_bar_icon_horizontal_margin --> - <dimen name="status_bar_icons_padding_start">10dp</dimen> - <!-- gap on either side of status bar notification icons --> <dimen name="status_bar_icon_horizontal_margin">1sp</dimen> @@ -30,9 +27,6 @@ <dimen name="large_screen_shade_header_height">56dp</dimen> - <!-- it's a bit smaller on 720dp to account for status_bar_icon_horizontal_margin --> - <dimen name="shade_header_system_icons_padding_start">10dp</dimen> - <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> <dimen name="biometric_auth_pattern_view_size">348dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index b6ef5947bcaf..4aad6e7485a1 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -774,6 +774,9 @@ <!-- Flag to enable privacy dot views, it shall be true for normal case --> <bool name="config_enablePrivacyDot">true</bool> + <!-- Flag to enable privacy chip animation, it shall be true for normal case --> + <bool name="config_enablePrivacyChipAnimation">true</bool> + <!-- Class for the communal source connector to be used --> <string name="config_communalSourceConnector" translatable="false"></string> @@ -914,4 +917,7 @@ "$packageName" part that will be replaced by the code with the package name of the target app. --> <string name="config_appStoreAppLinkTemplate" translatable="false"></string> + + <!-- Flag controlling whether visual query attention detection has been enabled. --> + <bool name="config_enableVisualQueryAttentionDetection">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a0564450381b..55978e65e85b 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -351,9 +351,9 @@ <!-- paddings for container with status icons and battery --> <!-- padding start is a bit smaller than end to account for status icon margin--> - <dimen name="status_bar_icons_padding_start">11dp</dimen> + <dimen name="status_bar_icons_padding_start">3dp</dimen> - <dimen name="status_bar_icons_padding_end">0dp</dimen> + <dimen name="status_bar_icons_padding_end">4dp</dimen> <dimen name="status_bar_icons_padding_bottom">0dp</dimen> <dimen name="status_bar_icons_padding_top">0dp</dimen> @@ -364,7 +364,7 @@ <dimen name="status_bar_padding_start">8dp</dimen> <!-- the padding on the end of the statusbar --> - <dimen name="status_bar_padding_end">8dp</dimen> + <dimen name="status_bar_padding_end">4dp</dimen> <!-- the padding on the top of the statusbar (usually 0) --> <dimen name="status_bar_padding_top">0dp</dimen> @@ -1607,7 +1607,7 @@ <!-- Status bar user chip --> <dimen name="status_bar_user_chip_avatar_size">16dp</dimen> <!-- below also works as break between user chip and hover state of status icons --> - <dimen name="status_bar_user_chip_end_margin">4dp</dimen> + <dimen name="status_bar_user_chip_end_margin">8dp</dimen> <dimen name="status_bar_user_chip_text_size">12sp</dimen> <!-- System UI Dialog --> diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index c2dba6c41d12..261b08d4356f 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -40,4 +40,7 @@ <!-- Whether face auth will immediately stop when the display state is OFF --> <bool name="flag_stop_face_auth_on_display_off">false</bool> + + <!-- Whether we want to stop pulsing while running the face scanning animation --> + <bool name="flag_stop_pulsing_face_scanning_animation">true</bool> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6ea8df8f5c6c..cddfda2f9ce7 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2406,6 +2406,8 @@ <string name="magnification_open_settings_click_label">Open magnification settings</string> <!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] --> <string name="magnification_close_settings_click_label">Close magnification settings</string> + <!-- Click action label for exiting magnifier edit mode. [CHAR LIMIT=NONE] --> + <string name="magnification_exit_edit_mode_click_label">Exit edit mode</string> <!-- Label of the corner of a rectangle that you can tap and drag to resize the magnification area. [CHAR LIMIT=NONE] --> <string name="magnification_drag_corner_to_resize">Drag corner to resize</string> diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml index cb2c3a19a6d5..2ec6180513bf 100644 --- a/packages/SystemUI/res/xml/large_screen_shade_header.xml +++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml @@ -60,7 +60,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/privacy_container" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintEnd_toEndOf="@id/carrier_group"/> + app:layout_constraintStart_toEndOf="@id/carrier_group"/> <PropertySet android:alpha="1" /> </Constraint> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt index c1429335292f..5edd283bf131 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt @@ -27,6 +27,6 @@ object ActivityManagerKt { */ fun ActivityManager.isInForeground(packageName: String): Boolean { val tasks: List<ActivityManager.RunningTaskInfo> = getRunningTasks(1) - return tasks.isNotEmpty() && packageName == tasks[0].topActivity.packageName + return tasks.isNotEmpty() && packageName == tasks[0].topActivity?.packageName } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt index d7e61d60aa55..ebc57d2f8af7 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt @@ -31,15 +31,15 @@ class SmartspaceState() : Parcelable { var visibleOnScreen = false constructor(parcel: Parcel) : this() { - this.boundsOnScreen = parcel.readParcelable(Rect::javaClass.javaClass.classLoader) + this.boundsOnScreen = parcel.readParcelable(Rect::javaClass.javaClass.classLoader) ?: Rect() this.selectedPage = parcel.readInt() this.visibleOnScreen = parcel.readBoolean() } - override fun writeToParcel(dest: Parcel?, flags: Int) { - dest?.writeParcelable(boundsOnScreen, 0) - dest?.writeInt(selectedPage) - dest?.writeBoolean(visibleOnScreen) + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeParcelable(boundsOnScreen, 0) + dest.writeInt(selectedPage) + dest.writeBoolean(visibleOnScreen) } override fun describeContents(): Int { diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt index aca9907fec1b..dac130d684bd 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt @@ -39,7 +39,7 @@ class NaturalRotationUnfoldProgressProvider( fun init() { rotationChangeProvider.addCallback(rotationListener) - rotationListener.onRotationChanged(context.display.rotation) + context.display?.rotation?.let { rotationListener.onRotationChanged(it) } } private val rotationListener = RotationListener { rotation -> diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt index 78a5c98f45c8..495367b69123 100644 --- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt +++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt @@ -105,7 +105,7 @@ open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : hideAnimator.addListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super@BouncerKeyguardMessageArea.setMessage(msg, animate) } } @@ -118,7 +118,7 @@ open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : showAnimator.addListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { textAboutToShow = null } } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 4a6e53d304f4..4f4eec60a117 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -144,7 +144,7 @@ constructor( smallClockOnAttachStateChangeListener = object : OnAttachStateChangeListener { var pastVisibility: Int? = null - override fun onViewAttachedToWindow(view: View?) { + override fun onViewAttachedToWindow(view: View) { value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) if (view != null) { smallClockFrame = view.parent as FrameLayout @@ -168,7 +168,7 @@ constructor( } } - override fun onViewDetachedFromWindow(p0: View?) { + override fun onViewDetachedFromWindow(p0: View) { smallClockFrame?.viewTreeObserver ?.removeOnGlobalLayoutListener(onGlobalLayoutListener) } @@ -178,10 +178,10 @@ constructor( largeClockOnAttachStateChangeListener = object : OnAttachStateChangeListener { - override fun onViewAttachedToWindow(p0: View?) { + override fun onViewAttachedToWindow(p0: View) { value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) } - override fun onViewDetachedFromWindow(p0: View?) { + override fun onViewDetachedFromWindow(p0: View) { } } value.largeClock.view diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index b81e08183cdc..e3f9de11bf98 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -102,6 +102,12 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey super.onViewAttached(); mView.setKeyDownListener(mKeyDownListener); mEmergencyButtonController.setEmergencyButtonCallback(mEmergencyButtonCallback); + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline( + KeyguardUpdateMonitor.getCurrentUser()); + if (shouldLockout(deadline)) { + handleAttemptLockout(deadline); + } } @Override @@ -278,12 +284,6 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey @Override public void onResume(int reason) { mResumed = true; - // if the user is currently locked out, enforce it. - long deadline = mLockPatternUtils.getLockoutAttemptDeadline( - KeyguardUpdateMonitor.getCurrentUser()); - if (shouldLockout(deadline)) { - handleAttemptLockout(deadline); - } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 20e465693108..42dbc487d774 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -30,13 +30,13 @@ import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; +import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; +import com.android.systemui.bouncer.ui.BouncerMessageView; +import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; -import com.android.systemui.bouncer.ui.BouncerMessageView; -import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder; import com.android.systemui.log.BouncerLogger; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.ViewController; @@ -95,6 +95,12 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> @CallSuper protected void onViewAttached() { updateMessageAreaVisibility(); + if (TextUtils.isEmpty(mMessageAreaController.getMessage()) + && getInitialMessageResId() != 0) { + mMessageAreaController.setMessage( + mView.getResources().getString(getInitialMessageResId()), + /* animate= */ false); + } } private void updateMessageAreaVisibility() { @@ -147,12 +153,6 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } public void startAppearAnimation() { - if (TextUtils.isEmpty(mMessageAreaController.getMessage()) - && getInitialMessageResId() != 0) { - mMessageAreaController.setMessage( - mView.getResources().getString(getInitialMessageResId()), - /* animate= */ false); - } mView.startAppearAnimation(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 49f788ce8f75..a30b4479fe95 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -238,6 +238,12 @@ public class KeyguardPatternViewController } mView.onDevicePostureChanged(mPostureController.getDevicePosture()); mPostureController.addCallback(mPostureCallback); + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline( + KeyguardUpdateMonitor.getCurrentUser()); + if (deadline != 0) { + handleAttemptLockout(deadline); + } } @Override @@ -268,12 +274,6 @@ public class KeyguardPatternViewController @Override public void onResume(int reason) { super.onResume(reason); - // if the user is currently locked out, enforce it. - long deadline = mLockPatternUtils.getLockoutAttemptDeadline( - KeyguardUpdateMonitor.getCurrentUser()); - if (deadline != 0) { - handleAttemptLockout(deadline); - } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index b3e08c0bc69f..574a0591bd51 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -79,6 +79,10 @@ public class KeyguardPinViewController mPasswordEntry.setUserActivityListener(this::onUserInput); mView.onDevicePostureChanged(mPostureController.getDevicePosture()); mPostureController.addCallback(mPostureCallback); + if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) { + mPasswordEntry.setUsePinShapes(true); + updateAutoConfirmationState(); + } } protected void onUserInput() { @@ -100,10 +104,6 @@ public class KeyguardPinViewController @Override public void startAppearAnimation() { - if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) { - mPasswordEntry.setUsePinShapes(true); - updateAutoConfirmationState(); - } super.startAppearAnimation(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 42a4e7202c82..42dceb82a9a7 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -100,6 +100,7 @@ import com.android.systemui.Gefingerpoken; import com.android.systemui.R; import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.shade.TouchLogger; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter; import com.android.systemui.statusbar.policy.UserSwitcherController; @@ -666,6 +667,11 @@ public class KeyguardSecurityContainer extends ConstraintLayout { } @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch(TAG, ev, super.dispatchTouchEvent(ev)); + } + + @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mViewMediatorCallback != null) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 4e1cbc737d5f..94d26bdcd083 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -33,7 +33,6 @@ import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.content.Intent; import android.content.res.ColorStateList; -import android.content.res.Configuration; import android.content.res.Resources; import android.hardware.biometrics.BiometricOverlayConstants; import android.media.AudioManager; @@ -69,6 +68,7 @@ import com.android.keyguard.dagger.KeyguardBouncerScope; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor; import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate; import com.android.systemui.biometrics.SideFpsController; import com.android.systemui.biometrics.SideFpsUiRequestSource; @@ -78,11 +78,10 @@ import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.scene.domain.interactor.SceneInteractor; -import com.android.systemui.scene.shared.model.SceneKey; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -128,6 +127,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor; private final BouncerMessageInteractor mBouncerMessageInteractor; private int mTranslationY; + private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; // Whether the volume keys should be handled by keyguard. If true, then // they will be handled here for specific media types such as music, otherwise // the audio service will bring up the volume dialog. @@ -143,11 +143,22 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private Runnable mCancelAction; private boolean mWillRunDismissFromKeyguard; - private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED; + private int mLastOrientation; private SecurityMode mCurrentSecurityMode = SecurityMode.Invalid; + private int mCurrentUser = UserHandle.USER_NULL; private UserSwitcherController.UserSwitchCallback mUserSwitchCallback = - () -> showPrimarySecurityScreen(false); + new UserSwitcherController.UserSwitchCallback() { + @Override + public void onUserSwitched() { + if (mCurrentUser == KeyguardUpdateMonitor.getCurrentUser()) { + return; + } + mCurrentUser = KeyguardUpdateMonitor.getCurrentUser(); + showPrimarySecurityScreen(false); + reinflateViewFlipper((l) -> {}); + } + }; @VisibleForTesting final Gefingerpoken mGlobalTouchListener = new Gefingerpoken() { @@ -301,6 +312,10 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mViewMediatorCallback.keyguardDone(fromPrimaryAuth, targetUserId); } } + + if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + mKeyguardTransitionInteractor.startDismissKeyguardTransition(); + } } @Override @@ -339,7 +354,6 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onThemeChanged() { reloadColors(); - reset(); } @Override @@ -349,7 +363,14 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onDensityOrFontScaleChanged() { - KeyguardSecurityContainerController.this.onDensityOrFontScaleChanged(); + KeyguardSecurityContainerController.this + .onDensityOrFontScaleOrOrientationChanged(); + } + + @Override + public void onOrientationChanged(int orientation) { + KeyguardSecurityContainerController.this + .onDensityOrFontScaleOrOrientationChanged(); } }; private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = @@ -388,7 +409,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } }; private final UserInteractor mUserInteractor; - private final Provider<SceneInteractor> mSceneInteractor; + private final Provider<AuthenticationInteractor> mAuthenticationInteractor; private final Provider<JavaAdapter> mJavaAdapter; @Nullable private Job mSceneTransitionCollectionJob; @@ -419,7 +440,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard Provider<JavaAdapter> javaAdapter, UserInteractor userInteractor, FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, - Provider<SceneInteractor> sceneInteractor + KeyguardTransitionInteractor keyguardTransitionInteractor, + Provider<AuthenticationInteractor> authenticationInteractor ) { super(view); view.setAccessibilityDelegate(faceAuthAccessibilityDelegate); @@ -448,8 +470,9 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; mBouncerMessageInteractor = bouncerMessageInteractor; mUserInteractor = userInteractor; - mSceneInteractor = sceneInteractor; + mAuthenticationInteractor = authenticationInteractor; mJavaAdapter = javaAdapter; + mKeyguardTransitionInteractor = keyguardTransitionInteractor; } @Override @@ -474,19 +497,21 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard showPrimarySecurityScreen(false); if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) { - // When the scene framework transitions from bouncer to gone, we dismiss the keyguard. + // When the scene framework says that the lockscreen has been dismissed, dismiss the + // keyguard here, revealing the underlying app or launcher: mSceneTransitionCollectionJob = mJavaAdapter.get().alwaysCollectFlow( - mSceneInteractor.get().finishedSceneTransitions( - /* from= */ SceneKey.Bouncer.INSTANCE, - /* to= */ SceneKey.Gone.INSTANCE), - unused -> { - final int selectedUserId = mUserInteractor.getSelectedUserId(); - showNextSecurityScreenOrFinish( + mAuthenticationInteractor.get().isLockscreenDismissed(), + isLockscreenDismissed -> { + if (isLockscreenDismissed) { + final int selectedUserId = mUserInteractor.getSelectedUserId(); + showNextSecurityScreenOrFinish( /* authenticated= */ true, selectedUserId, /* bypassSecondaryLockScreen= */ true, mSecurityModel.getSecurityMode(selectedUserId)); - }); + } + } + ); } } @@ -833,8 +858,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); boolean isLockscreenDisabled = mLockPatternUtils.isLockScreenDisabled( KeyguardUpdateMonitor.getCurrentUser()); - - if (securityMode == SecurityMode.None) { + if (securityMode == SecurityMode.None || isLockscreenDisabled) { finish = isLockscreenDisabled; eventSubtype = BOUNCER_DISMISS_SIM; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_SIM; @@ -1150,11 +1174,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } private void reloadColors() { - reinflateViewFlipper(controller -> mView.reloadColors()); + mView.reloadColors(); } /** Handles density or font scale changes. */ - private void onDensityOrFontScaleChanged() { + private void onDensityOrFontScaleOrOrientationChanged() { reinflateViewFlipper(controller -> mView.onDensityOrFontScaleChanged()); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt index 96ac8ad56651..e1c060f0e8cb 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt @@ -66,11 +66,11 @@ class KeyguardSecurityViewTransition : Transition() { } override fun createAnimator( - sceneRoot: ViewGroup?, + sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues? ): Animator? { - if (sceneRoot == null || startValues == null || endValues == null) { + if (startValues == null || endValues == null) { return null } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index 75852793c24d..5774e42d2425 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -23,12 +23,14 @@ import android.graphics.Canvas; import android.os.Build; import android.os.Trace; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; import android.widget.GridLayout; import com.android.systemui.R; +import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.CrossFadeHelper; import java.io.PrintWriter; @@ -110,6 +112,11 @@ public class KeyguardStatusView extends GridLayout { } } + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch(TAG, ev, super.dispatchTouchEvent(ev)); + } + public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardStatusView:"); pw.println(" mDarkAmount: " + mDarkAmount); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 73b4c5f47cde..757022dd153b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -543,7 +543,8 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV @Nullable @Override - public Animator createAnimator(ViewGroup sceneRoot, @Nullable TransitionValues startValues, + public Animator createAnimator(@NonNull ViewGroup sceneRoot, + @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) { if (startValues == null || endValues == null) { return null; diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconView.java b/packages/SystemUI/src/com/android/keyguard/LockIconView.java index 1a0c7f97e146..8611dbbbcb70 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconView.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconView.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.Point; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; @@ -154,11 +155,16 @@ public class LockIconView extends FrameLayout implements Dumpable { } float getLocationTop() { - return mLockIconCenter.y - mRadius; + Rect r = new Rect(); + mLockIcon.getGlobalVisibleRect(r); + return r.top; } float getLocationBottom() { - return mLockIconCenter.y + mRadius; + Rect r = new Rect(); + mLockIcon.getGlobalVisibleRect(r); + return r.bottom; + } /** diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java index a04a48db5d7a..e77341651a8e 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java @@ -58,6 +58,7 @@ class NumPadAnimator { private float mStartRadius; private float mEndRadius; private int mHeight; + private int mWidth; private static final int EXPAND_ANIMATION_MS = 100; private static final int EXPAND_COLOR_ANIMATION_MS = 50; @@ -95,11 +96,17 @@ class NumPadAnimator { mBackground.setCornerRadius(mEndRadius + (mStartRadius - mEndRadius) * progress); int height = (int) (mHeight * 0.7f + mHeight * 0.3 * progress); int difference = mHeight - height; - mBackground.setBounds(0, difference / 2, mHeight, mHeight - difference / 2); + + int left = 0; + int top = difference / 2; + int right = mWidth; + int bottom = mHeight - difference / 2; + mBackground.setBounds(left, top, right, bottom); } - void onLayout(int height) { + void onLayout(int width, int height) { boolean shouldUpdateHeight = height != mHeight; + mWidth = width; mHeight = height; mStartRadius = height / 2f; mEndRadius = height / 4f; @@ -121,7 +128,7 @@ class NumPadAnimator { ContextThemeWrapper ctw = new ContextThemeWrapper(context, mStyle); @SuppressLint("ResourceType") TypedArray a = ctw.obtainStyledAttributes(customAttrs); mNormalBackgroundColor = getPrivateAttrColorIfUnset(ctw, a, 0, 0, - NUM_PAD_BACKGROUND); + NUM_PAD_BACKGROUND); a.recycle(); mPressedBackgroundColor = getColorAttrDefaultColor(context, NUM_PAD_BACKGROUND_PRESSED); diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 3f1741a67a76..5c2f3b368f96 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -74,8 +74,9 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); - - if (mAnimator != null) mAnimator.onLayout(b - t); + int width = r - l; + int height = b - t; + if (mAnimator != null) mAnimator.onLayout(width, height); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java index edc298cde032..466d15474679 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java @@ -211,7 +211,9 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener { left = centerX - mKlondikeText.getMeasuredWidth() / 2; mKlondikeText.layout(left, top, left + mKlondikeText.getMeasuredWidth(), bottom); - if (mAnimator != null) mAnimator.onLayout(b - t); + int width = r - l; + int height = b - t; + if (mAnimator != null) mAnimator.onLayout(width, height); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 04acd0b91173..27b9056da81c 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -131,14 +131,14 @@ import com.android.systemui.util.leak.LeakDetector; import com.android.systemui.util.leak.LeakReporter; import com.android.systemui.util.sensors.AsyncSensorManager; -import dagger.Lazy; - import java.util.concurrent.Executor; import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; +import dagger.Lazy; + /** * Class to handle ugly dependencies throughout sysui until we determine the * long-term dependency injection solution. @@ -280,7 +280,6 @@ public class Dependency { @Inject Lazy<AccessibilityManagerWrapper> mAccessibilityManagerWrapper; @Inject Lazy<SysuiColorExtractor> mSysuiColorExtractor; @Inject Lazy<TunablePaddingService> mTunablePaddingService; - @Inject Lazy<ForegroundServiceController> mForegroundServiceController; @Inject Lazy<UiOffloadThread> mUiOffloadThread; @Inject Lazy<PowerUI.WarningsUI> mWarningsUI; @Inject Lazy<LightBarController> mLightBarController; @@ -458,8 +457,6 @@ public class Dependency { mProviders.put(TunablePaddingService.class, mTunablePaddingService::get); - mProviders.put(ForegroundServiceController.class, mForegroundServiceController::get); - mProviders.put(UiOffloadThread.class, mUiOffloadThread::get); mProviders.put(PowerUI.WarningsUI.class, mWarningsUI::get); diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index 403c80985377..95e2dba88b90 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -36,6 +36,8 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils import com.android.systemui.biometrics.AuthController +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.log.ScreenDecorationsLogger import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.util.asIndenting @@ -54,6 +56,7 @@ class FaceScanningOverlay( val mainExecutor: Executor, val logger: ScreenDecorationsLogger, val authController: AuthController, + val featureFlags: FeatureFlags, ) : ScreenDecorations.DisplayCutoutView(context, pos) { private var showScanningAnim = false private val rimPaint = Paint() @@ -294,6 +297,15 @@ class FaceScanningOverlay( } private fun createFaceScanningRimAnimator(): AnimatorSet { + val dontPulse = featureFlags.isEnabled(Flags.STOP_PULSING_FACE_SCANNING_ANIMATION) + if (dontPulse) { + return AnimatorSet().apply { + playSequentially( + cameraProtectionAnimator, + createRimAppearAnimator(), + ) + } + } return AnimatorSet().apply { playSequentially( cameraProtectionAnimator, diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java deleted file mode 100644 index 15e8c4e7abfb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.systemui; - -import android.annotation.Nullable; -import android.app.AppOpsManager; -import android.os.Handler; -import android.os.UserHandle; -import android.service.notification.StatusBarNotification; -import android.util.ArraySet; -import android.util.SparseArray; - -import com.android.internal.messages.nano.SystemMessageProto; -import com.android.systemui.appops.AppOpsController; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.util.Assert; - -import javax.inject.Inject; - -/** - * Tracks state of foreground services and notifications related to foreground services per user. - */ -@SysUISingleton -public class ForegroundServiceController { - public static final int[] APP_OPS = new int[] {AppOpsManager.OP_SYSTEM_ALERT_WINDOW}; - - private final SparseArray<ForegroundServicesUserState> mUserServices = new SparseArray<>(); - private final Object mMutex = new Object(); - private final Handler mMainHandler; - - @Inject - public ForegroundServiceController( - AppOpsController appOpsController, - @Main Handler mainHandler) { - mMainHandler = mainHandler; - appOpsController.addCallback(APP_OPS, (code, uid, packageName, active) -> { - mMainHandler.post(() -> { - onAppOpChanged(code, uid, packageName, active); - }); - }); - } - - /** - * @return true if this user has services missing notifications and therefore needs a - * disclosure notification for running a foreground service. - */ - public boolean isDisclosureNeededForUser(int userId) { - synchronized (mMutex) { - final ForegroundServicesUserState services = mUserServices.get(userId); - if (services == null) return false; - return services.isDisclosureNeeded(); - } - } - - /** - * @return true if this user/pkg has a missing or custom layout notification and therefore needs - * a disclosure notification showing the user which appsOps the app is using. - */ - public boolean isSystemAlertWarningNeeded(int userId, String pkg) { - synchronized (mMutex) { - final ForegroundServicesUserState services = mUserServices.get(userId); - if (services == null) return false; - return services.getStandardLayoutKeys(pkg) == null; - } - } - - /** - * Gets active app ops for this user and package - */ - @Nullable - public ArraySet<Integer> getAppOps(int userId, String pkg) { - synchronized (mMutex) { - final ForegroundServicesUserState services = mUserServices.get(userId); - if (services == null) { - return null; - } - return services.getFeatures(pkg); - } - } - - /** - * Records active app ops and updates the app op for the pending or visible notifications - * with the given parameters. - * App Ops are stored in FSC in addition to NotificationEntry in case they change before we - * have a notification to tag. - * @param appOpCode code for appOp to add/remove - * @param uid of user the notification is sent to - * @param packageName package that created the notification - * @param active whether the appOpCode is active or not - */ - void onAppOpChanged(int appOpCode, int uid, String packageName, boolean active) { - Assert.isMainThread(); - - int userId = UserHandle.getUserId(uid); - // Record active app ops - synchronized (mMutex) { - ForegroundServicesUserState userServices = mUserServices.get(userId); - if (userServices == null) { - userServices = new ForegroundServicesUserState(); - mUserServices.put(userId, userServices); - } - if (active) { - userServices.addOp(packageName, appOpCode); - } else { - userServices.removeOp(packageName, appOpCode); - } - } - } - - /** - * Looks up the {@link ForegroundServicesUserState} for the given {@code userId}, then performs - * the given {@link UserStateUpdateCallback} on it. If no state exists for the user ID, creates - * a new one if {@code createIfNotFound} is true, then performs the update on the new state. - * If {@code createIfNotFound} is false, no update is performed. - * - * @return false if no user state was found and none was created; true otherwise. - */ - boolean updateUserState(int userId, - UserStateUpdateCallback updateCallback, - boolean createIfNotFound) { - synchronized (mMutex) { - ForegroundServicesUserState userState = mUserServices.get(userId); - if (userState == null) { - if (createIfNotFound) { - userState = new ForegroundServicesUserState(); - mUserServices.put(userId, userState); - } else { - return false; - } - } - return updateCallback.updateUserState(userState); - } - } - - /** - * @return true if {@code sbn} is the system-provided disclosure notification containing the - * list of running foreground services. - */ - public boolean isDisclosureNotification(StatusBarNotification sbn) { - return sbn.getId() == SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES - && sbn.getTag() == null - && sbn.getPackageName().equals("android"); - } - - /** - * @return true if sbn is one of the window manager "drawing over other apps" notifications - */ - public boolean isSystemAlertNotification(StatusBarNotification sbn) { - return sbn.getPackageName().equals("android") - && sbn.getTag() != null - && sbn.getTag().contains("AlertWindowNotification"); - } - - /** - * Callback provided to {@link #updateUserState(int, UserStateUpdateCallback, boolean)} - * to perform the update. - */ - interface UserStateUpdateCallback { - /** - * Perform update operations on the provided {@code userState}. - * - * @return true if the update succeeded. - */ - boolean updateUserState(ForegroundServicesUserState userState); - - /** Called if the state was not found and was not created. */ - default void userStateNotFound(int userId) { - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java deleted file mode 100644 index a1a3b7280fa9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2018 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; - -import android.app.Notification; -import android.app.NotificationManager; -import android.content.Context; -import android.os.Bundle; -import android.service.notification.StatusBarNotification; -import android.util.Log; - -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; - -import javax.inject.Inject; - -/** Updates foreground service notification state in response to notification data events. */ -@SysUISingleton -public class ForegroundServiceNotificationListener { - - private static final String TAG = "FgServiceController"; - private static final boolean DBG = false; - - private final Context mContext; - private final ForegroundServiceController mForegroundServiceController; - private final NotifPipeline mNotifPipeline; - - @Inject - public ForegroundServiceNotificationListener(Context context, - ForegroundServiceController foregroundServiceController, - NotifPipeline notifPipeline) { - mContext = context; - mForegroundServiceController = foregroundServiceController; - mNotifPipeline = notifPipeline; - } - - /** Initializes this listener by connecting it to the notification pipeline. */ - public void init() { - mNotifPipeline.addCollectionListener(new NotifCollectionListener() { - @Override - public void onEntryAdded(NotificationEntry entry) { - addNotification(entry, entry.getImportance()); - } - - @Override - public void onEntryUpdated(NotificationEntry entry) { - updateNotification(entry, entry.getImportance()); - } - - @Override - public void onEntryRemoved(NotificationEntry entry, int reason) { - removeNotification(entry.getSbn()); - } - }); - } - - /** - * @param entry notification that was just posted - */ - private void addNotification(NotificationEntry entry, int importance) { - updateNotification(entry, importance); - } - - /** - * @param sbn notification that was just removed - */ - private void removeNotification(StatusBarNotification sbn) { - mForegroundServiceController.updateUserState( - sbn.getUserId(), - new ForegroundServiceController.UserStateUpdateCallback() { - @Override - public boolean updateUserState(ForegroundServicesUserState userState) { - if (mForegroundServiceController.isDisclosureNotification(sbn)) { - // if you remove the dungeon entirely, we take that to mean there are - // no running services - userState.setRunningServices(null, 0); - return true; - } else { - // this is safe to call on any notification, not just - // FLAG_FOREGROUND_SERVICE - return userState.removeNotification(sbn.getPackageName(), sbn.getKey()); - } - } - - @Override - public void userStateNotFound(int userId) { - if (DBG) { - Log.w(TAG, String.format( - "user %d with no known notifications got removeNotification " - + "for %s", - sbn.getUserId(), sbn)); - } - } - }, - false /* don't create */); - } - - /** - * @param entry notification that was just changed in some way - */ - private void updateNotification(NotificationEntry entry, int newImportance) { - final StatusBarNotification sbn = entry.getSbn(); - mForegroundServiceController.updateUserState( - sbn.getUserId(), - userState -> { - if (mForegroundServiceController.isDisclosureNotification(sbn)) { - final Bundle extras = sbn.getNotification().extras; - if (extras != null) { - final String[] svcs = extras.getStringArray( - Notification.EXTRA_FOREGROUND_APPS); - userState.setRunningServices(svcs, sbn.getNotification().when); - } - } else { - userState.removeNotification(sbn.getPackageName(), sbn.getKey()); - if (0 != (sbn.getNotification().flags - & Notification.FLAG_FOREGROUND_SERVICE)) { - if (newImportance > NotificationManager.IMPORTANCE_MIN) { - userState.addImportantNotification(sbn.getPackageName(), - sbn.getKey()); - } - } - final Notification.Builder builder = - Notification.Builder.recoverBuilder( - mContext, sbn.getNotification()); - if (builder.usesStandardHeader()) { - userState.addStandardLayoutNotification( - sbn.getPackageName(), sbn.getKey()); - } - } - return true; - }, - true /* create if not found */); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index 602f817f826b..28d59c21086b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -1446,6 +1446,12 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate { private CharSequence getClickAccessibilityActionLabel() { + if (mEditSizeEnable) { + // Perform click action to exit edit mode + return mContext.getResources().getString( + R.string.magnification_exit_edit_mode_click_label); + } + return mSettingsPanelVisibility ? mContext.getResources().getString( R.string.magnification_close_settings_click_label) @@ -1488,8 +1494,14 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private boolean performA11yAction(int action) { if (action == AccessibilityAction.ACTION_CLICK.getId()) { - // Simulate tapping the drag view so it opens the Settings. - handleSingleTap(mDragView); + if (mEditSizeEnable) { + // When edit mode is enabled, click the magnifier to exit edit mode. + setEditMagnifierSizeMode(false); + } else { + // Simulate tapping the drag view so it opens the Settings. + handleSingleTap(mDragView); + } + } else if (action == R.id.accessibility_action_zoom_in) { performScale(mScale + A11Y_CHANGE_SCALE_DIFFERENCE); } else if (action == R.id.accessibility_action_zoom_out) { diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index 2b83e6b05bbc..590056f2f8c2 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -23,6 +23,8 @@ import android.service.voice.VoiceInteractionSession; import android.util.Log; import com.android.internal.app.AssistUtils; +import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.internal.app.IVisualQueryRecognitionStatusListener; import com.android.internal.app.IVoiceInteractionSessionListener; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -39,10 +41,13 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.settings.SecureSettings; -import javax.inject.Inject; - import dagger.Lazy; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + /** * Class to manage everything related to assist in SystemUI. */ @@ -78,6 +83,18 @@ public class AssistManager { void hide(); } + /** + * An interface for a listener that receives notification that visual query attention has + * either been gained or lost. + */ + public interface VisualQueryAttentionListener { + /** Called when visual query attention has been gained. */ + void onAttentionGained(); + + /** Called when visual query attention has been lost. */ + void onAttentionLost(); + } + private static final String TAG = "AssistManager"; // Note that VERBOSE logging may leak PII (e.g. transcription contents). @@ -127,6 +144,23 @@ public class AssistManager { private final SecureSettings mSecureSettings; private final DeviceProvisionedController mDeviceProvisionedController; + + private final List<VisualQueryAttentionListener> mVisualQueryAttentionListeners = + new ArrayList<>(); + + private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener = + new IVisualQueryDetectionAttentionListener.Stub() { + @Override + public void onAttentionGained() { + mVisualQueryAttentionListeners.forEach(VisualQueryAttentionListener::onAttentionGained); + } + + @Override + public void onAttentionLost() { + mVisualQueryAttentionListeners.forEach(VisualQueryAttentionListener::onAttentionLost); + } + }; + private final CommandQueue mCommandQueue; protected final AssistUtils mAssistUtils; @@ -157,6 +191,7 @@ public class AssistManager { mSecureSettings = secureSettings; registerVoiceInteractionSessionListener(); + registerVisualQueryRecognitionStatusListener(); mUiController = defaultUiController; @@ -266,6 +301,24 @@ public class AssistManager { mAssistUtils.hideCurrentSession(); } + /** + * Add the given {@link VisualQueryAttentionListener} to the list of listeners awaiting + * notification of gaining/losing visual query attention. + */ + public void addVisualQueryAttentionListener(VisualQueryAttentionListener listener) { + if (!mVisualQueryAttentionListeners.contains(listener)) { + mVisualQueryAttentionListeners.add(listener); + } + } + + /** + * Remove the given {@link VisualQueryAttentionListener} from the list of listeners awaiting + * notification of gaining/losing visual query attention. + */ + public void removeVisualQueryAttentionListener(VisualQueryAttentionListener listener) { + mVisualQueryAttentionListeners.remove(listener); + } + private void startAssistInternal(Bundle args, @NonNull ComponentName assistComponent, boolean isService) { if (isService) { @@ -326,6 +379,27 @@ public class AssistManager { null, null); } + private void registerVisualQueryRecognitionStatusListener() { + if (!mContext.getResources() + .getBoolean(R.bool.config_enableVisualQueryAttentionDetection)) { + return; + } + + mAssistUtils.subscribeVisualQueryRecognitionStatus( + new IVisualQueryRecognitionStatusListener.Stub() { + @Override + public void onStartPerceiving() { + mAssistUtils.enableVisualQueryDetection( + mVisualQueryDetectionAttentionListener); + } + + @Override + public void onStopPerceiving() { + mAssistUtils.disableVisualQueryDetection(); + } + }); + } + public void launchVoiceAssistFromKeyguard() { mAssistUtils.launchVoiceAssistFromKeyguard(); } diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index e121790f07b0..ecd7baea7779 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -102,7 +102,7 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, - initialValue = true, + initialValue = false, ) /** @@ -114,14 +114,18 @@ constructor( * - `true` doesn't mean the lockscreen is invisible (since this state changes before the * transition occurs). */ - private val isLockscreenDismissed = + val isLockscreenDismissed: StateFlow<Boolean> = sceneInteractor.desiredScene .map { it.key } .filter { currentScene -> currentScene == SceneKey.Gone || currentScene == SceneKey.Lockscreen } .map { it == SceneKey.Gone } - .distinctUntilChanged() + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) /** * Whether it's currently possible to swipe up to dismiss the lockscreen without requiring diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 58adfa1d882c..58c8000a2328 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -82,6 +82,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.PrintWriter; @@ -288,12 +289,13 @@ public class AuthContainerView extends LinearLayout @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor, @NonNull PromptViewModel promptViewModel, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, - @NonNull @Background DelayableExecutor bgExecutor) { + @NonNull @Background DelayableExecutor bgExecutor, + @NonNull VibratorHelper vibratorHelper) { this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps, wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor, promptCredentialInteractor, promptViewModel, credentialViewModelProvider, - new Handler(Looper.getMainLooper()), bgExecutor); + new Handler(Looper.getMainLooper()), bgExecutor, vibratorHelper); } @VisibleForTesting @@ -314,7 +316,8 @@ public class AuthContainerView extends LinearLayout @NonNull PromptViewModel promptViewModel, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull Handler mainHandler, - @NonNull @Background DelayableExecutor bgExecutor) { + @NonNull @Background DelayableExecutor bgExecutor, + @NonNull VibratorHelper vibratorHelper) { super(config.mContext); mConfig = config; @@ -364,7 +367,8 @@ public class AuthContainerView extends LinearLayout if (featureFlags.isEnabled(Flags.BIOMETRIC_BP_STRONG)) { showPrompt(config, layoutInflater, promptViewModel, Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds), - Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds)); + Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds), + vibratorHelper, featureFlags); } else { showLegacyPrompt(config, layoutInflater, fpProps, faceProps); } @@ -388,7 +392,10 @@ public class AuthContainerView extends LinearLayout private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater, @NonNull PromptViewModel viewModel, @Nullable FingerprintSensorPropertiesInternal fpProps, - @Nullable FaceSensorPropertiesInternal faceProps) { + @Nullable FaceSensorPropertiesInternal faceProps, + @NonNull VibratorHelper vibratorHelper, + @NonNull FeatureFlags featureFlags + ) { if (Utils.isBiometricAllowed(config.mPromptInfo)) { mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication( config.mPromptInfo, @@ -401,7 +408,8 @@ public class AuthContainerView extends LinearLayout mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController, // TODO(b/201510778): This uses the wrong timeout in some cases getJankListener(view, TRANSIT, AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), - mBackgroundView, mBiometricCallback, mApplicationCoroutineScope); + mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, + vibratorHelper, featureFlags); // TODO(b/251476085): migrate these dependencies if (fpProps != null && fpProps.isAnyUdfpsType()) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 3df7ca58736a..7b288a8d49f1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -85,9 +85,12 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.data.repository.BiometricType; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.Execution; +import kotlin.Unit; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; @@ -101,7 +104,6 @@ import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; -import kotlin.Unit; import kotlinx.coroutines.CoroutineScope; /** @@ -183,6 +185,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull private final UdfpsUtils mUdfpsUtils; private final @Background DelayableExecutor mBackgroundExecutor; private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); + @NonNull private final VibratorHelper mVibratorHelper; @VisibleForTesting final TaskStackListener mTaskStackListener = new TaskStackListener() { @@ -771,7 +774,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull InteractionJankMonitor jankMonitor, @Main Handler handler, @Background DelayableExecutor bgExecutor, - @NonNull UdfpsUtils udfpsUtils) { + @NonNull UdfpsUtils udfpsUtils, + @NonNull VibratorHelper vibratorHelper) { mContext = context; mFeatureFlags = featureFlags; mExecution = execution; @@ -794,6 +798,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mFaceEnrolledForUser = new SparseBooleanArray(); mUdfpsUtils = udfpsUtils; mApplicationCoroutineScope = applicationCoroutineScope; + mVibratorHelper = vibratorHelper; mLogContextInteractor = logContextInteractor; mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider; @@ -1044,7 +1049,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, final int userId = mCurrentDialogArgs.argi1; if (isFaceAuthEnrolled(userId) && isFingerprintEnrolled(userId)) { messageRes = modality == TYPE_FACE - ? R.string.biometric_face_not_recognized + ? R.string.fingerprint_dialog_use_fingerprint_instead : R.string.fingerprint_error_not_match; } else { messageRes = R.string.biometric_not_recognized; @@ -1341,7 +1346,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider, mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel, - mCredentialViewModelProvider, bgExecutor); + mCredentialViewModelProvider, bgExecutor, mVibratorHelper); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 946ddba6a341..ea9fe5fce53e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -230,7 +230,7 @@ class AuthRippleController @Inject constructor( lightRevealScrim.revealAmount = animator.animatedValue as Float } addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { // Reset light reveal scrim to the default, so the CentralSurfaces // can handle any subsequent light reveal changes // (ie: from dozing changes) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index 5ede16d221b7..4c2dc41fb759 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -147,12 +147,12 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at retractDwellAnimator = AnimatorSet().apply { playTogether(retractDwellRippleAnimator, retractAlphaAnimator) addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { dwellPulseOutAnimator?.cancel() drawDwell = true } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { drawDwell = false resetDwellAlpha() } @@ -182,13 +182,13 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at invalidate() } addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { retractDwellAnimator?.cancel() dwellPulseOutAnimator?.cancel() drawDwell = true } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { drawDwell = false resetDwellAlpha() } @@ -239,14 +239,14 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at expandDwellRippleAnimator ) addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { retractDwellAnimator?.cancel() fadeDwellAnimator?.cancel() visibility = VISIBLE drawDwell = true } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { drawDwell = false } }) @@ -273,12 +273,12 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at unlockedRippleAnimator = rippleAnimator.apply { addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { drawRipple = true visibility = VISIBLE } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { onAnimationEnd?.run() drawRipple = false visibility = GONE @@ -327,7 +327,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at } } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover // the active effect area. Values here should be kept in sync with the // animation implementation in the ripple shader. (Twice bigger) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt index b9fa24022ad5..a24a47bb44df 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt @@ -40,7 +40,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val faceAuthInteractor: KeyguardFaceAuthInteractor, ) : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) { + override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(host, info) if (keyguardUpdateMonitor.shouldListenForFace()) { val clickActionToRetryFace = @@ -52,7 +52,7 @@ constructor( } } - override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean { + override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) { keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION) faceAuthInteractor.onAccessibilityAction() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index 7f696fd1eeae..eb6864d91ba4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -119,7 +119,7 @@ constructor( private var overlayView: View? = null set(value) { field?.let { oldView -> - val lottie = oldView.findViewById(R.id.sidefps_animation) as LottieAnimationView + val lottie = oldView.requireViewById(R.id.sidefps_animation) as LottieAnimationView lottie.pauseAnimation() windowManager.removeView(oldView) orientationListener.disable() @@ -274,7 +274,7 @@ constructor( } overlayOffsets = offsets - val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView + val lottie = view.requireViewById(R.id.sidefps_animation) as LottieAnimationView view.rotation = display.asSideFpsAnimationRotation( offsets.isYAligned(), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 39a45f7f60a2..b23e08538b5c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -646,8 +646,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { shouldPilfer = true; } - // Pilfer only once per gesture - if (shouldPilfer && !mPointerPilfered) { + // Pilfer only once per gesture, don't pilfer for BP + if (shouldPilfer && !mPointerPilfered + && getBiometricSessionType() != SESSION_BIOMETRIC_PROMPT) { mInputManager.pilferPointers( mOverlay.getOverlayView().getViewRootImpl().getInputToken()); mPointerPilfered = true; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt index 8352d0aeab35..5dafa61f9a27 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt @@ -38,7 +38,8 @@ class UdfpsFpmEmptyView( override fun getDrawable(): UdfpsDrawable = fingerprintDrawable fun updateAccessibilityViewLocation(sensorBounds: Rect) { - val fingerprintAccessibilityView: View = findViewById(R.id.udfps_enroll_accessibility_view) + val fingerprintAccessibilityView: View = + requireViewById(R.id.udfps_enroll_accessibility_view) val params: ViewGroup.LayoutParams = fingerprintAccessibilityView.layoutParams params.width = sensorBounds.width() params.height = sensorBounds.height() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt index fb7b56e12996..84978790f49c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt @@ -33,7 +33,7 @@ constructor( @Main private val resources: Resources, private val keyguardViewManager: StatusBarKeyguardViewManager, ) : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) { + override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(host, info) val clickAction = AccessibilityNodeInfo.AccessibilityAction( @@ -43,7 +43,7 @@ constructor( info.addAction(clickAction) } - override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean { + override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { // when an a11y service is enabled, double tapping on the fingerprint sensor should // show the primary bouncer return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt index db30a55ea1e1..e3fd3ce1bcbf 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt @@ -306,8 +306,9 @@ constructor( activityLaunchAnimator.addListener(activityLaunchAnimatorListener) view.mUseExpandedOverlay = useExpandedOverlay view.startIconAsyncInflate { - (view.findViewById(R.id.udfps_animation_view_internal) as View).accessibilityDelegate = - udfpsKeyguardAccessibilityDelegate + val animationViewInternal: View = + view.requireViewById(R.id.udfps_animation_view_internal) + animationViewInternal.accessibilityDelegate = udfpsKeyguardAccessibilityDelegate } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt index b538085fa40d..1ca57e77034c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt @@ -60,6 +60,13 @@ object Utils { return dp * (density / DisplayMetrics.DENSITY_DEFAULT) } + /** + * Note: Talkback 14.0 has new rate-limitation design to reduce frequency + * of TYPE_WINDOW_CONTENT_CHANGED events to once every 30 seconds. + * (context: b/281765653#comment18) + * Using {@link View#announceForAccessibility} instead as workaround when sending events + * exceeding this frequency is required. + */ @JvmStatic fun notifyAccessibilityContentChanged(am: AccessibilityManager, view: ViewGroup) { if (!am.isEnabled) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt index 1f1a1b5c83bd..2a026675adcb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt @@ -89,7 +89,7 @@ constructor( ) val hat = gkResponse.gatekeeperHAT lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle) - emit(CredentialStatus.Success.Verified(hat)) + emit(CredentialStatus.Success.Verified(checkNotNull(hat))) } else if (response.timeout > 0) { // if requests are being throttled, update the error message every // second until the temporary lock has expired @@ -226,8 +226,7 @@ private fun Context.getLastAttemptBeforeWipeProfileMessage( is BiometricPromptRequest.Credential.Password -> DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT } - return devicePolicyManager.resources.getString(id) { - // use fallback a string if not found + val getFallbackString = { val defaultId = when (request) { is BiometricPromptRequest.Credential.Pin -> @@ -239,6 +238,8 @@ private fun Context.getLastAttemptBeforeWipeProfileMessage( } getString(defaultId) } + + return devicePolicyManager.resources?.getString(id, getFallbackString) ?: getFallbackString() } private fun Context.getLastAttemptBeforeWipeUserMessage( @@ -266,8 +267,8 @@ private fun Context.getNowWipingMessage( DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS else -> DevicePolicyResources.UNDEFINED } - return devicePolicyManager.resources.getString(id) { - // use fallback a string if not found + + val getFallbackString = { val defaultId = when (userType) { UserType.PRIMARY -> @@ -279,4 +280,6 @@ private fun Context.getNowWipingMessage( } getString(defaultId) } + + return devicePolicyManager.resources?.getString(id, getFallbackString) ?: getFallbackString() } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt index a3f34ce7471d..b940ec85efbf 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt @@ -122,7 +122,7 @@ class CredentialPasswordView(context: Context, attrs: AttributeSet?) : titleView.ellipsize = TextUtils.TruncateAt.MARQUEE titleView.marqueeRepeatLimit = -1 // select to enable marquee unless a screen reader is enabled - titleView.isSelected = accessibilityManager.shouldMarquee() + titleView.isSelected = accessibilityManager?.shouldMarquee() ?: false } else { titleView.isSingleLine = false titleView.ellipsize = null diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 7b78761af02b..d054751b760b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.ui.binder import android.animation.Animator +import android.annotation.SuppressLint import android.content.Context import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants @@ -25,6 +26,8 @@ import android.hardware.face.FaceManager import android.os.Bundle import android.text.method.ScrollingMovementMethod import android.util.Log +import android.view.HapticFeedbackConstants +import android.view.MotionEvent import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.accessibility.AccessibilityManager @@ -45,7 +48,6 @@ import com.android.systemui.biometrics.AuthBiometricView.Callback import com.android.systemui.biometrics.AuthBiometricViewAdapter import com.android.systemui.biometrics.AuthIconController import com.android.systemui.biometrics.AuthPanelController -import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.PromptKind @@ -55,9 +57,13 @@ import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode import com.android.systemui.biometrics.ui.viewmodel.PromptMessage import com.android.systemui.biometrics.ui.viewmodel.PromptSize import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.VibratorHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -69,6 +75,7 @@ private const val TAG = "BiometricViewBinder" object BiometricViewBinder { /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ + @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( view: BiometricPromptLayout, @@ -78,20 +85,19 @@ object BiometricViewBinder { backgroundView: View, legacyCallback: Callback, applicationScope: CoroutineScope, + vibratorHelper: VibratorHelper, + featureFlags: FeatureFlags, ): AuthBiometricViewAdapter { val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!! - fun notifyAccessibilityChanged() { - Utils.notifyAccessibilityContentChanged(accessibilityManager, view) - } val textColorError = view.resources.getColor(R.color.biometric_dialog_error, view.context.theme) val textColorHint = view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) - val titleView = view.findViewById<TextView>(R.id.title) - val subtitleView = view.findViewById<TextView>(R.id.subtitle) - val descriptionView = view.findViewById<TextView>(R.id.description) + val titleView = view.requireViewById<TextView>(R.id.title) + val subtitleView = view.requireViewById<TextView>(R.id.subtitle) + val descriptionView = view.requireViewById<TextView>(R.id.description) // set selected to enable marquee unless a screen reader is enabled titleView.isSelected = @@ -100,18 +106,18 @@ object BiometricViewBinder { !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled descriptionView.movementMethod = ScrollingMovementMethod() - val iconViewOverlay = view.findViewById<LottieAnimationView>(R.id.biometric_icon_overlay) - val iconView = view.findViewById<LottieAnimationView>(R.id.biometric_icon) - val indicatorMessageView = view.findViewById<TextView>(R.id.indicator) + val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay) + val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon) + val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) // Negative-side (left) buttons - val negativeButton = view.findViewById<Button>(R.id.button_negative) - val cancelButton = view.findViewById<Button>(R.id.button_cancel) - val credentialFallbackButton = view.findViewById<Button>(R.id.button_use_credential) + val negativeButton = view.requireViewById<Button>(R.id.button_negative) + val cancelButton = view.requireViewById<Button>(R.id.button_cancel) + val credentialFallbackButton = view.requireViewById<Button>(R.id.button_use_credential) // Positive-side (right) buttons - val confirmationButton = view.findViewById<Button>(R.id.button_confirm) - val retryButton = view.findViewById<Button>(R.id.button_try_again) + val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) + val retryButton = view.requireViewById<Button>(R.id.button_try_again) // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers val adapter = @@ -297,21 +303,19 @@ object BiometricViewBinder { // reuse the icon as a confirm button launch { - viewModel.isConfirmButtonVisible + viewModel.isIconConfirmButton .map { isPending -> when { isPending && iconController.actsAsConfirmButton -> - View.OnClickListener { viewModel.confirmAuthenticated() } + View.OnTouchListener { _: View, event: MotionEvent -> + viewModel.onOverlayTouch(event) + } else -> null } } - .collect { onClick -> - iconViewOverlay.setOnClickListener(onClick) - iconView.setOnClickListener(onClick) - if (onClick == null) { - iconViewOverlay.isClickable = false - iconView.isClickable = false - } + .collect { onTouch -> + iconViewOverlay.setOnTouchListener(onTouch) + iconView.setOnTouchListener(onTouch) } } @@ -326,30 +330,30 @@ object BiometricViewBinder { } } - // not sure why this is here, but the legacy code did it probably needed? - launch { - viewModel.isAuthenticating.collect { isAuthenticating -> - if (isAuthenticating) { - notifyAccessibilityChanged() - } - } - } - // dismiss prompt when authenticated and confirmed launch { viewModel.isAuthenticated.collect { authState -> // Disable background view for cancelling authentication once authenticated, // and remove from talkback if (authState.isAuthenticated) { + // Prevents Talkback from speaking subtitle after already authenticated + subtitleView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO backgroundView.setOnClickListener(null) backgroundView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO + + // Allow icon to be used as confirmation button with a11y enabled + if (accessibilityManager.isTouchExplorationEnabled) { + iconViewOverlay.setOnClickListener { + viewModel.confirmAuthenticated() + } + iconView.setOnClickListener { viewModel.confirmAuthenticated() } + } } if (authState.isAuthenticatedAndConfirmed) { view.announceForAccessibility( view.resources.getString(R.string.biometric_dialog_authenticated) ) - notifyAccessibilityChanged() launch { delay(authState.delay) @@ -381,7 +385,30 @@ object BiometricViewBinder { !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled - notifyAccessibilityChanged() + /** + * Note: Talkback 14.0 has new rate-limitation design to reduce frequency of + * TYPE_WINDOW_CONTENT_CHANGED events to once every 30 seconds. (context: + * b/281765653#comment18) Using {@link View#announceForAccessibility} + * instead as workaround since sending events exceeding this frequency is + * required. + */ + indicatorMessageView?.text?.let { + if (it.isNotBlank()) { + view.announceForAccessibility(it) + } + } + } + } + + // Play haptics + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + launch { + viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> + if (hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) { + vibratorHelper.performHapticFeedback(view, hapticFeedbackConstant) + viewModel.clearHaptics() + } + } } } } @@ -477,7 +504,7 @@ private class Spaghetti( modalities.hasFaceAndFingerprint && (viewModel.fingerprintStartMode.first() != FingerprintStartMode.Pending) && (authenticatedModality == BiometricModality.Face) -> - R.string.biometric_dialog_tap_confirm_with_face + R.string.biometric_dialog_tap_confirm_with_face_alt_1 else -> null } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 1a286cf57982..370b36bcaec2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -72,7 +72,7 @@ object BiometricViewSizeBinder { } } - val iconHolderView = view.findViewById<View>(R.id.biometric_icon_frame) + val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame) val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding) val fullSizeYOffset = view.resources.getDimension(R.dimen.biometric_dialog_medium_to_large_translation_offset) @@ -205,7 +205,7 @@ object BiometricViewSizeBinder { } private fun View.isLandscape(): Boolean { - val r = context.display.rotation + val r = context.display?.rotation return r == Surface.ROTATION_90 || r == Surface.ROTATION_270 } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index dca19c503fd3..89561a5a212b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -17,11 +17,15 @@ package com.android.systemui.biometrics.ui.viewmodel import android.hardware.biometrics.BiometricPrompt import android.util.Log +import android.view.HapticFeedbackConstants +import android.view.MotionEvent import com.android.systemui.biometrics.AuthBiometricView import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.PromptKind +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.statusbar.VibratorHelper import javax.inject.Inject import kotlinx.coroutines.Job @@ -43,6 +47,7 @@ class PromptViewModel constructor( private val interactor: PromptSelectorInteractor, private val vibrator: VibratorHelper, + private val featureFlags: FeatureFlags, ) { /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = @@ -63,11 +68,18 @@ constructor( /** If the user has successfully authenticated and confirmed (when explicitly required). */ val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() + private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false) + /** * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. */ - val isConfirmationRequired: Flow<Boolean> = interactor.isConfirmationRequired + val isConfirmationRequired: Flow<Boolean> = + combine(_isOverlayTouched, interactor.isConfirmationRequired) { + isOverlayTouched, + isConfirmationRequired -> + !isOverlayTouched && isConfirmationRequired + } /** The kind of credential the user has. */ val credentialKind: Flow<PromptKind> = interactor.credentialKind @@ -90,6 +102,11 @@ constructor( private val _forceLargeSize = MutableStateFlow(false) private val _forceMediumSize = MutableStateFlow(false) + private val _hapticsToPlay = MutableStateFlow(HapticFeedbackConstants.NO_HAPTICS) + + /** Event fired to the view indicating a [HapticFeedbackConstants] to be played */ + val hapticsToPlay = _hapticsToPlay.asStateFlow() + /** The size of the prompt. */ val size: Flow<PromptSize> = combine( @@ -141,6 +158,12 @@ constructor( } .distinctUntilChanged() + /** If the icon can be used as a confirmation button. */ + val isIconConfirmButton: Flow<Boolean> = + combine(size, interactor.isConfirmationRequired) { size, isConfirmationRequired -> + size.isNotSmall && isConfirmationRequired + } + /** If the negative button should be shown. */ val isNegativeButtonVisible: Flow<Boolean> = combine( @@ -289,8 +312,10 @@ constructor( if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty _forceMediumSize.value = true _legacyState.value = - if (alreadyAuthenticated) { + if (alreadyAuthenticated && isConfirmationRequired.first()) { AuthBiometricView.STATE_PENDING_CONFIRMATION + } else if (alreadyAuthenticated && !isConfirmationRequired.first()) { + AuthBiometricView.STATE_AUTHENTICATED } else { AuthBiometricView.STATE_HELP } @@ -388,18 +413,10 @@ constructor( } private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { - val availableModalities = modalities.first() val confirmationRequired = isConfirmationRequired.first() - if (availableModalities.hasFaceAndFingerprint) { - // coex only needs confirmation when face is successful, unless it happens on the - // first attempt (i.e. without failure) before fingerprint scanning starts - val fingerprintStarted = fingerprintStartMode.first() != FingerprintStartMode.Pending - if (modality == BiometricModality.Face) { - return fingerprintStarted || confirmationRequired - } - } - if (availableModalities.hasFaceOnly) { + // Only worry about confirmationRequired if face was used to unlock + if (modality == BiometricModality.Face) { return confirmationRequired } // fingerprint only never requires confirmation @@ -430,6 +447,26 @@ constructor( } /** + * Touch event occurred on the overlay + * + * Tracks whether a finger is currently down to set [_isOverlayTouched] to be used as user + * confirmation + */ + fun onOverlayTouch(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + _isOverlayTouched.value = true + + if (_isAuthenticated.value.needsUserConfirmation) { + confirmAuthenticated() + } + return true + } else if (event.actionMasked == MotionEvent.ACTION_UP) { + _isOverlayTouched.value = false + } + return false + } + + /** * Switch to the credential view. * * TODO(b/251476085): this should be decoupled from the shared panel controller @@ -438,11 +475,26 @@ constructor( _forceLargeSize.value = true } - private fun VibratorHelper.success(modality: BiometricModality) = - vibrateAuthSuccess("$TAG, modality = $modality BP::success") + private fun VibratorHelper.success(modality: BiometricModality) { + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + _hapticsToPlay.value = HapticFeedbackConstants.CONFIRM + } else { + vibrateAuthSuccess("$TAG, modality = $modality BP::success") + } + } + + private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) { + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + _hapticsToPlay.value = HapticFeedbackConstants.REJECT + } else { + vibrateAuthError("$TAG, modality = $modality BP::error") + } + } - private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) = - vibrateAuthError("$TAG, modality = $modality BP::error") + /** Clears the [hapticsToPlay] variable by setting it to the NO_HAPTICS default. */ + fun clearHaptics() { + _hapticsToPlay.value = HapticFeedbackConstants.NO_HAPTICS + } companion object { private const val TAG = "PromptViewModel" diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt index 5ca36ab39dba..ddb097491988 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt @@ -156,9 +156,9 @@ class WiredChargingRippleController @Inject constructor( } windowLayoutParams.packageName = context.opPackageName rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { - override fun onViewDetachedFromWindow(view: View?) {} + override fun onViewDetachedFromWindow(view: View) {} - override fun onViewAttachedToWindow(view: View?) { + override fun onViewAttachedToWindow(view: View) { layoutRipple() rippleView.startRipple(Runnable { windowManager.removeView(rippleView) @@ -176,7 +176,7 @@ class WiredChargingRippleController @Inject constructor( val height = bounds.height() val maxDiameter = Integer.max(width, height) * 2f rippleView.setMaxSize(maxDiameter, maxDiameter) - when (context.display.rotation) { + when (context.display?.rotation) { Surface.ROTATION_0 -> { rippleView.setCenter( width * normalizedPortPosX, height * normalizedPortPosY) diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt index 63d57cc3fc8d..a9f3b77ceb5e 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt @@ -29,7 +29,7 @@ import javax.inject.Inject */ class FalsingA11yDelegate @Inject constructor(private val falsingCollector: FalsingCollector) : View.AccessibilityDelegate() { - override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean { + override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { if (action == ACTION_CLICK) { falsingCollector.onA11yAction() } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt index 0b8e83edc88a..1b45ecd09d31 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt @@ -59,7 +59,7 @@ constructor(val context: Context, val displayTracker: DisplayTracker) { context.startActivity(intent, transition.first.toBundle()) val runner = RemoteAnimationAdapter(NULL_ACTIVITY_TRANSITION, 0, 0) try { - WindowManagerGlobal.getWindowManagerService() + checkNotNull(WindowManagerGlobal.getWindowManagerService()) .overridePendingAppTransitionRemote(runner, displayTracker.defaultDisplayId) } catch (e: Exception) { Log.e(TAG, "Error overriding clipboard app transition", e) 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 index 3e6ac86e2417..c0d1951c22ce 100644 --- 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 @@ -100,7 +100,7 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale()) override fun getResolutionScale(): Float { - context.display.getDisplayInfo(displayInfo.value) + context.display?.getDisplayInfo(displayInfo.value) val maxDisplayMode = displayUtils.getMaximumResolutionDisplayMode(displayInfo.value.supportedModes) maxDisplayMode?.let { diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt index 2dd98dcf41b6..323070a84863 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt @@ -22,6 +22,7 @@ import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.view.View +import com.android.systemui.shade.TouchLogger import kotlin.math.pow import kotlin.math.sqrt import kotlinx.coroutines.DisposableHandle @@ -83,6 +84,10 @@ class LongPressHandlingView( interactionHandler.isLongPressHandlingEnabled = isEnabled } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event)) + } + @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent?): Boolean { return interactionHandler.onTouchEvent(event?.toModel()) diff --git a/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt index f17d0f307473..0b327a19ef81 100644 --- a/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt @@ -66,9 +66,9 @@ class ContrastDialog( contrastButtons = mapOf( - CONTRAST_LEVEL_STANDARD to findViewById(R.id.contrast_button_standard), - CONTRAST_LEVEL_MEDIUM to findViewById(R.id.contrast_button_medium), - CONTRAST_LEVEL_HIGH to findViewById(R.id.contrast_button_high) + CONTRAST_LEVEL_STANDARD to requireViewById(R.id.contrast_button_standard), + CONTRAST_LEVEL_MEDIUM to requireViewById(R.id.contrast_button_medium), + CONTRAST_LEVEL_HIGH to requireViewById(R.id.contrast_button_high) ) contrastButtons.forEach { (contrastLevel, contrastButton) -> diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index e8c97bf77271..4a534e9cc74d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -190,7 +190,7 @@ class ControlsControllerImpl @Inject constructor ( PREFS_CONTROLS_SEEDING_COMPLETED, mutableSetOf<String>()) val servicePackageSet = serviceInfoSet.map { it.packageName } prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, - completedSeedingPackageSet.intersect(servicePackageSet)).apply() + completedSeedingPackageSet?.intersect(servicePackageSet) ?: emptySet()).apply() var changed = false favoriteComponentSet.subtract(serviceInfoSet).forEach { diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt index 23721c93508b..8bae6674aa11 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -193,7 +193,7 @@ open class ControlsFavoritingActivity @Inject constructor( ControlsAnimations.enterAnimation(pageIndicator).apply { addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { // Position the tooltip if necessary after animations are complete // so we can get the position on screen. The tooltip is not // rooted in the layout root. diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt index ff55b76d4db7..a13f717bc77d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt @@ -106,10 +106,8 @@ object ChallengeDialogs { } ) - getWindow().apply { - setType(WINDOW_TYPE) - setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) - } + window?.setType(WINDOW_TYPE) + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) setOnShowListener(DialogInterface.OnShowListener { _ -> val editText = requireViewById<EditText>(R.id.controls_pin_input) editText.setHint(instructions) @@ -153,9 +151,7 @@ object ChallengeDialogs { ) } return builder.create().apply { - getWindow().apply { - setType(WINDOW_TYPE) - } + window?.setType(WINDOW_TYPE) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index c04bc8792223..abe342320858 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -384,7 +384,7 @@ class ControlViewHolder( ) } addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { stateAnimator = null } }) @@ -438,7 +438,7 @@ class ControlViewHolder( duration = 200L interpolator = Interpolators.LINEAR addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { statusRowUpdater.invoke() } }) @@ -450,7 +450,7 @@ class ControlViewHolder( statusAnimator = AnimatorSet().apply { playSequentially(fadeOut, fadeIn) addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { status.alpha = STATUS_ALPHA_ENABLED statusAnimator = null } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt index be50a1468f07..98f17f409184 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt @@ -132,8 +132,8 @@ class DetailDialog( init { // To pass touches to the task inside TaskView. - window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL) - window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL) + window?.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) setContentView(R.layout.controls_detail_dialog) @@ -182,7 +182,7 @@ class DetailDialog( } // consume all insets to achieve slide under effect - window.getDecorView().setOnApplyWindowInsetsListener { + checkNotNull(window).decorView.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> val l = v.getPaddingLeft() val r = v.getPaddingRight() @@ -202,7 +202,7 @@ class DetailDialog( } fun getTaskViewBounds(): Rect { - val wm = context.getSystemService(WindowManager::class.java) + val wm = checkNotNull(context.getSystemService(WindowManager::class.java)) val windowMetrics = wm.getCurrentWindowMetrics() val rect = windowMetrics.bounds val metricInsets = windowMetrics.windowInsets diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt index ad2b785ea5c5..dbbda9aad518 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt @@ -67,7 +67,8 @@ data class RenderInfo( iconMap.put(resourceId, icon) } } - return RenderInfo(icon!!.constantState.newDrawable(context.resources), fg, bg) + return RenderInfo( + checkNotNull(icon?.constantState).newDrawable(context.resources), fg, bg) } fun registerComponentIcon(componentName: ComponentName, icon: Drawable) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt index 84cda5a541e3..3c2bfa0efc39 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt @@ -94,10 +94,8 @@ class StatusBehavior : Behavior { ) } cvh.visibleDialog = builder.create().apply { - getWindow().apply { - setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY) - show() - } + window?.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY) + show() } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt index 1461135d3d3b..b2c95a63ad72 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt @@ -244,7 +244,7 @@ class ToggleRangeBehavior : Behavior { cvh.clipLayer.level = it.animatedValue as Int } addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { rangeAnimator = null } }) @@ -335,7 +335,7 @@ class ToggleRangeBehavior : Behavior { } override fun onScroll( - e1: MotionEvent, + e1: MotionEvent?, e2: MotionEvent, xDiff: Float, yDiff: Float diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index 4e62104034ee..ac0d3c8f3d36 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -34,6 +34,7 @@ import com.android.systemui.FaceScanningOverlay import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags import com.android.systemui.log.ScreenDecorationsLogger import com.android.systemui.plugins.statusbar.StatusBarStateController import java.util.concurrent.Executor @@ -47,6 +48,7 @@ class FaceScanningProviderFactory @Inject constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @Main private val mainExecutor: Executor, private val logger: ScreenDecorationsLogger, + private val featureFlags: FeatureFlags, ) : DecorProviderFactory() { private val display = context.display private val displayInfo = DisplayInfo() @@ -86,6 +88,7 @@ class FaceScanningProviderFactory @Inject constructor( keyguardUpdateMonitor, mainExecutor, logger, + featureFlags, ) ) } @@ -110,6 +113,7 @@ class FaceScanningOverlayProviderImpl( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val mainExecutor: Executor, private val logger: ScreenDecorationsLogger, + private val featureFlags: FeatureFlags, ) : BoundDecorProvider() { override val viewId: Int = com.android.systemui.R.id.face_scanning_anim @@ -144,6 +148,7 @@ class FaceScanningOverlayProviderImpl( mainExecutor, logger, authController, + featureFlags ) view.id = viewId view.setColor(tintColor) diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt index bcfeeb9eaadd..cef45dcae43e 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt @@ -51,7 +51,7 @@ constructor( val callback = object : ConfigurationController.ConfigurationListener { override fun onConfigChanged(newConfig: Configuration?) { - context.display.getMetrics(displayMetricsHolder) + context.display?.getMetrics(displayMetricsHolder) trySend(displayMetricsHolder) } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java index 7c816cec08f2..34a80e867153 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java @@ -26,9 +26,9 @@ import android.os.SystemClock; import android.text.format.Formatter; import android.util.Log; +import com.android.systemui.DejankUtils; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.dagger.DozeScope; -import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.util.AlarmTimeout; import com.android.systemui.util.wakelock.WakeLock; @@ -52,15 +52,21 @@ public class DozeUi implements DozeMachine.Part { private final boolean mCanAnimateTransition; private final DozeParameters mDozeParameters; private final DozeLog mDozeLog; - private final StatusBarStateController mStatusBarStateController; private long mLastTimeTickElapsed = 0; + // If time tick is scheduled and there's not a pending runnable to cancel: + private boolean mTimeTickScheduled; + private final Runnable mCancelTimeTickerRunnable = new Runnable() { + @Override + public void run() { + mTimeTicker.cancel(); + } + }; @Inject public DozeUi(Context context, AlarmManager alarmManager, WakeLock wakeLock, DozeHost host, @Main Handler handler, DozeParameters params, - StatusBarStateController statusBarStateController, DozeLog dozeLog) { mContext = context; mWakeLock = wakeLock; @@ -70,7 +76,6 @@ public class DozeUi implements DozeMachine.Part { mDozeParameters = params; mTimeTicker = new AlarmTimeout(alarmManager, this::onTimeTick, "doze_time_tick", handler); mDozeLog = dozeLog; - mStatusBarStateController = statusBarStateController; } @Override @@ -157,13 +162,15 @@ public class DozeUi implements DozeMachine.Part { } private void scheduleTimeTick() { - if (mTimeTicker.isScheduled()) { + if (mTimeTickScheduled) { return; } + mTimeTickScheduled = true; + DejankUtils.removeCallbacks(mCancelTimeTickerRunnable); long time = System.currentTimeMillis(); long delta = roundToNextMinute(time) - System.currentTimeMillis(); - boolean scheduled = mTimeTicker.schedule(delta, AlarmTimeout.MODE_IGNORE_IF_SCHEDULED); + boolean scheduled = mTimeTicker.schedule(delta, AlarmTimeout.MODE_RESCHEDULE_IF_SCHEDULED); if (scheduled) { mDozeLog.traceTimeTickScheduled(time, time + delta); } @@ -171,11 +178,11 @@ public class DozeUi implements DozeMachine.Part { } private void unscheduleTimeTick() { - if (!mTimeTicker.isScheduled()) { + if (!mTimeTickScheduled) { return; } - verifyLastTimeTick(); - mTimeTicker.cancel(); + mTimeTickScheduled = false; + DejankUtils.postAfterTraversal(mCancelTimeTickerRunnable); } private void verifyLastTimeTick() { @@ -205,6 +212,7 @@ public class DozeUi implements DozeMachine.Part { // Keep wakelock until a frame has been pushed. mHandler.post(mWakeLock.wrap(() -> {})); + mTimeTickScheduled = false; scheduleTimeTick(); } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java index c889ac214cda..4dd97d557b30 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java @@ -16,10 +16,9 @@ package com.android.systemui.dreams.conditions; -import com.android.internal.app.AssistUtils; -import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.systemui.assist.AssistManager; +import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener; import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.shared.condition.Condition; import javax.inject.Inject; @@ -30,12 +29,10 @@ import kotlinx.coroutines.CoroutineScope; * {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention. */ public class AssistantAttentionCondition extends Condition { - private final DreamOverlayStateController mDreamOverlayStateController; - private final AssistUtils mAssistUtils; - private boolean mEnabled; + private final AssistManager mAssistManager; - private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener = - new IVisualQueryDetectionAttentionListener.Stub() { + private final VisualQueryAttentionListener mVisualQueryAttentionListener = + new VisualQueryAttentionListener() { @Override public void onAttentionGained() { updateCondition(true); @@ -47,59 +44,26 @@ public class AssistantAttentionCondition extends Condition { } }; - private final DreamOverlayStateController.Callback mCallback = - new DreamOverlayStateController.Callback() { - @Override - public void onStateChanged() { - if (mDreamOverlayStateController.isDreamOverlayStatusBarVisible()) { - enableVisualQueryDetection(); - } else { - disableVisualQueryDetection(); - } - } - }; - @Inject public AssistantAttentionCondition( @Application CoroutineScope scope, - DreamOverlayStateController dreamOverlayStateController, - AssistUtils assistUtils) { + AssistManager assistManager) { super(scope); - mDreamOverlayStateController = dreamOverlayStateController; - mAssistUtils = assistUtils; + mAssistManager = assistManager; } @Override protected void start() { - mDreamOverlayStateController.addCallback(mCallback); + mAssistManager.addVisualQueryAttentionListener(mVisualQueryAttentionListener); } @Override protected void stop() { - disableVisualQueryDetection(); - mDreamOverlayStateController.removeCallback(mCallback); + mAssistManager.removeVisualQueryAttentionListener(mVisualQueryAttentionListener); } @Override protected int getStartStrategy() { return START_EAGERLY; } - - private void enableVisualQueryDetection() { - if (mEnabled) { - return; - } - mEnabled = true; - mAssistUtils.enableVisualQueryDetection(mVisualQueryDetectionAttentionListener); - } - - private void disableVisualQueryDetection() { - if (!mEnabled) { - return; - } - mEnabled = false; - mAssistUtils.disableVisualQueryDetection(); - // Make sure the condition is set to false as well. - updateCondition(false); - } } diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt index ae40f7e8d7c0..7150d69e130d 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt @@ -432,9 +432,11 @@ constructor( } private inline fun PrintWriter.wrapSection(entry: DumpsysEntry, block: () -> Unit) { + Trace.beginSection(entry.name) preamble(entry) val dumpTime = measureTimeMillis(block) footer(entry, dumpTime) + Trace.endSection() } /** diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt index f7e6b98a0f06..2e9d04bf0bea 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt @@ -16,6 +16,7 @@ package com.android.systemui.dump +import android.os.Trace import java.io.PrintWriter /** @@ -83,31 +84,33 @@ class DumpsysTableLogger( ) { fun printTableData(pw: PrintWriter) { + Trace.beginSection("DumpsysTableLogger#printTableData") printSectionStart(pw) printSchema(pw) printData(pw) printSectionEnd(pw) + Trace.endSection() } private fun printSectionStart(pw: PrintWriter) { - pw.println(HEADER_PREFIX + sectionName) - pw.println("version $VERSION") + pw.append(HEADER_PREFIX).println(sectionName) + pw.append("version ").println(VERSION) } private fun printSectionEnd(pw: PrintWriter) { - pw.println(FOOTER_PREFIX + sectionName) + pw.append(FOOTER_PREFIX).println(sectionName) } private fun printSchema(pw: PrintWriter) { - pw.println(columns.joinToString(separator = SEPARATOR)) + columns.joinTo(pw, separator = SEPARATOR).println() } private fun printData(pw: PrintWriter) { val count = columns.size - rows - .filter { it.size == count } - .forEach { dataLine -> - pw.println(dataLine.joinToString(separator = SEPARATOR)) + rows.forEach { dataLine -> + if (dataLine.size == count) { + dataLine.joinTo(pw, separator = SEPARATOR).println() + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index cfab00125e8c..93937547ee14 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -195,7 +195,7 @@ object Flags { // TODO(b/294110497): Tracking Bug @JvmField val ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS = - unreleasedFlag("enable_wallet_contextual_loyalty_cards", teamfood = true) + releasedFlag("enable_wallet_contextual_loyalty_cards") // TODO(b/242908637): Tracking Bug @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag("wallpaper_fullscreen_preview") @@ -230,7 +230,7 @@ object Flags { // TODO(b/294866904): Tracking bug. @JvmField val WALLPAPER_PICKER_GRID_APPLY_BUTTON = - unreleasedFlag("wallpaper_picker_grid_apply_button") + unreleasedFlag("wallpaper_picker_grid_apply_button") /** Whether to run the new udfps keyguard refactor code. */ // TODO(b/279440316): Tracking bug. @@ -258,8 +258,7 @@ object Flags { /** Whether to listen for fingerprint authentication over keyguard occluding activities. */ // TODO(b/283260512): Tracking bug. - @JvmField val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag("fp_listen_occluding_apps", - teamfood = true) + @JvmField val FP_LISTEN_OCCLUDING_APPS = releasedFlag("fp_listen_occluding_apps") /** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */ // TODO(b/286563884): Tracking bug @@ -297,6 +296,25 @@ object Flags { @JvmField val STOP_FACE_AUTH_ON_DISPLAY_OFF = resourceBooleanFlag( R.bool.flag_stop_face_auth_on_display_off, "stop_face_auth_on_display_off") + /** Flag to disable the face scanning animation pulsing. */ + // TODO(b/295245791): Tracking bug. + @JvmField val STOP_PULSING_FACE_SCANNING_ANIMATION = resourceBooleanFlag( + R.bool.flag_stop_pulsing_face_scanning_animation, + "stop_pulsing_face_scanning_animation") + + /** + * TODO(b/278086361): Tracking bug + * Complete rewrite of the interactions between System UI and Window Manager involving keyguard + * state. When enabled, calls to ActivityTaskManagerService from System UI will exclusively + * occur from [WmLockscreenVisibilityManager] rather than the legacy KeyguardViewMediator. + * + * This flag is under development; some types of unlock may not animate properly if you enable + * it. + */ + @JvmField + val KEYGUARD_WM_STATE_REFACTOR: UnreleasedFlag = + unreleasedFlag("keyguard_wm_state_refactor") + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag("power_menu_lite") @@ -372,7 +390,7 @@ object Flags { // TODO(b/293863612): Tracking Bug @JvmField val INCOMPATIBLE_CHARGING_BATTERY_ICON = - unreleasedFlag("incompatible_charging_battery_icon") + releasedFlag("incompatible_charging_battery_icon") // TODO(b/293585143): Tracking Bug val INSTANT_TETHER = unreleasedFlag("instant_tether") @@ -532,6 +550,12 @@ object Flags { val ENABLE_PIP_APP_ICON_OVERLAY = sysPropBooleanFlag("persist.wm.debug.enable_pip_app_icon_overlay", default = true) + + // TODO(b/293252410) : Tracking Bug + @JvmField + val LOCKSCREEN_ENABLE_LANDSCAPE = + unreleasedFlag("lockscreen.enable_landscape") + // TODO(b/273443374): Tracking Bug @Keep @JvmField @@ -758,4 +782,12 @@ object Flags { /** Enable the Compose implementation of the Quick Settings footer actions. */ @JvmField val COMPOSE_QS_FOOTER_ACTIONS = unreleasedFlag("compose_qs_footer_actions") + + /** Enable the share wifi button in Quick Settings internet dialog. */ + @JvmField + val SHARE_WIFI_QS_BUTTON = unreleasedFlag("share_wifi_qs_button") + + /** Enable haptic slider component in the brightness slider */ + @JvmField + val HAPTIC_BRIGHTNESS_SLIDER = unreleasedFlag("haptic_brightness_slider") } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt index 70783418ddb4..b5b56b2b2108 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt @@ -161,7 +161,7 @@ class KeyboardBacklightDialog( } private fun updateIconTile() { - val iconTile = rootView.findViewById(BACKLIGHT_ICON_ID) as ImageView + val iconTile = rootView.requireViewById(BACKLIGHT_ICON_ID) as ImageView val backgroundDrawable = iconTile.background as ShapeDrawable if (currentLevel == 0) { iconTile.setColorFilter(dimmedIconColor) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index e6053fb3e352..9d2771ec4d89 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -73,6 +73,14 @@ import com.android.internal.policy.IKeyguardService; import com.android.internal.policy.IKeyguardStateCallback; import com.android.keyguard.mediator.ScreenOnCoordinator; import com.android.systemui.SystemUIApplication; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier; +import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder; +import com.android.systemui.keyguard.ui.binder.WindowManagerLockscreenVisibilityViewBinder; +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSurfaceBehindViewModel; +import com.android.systemui.keyguard.ui.viewmodel.WindowManagerLockscreenVisibilityViewModel; import com.android.systemui.settings.DisplayTracker; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; @@ -85,10 +93,13 @@ import java.util.WeakHashMap; import javax.inject.Inject; +import kotlinx.coroutines.CoroutineScope; + public class KeyguardService extends Service { static final String TAG = "KeyguardService"; static final String PERMISSION = android.Manifest.permission.CONTROL_KEYGUARD; + private final FeatureFlags mFlags; private final KeyguardViewMediator mKeyguardViewMediator; private final KeyguardLifecyclesDispatcher mKeyguardLifecyclesDispatcher; private final ScreenOnCoordinator mScreenOnCoordinator; @@ -291,13 +302,33 @@ public class KeyguardService extends Service { KeyguardLifecyclesDispatcher keyguardLifecyclesDispatcher, ScreenOnCoordinator screenOnCoordinator, ShellTransitions shellTransitions, - DisplayTracker displayTracker) { + DisplayTracker displayTracker, + WindowManagerLockscreenVisibilityViewModel + wmLockscreenVisibilityViewModel, + WindowManagerLockscreenVisibilityManager wmLockscreenVisibilityManager, + KeyguardSurfaceBehindViewModel keyguardSurfaceBehindViewModel, + KeyguardSurfaceBehindParamsApplier keyguardSurfaceBehindAnimator, + @Application CoroutineScope scope, + FeatureFlags featureFlags) { super(); mKeyguardViewMediator = keyguardViewMediator; mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher; mScreenOnCoordinator = screenOnCoordinator; mShellTransitions = shellTransitions; mDisplayTracker = displayTracker; + mFlags = featureFlags; + + if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + WindowManagerLockscreenVisibilityViewBinder.bind( + wmLockscreenVisibilityViewModel, + wmLockscreenVisibilityManager, + scope); + + KeyguardSurfaceBehindViewBinder.bind( + keyguardSurfaceBehindViewModel, + keyguardSurfaceBehindAnimator, + scope); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 364614421567..32a9559127a2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -405,7 +405,9 @@ class KeyguardUnlockAnimationController @Inject constructor( * the device. */ fun canPerformInWindowLauncherAnimations(): Boolean { - return isNexusLauncherUnderneath() && + // TODO(b/278086361): Refactor in-window animations. + return !featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR) && + isNexusLauncherUnderneath() && // If the launcher is underneath, but we're about to launch an activity, don't do // the animations since they won't be visible. !notificationShadeWindowController.isLaunchingActivity && @@ -849,54 +851,57 @@ class KeyguardUnlockAnimationController @Inject constructor( } surfaceBehindRemoteAnimationTargets?.forEach { surfaceBehindRemoteAnimationTarget -> - val surfaceHeight: Int = surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height() - - var scaleFactor = (SURFACE_BEHIND_START_SCALE_FACTOR + - (1f - SURFACE_BEHIND_START_SCALE_FACTOR) * - MathUtils.clamp(amount, 0f, 1f)) - - // If we're dismissing via swipe to the Launcher, we'll play in-window scale animations, - // so don't also scale the window. - if (keyguardStateController.isDismissingFromSwipe && - willUnlockWithInWindowLauncherAnimations) { - scaleFactor = 1f - } + if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + val surfaceHeight: Int = + surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height() + + var scaleFactor = (SURFACE_BEHIND_START_SCALE_FACTOR + + (1f - SURFACE_BEHIND_START_SCALE_FACTOR) * + MathUtils.clamp(amount, 0f, 1f)) + + // If we're dismissing via swipe to the Launcher, we'll play in-window scale + // animations, so don't also scale the window. + if (keyguardStateController.isDismissingFromSwipe && + willUnlockWithInWindowLauncherAnimations) { + scaleFactor = 1f + } - // Translate up from the bottom. - surfaceBehindMatrix.setTranslate( - surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(), - surfaceBehindRemoteAnimationTarget.screenSpaceBounds.top.toFloat() + - surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount) - ) + // Translate up from the bottom. + surfaceBehindMatrix.setTranslate( + surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(), + surfaceBehindRemoteAnimationTarget.screenSpaceBounds.top.toFloat() + + surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount) + ) - // Scale up from a point at the center-bottom of the surface. - surfaceBehindMatrix.postScale( - scaleFactor, - scaleFactor, - keyguardViewController.viewRootImpl.width / 2f, - surfaceHeight * SURFACE_BEHIND_SCALE_PIVOT_Y - ) + // Scale up from a point at the center-bottom of the surface. + surfaceBehindMatrix.postScale( + scaleFactor, + scaleFactor, + keyguardViewController.viewRootImpl.width / 2f, + surfaceHeight * SURFACE_BEHIND_SCALE_PIVOT_Y + ) - // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is - // unable to draw - val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget.leash - if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE && - sc?.isValid == true) { - with(SurfaceControl.Transaction()) { - setMatrix(sc, surfaceBehindMatrix, tmpFloat) - setCornerRadius(sc, roundedCornerRadius) - setAlpha(sc, animationAlpha) - apply() + // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is + // unable to draw + val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget.leash + if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE && + sc?.isValid == true) { + with(SurfaceControl.Transaction()) { + setMatrix(sc, surfaceBehindMatrix, tmpFloat) + setCornerRadius(sc, roundedCornerRadius) + setAlpha(sc, animationAlpha) + apply() + } + } else { + applyParamsToSurface( + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( + surfaceBehindRemoteAnimationTarget.leash) + .withMatrix(surfaceBehindMatrix) + .withCornerRadius(roundedCornerRadius) + .withAlpha(animationAlpha) + .build() + ) } - } else { - applyParamsToSurface( - SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( - surfaceBehindRemoteAnimationTarget.leash) - .withMatrix(surfaceBehindMatrix) - .withCornerRadius(roundedCornerRadius) - .withAlpha(animationAlpha) - .build() - ) } } @@ -931,28 +936,41 @@ class KeyguardUnlockAnimationController @Inject constructor( /** * Called by [KeyguardViewMediator] to let us know that the remote animation has finished, and - * we should clean up all of our state. + * we should clean up all of our state. [showKeyguard] will tell us which surface should be + * visible after the animation has been completed or canceled. * * This is generally triggered by us, calling * [KeyguardViewMediator.finishSurfaceBehindRemoteAnimation]. */ - fun notifyFinishedKeyguardExitAnimation(cancelled: Boolean) { + fun notifyFinishedKeyguardExitAnimation(showKeyguard: Boolean) { // Cancel any pending actions. handler.removeCallbacksAndMessages(null) - // Make sure we made the surface behind fully visible, just in case. It should already be - // fully visible. The exit animation is finished, and we should not hold the leash anymore, - // so forcing it to 1f. - surfaceBehindAlpha = 1f - setSurfaceBehindAppearAmount(1f) + // The lockscreen surface is gone, so it is now safe to re-show the smartspace. + if (lockscreenSmartspace?.visibility == View.INVISIBLE) { + lockscreenSmartspace?.visibility = View.VISIBLE + } + + if (!showKeyguard) { + // Make sure we made the surface behind fully visible, just in case. It should already be + // fully visible. The exit animation is finished, and we should not hold the leash anymore, + // so forcing it to 1f. + surfaceBehindAlpha = 1f + setSurfaceBehindAppearAmount(1f) + + try { + launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */) + } catch (e: RemoteException) { + Log.e(TAG, "Remote exception in notifyFinishedKeyguardExitAnimation", e) + } + } + + listeners.forEach { it.onUnlockAnimationFinished() } + + // Reset all state surfaceBehindAlphaAnimator.cancel() surfaceBehindEntryAnimator.cancel() wallpaperCannedUnlockAnimator.cancel() - try { - launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */) - } catch (e: RemoteException) { - Log.e(TAG, "Remote exception in notifyFinishedKeyguardExitAnimation", e) - } // That target is no longer valid since the animation finished, null it out. surfaceBehindRemoteAnimationTargets = null @@ -962,13 +980,6 @@ class KeyguardUnlockAnimationController @Inject constructor( dismissAmountThresholdsReached = false willUnlockWithInWindowLauncherAnimations = false willUnlockWithSmartspaceTransition = false - - // The lockscreen surface is gone, so it is now safe to re-show the smartspace. - if (lockscreenSmartspace?.visibility == View.INVISIBLE) { - lockscreenSmartspace?.visibility = View.VISIBLE - } - - listeners.forEach { it.onUnlockAnimationFinished() } } /** @@ -979,10 +990,12 @@ class KeyguardUnlockAnimationController @Inject constructor( if (keyguardStateController.isShowing) { // Hide the keyguard, with no fade out since we animated it away during the unlock. - keyguardViewController.hide( - surfaceBehindRemoteAnimationStartTime, - 0 /* fadeOutDuration */ - ) + if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + keyguardViewController.hide( + surfaceBehindRemoteAnimationStartTime, + 0 /* fadeOutDuration */ + ) + } } else { Log.i(TAG, "#hideKeyguardViewAfterRemoteAnimation called when keyguard view is not " + "showing. Ignoring...") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 2b6f77d280f8..8e323d8140d5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -137,6 +137,12 @@ constructor( fun bindIndicationArea() { indicationAreaHandle?.dispose() + if (!featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let { + keyguardRootView.removeView(it) + } + } + indicationAreaHandle = KeyguardIndicationAreaBinder.bind( notificationShadeWindowView, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index e2929aec0295..a55cfa7c066d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -171,8 +171,6 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.wallpapers.data.repository.WallpaperRepository; import com.android.wm.shell.keyguard.KeyguardTransitions; -import dagger.Lazy; - import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -182,6 +180,7 @@ import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; +import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; /** @@ -1035,12 +1034,19 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, IRemoteAnimationFinishedCallback finishedCallback) { Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation"); startKeyguardExitAnimation(transit, apps, wallpapers, nonApps, finishedCallback); + if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationStart( + transit, apps, wallpapers, nonApps, finishedCallback); + } Trace.endSection(); } @Override // Binder interface public void onAnimationCancelled() { cancelKeyguardExitAnimation(); + if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationCancelled(); + } } }; @@ -1106,7 +1112,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f); mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration); - mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR); + //mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR); mOccludeByDreamAnimator.addUpdateListener( animation -> { SyncRtSurfaceTransactionApplier.SurfaceParams.Builder @@ -1335,6 +1341,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamingToLockscreenTransitionViewModel; private RemoteAnimationTarget mRemoteAnimationTarget; + private Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager; + /** * Injected constructor. See {@link KeyguardModule}. */ @@ -1377,7 +1385,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, SystemClock systemClock, @Main CoroutineDispatcher mainDispatcher, Lazy<DreamingToLockscreenTransitionViewModel> dreamingToLockscreenTransitionViewModel, - SystemPropertiesHelper systemPropertiesHelper) { + SystemPropertiesHelper systemPropertiesHelper, + Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager) { mContext = context; mUserTracker = userTracker; mFalsingCollector = falsingCollector; @@ -1443,8 +1452,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiEventLogger = uiEventLogger; mSessionTracker = sessionTracker; - mMainDispatcher = mainDispatcher; mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel; + mWmLockscreenVisibilityManager = wmLockscreenVisibilityManager; + mMainDispatcher = mainDispatcher; } public void userActivity() { @@ -2553,7 +2563,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } else if (mSurfaceBehindRemoteAnimationRunning) { // We're already running the keyguard exit animation, likely due to an in-progress swipe // to unlock. - exitKeyguardAndFinishSurfaceBehindRemoteAnimation(false /* cancelled */); + exitKeyguardAndFinishSurfaceBehindRemoteAnimation(false /* showKeyguard */); } else if (!mHideAnimationRun) { if (DEBUG) Log.d(TAG, "tryKeyguardDone: starting pre-hide animation"); mHideAnimationRun = true; @@ -2677,6 +2687,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) { Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ")"); } + + if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + // Handled in WmLockscreenVisibilityManager if flag is enabled. + return; + } + try { ActivityTaskManager.getService().setLockScreenShown(showing, aodShowing); } catch (RemoteException e) { @@ -2716,7 +2732,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } mHiding = false; - mKeyguardViewControllerLazy.get().show(options); + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + // Handled directly in StatusBarKeyguardViewManager if enabled. + mKeyguardViewControllerLazy.get().show(options); + } + resetKeyguardDonePendingLocked(); mHideAnimationRun = false; adjustStatusBarLocked(); @@ -2787,19 +2807,22 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUpdateMonitor.setKeyguardGoingAway(true); mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(true); - // Don't actually hide the Keyguard at the moment, wait for window - // manager until it tells us it's safe to do so with - // startKeyguardExitAnimation. - // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager will be in - // order. - final int keyguardFlag = flags; - mUiBgExecutor.execute(() -> { - try { - ActivityTaskManager.getService().keyguardGoingAway(keyguardFlag); - } catch (RemoteException e) { - Log.e(TAG, "Error while calling WindowManager", e); - } - }); + // Handled in WmLockscreenVisibilityManager if flag is enabled. + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + // Don't actually hide the Keyguard at the moment, wait for window manager until it + // tells us it's safe to do so with startKeyguardExitAnimation. + // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager will be + // in order. + final int keyguardFlag = flags; + mUiBgExecutor.execute(() -> { + try { + ActivityTaskManager.getService().keyguardGoingAway(keyguardFlag); + } catch (RemoteException e) { + Log.e(TAG, "Error while calling WindowManager", e); + } + }); + } + Trace.endSection(); } }; @@ -2913,7 +2936,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (!mHiding && !mSurfaceBehindRemoteAnimationRequested && !mKeyguardStateController.isFlingingToDismissKeyguardDuringSwipeGesture()) { - if (finishedCallback != null) { + // If the flag is enabled, remote animation state is handled in + // WmLockscreenVisibilityManager. + if (finishedCallback != null + && !mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { // There will not execute animation, send a finish callback to ensure the remote // animation won't hang there. try { @@ -2939,10 +2965,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, new IRemoteAnimationFinishedCallback() { @Override public void onAnimationFinished() throws RemoteException { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onAnimationFinished", e); + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onAnimationFinished", e); + } } onKeyguardExitFinished(); mKeyguardViewControllerLazy.get().hide(0 /* startTime */, @@ -2969,7 +2997,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // it will dismiss the panel in that case. } else if (!mStatusBarStateController.leaveOpenOnKeyguardHide() && apps != null && apps.length > 0) { - mSurfaceBehindRemoteAnimationFinishedCallback = finishedCallback; + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + // Handled in WmLockscreenVisibilityManager. Other logic in this class will + // short circuit when this is null. + mSurfaceBehindRemoteAnimationFinishedCallback = finishedCallback; + } mSurfaceBehindRemoteAnimationRunning = true; mInteractionJankMonitor.begin( @@ -2989,7 +3021,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, createInteractionJankMonitorConf( CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RemoteAnimationDisabled")); - mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration); + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + // Handled directly in StatusBarKeyguardViewManager if enabled. + mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration); + } // TODO(bc-animation): When remote animation is enabled for keyguard exit animation, // apps, wallpapers and finishedCallback are set to non-null. nonApps is not yet @@ -2997,19 +3032,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mContext.getMainExecutor().execute(() -> { if (finishedCallback == null) { mKeyguardUnlockAnimationControllerLazy.get() - .notifyFinishedKeyguardExitAnimation(false /* cancelled */); + .notifyFinishedKeyguardExitAnimation(false /* showKeyguard */); mInteractionJankMonitor.end(CUJ_LOCKSCREEN_UNLOCK_ANIMATION); return; } if (apps == null || apps.length == 0) { Slog.e(TAG, "Keyguard exit without a corresponding app to show."); + try { - finishedCallback.onAnimationFinished(); + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + finishedCallback.onAnimationFinished(); + } } catch (RemoteException e) { Slog.e(TAG, "RemoteException"); } finally { mInteractionJankMonitor.end(CUJ_LOCKSCREEN_UNLOCK_ANIMATION); } + return; } @@ -3033,7 +3072,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void onAnimationEnd(Animator animation) { try { - finishedCallback.onAnimationFinished(); + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + finishedCallback.onAnimationFinished(); + } } catch (RemoteException e) { Slog.e(TAG, "RemoteException"); } finally { @@ -3044,7 +3085,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void onAnimationCancel(Animator animation) { try { - finishedCallback.onAnimationFinished(); + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + finishedCallback.onAnimationFinished(); + } } catch (RemoteException e) { Slog.e(TAG, "RemoteException"); } finally { @@ -3114,7 +3157,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // A lock is pending, meaning the keyguard exit animation was cancelled because we're // re-locking. We should just end the surface-behind animation without exiting the // keyguard. The pending lock will be handled by onFinishedGoingToSleep(). - finishSurfaceBehindRemoteAnimation(true); + finishSurfaceBehindRemoteAnimation(true /* showKeyguard */); maybeHandlePendingLock(); } else { Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. " @@ -3123,7 +3166,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // No lock is pending, so the animation was cancelled during the unlock sequence, but // we should end up unlocked. Show the surface and exit the keyguard. showSurfaceBehindKeyguard(); - exitKeyguardAndFinishSurfaceBehindRemoteAnimation(true /* cancelled */); + exitKeyguardAndFinishSurfaceBehindRemoteAnimation(false /* showKeyguard */); } } @@ -3134,12 +3177,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * with the RemoteAnimation, actually hide the keyguard, and clean up state related to the * keyguard exit animation. * - * @param cancelled {@code true} if the animation was cancelled before it finishes. + * @param showKeyguard {@code true} if the animation was cancelled and keyguard should remain + * visible */ - public void exitKeyguardAndFinishSurfaceBehindRemoteAnimation(boolean cancelled) { + public void exitKeyguardAndFinishSurfaceBehindRemoteAnimation(boolean showKeyguard) { Log.d(TAG, "onKeyguardExitRemoteAnimationFinished"); if (!mSurfaceBehindRemoteAnimationRunning && !mSurfaceBehindRemoteAnimationRequested) { - Log.d(TAG, "skip onKeyguardExitRemoteAnimationFinished cancelled=" + cancelled + Log.d(TAG, "skip onKeyguardExitRemoteAnimationFinished showKeyguard=" + showKeyguard + " surfaceAnimationRunning=" + mSurfaceBehindRemoteAnimationRunning + " surfaceAnimationRequested=" + mSurfaceBehindRemoteAnimationRequested); return; @@ -3164,9 +3208,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, + " wasShowing=" + wasShowing); } - mKeyguardUnlockAnimationControllerLazy.get() - .notifyFinishedKeyguardExitAnimation(cancelled); - finishSurfaceBehindRemoteAnimation(cancelled); + finishSurfaceBehindRemoteAnimation(showKeyguard); // Dispatch the callback on animation finishes. mUpdateMonitor.dispatchKeyguardDismissAnimationFinished(); @@ -3194,7 +3236,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, flags |= KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; } - ActivityTaskManager.getService().keyguardGoingAway(flags); + if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + // Handled in WmLockscreenVisibilityManager. + ActivityTaskManager.getService().keyguardGoingAway(flags); + } mKeyguardStateController.notifyKeyguardGoingAway(true); } catch (RemoteException e) { mSurfaceBehindRemoteAnimationRequested = false; @@ -3230,7 +3275,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * This does not set keyguard state to either locked or unlocked, it simply ends the remote * animation on the surface behind the keyguard. This can be called by */ - void finishSurfaceBehindRemoteAnimation(boolean cancelled) { + void finishSurfaceBehindRemoteAnimation(boolean showKeyguard) { + mKeyguardUnlockAnimationControllerLazy.get() + .notifyFinishedKeyguardExitAnimation(showKeyguard); + mSurfaceBehindRemoteAnimationRequested = false; mSurfaceBehindRemoteAnimationRunning = false; mKeyguardStateController.notifyKeyguardGoingAway(false); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt new file mode 100644 index 000000000000..75677f2d9e9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt @@ -0,0 +1,206 @@ +/* + * 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 + +import android.app.IActivityTaskManager +import android.util.Log +import android.view.IRemoteAnimationFinishedCallback +import android.view.RemoteAnimationTarget +import android.view.WindowManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier +import com.android.systemui.statusbar.policy.KeyguardStateController +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * Manages lockscreen and AOD visibility state via the [IActivityTaskManager], and keeps track of + * remote animations related to changes in lockscreen visibility. + */ +@SysUISingleton +class WindowManagerLockscreenVisibilityManager +@Inject +constructor( + @Main private val executor: Executor, + private val activityTaskManagerService: IActivityTaskManager, + private val keyguardStateController: KeyguardStateController, + private val keyguardSurfaceBehindAnimator: KeyguardSurfaceBehindParamsApplier, +) { + + /** + * Whether the lockscreen is showing, which we pass to [IActivityTaskManager.setLockScreenShown] + * in order to show the lockscreen and hide the surface behind the keyguard (or the inverse). + */ + private var isLockscreenShowing = true + + /** + * Whether AOD is showing, which we pass to [IActivityTaskManager.setLockScreenShown] in order + * to show AOD when the lockscreen is visible. + */ + private var isAodVisible = false + + /** + * Whether the keyguard is currently "going away", which we triggered via a call to + * [IActivityTaskManager.keyguardGoingAway]. When we tell WM that the keyguard is going away, + * the app/launcher surface behind the keyguard is made visible, and WM calls + * [onKeyguardGoingAwayRemoteAnimationStart] with a RemoteAnimationTarget so that we can animate + * it. + * + * Going away does not inherently result in [isLockscreenShowing] being set to false; we need to + * do that ourselves once we are done animating the surface. + * + * THIS IS THE ONLY PLACE 'GOING AWAY' TERMINOLOGY SHOULD BE USED. 'Going away' is a WM concept + * and we have gotten into trouble using it to mean various different things in the past. Unlock + * animations may still be visible when the keyguard is NOT 'going away', for example, when we + * play in-window animations, we set the surface to alpha=1f and end the animation immediately. + * The remainder of the animation occurs in-window, so while you might expect that the keyguard + * is still 'going away' because unlock animations are playing, it's actually not. + * + * If you want to know if the keyguard is 'going away', you probably want to check if we have + * STARTED but not FINISHED a transition to GONE. + * + * The going away animation will run until: + * - We manually call [endKeyguardGoingAwayAnimation] after we're done animating. + * - We call [setLockscreenShown] = true, which cancels the going away animation. + * - WM calls [onKeyguardGoingAwayRemoteAnimationCancelled] for another reason (such as the 10 + * second timeout). + */ + private var isKeyguardGoingAway = false + private set(value) { + // TODO(b/278086361): Extricate the keyguard state controller. + keyguardStateController.notifyKeyguardGoingAway(value) + field = value + } + + /** Callback provided by WM to call once we're done with the going away animation. */ + private var goingAwayRemoteAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null + + /** + * Set the visibility of the surface behind the keyguard, making the appropriate calls to Window + * Manager to effect the change. + */ + fun setSurfaceBehindVisibility(visible: Boolean) { + if (isKeyguardGoingAway == visible) { + Log.d(TAG, "WmLockscreenVisibilityManager#setVisibility -> already visible=$visible") + return + } + + // The surface behind is always visible if the lockscreen is not showing, so we're already + // visible. + if (visible && !isLockscreenShowing) { + Log.d(TAG, "#setVisibility -> already visible since the lockscreen isn't showing") + return + } + + if (visible) { + // Make the surface visible behind the keyguard by calling keyguardGoingAway. The + // lockscreen is still showing as well, allowing us to animate unlocked. + Log.d(TAG, "ActivityTaskManagerService#keyguardGoingAway()") + activityTaskManagerService.keyguardGoingAway(0) + isKeyguardGoingAway = true + } else { + // Hide the surface by setting the lockscreen showing. + setLockscreenShown(true) + } + } + + fun setAodVisible(aodVisible: Boolean) { + setWmLockscreenState(aodVisible = aodVisible) + } + + /** Sets the visibility of the lockscreen. */ + fun setLockscreenShown(lockscreenShown: Boolean) { + setWmLockscreenState(lockscreenShowing = lockscreenShown) + } + + fun onKeyguardGoingAwayRemoteAnimationStart( + @WindowManager.TransitionOldType transit: Int, + apps: Array<RemoteAnimationTarget>, + wallpapers: Array<RemoteAnimationTarget>, + nonApps: Array<RemoteAnimationTarget>, + finishedCallback: IRemoteAnimationFinishedCallback + ) { + goingAwayRemoteAnimationFinishedCallback = finishedCallback + keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0]) + } + + fun onKeyguardGoingAwayRemoteAnimationCancelled() { + // If WM cancelled the animation, we need to end immediately even if we're still using the + // animation. + endKeyguardGoingAwayAnimation() + } + + /** + * Whether the going away remote animation target is in-use, which means we're animating it or + * intend to animate it. + * + * Some unlock animations (such as the translation spring animation) are non-deterministic and + * might end after the transition to GONE ends. In that case, we want to keep the remote + * animation running until the spring ends. + */ + fun setUsingGoingAwayRemoteAnimation(usingTarget: Boolean) { + if (!usingTarget) { + endKeyguardGoingAwayAnimation() + } + } + + private fun setWmLockscreenState( + lockscreenShowing: Boolean = this.isLockscreenShowing, + aodVisible: Boolean = this.isAodVisible + ) { + Log.d( + TAG, + "#setWmLockscreenState(" + + "isLockscreenShowing=$lockscreenShowing, " + + "aodVisible=$aodVisible)." + ) + + if (this.isLockscreenShowing == lockscreenShowing && this.isAodVisible == aodVisible) { + return + } + + activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible) + this.isLockscreenShowing = lockscreenShowing + this.isAodVisible = aodVisible + } + + private fun endKeyguardGoingAwayAnimation() { + if (!isKeyguardGoingAway) { + Log.d( + TAG, + "#endKeyguardGoingAwayAnimation() called when isKeyguardGoingAway=false. " + + "Short-circuiting." + ) + return + } + + executor.execute { + Log.d(TAG, "Finishing remote animation.") + goingAwayRemoteAnimationFinishedCallback?.onAnimationFinished() + goingAwayRemoteAnimationFinishedCallback = null + + isKeyguardGoingAway = false + + keyguardSurfaceBehindAnimator.notifySurfaceReleased() + } + } + + companion object { + private val TAG = this::class.java.simpleName + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 4205ed22ff24..dc19ea251546 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -46,6 +46,7 @@ import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager; import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule; import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; @@ -73,12 +74,11 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.wallpapers.data.repository.WallpaperRepository; import com.android.wm.shell.keyguard.KeyguardTransitions; +import java.util.concurrent.Executor; + import dagger.Lazy; import dagger.Module; import dagger.Provides; - -import java.util.concurrent.Executor; - import kotlinx.coroutines.CoroutineDispatcher; /** @@ -144,7 +144,8 @@ public class KeyguardModule { SystemClock systemClock, @Main CoroutineDispatcher mainDispatcher, Lazy<DreamingToLockscreenTransitionViewModel> dreamingToLockscreenTransitionViewModel, - SystemPropertiesHelper systemPropertiesHelper) { + SystemPropertiesHelper systemPropertiesHelper, + Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager) { return new KeyguardViewMediator( context, uiEventLogger, @@ -186,7 +187,8 @@ public class KeyguardModule { systemClock, mainDispatcher, dreamingToLockscreenTransitionViewModel, - systemPropertiesHelper); + systemPropertiesHelper, + wmLockscreenVisibilityManager); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index c019d21e00ed..5d7a3d432dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -59,7 +59,7 @@ constructor( conflatedCallbackFlow { val callback = object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { - override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) { + override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) { val hasCards = response?.walletCards?.isNotEmpty() == true trySendWithFailureLogging( state( @@ -71,7 +71,7 @@ constructor( ) } - override fun onWalletCardRetrievalError(error: GetWalletCardsError?) { + override fun onWalletCardRetrievalError(error: GetWalletCardsError) { Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"") trySendWithFailureLogging( KeyguardQuickAffordanceConfig.LockScreenState.Hidden, @@ -133,13 +133,13 @@ constructor( return suspendCancellableCoroutine { continuation -> val callback = object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { - override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) { + override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) { continuation.resumeWith( Result.success(response?.walletCards ?: emptyList()) ) } - override fun onWalletCardRetrievalError(error: GetWalletCardsError?) { + override fun onWalletCardRetrievalError(error: GetWalletCardsError) { continuation.resumeWith(Result.success(emptyList())) } } 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 30f8f3edfa8f..689414711388 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 @@ -46,6 +46,7 @@ import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.FaceDetectionStatus import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger @@ -160,7 +161,7 @@ constructor( @FaceDetectTableLog private val faceDetectLog: TableLogBuffer, @FaceAuthTableLog private val faceAuthLog: TableLogBuffer, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, - featureFlags: FeatureFlags, + private val featureFlags: FeatureFlags, facePropertyRepository: FacePropertyRepository, dumpManager: DumpManager, ) : DeviceEntryFaceAuthRepository, Dumpable { @@ -286,8 +287,12 @@ constructor( // starts going to sleep. merge( keyguardRepository.wakefulness.map { it.isStartingToSleepOrAsleep() }, - keyguardRepository.isKeyguardGoingAway, - userRepository.userSwitchingInProgress + if (featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + keyguardTransitionInteractor.isInTransitionToState(KeyguardState.GONE) + } else { + keyguardRepository.isKeyguardGoingAway + }, + userRepository.userSwitchingInProgress, ) .onEach { anyOfThemIsTrue -> if (anyOfThemIsTrue) { @@ -581,7 +586,7 @@ constructor( // We always want to invoke face detect in the main thread. faceAuthLogger.faceDetectionStarted() faceManager?.detectFace( - detectCancellationSignal, + checkNotNull(detectCancellationSignal), detectionCallback, FaceAuthenticateOptions.Builder().setUserId(currentUserId).build() ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index e35c3699aabf..42cd3a576926 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -99,7 +99,16 @@ interface KeyguardRepository { /** Is an activity showing over the keyguard? */ val isKeyguardOccluded: Flow<Boolean> - /** Observable for the signal that keyguard is about to go away. */ + /** + * Observable for the signal that keyguard is about to go away. + * + * TODO(b/278086361): Remove once KEYGUARD_WM_STATE_REFACTOR flag is removed. + */ + @Deprecated( + "Use KeyguardTransitionInteractor flows instead. The closest match for 'going " + + "away' is isInTransitionToState(GONE), but consider using more specific flows " + + "whenever possible." + ) val isKeyguardGoingAway: Flow<Boolean> /** Is the always-on display available to be used? */ @@ -365,10 +374,11 @@ constructor( awaitClose { keyguardStateController.removeCallback(callback) } } + .distinctUntilChanged() .stateIn( - scope = scope, - started = SharingStarted.WhileSubscribed(), - initialValue = keyguardStateController.isUnlocked, + scope, + SharingStarted.Eagerly, + initialValue = false, ) override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt index 246ee33fa2e8..2f8010620e5e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt @@ -32,6 +32,11 @@ interface KeyguardRepositoryModule { @Binds fun keyguardRepository(impl: KeyguardRepositoryImpl): KeyguardRepository @Binds + fun keyguardSurfaceBehindRepository( + impl: KeyguardSurfaceBehindRepositoryImpl + ): KeyguardSurfaceBehindRepository + + @Binds fun keyguardTransitionRepository( impl: KeyguardTransitionRepositoryImpl ): KeyguardTransitionRepository diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt new file mode 100644 index 000000000000..014b7fa0aeeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt @@ -0,0 +1,46 @@ +/* + * 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 com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * State related to SysUI's handling of the surface behind the keyguard (typically an app or the + * launcher). We manipulate this surface during unlock animations. + */ +interface KeyguardSurfaceBehindRepository { + + /** Whether we're running animations on the surface. */ + val isAnimatingSurface: Flow<Boolean> + + /** Set whether we're running animations on the surface. */ + fun setAnimatingSurface(animating: Boolean) +} + +@SysUISingleton +class KeyguardSurfaceBehindRepositoryImpl @Inject constructor() : KeyguardSurfaceBehindRepository { + private val _isAnimatingSurface = MutableStateFlow(false) + override val isAnimatingSurface = _isAnimatingSurface.asStateFlow() + + override fun setAnimatingSurface(animating: Boolean) { + _isAnimatingSurface.value = animating + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt index 6a2511fdb90e..1c0b73fcfa2d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -163,12 +163,13 @@ constructor( private fun constructCircleRevealFromPoint(point: Point): LightRevealEffect { return with(point) { + val display = checkNotNull(context.display) CircleReveal( x, y, startRadius = 0, endRadius = - max(max(x, context.display.width - x), max(y, context.display.height - y)), + max(max(x, display.width - x), max(y, display.height - y)), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt index 8f0b91b6d864..271bc38899ec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator -import com.android.app.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository @@ -26,6 +25,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.Utils.Companion.toQuad import com.android.systemui.util.kotlin.Utils.Companion.toQuint import com.android.systemui.util.kotlin.sample +import com.android.wm.shell.animation.Interpolators import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 6b28b270032e..aa771fd16af8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -20,8 +20,11 @@ import android.animation.ValueAnimator import com.android.app.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState @@ -34,7 +37,11 @@ import java.util.UUID import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @SysUISingleton @@ -45,6 +52,7 @@ constructor( override val transitionInteractor: KeyguardTransitionInteractor, @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, + private val flags: FeatureFlags, private val shadeRepository: ShadeRepository, ) : TransitionInteractor( @@ -53,6 +61,7 @@ constructor( override fun start() { listenForLockscreenToGone() + listenForLockscreenToGoneDragging() listenForLockscreenToOccluded() listenForLockscreenToCamera() listenForLockscreenToAodOrDozing() @@ -62,6 +71,63 @@ constructor( listenForLockscreenToAlternateBouncer() } + /** + * Whether we want the surface behind the keyguard visible for the transition from LOCKSCREEN, + * or null if we don't care and should just use a reasonable default. + * + * [KeyguardSurfaceBehindInteractor] will switch to this flow whenever a transition from + * LOCKSCREEN is running. + */ + val surfaceBehindVisibility: Flow<Boolean?> = + transitionInteractor.startedKeyguardTransitionStep + .map { startedStep -> + if (startedStep.to != KeyguardState.GONE) { + // LOCKSCREEN to anything but GONE does not require any special surface + // visibility handling. + return@map null + } + + true // TODO(b/278086361): Implement continuous swipe to unlock. + } + .onStart { + // Default to null ("don't care, use a reasonable default"). + emit(null) + } + .distinctUntilChanged() + + /** + * The surface behind view params to use for the transition from LOCKSCREEN, or null if we don't + * care and should use a reasonable default. + */ + val surfaceBehindModel: Flow<KeyguardSurfaceBehindModel?> = + combine( + transitionInteractor.startedKeyguardTransitionStep, + transitionInteractor.transitionStepsFromState(KeyguardState.LOCKSCREEN) + ) { startedStep, fromLockscreenStep -> + if (startedStep.to != KeyguardState.GONE) { + // Only LOCKSCREEN -> GONE has specific surface params (for the unlock + // animation). + return@combine null + } else if (fromLockscreenStep.value > 0.5f) { + // Start the animation once we're 50% transitioned to GONE. + KeyguardSurfaceBehindModel( + animateFromAlpha = 0f, + alpha = 1f, + animateFromTranslationY = 500f, + translationY = 0f + ) + } else { + KeyguardSurfaceBehindModel( + alpha = 0f, + ) + } + } + .onStart { + // Default to null ("don't care, use a reasonable default"). + emit(null) + } + .distinctUntilChanged() + private fun listenForLockscreenToDreaming() { val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING) scope.launch { @@ -169,7 +235,8 @@ constructor( } // If canceled, just put the state back - // TODO: This logic should happen in FromPrimaryBouncerInteractor. + // TODO(b/278086361): This logic should happen in + // FromPrimaryBouncerInteractor. if (nextState == TransitionState.CANCELED) { transitionRepository.startTransition( TransitionInfo( @@ -201,7 +268,32 @@ constructor( } } + fun dismissKeyguard() { + startTransitionTo(KeyguardState.GONE) + } + private fun listenForLockscreenToGone() { + if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + return + } + + scope.launch { + keyguardInteractor.isKeyguardGoingAway + .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isKeyguardGoingAway, lastStartedStep) = pair + if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { + startTransitionTo(KeyguardState.GONE) + } + } + } + } + + private fun listenForLockscreenToGoneDragging() { + if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + return + } + scope.launch { keyguardInteractor.isKeyguardGoingAway .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair) @@ -291,7 +383,7 @@ constructor( } companion object { - private val DEFAULT_DURATION = 500.milliseconds + private val DEFAULT_DURATION = 400.milliseconds val TO_DREAMING_DURATION = 933.milliseconds val TO_OCCLUDED_DURATION = 450.milliseconds } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 9142d1faa90a..c9f32dabe736 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -17,23 +17,28 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator -import com.android.app.animation.Interpolators import com.android.keyguard.KeyguardSecurityModel -import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.Utils.Companion.toQuad import com.android.systemui.util.kotlin.Utils.Companion.toQuint import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample +import com.android.wm.shell.animation.Interpolators import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @SysUISingleton @@ -44,6 +49,7 @@ constructor( override val transitionInteractor: KeyguardTransitionInteractor, @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, + private val flags: FeatureFlags, private val keyguardSecurityModel: KeyguardSecurityModel, ) : TransitionInteractor( @@ -57,6 +63,57 @@ constructor( listenForPrimaryBouncerToDreamingLockscreenHosted() } + val surfaceBehindVisibility: Flow<Boolean?> = + combine( + transitionInteractor.startedKeyguardTransitionStep, + transitionInteractor.transitionStepsFromState(KeyguardState.PRIMARY_BOUNCER) + ) { startedStep, fromBouncerStep -> + if (startedStep.to != KeyguardState.GONE) { + return@combine null + } + + fromBouncerStep.value > 0.5f + } + .onStart { + // Default to null ("don't care, use a reasonable default"). + emit(null) + } + .distinctUntilChanged() + + val surfaceBehindModel: Flow<KeyguardSurfaceBehindModel?> = + combine( + transitionInteractor.startedKeyguardTransitionStep, + transitionInteractor.transitionStepsFromState(KeyguardState.PRIMARY_BOUNCER) + ) { startedStep, fromBouncerStep -> + if (startedStep.to != KeyguardState.GONE) { + // BOUNCER to anything but GONE does not require any special surface + // visibility handling. + return@combine null + } + + if (fromBouncerStep.value > 0.5f) { + KeyguardSurfaceBehindModel( + animateFromAlpha = 0f, + alpha = 1f, + animateFromTranslationY = 500f, + translationY = 0f, + ) + } else { + KeyguardSurfaceBehindModel( + alpha = 0f, + ) + } + } + .onStart { + // Default to null ("don't care, use a reasonable default"). + emit(null) + } + .distinctUntilChanged() + + fun dismissPrimaryBouncer() { + startTransitionTo(KeyguardState.GONE) + } + private fun listenForPrimaryBouncerToLockscreenOrOccluded() { scope.launch { keyguardInteractor.primaryBouncerShowing @@ -124,28 +181,34 @@ constructor( private fun listenForPrimaryBouncerToDreamingLockscreenHosted() { scope.launch { keyguardInteractor.primaryBouncerShowing - .sample( - combine( - keyguardInteractor.isActiveDreamLockscreenHosted, - transitionInteractor.startedKeyguardTransitionStep, - ::Pair - ), - ::toTriple - ) - .collect { - (isBouncerShowing, isActiveDreamLockscreenHosted, lastStartedTransitionStep) -> - if ( - !isBouncerShowing && - isActiveDreamLockscreenHosted && - lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER - ) { - startTransitionTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED) + .sample( + combine( + keyguardInteractor.isActiveDreamLockscreenHosted, + transitionInteractor.startedKeyguardTransitionStep, + ::Pair + ), + ::toTriple + ) + .collect { (isBouncerShowing, isActiveDreamLockscreenHosted, lastStartedTransitionStep) -> + if ( + !isBouncerShowing && + isActiveDreamLockscreenHosted && + lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER + ) { + startTransitionTo(KeyguardState.DREAMING_LOCKSCREEN_HOSTED) + } } - } } } private fun listenForPrimaryBouncerToGone() { + if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + // This is handled in KeyguardSecurityContainerController and + // StatusBarKeyguardViewManager, which calls the transition interactor to kick off a + // transition vs. listening to legacy state flags. + return + } + scope.launch { keyguardInteractor.isKeyguardGoingAway .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair) @@ -160,7 +223,7 @@ constructor( ) // IME for password requires a slightly faster animation val duration = - if (securityMode == Password) { + if (securityMode == KeyguardSecurityModel.SecurityMode.Password) { TO_GONE_SHORT_DURATION } else { TO_GONE_DURATION @@ -188,7 +251,7 @@ constructor( companion object { private val DEFAULT_DURATION = 300.milliseconds - val TO_GONE_DURATION = 250.milliseconds + val TO_GONE_DURATION = 500.milliseconds val TO_GONE_SHORT_DURATION = 200.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 53d3c076247e..562c4db44be6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -34,12 +34,12 @@ import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.ScreenModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.statusbar.CommandQueue import com.android.systemui.util.kotlin.sample -import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart +import javax.inject.Inject /** * Encapsulates business-logic related to the keyguard but not to a more specific part within it. @@ -264,7 +265,26 @@ constructor( repository.setAnimateDozingTransitions(animate) } + fun isKeyguardDismissable(): Boolean { + return repository.isKeyguardUnlocked.value + } + companion object { private const val TAG = "KeyguardInteractor" + + fun isKeyguardVisibleInState(state: KeyguardState): Boolean { + return when (state) { + KeyguardState.OFF -> true + KeyguardState.DOZING -> true + KeyguardState.DREAMING -> true + KeyguardState.AOD -> true + KeyguardState.ALTERNATE_BOUNCER -> true + KeyguardState.PRIMARY_BOUNCER -> true + KeyguardState.LOCKSCREEN -> true + KeyguardState.GONE -> false + KeyguardState.OCCLUDED -> true + KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> false + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt new file mode 100644 index 000000000000..bf04f8f82430 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardSurfaceBehindRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@SysUISingleton +class KeyguardSurfaceBehindInteractor +@Inject +constructor( + private val repository: KeyguardSurfaceBehindRepository, + private val fromLockscreenInteractor: FromLockscreenTransitionInteractor, + private val fromPrimaryBouncerInteractor: FromPrimaryBouncerTransitionInteractor, + transitionInteractor: KeyguardTransitionInteractor, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + val viewParams: Flow<KeyguardSurfaceBehindModel> = + transitionInteractor.isInTransitionToAnyState + .flatMapLatest { isInTransition -> + if (!isInTransition) { + defaultParams + } else { + combine( + transitionSpecificViewParams, + defaultParams, + ) { transitionParams, defaultParams -> + transitionParams ?: defaultParams + } + } + } + + val isAnimatingSurface = repository.isAnimatingSurface + + private val defaultParams = + transitionInteractor.finishedKeyguardState.map { state -> + KeyguardSurfaceBehindModel( + alpha = + if (WindowManagerLockscreenVisibilityInteractor.isSurfaceVisible(state)) 1f + else 0f + ) + } + + /** + * View params provided by the transition interactor for the most recently STARTED transition. + * This is used to run transition-specific animations on the surface. + * + * If null, there are no transition-specific view params needed for this transition and we will + * use a reasonable default. + */ + @OptIn(ExperimentalCoroutinesApi::class) + private val transitionSpecificViewParams: Flow<KeyguardSurfaceBehindModel?> = + transitionInteractor.startedKeyguardTransitionStep.flatMapLatest { startedStep -> + when (startedStep.from) { + KeyguardState.LOCKSCREEN -> fromLockscreenInteractor.surfaceBehindModel + KeyguardState.PRIMARY_BOUNCER -> fromPrimaryBouncerInteractor.surfaceBehindModel + // Return null for other states, where no transition specific params are needed. + else -> flowOf(null) + } + } + + fun setAnimatingSurface(animating: Boolean) { + repository.setAnimatingSurface(animating) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 8c4c7ae53ce6..93826183a183 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -17,17 +17,20 @@ package com.android.systemui.keyguard.domain.interactor +import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -36,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -46,8 +50,12 @@ import kotlinx.coroutines.flow.stateIn class KeyguardTransitionInteractor @Inject constructor( - private val repository: KeyguardTransitionRepository, @Application val scope: CoroutineScope, + private val repository: KeyguardTransitionRepository, + private val keyguardInteractor: dagger.Lazy<KeyguardInteractor>, + private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>, + private val fromPrimaryBouncerTransitionInteractor: + dagger.Lazy<FromPrimaryBouncerTransitionInteractor>, ) { private val TAG = this::class.simpleName @@ -128,12 +136,11 @@ constructor( repository.transition(PRIMARY_BOUNCER, GONE) /** OFF->LOCKSCREEN transition information. */ - val offToLockscreenTransition: Flow<TransitionStep> = - repository.transition(KeyguardState.OFF, LOCKSCREEN) + val offToLockscreenTransition: Flow<TransitionStep> = repository.transition(OFF, LOCKSCREEN) /** DOZING->LOCKSCREEN transition information. */ val dozingToLockscreenTransition: Flow<TransitionStep> = - repository.transition(KeyguardState.DOZING, LOCKSCREEN) + repository.transition(DOZING, LOCKSCREEN) /** * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> @@ -157,17 +164,30 @@ constructor( val finishedKeyguardTransitionStep: Flow<TransitionStep> = repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } - /** The destination state of the last started transition */ + /** The destination state of the last started transition. */ val startedKeyguardState: StateFlow<KeyguardState> = startedKeyguardTransitionStep .map { step -> step.to } - .stateIn(scope, SharingStarted.Eagerly, KeyguardState.OFF) + .stateIn(scope, SharingStarted.Eagerly, OFF) /** The last completed [KeyguardState] transition */ val finishedKeyguardState: StateFlow<KeyguardState> = finishedKeyguardTransitionStep .map { step -> step.to } .stateIn(scope, SharingStarted.Eagerly, LOCKSCREEN) + + /** + * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed + * it. + */ + val isInTransitionToAnyState = + combine( + startedKeyguardTransitionStep, + finishedKeyguardState, + ) { startedStep, finishedState -> + startedStep.to != finishedState + } + /** * The amount of transition into or out of the given [KeyguardState]. * @@ -187,4 +207,41 @@ constructor( } } } + + fun transitionStepsFromState(fromState: KeyguardState): Flow<TransitionStep> { + return repository.transitions.filter { step -> step.from == fromState } + } + + fun transitionStepsToState(toState: KeyguardState): Flow<TransitionStep> { + return repository.transitions.filter { step -> step.to == toState } + } + + /** + * Called to start a transition that will ultimately dismiss the keyguard from the current + * state. + */ + fun startDismissKeyguardTransition() { + when (startedKeyguardState.value) { + LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard() + PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer() + else -> + Log.e( + "KeyguardTransitionInteractor", + "We don't know how to dismiss keyguard from state " + + "${startedKeyguardState.value}" + ) + } + } + + /** Whether we're in a transition to the given [KeyguardState], but haven't yet completed it. */ + fun isInTransitionToState( + state: KeyguardState, + ): Flow<Boolean> { + return combine( + startedKeyguardTransitionStep, + finishedKeyguardState, + ) { startedStep, finishedState -> + startedStep.to == state && finishedState != state + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt index ff8d5c908735..3ec660a5b6b9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt @@ -22,10 +22,14 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,6 +59,8 @@ constructor( @Application scope: CoroutineScope, private val context: Context, activityStarter: ActivityStarter, + powerInteractor: PowerInteractor, + featureFlags: FeatureFlags, ) { private val keyguardOccludedByApp: Flow<Boolean> = combine( @@ -87,29 +93,37 @@ constructor( .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null)) init { - scope.launch { - // On fingerprint success, go to the home screen - fingerprintUnlockSuccessEvents.collect { goToHomeScreen() } - } + if (featureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) { + scope.launch { + // On fingerprint success when the screen is on, go to the home screen + fingerprintUnlockSuccessEvents.sample(powerInteractor.isInteractive).collect { + if (it) { + goToHomeScreen() + } + // don't go to the home screen if the authentication is from AOD/dozing/off + } + } - scope.launch { - // On device fingerprint lockout, request the bouncer with a runnable to - // go to the home screen. Without this, the bouncer won't proceed to the home screen. - fingerprintLockoutEvents.collect { - activityStarter.dismissKeyguardThenExecute( - object : ActivityStarter.OnDismissAction { - override fun onDismiss(): Boolean { - goToHomeScreen() - return false - } + scope.launch { + // On device fingerprint lockout, request the bouncer with a runnable to + // go to the home screen. Without this, the bouncer won't proceed to the home + // screen. + fingerprintLockoutEvents.collect { + activityStarter.dismissKeyguardThenExecute( + object : ActivityStarter.OnDismissAction { + override fun onDismiss(): Boolean { + goToHomeScreen() + return false + } - override fun willRunAnimationOnKeyguard(): Boolean { - return false - } - }, - /* cancel= */ null, - /* afterKeyguardGone */ false - ) + override fun willRunAnimationOnKeyguard(): Boolean { + return false + } + }, + /* cancel= */ null, + /* afterKeyguardGone */ false + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt new file mode 100644 index 000000000000..96bfdc600a71 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.KeyguardState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@SysUISingleton +class WindowManagerLockscreenVisibilityInteractor +@Inject +constructor( + keyguardInteractor: KeyguardInteractor, + transitionInteractor: KeyguardTransitionInteractor, + surfaceBehindInteractor: KeyguardSurfaceBehindInteractor, + fromLockscreenInteractor: FromLockscreenTransitionInteractor, + fromBouncerInteractor: FromPrimaryBouncerTransitionInteractor, +) { + private val defaultSurfaceBehindVisibility = + transitionInteractor.finishedKeyguardState.map(::isSurfaceVisible) + + /** + * Surface visibility provided by the From*TransitionInteractor responsible for the currently + * RUNNING transition, or null if the current transition does not require special surface + * visibility handling. + * + * An example of transition-specific visibility is swipe to unlock, where the surface should + * only be visible after swiping 20% of the way up the screen, and should become invisible again + * if the user swipes back down. + */ + @OptIn(ExperimentalCoroutinesApi::class) + private val transitionSpecificSurfaceBehindVisibility: Flow<Boolean?> = + transitionInteractor.startedKeyguardTransitionStep + .flatMapLatest { startedStep -> + when (startedStep.from) { + KeyguardState.LOCKSCREEN -> { + fromLockscreenInteractor.surfaceBehindVisibility + } + KeyguardState.PRIMARY_BOUNCER -> { + fromBouncerInteractor.surfaceBehindVisibility + } + else -> flowOf(null) + } + } + .distinctUntilChanged() + + /** + * Surface visibility, which is either determined by the default visibility in the FINISHED + * KeyguardState, or the transition-specific visibility used during certain RUNNING transitions. + */ + @OptIn(ExperimentalCoroutinesApi::class) + val surfaceBehindVisibility: Flow<Boolean> = + transitionInteractor + .isInTransitionToAnyState + .flatMapLatest { isInTransition -> + if (!isInTransition) { + defaultSurfaceBehindVisibility + } else { + combine( + transitionSpecificSurfaceBehindVisibility, + defaultSurfaceBehindVisibility, + ) { transitionVisibility, defaultVisibility -> + // Defer to the transition-specific visibility since we're RUNNING a + // transition, but fall back to the default visibility if the current + // transition's interactor did not specify a visibility. + transitionVisibility ?: defaultVisibility + } + } + } + .distinctUntilChanged() + + /** + * Whether we're animating, or intend to animate, the surface behind the keyguard via remote + * animation. This is used to keep the RemoteAnimationTarget alive until we're done using it. + */ + val usingKeyguardGoingAwayAnimation: Flow<Boolean> = + combine( + transitionInteractor.isInTransitionToState(KeyguardState.GONE), + transitionInteractor.finishedKeyguardState, + surfaceBehindInteractor.isAnimatingSurface + ) { isInTransitionToGone, finishedState, isAnimatingSurface -> + // We may still be animating the surface after the keyguard is fully GONE, since + // some animations (like the translation spring) are not tied directly to the + // transition step amount. + isInTransitionToGone || (finishedState == KeyguardState.GONE && isAnimatingSurface) + } + .distinctUntilChanged() + + /** + * Whether the lockscreen is visible, from the Window Manager (WM) perspective. + * + * Note: This may briefly be true even if the lockscreen UI has animated out (alpha = 0f), as we + * only inform WM once we're done with the keyguard and we're fully GONE. Don't use this if you + * want to know if the AOD/clock/notifs/etc. are visible. + */ + val lockscreenVisibility: Flow<Boolean> = + combine( + transitionInteractor.startedKeyguardTransitionStep, + transitionInteractor.finishedKeyguardState, + ) { startedStep, finishedState -> + // If we finished the transition, use the finished state. If we're running a + // transition, use the state we're transitioning FROM. This can be different from + // the last finished state if a transition is interrupted. For example, if we were + // transitioning from GONE to AOD and then started AOD -> LOCKSCREEN mid-transition, + // we want to immediately use the visibility for AOD (lockscreenVisibility=true) + // even though the lastFinishedState is still GONE (lockscreenVisibility=false). + if (finishedState == startedStep.to) finishedState else startedStep.from + } + .map(::isLockscreenVisible) + .distinctUntilChanged() + + /** + * Whether always-on-display (AOD) is visible when the lockscreen is visible, from window + * manager's perspective. + * + * Note: This may be true even if AOD is not user-visible, such as when the light sensor + * indicates the device is in the user's pocket. Don't use this if you want to know if the AOD + * clock/smartspace/notif icons are visible. + */ + val aodVisibility: Flow<Boolean> = + combine( + keyguardInteractor.isDozing, + keyguardInteractor.biometricUnlockState, + ) { isDozing, biometricUnlockState -> + // AOD is visible if we're dozing, unless we are wake and unlocking (where we go + // directly from AOD to unlocked while dozing). + isDozing && !BiometricUnlockModel.isWakeAndUnlock(biometricUnlockState) + } + .distinctUntilChanged() + + companion object { + fun isSurfaceVisible(state: KeyguardState): Boolean { + return !isLockscreenVisible(state) + } + + fun isLockscreenVisible(state: KeyguardState): Boolean { + return state != KeyguardState.GONE + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt new file mode 100644 index 000000000000..7fb5cfd8f930 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSurfaceBehindModel.kt @@ -0,0 +1,55 @@ +/* + * 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.shared.model + +/** + * Models the appearance of the surface behind the keyguard, and (optionally) how it should be + * animating. + * + * This is intended to be an atomic, high-level description of the surface's appearance and related + * animations, which we can derive from the STARTED/FINISHED transition states rather than the + * individual TransitionSteps. + * + * For example, if we're transitioning from LOCKSCREEN to GONE, that means we should be + * animatingFromAlpha 0f -> 1f and animatingFromTranslationY 500f -> 0f. + * KeyguardSurfaceBehindAnimator can decide how best to implement this, depending on previously + * running animations, spring momentum, and other state. + */ +data class KeyguardSurfaceBehindModel( + val alpha: Float = 1f, + + /** + * If provided, animate from this value to [alpha] unless an animation is already running, in + * which case we'll animate from the current value to [alpha]. + */ + val animateFromAlpha: Float = alpha, + val translationY: Float = 0f, + + /** + * If provided, animate from this value to [translationY] unless an animation is already + * running, in which case we'll animate from the current value to [translationY]. + */ + val animateFromTranslationY: Float = translationY, +) { + fun willAnimateAlpha(): Boolean { + return animateFromAlpha != alpha + } + + fun willAnimateTranslationY(): Boolean { + return animateFromTranslationY != translationY + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt index d6883dddba26..5c072fbfdb01 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt @@ -67,14 +67,15 @@ object KeyguardAmbientIndicationAreaViewBinder { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { keyguardRootViewModel.alpha.collect { alpha -> - view.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO - } - - ambientIndicationArea?.alpha = alpha + ambientIndicationArea?.apply { + this.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + this.alpha = alpha + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index a0a2abe83315..44acf4f0fd2d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -176,14 +176,15 @@ object KeyguardBottomAreaViewBinder { launch { viewModel.alpha.collect { alpha -> - view.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO - } - - ambientIndicationArea?.alpha = alpha + ambientIndicationArea?.apply { + this.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + this.alpha = alpha + } } } @@ -471,7 +472,7 @@ object KeyguardBottomAreaViewBinder { return true } - override fun onLongClickUseDefaultHapticFeedback(view: View?) = false + override fun onLongClickUseDefaultHapticFeedback(view: View) = false } @Deprecated("Deprecated as part of b/278057014") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index a385a0e5754b..dc51944ddc08 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -73,25 +73,27 @@ object KeyguardIndicationAreaBinder { launch { if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { keyguardRootViewModel.alpha.collect { alpha -> - view.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO - } - - indicationArea.alpha = alpha + indicationArea.apply { + this.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + this.alpha = alpha + } } } else { viewModel.alpha.collect { alpha -> - view.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO - } - - indicationArea.alpha = alpha + indicationArea.apply { + this.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + this.alpha = alpha + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt index 63a67913d6cd..83b5463ea19c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -304,7 +304,7 @@ object KeyguardQuickAffordanceViewBinder { return true } - override fun onLongClickUseDefaultHapticFeedback(view: View?) = false + override fun onLongClickUseDefaultHapticFeedback(view: View) = false } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt index 162c109ee299..82610e6ea59d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt @@ -43,7 +43,7 @@ object KeyguardSettingsViewBinder { vibratorHelper: VibratorHelper, activityStarter: ActivityStarter ): DisposableHandle { - val view = parentView.findViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button) + val view = parentView.requireViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button) val disposableHandle = view.repeatWhenAttached { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt new file mode 100644 index 000000000000..fe2df21a35e1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.binder + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.Matrix +import android.util.Log +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.SyncRtSurfaceTransactionApplier +import android.view.View +import androidx.dynamicanimation.animation.FloatValueHolder +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.keyguard.KeyguardViewController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.TAG +import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor +import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel +import com.android.wm.shell.animation.Interpolators +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * Applies [KeyguardSurfaceBehindViewParams] to a RemoteAnimationTarget, starting and managing + * animations as needed. + */ +@SysUISingleton +class KeyguardSurfaceBehindParamsApplier +@Inject +constructor( + @Main private val executor: Executor, + private val keyguardViewController: KeyguardViewController, + private val interactor: KeyguardSurfaceBehindInteractor, +) { + private var surfaceBehind: RemoteAnimationTarget? = null + private val surfaceTransactionApplier: SyncRtSurfaceTransactionApplier + get() = SyncRtSurfaceTransactionApplier(keyguardViewController.viewRootImpl.view) + + private val matrix = Matrix() + private val tmpFloat = FloatArray(9) + + private var animatedTranslationY = FloatValueHolder() + private val translateYSpring = + SpringAnimation(animatedTranslationY).apply { + spring = + SpringForce().apply { + stiffness = 200f + dampingRatio = 1f + } + addUpdateListener { _, _, _ -> applyToSurfaceBehind() } + addEndListener { _, _, _, _ -> + try { + updateIsAnimatingSurface() + } catch (e: NullPointerException) { + // TODO(b/291645410): Remove when we can isolate DynamicAnimations. + e.printStackTrace() + } + } + } + + private var animatedAlpha = 0f + private var alphaAnimator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = 500 + interpolator = Interpolators.ALPHA_IN + addUpdateListener { + animatedAlpha = it.animatedValue as Float + applyToSurfaceBehind() + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + updateIsAnimatingSurface() + } + } + ) + } + + /** + * ViewParams to apply to the surface provided to [applyParamsToSurface]. If the surface is null + * these will be applied once someone gives us a surface via [applyParamsToSurface]. + */ + var viewParams: KeyguardSurfaceBehindModel = KeyguardSurfaceBehindModel() + set(newParams) { + field = newParams + startOrUpdateAnimators() + applyToSurfaceBehind() + } + + /** + * Provides us with a surface to animate. We'll apply the [viewParams] to this surface and start + * any necessary animations. + */ + fun applyParamsToSurface(surface: RemoteAnimationTarget) { + this.surfaceBehind = surface + startOrUpdateAnimators() + } + + /** + * Notifies us that the [RemoteAnimationTarget] has been released, one way or another. + * Attempting to animate a released target will cause a crash. + * + * This can be called either because we finished animating the surface naturally, or by WM + * because external factors cancelled the remote animation (timeout, re-lock, etc). If it's the + * latter, cancel any outstanding animations we have. + */ + fun notifySurfaceReleased() { + surfaceBehind = null + + if (alphaAnimator.isRunning) { + alphaAnimator.cancel() + } + + if (translateYSpring.isRunning) { + translateYSpring.cancel() + } + } + + private fun startOrUpdateAnimators() { + if (surfaceBehind == null) { + return + } + + if (viewParams.willAnimateAlpha()) { + var fromAlpha = viewParams.animateFromAlpha + + if (alphaAnimator.isRunning) { + alphaAnimator.cancel() + fromAlpha = animatedAlpha + } + + alphaAnimator.setFloatValues(fromAlpha, viewParams.alpha) + alphaAnimator.start() + } + + if (viewParams.willAnimateTranslationY()) { + if (!translateYSpring.isRunning) { + // If the spring isn't running yet, set the start value. Otherwise, respect the + // current position. + animatedTranslationY.value = viewParams.animateFromTranslationY + } + + translateYSpring.animateToFinalPosition(viewParams.translationY) + } + + updateIsAnimatingSurface() + } + + private fun updateIsAnimatingSurface() { + interactor.setAnimatingSurface(translateYSpring.isRunning || alphaAnimator.isRunning) + } + + private fun applyToSurfaceBehind() { + surfaceBehind?.leash?.let { sc -> + executor.execute { + if (surfaceBehind == null) { + Log.d( + TAG, + "Attempting to modify params of surface that isn't " + + "animating. Ignoring." + ) + matrix.set(Matrix.IDENTITY_MATRIX) + return@execute + } + + val translationY = + if (translateYSpring.isRunning) animatedTranslationY.value + else viewParams.translationY + + val alpha = + if (alphaAnimator.isRunning) { + animatedAlpha + } else { + viewParams.alpha + } + + if ( + keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE && + sc.isValid + ) { + with(SurfaceControl.Transaction()) { + setMatrix( + sc, + matrix.apply { setTranslate(/* dx= */ 0f, translationY) }, + tmpFloat + ) + setAlpha(sc, alpha) + apply() + } + } else { + surfaceTransactionApplier.scheduleApply( + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(sc) + .withMatrix(matrix.apply { setTranslate(/* dx= */ 0f, translationY) }) + .withAlpha(alpha) + .build() + ) + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindViewBinder.kt new file mode 100644 index 000000000000..599f69f4bc38 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindViewBinder.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.binder + +import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSurfaceBehindViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Binds the [WindowManagerLockscreenVisibilityManager] "view", which manages the visibility of the + * surface behind the keyguard. + */ +object KeyguardSurfaceBehindViewBinder { + @JvmStatic + fun bind( + viewModel: KeyguardSurfaceBehindViewModel, + applier: KeyguardSurfaceBehindParamsApplier, + scope: CoroutineScope + ) { + scope.launch { viewModel.surfaceBehindViewParams.collect { applier.viewParams = it } } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt index b568a9af9bb8..3bb01f29aadf 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt @@ -42,13 +42,13 @@ object UdfpsKeyguardInternalViewBinder { view.accessibilityDelegate = viewModel.accessibilityDelegate // bind child views - UdfpsAodFingerprintViewBinder.bind(view.findViewById(R.id.udfps_aod_fp), aodViewModel) + UdfpsAodFingerprintViewBinder.bind(view.requireViewById(R.id.udfps_aod_fp), aodViewModel) UdfpsFingerprintViewBinder.bind( - view.findViewById(R.id.udfps_lockscreen_fp), + view.requireViewById(R.id.udfps_lockscreen_fp), fingerprintViewModel ) UdfpsBackgroundViewBinder.bind( - view.findViewById(R.id.udfps_keyguard_fp_bg), + view.requireViewById(R.id.udfps_keyguard_fp_bg), backgroundViewModel ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityViewBinder.kt new file mode 100644 index 000000000000..fc0c78a6f76b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityViewBinder.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.binder + +import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager +import com.android.systemui.keyguard.ui.viewmodel.WindowManagerLockscreenVisibilityViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Binds the [WindowManagerLockscreenVisibilityManager] "view", which manages the visibility of the + * surface behind the keyguard. + */ +object WindowManagerLockscreenVisibilityViewBinder { + @JvmStatic + fun bind( + viewModel: WindowManagerLockscreenVisibilityViewModel, + lockscreenVisibilityManager: WindowManagerLockscreenVisibilityManager, + scope: CoroutineScope + ) { + scope.launch { + viewModel.surfaceBehindVisibility.collect { + lockscreenVisibilityManager.setSurfaceBehindVisibility(it) + } + } + + scope.launch { + viewModel.lockscreenVisibility.collect { + lockscreenVisibilityManager.setLockscreenShown(it) + } + } + + scope.launch { + viewModel.aodVisibility.collect { lockscreenVisibilityManager.setAodVisible(it) } + } + + scope.launch { + viewModel.surfaceBehindAnimating.collect { + lockscreenVisibilityManager.setUsingGoingAwayRemoteAnimation(it) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 58bc552ad4ef..580db3509997 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -117,7 +117,7 @@ constructor( private var host: SurfaceControlViewHost val surfacePackage: SurfaceControlViewHost.SurfacePackage - get() = host.surfacePackage + get() = checkNotNull(host.surfacePackage) private lateinit var largeClockHostView: FrameLayout private lateinit var smallClockHostView: FrameLayout diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSurfaceBehindViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSurfaceBehindViewModel.kt new file mode 100644 index 000000000000..4f5296202515 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSurfaceBehindViewModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor +import javax.inject.Inject + +@SysUISingleton +class KeyguardSurfaceBehindViewModel +@Inject +constructor(interactor: KeyguardSurfaceBehindInteractor) { + val surfaceBehindViewParams = interactor.viewParams +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index 11e85d0d85e1..6d3b7f18e974 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneKey import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -51,15 +52,14 @@ constructor( ) /** The key of the scene we should switch to when swiping up. */ - val upDestinationSceneKey = - authenticationInteractor.canSwipeToDismiss - .map { canSwipeToDismiss -> upDestinationSceneKey(canSwipeToDismiss) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = - upDestinationSceneKey(authenticationInteractor.canSwipeToDismiss.value), - ) + val upDestinationSceneKey: Flow<SceneKey> = + authenticationInteractor.isUnlocked.map { isUnlocked -> + if (isUnlocked) { + SceneKey.Gone + } else { + SceneKey.Bouncer + } + } /** Notifies that the lock button on the lock screen was clicked. */ fun onLockButtonClicked() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/WindowManagerLockscreenVisibilityViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/WindowManagerLockscreenVisibilityViewModel.kt new file mode 100644 index 000000000000..f797640fc48c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/WindowManagerLockscreenVisibilityViewModel.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.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor +import javax.inject.Inject + +@SysUISingleton +class WindowManagerLockscreenVisibilityViewModel +@Inject +constructor(interactor: WindowManagerLockscreenVisibilityInteractor) { + val surfaceBehindVisibility = interactor.surfaceBehindVisibility + val surfaceBehindAnimating = interactor.usingKeyguardGoingAwayAnimation + val lockscreenVisibility = interactor.lockscreenVisibility + val aodVisibility = interactor.aodVisibility +} diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index e06483990c90..5f7991e62cd7 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -70,7 +70,7 @@ fun View.repeatWhenAttached( var lifecycleOwner: ViewLifecycleOwner? = null val onAttachListener = object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View?) { + override fun onViewAttachedToWindow(v: View) { Assert.isMainThread() lifecycleOwner?.onDestroy() lifecycleOwner = @@ -81,7 +81,7 @@ fun View.repeatWhenAttached( ) } - override fun onViewDetachedFromWindow(v: View?) { + override fun onViewDetachedFromWindow(v: View) { lifecycleOwner?.onDestroy() lifecycleOwner = null } 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 cc1504a1df97..b6577f747f03 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -120,6 +120,14 @@ public class LogModule { return factory.create("ShadeLog", 500, false); } + /** Provides a logging buffer for Shade messages. */ + @Provides + @SysUISingleton + @ShadeTouchLog + public static LogBuffer provideShadeTouchLogBuffer(LogBufferFactory factory) { + return factory.create("ShadeTouchLog", 500, false); + } + /** Provides a logging buffer for all logs related to managing notification sections. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeTouchLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeTouchLog.java new file mode 100644 index 000000000000..b13667e4822b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeTouchLog.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.android.systemui.log.LogBuffer; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** A {@link LogBuffer} for tracking touches in various shade child views. */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface ShadeTouchLog { +} 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 67a985eb44bc..a7ffc5fa8054 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt @@ -302,14 +302,14 @@ class TableLogBuffer( @Synchronized override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.println(HEADER_PREFIX + name) - pw.println("version $VERSION") + pw.append(HEADER_PREFIX).println(name) + pw.append("version ").println(VERSION) lastEvictedValues.values.sortedBy { it.timestamp }.forEach { it.dump(pw) } for (i in 0 until buffer.size) { buffer[i].dump(pw) } - pw.println(FOOTER_PREFIX + name) + pw.append(FOOTER_PREFIX).println(name) } /** Dumps an individual [TableChange]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt index 35f5a8ca4345..a91917ab97c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt @@ -514,7 +514,7 @@ constructor( * Returns true when the down event of the scroll hits within the target box of the thumb. */ override fun onScroll( - eventStart: MotionEvent, + eventStart: MotionEvent?, event: MotionEvent, distanceX: Float, distanceY: Float @@ -528,7 +528,7 @@ constructor( * Gestures that include a fling are considered a false gesture on the seek bar. */ override fun onFling( - eventStart: MotionEvent, + eventStart: MotionEvent?, event: MotionEvent, velocityX: Float, velocityY: Float diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt index 207df6bc4422..a1291a4d6b0f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt @@ -149,11 +149,7 @@ constructor( // Check if smartspace has explicitly specified whether to re-activate resumable media. // The default behavior is to trigger if the smartspace data is active. val shouldTriggerResume = - if (data.cardAction?.extras?.containsKey(EXTRA_KEY_TRIGGER_RESUME) == true) { - data.cardAction.extras.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) - } else { - true - } + data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true val shouldReactivate = shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive @@ -269,9 +265,7 @@ constructor( "Cannot create dismiss action click action: extras missing dismiss_intent." ) } else if ( - dismissIntent.getComponent() != null && - dismissIntent.getComponent().getClassName() == - EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME + dismissIntent.component?.className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME ) { // Dismiss the card Smartspace data through Smartspace trampoline activity. context.startActivity(dismissIntent) 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 576eb9e6cb07..282a65af1424 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 @@ -22,6 +22,7 @@ import android.app.Notification import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME import android.app.PendingIntent import android.app.StatusBarManager +import android.app.smartspace.SmartspaceAction import android.app.smartspace.SmartspaceConfig import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceSession @@ -1623,20 +1624,18 @@ class MediaDataManager( * SmartspaceTarget's data is invalid. */ private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { - var dismissIntent: Intent? = null - if (target.baseAction != null && target.baseAction.extras != null) { - dismissIntent = - target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) - as Intent? - } + val baseAction: SmartspaceAction? = target.baseAction + val dismissIntent = + baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? val isActive = when { !mediaFlags.isPersistentSsCardEnabled() -> true - target.baseAction == null -> true - else -> - target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) != - EXTRA_VALUE_TRIGGER_PERIODIC + baseAction == null -> true + else -> { + val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) + triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC + } } packageName(target)?.let { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt index d6f941de1b6d..6a8ffb7d8c42 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt @@ -65,7 +65,7 @@ constructor( private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener { - override fun onActiveSessionsChanged(controllers: List<MediaController>) { + override fun onActiveSessionsChanged(controllers: List<MediaController>?) { handleControllersChanged(controllers) } } @@ -190,16 +190,18 @@ constructor( } } - private fun handleControllersChanged(controllers: List<MediaController>) { + private fun handleControllersChanged(controllers: List<MediaController>?) { packageControllers.clear() - controllers.forEach { controller -> + controllers?.forEach { controller -> packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) } ?: run { val tokens = mutableListOf(controller) packageControllers.put(controller.packageName, tokens) } } - tokensWithNotifications.retainAll(controllers.map { TokenId(it.sessionToken) }) + controllers?.map { TokenId(it.sessionToken) }?.let { + tokensWithNotifications.retainAll(it) + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt index b46ebb22ff05..b9cc772e7136 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt @@ -195,7 +195,7 @@ class IlluminationDrawable : Drawable() { } addListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { backgroundAnimation = null } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt index 937a618df68f..646d1d0ff0fc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt @@ -98,11 +98,11 @@ class LightSourceDrawable : Drawable() { addListener( object : AnimatorListenerAdapter() { var cancelled = false - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { cancelled = true } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { if (cancelled) { return } @@ -226,7 +226,7 @@ class LightSourceDrawable : Drawable() { ) addListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { rippleData.progress = 0f rippleAnimation = null invalidateSelf() @@ -270,11 +270,8 @@ class LightSourceDrawable : Drawable() { return bounds } - override fun onStateChange(stateSet: IntArray?): Boolean { + override fun onStateChange(stateSet: IntArray): Boolean { val changed = super.onStateChange(stateSet) - if (stateSet == null) { - return changed - } val wasPressed = pressed var enabled = false diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt index 1ace3168780a..ce50a11cd85d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt @@ -127,19 +127,19 @@ class MediaCarouselScrollHandler( object : GestureDetector.SimpleOnGestureListener() { override fun onFling( eStart: MotionEvent?, - eCurrent: MotionEvent?, + eCurrent: MotionEvent, vX: Float, vY: Float ) = onFling(vX, vY) override fun onScroll( down: MotionEvent?, - lastMotion: MotionEvent?, + lastMotion: MotionEvent, distanceX: Float, distanceY: Float - ) = onScroll(down!!, lastMotion!!, distanceX) + ) = onScroll(down!!, lastMotion, distanceX) - override fun onDown(e: MotionEvent?): Boolean { + override fun onDown(e: MotionEvent): Boolean { if (falsingProtectionNeeded) { falsingCollector.onNotificationStartDismissing() } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index fe8ebafdf9b4..c1c757e424dc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -180,20 +180,20 @@ constructor( object : AnimatorListenerAdapter() { private var cancelled: Boolean = false - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { cancelled = true animationPending = false rootView?.removeCallbacks(startAnimation) } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { isCrossFadeAnimatorRunning = false if (!cancelled) { applyTargetStateIfNotAnimating() } } - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { cancelled = false animationPending = false } @@ -606,7 +606,7 @@ constructor( val viewHost = UniqueObjectHostView(context) viewHost.addOnAttachStateChangeListener( object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(p0: View?) { + override fun onViewAttachedToWindow(p0: View) { if (rootOverlay == null) { rootView = viewHost.viewRootImpl.view rootOverlay = (rootView!!.overlay as ViewGroupOverlay) @@ -614,7 +614,7 @@ constructor( viewHost.removeOnAttachStateChangeListener(this) } - override fun onViewDetachedFromWindow(p0: View?) {} + override fun onViewDetachedFromWindow(p0: View) {} } ) return viewHost diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt index be570b4a1119..631a0b8471b8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt @@ -144,12 +144,12 @@ constructor( setListeningToMediaData(true) hostView.addOnAttachStateChangeListener( object : OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View?) { + override fun onViewAttachedToWindow(v: View) { setListeningToMediaData(true) updateViewVisibility() } - override fun onViewDetachedFromWindow(v: View?) { + override fun onViewDetachedFromWindow(v: View) { setListeningToMediaData(false) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt index 583c626d2156..16dfc2130402 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt @@ -117,7 +117,7 @@ class SquigglyProgress : Drawable() { } addListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { heightAnimator = null } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 7712690de195..3a1d8b0e238e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -296,7 +296,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements @Override public void stop() { - if (isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) { + // unregister broadcast callback should only depend on profile and registered flag + // rather than remote device or broadcast state + // otherwise it might have risks of leaking registered callback handle + if (mMediaOutputController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) { mMediaOutputController.unregisterLeBroadcastServiceCallback(mBroadcastCallback); mIsLeBroadcastCallbackRegistered = false; } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index f3865f52e863..e5a6bb523916 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -104,11 +104,16 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { @Override public boolean isBroadcastSupported() { boolean isBluetoothLeDevice = false; + boolean isBroadcastEnabled = false; if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST)) { if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) { isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice( mMediaOutputController.getCurrentConnectedMediaDevice()); + // if broadcast is active, broadcast should be considered as supported + // there could be a valid case that broadcast is ongoing + // without active LEA device connected + isBroadcastEnabled = mMediaOutputController.isBluetoothLeBroadcastEnabled(); } } else { // To decouple LE Audio Broadcast and Unicast, it always displays the button when there @@ -116,7 +121,8 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { isBluetoothLeDevice = true; } - return mMediaOutputController.isBroadcastSupported() && isBluetoothLeDevice; + return mMediaOutputController.isBroadcastSupported() + && (isBluetoothLeDevice || isBroadcastEnabled); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java index 39d4e6e8d68a..412d1a3a5013 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java @@ -56,6 +56,7 @@ public class MediaOutputMetricLogger { * Update the endpoints of a content switching operation. * This method should be called before a switching operation, so the metric logger can track * source and target devices. + * * @param source the current connected media device * @param target the target media device for content switching to */ @@ -72,37 +73,9 @@ public class MediaOutputMetricLogger { /** * Do the metric logging of content switching success. + * * @param selectedDeviceType string representation of the target media device - * @param deviceList media device list for device count updating - */ - public void logOutputSuccess(String selectedDeviceType, List<MediaDevice> deviceList) { - if (DEBUG) { - Log.d(TAG, "logOutputSuccess - selected device: " + selectedDeviceType); - } - - if (mSourceDevice == null && mTargetDevice == null) { - return; - } - - updateLoggingDeviceCount(deviceList); - - SysUiStatsLog.write( - SysUiStatsLog.MEDIAOUTPUT_OP_SWITCH_REPORTED, - getLoggingDeviceType(mSourceDevice, true), - getLoggingDeviceType(mTargetDevice, false), - SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__RESULT__OK, - SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SUBRESULT__NO_ERROR, - getLoggingPackageName(), - mWiredDeviceCount, - mConnectedBluetoothDeviceCount, - mRemoteDeviceCount, - mAppliedDeviceCountWithinRemoteGroup); - } - - /** - * Do the metric logging of content switching success. - * @param selectedDeviceType string representation of the target media device - * @param deviceItemList media item list for device count updating + * @param deviceItemList media item list for device count updating */ public void logOutputItemSuccess(String selectedDeviceType, List<MediaItem> deviceItemList) { if (DEBUG) { @@ -125,11 +98,14 @@ public class MediaOutputMetricLogger { mWiredDeviceCount, mConnectedBluetoothDeviceCount, mRemoteDeviceCount, - mAppliedDeviceCountWithinRemoteGroup); + mAppliedDeviceCountWithinRemoteGroup, + mTargetDevice.isSuggestedDevice(), + mTargetDevice.hasOngoingSession()); } /** * Do the metric logging of volume adjustment. + * * @param source the device been adjusted */ public void logInteractionAdjustVolume(MediaDevice source) { @@ -141,7 +117,8 @@ public class MediaOutputMetricLogger { SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__ADJUST_VOLUME, getInteractionDeviceType(source), - getLoggingPackageName()); + getLoggingPackageName(), + source.isSuggestedDevice()); } /** @@ -156,7 +133,8 @@ public class MediaOutputMetricLogger { SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__STOP_CASTING, SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE, - getLoggingPackageName()); + getLoggingPackageName(), + /*isSuggestedDevice = */false); } /** @@ -171,42 +149,15 @@ public class MediaOutputMetricLogger { SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__EXPANSION, getInteractionDeviceType(source), - getLoggingPackageName()); - } - - /** - * Do the metric logging of content switching failure. - * @param deviceList media device list for device count updating - * @param reason the reason of content switching failure - */ - public void logOutputFailure(List<MediaDevice> deviceList, int reason) { - if (DEBUG) { - Log.e(TAG, "logRequestFailed - " + reason); - } - - if (mSourceDevice == null && mTargetDevice == null) { - return; - } - - updateLoggingDeviceCount(deviceList); - - SysUiStatsLog.write( - SysUiStatsLog.MEDIAOUTPUT_OP_SWITCH_REPORTED, - getLoggingDeviceType(mSourceDevice, true), - getLoggingDeviceType(mTargetDevice, false), - SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__RESULT__ERROR, - getLoggingSwitchOpSubResult(reason), getLoggingPackageName(), - mWiredDeviceCount, - mConnectedBluetoothDeviceCount, - mRemoteDeviceCount, - mAppliedDeviceCountWithinRemoteGroup); + source.isSuggestedDevice()); } /** * Do the metric logging of content switching failure. + * * @param deviceItemList media item list for device count updating - * @param reason the reason of content switching failure + * @param reason the reason of content switching failure */ public void logOutputItemFailure(List<MediaItem> deviceItemList, int reason) { if (DEBUG) { @@ -229,7 +180,9 @@ public class MediaOutputMetricLogger { mWiredDeviceCount, mConnectedBluetoothDeviceCount, mRemoteDeviceCount, - mAppliedDeviceCountWithinRemoteGroup); + mAppliedDeviceCountWithinRemoteGroup, + mTargetDevice.isSuggestedDevice(), + mTargetDevice.hasOngoingSession()); } private void updateLoggingDeviceCount(List<MediaDevice> deviceList) { @@ -266,7 +219,7 @@ public class MediaOutputMetricLogger { mWiredDeviceCount = mConnectedBluetoothDeviceCount = mRemoteDeviceCount = 0; mAppliedDeviceCountWithinRemoteGroup = 0; - for (MediaItem mediaItem: deviceItemList) { + for (MediaItem mediaItem : deviceItemList) { if (mediaItem.getMediaDevice().isPresent() && mediaItem.getMediaDevice().get().isConnected()) { switch (mediaItem.getMediaDevice().get().getDeviceType()) { @@ -326,6 +279,10 @@ public class MediaOutputMetricLogger { return isSourceDevice ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__REMOTE_GROUP : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__REMOTE_GROUP; + case MediaDevice.MediaDeviceType.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER: + return isSourceDevice + ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__AVR + : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__AVR; default: return isSourceDevice ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__UNKNOWN_TYPE diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index bbd3d33e45c8..da8e106d3019 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -201,13 +201,13 @@ open class MediaTttChipControllerReceiver @Inject constructor( } override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) { - val packageName = newInfo.routeInfo.clientPackageName + val packageName: String? = newInfo.routeInfo.clientPackageName var iconInfo = MediaTttUtils.getIconInfoFromPackageName( context, packageName, isReceiver = true, ) { - logger.logPackageNotFound(packageName) + packageName?.let { logger.logPackageNotFound(it) } } if (newInfo.appNameOverride != null) { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt index 50138024e268..fbf7e25981da 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt @@ -68,9 +68,9 @@ constructor( ) rippleView.addOnAttachStateChangeListener( object : View.OnAttachStateChangeListener { - override fun onViewDetachedFromWindow(view: View?) {} + override fun onViewDetachedFromWindow(view: View) {} - override fun onViewAttachedToWindow(view: View?) { + override fun onViewAttachedToWindow(view: View) { if (view == null) { return } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt index 0b0535df6228..35018f190394 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt @@ -54,7 +54,7 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi // Reset all listeners to animator. animator.removeAllListeners() animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { onAnimationEnd?.run() isStarted = false } @@ -86,7 +86,7 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi invalidate() } animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { animation?.let { visibility = GONE } onAnimationEnd?.run() isStarted = false diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index f75f8b9a18f7..87d0098805b3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -162,7 +162,7 @@ constructor( logger: MediaTttSenderLogger, instanceId: InstanceId, ): ChipbarInfo { - val packageName = routeInfo.clientPackageName + val packageName = checkNotNull(routeInfo.clientPackageName) val otherDeviceName = if (routeInfo.name.isBlank()) { context.getString(R.string.media_ttt_default_device_type) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt index c816446d5c25..64de9bd76122 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt @@ -88,7 +88,7 @@ constructor( .inflate(R.layout.media_projection_recent_tasks, parent, /* attachToRoot= */ false) as ViewGroup - val container = recentsRoot.findViewById<View>(R.id.media_projection_recent_tasks_container) + val container = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_container) container.setTaskHeightSize() val progress = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_loader) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt index 38d4e698f2d9..6480a47e8ea2 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt @@ -81,8 +81,8 @@ constructor( return MediaProjectionState.EntireScreen } val matchingTask = - tasksRepository.findRunningTaskFromWindowContainerToken(session.tokenToRecord) - ?: return MediaProjectionState.EntireScreen + tasksRepository.findRunningTaskFromWindowContainerToken( + checkNotNull(session.tokenToRecord)) ?: return MediaProjectionState.EntireScreen return MediaProjectionState.SingleTask(matchingTask) } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt index 4d30634cda38..63d463402655 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt @@ -77,8 +77,13 @@ internal object NoteTaskRoleManagerExt { .build() } - private fun PackageManager.getApplicationLabel(packageName: String?): String? = - runCatching { getApplicationInfo(packageName, /* flags= */ 0)!! } + private fun PackageManager.getApplicationLabel(packageName: String?): String? { + if (packageName == null) { + return null + } + + return runCatching { getApplicationInfo(packageName, /* flags= */ 0)!! } .getOrNull() ?.let { info -> getApplicationLabel(info).toString() } + } } diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt index 5f338c30c966..46c8d358aaac 100644 --- a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt @@ -132,13 +132,13 @@ object PeopleViewBinder { LayoutInflater.from(context) .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view) - noConversationsView.findViewById<View>(R.id.got_it_button).setOnClickListener { + noConversationsView.requireViewById<View>(R.id.got_it_button).setOnClickListener { onGotItClicked() } // The Tile preview has colorBackground as its background. Change it so it's different than // the activity's background. - val item = noConversationsView.findViewById<LinearLayout>(android.R.id.background) + val item = noConversationsView.requireViewById<LinearLayout>(android.R.id.background) val shape = item.background as GradientDrawable val ta = context.theme.obtainStyledAttributes( diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt index f4aa27d5fcbb..c202f14edc4d 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt @@ -192,7 +192,7 @@ class PrivacyDialogV2( return null } val closeAppButton = - window.layoutInflater.inflate( + checkNotNull(window).layoutInflater.inflate( R.layout.privacy_dialog_card_button, expandedLayout, false @@ -248,7 +248,7 @@ class PrivacyDialogV2( private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View { val manageButton = - window.layoutInflater.inflate( + checkNotNull(window).layoutInflater.inflate( R.layout.privacy_dialog_card_button, expandedLayout, false diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 1afc885cb70d..d2eac45754bd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -23,12 +23,14 @@ import android.graphics.Canvas; import android.graphics.Path; import android.graphics.PointF; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.qs.customize.QSCustomizer; +import com.android.systemui.shade.TouchLogger; import com.android.systemui.util.LargeScreenUtils; import java.io.PrintWriter; @@ -129,6 +131,11 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { } @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch("QS", ev, super.dispatchTouchEvent(ev)); + } + + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); updateExpansion(); diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index cf7abdd34b70..76d9b039e112 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -17,21 +17,22 @@ package com.android.systemui.scene.domain.interactor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.RemoteUserInput import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel -import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn /** * Generic business logic and app state accessors for the scene framework. @@ -44,6 +45,7 @@ import kotlinx.coroutines.flow.mapNotNull class SceneInteractor @Inject constructor( + @Application applicationScope: CoroutineScope, private val repository: SceneContainerRepository, private val logger: SceneLogger, ) { @@ -88,6 +90,22 @@ constructor( */ val transitionState: StateFlow<ObservableTransitionState> = repository.transitionState + /** + * The key of the scene that the UI is currently transitioning to or `null` if there is no + * active transition at the moment. + * + * This is a convenience wrapper around [transitionState], meant for flow-challenged consumers + * like Java code. + */ + val transitioningTo: StateFlow<SceneKey?> = + transitionState + .map { state -> (state as? ObservableTransitionState.Transition)?.toScene } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) + /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = repository.isVisible @@ -142,21 +160,6 @@ constructor( repository.setTransitionState(transitionState) } - /** - * Returns a stream of events that emits one [Unit] every time the framework transitions from - * [from] to [to]. - */ - fun finishedSceneTransitions(from: SceneKey, to: SceneKey): Flow<Unit> { - return transitionState - .mapNotNull { it as? ObservableTransitionState.Idle } - .map { idleState -> idleState.scene } - .distinctUntilChanged() - .pairwise() - .mapNotNull { (previousSceneKey, currentSceneKey) -> - Unit.takeIf { previousSceneKey == from && currentSceneKey == to } - } - } - /** Handles a remote user input. */ fun onRemoteUserInput(input: RemoteUserInput) { _remoteUserInput.value = input diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index afefccb27214..17470998cf74 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -178,12 +178,24 @@ constructor( } WakefulnessState.STARTING_TO_WAKE -> { val authMethod = authenticationInteractor.getAuthenticationMethod() - if (authMethod == AuthenticationMethodModel.None) { - switchToScene( - targetSceneKey = SceneKey.Gone, - loggingReason = - "device is starting to wake up while auth method is None", - ) + val isUnlocked = authenticationInteractor.isUnlocked.value + when { + authMethod == AuthenticationMethodModel.None -> { + switchToScene( + targetSceneKey = SceneKey.Gone, + loggingReason = + "device is starting to wake up while auth method is" + + " none", + ) + } + authMethod.isSecure && isUnlocked -> { + switchToScene( + targetSceneKey = SceneKey.Gone, + loggingReason = + "device is starting to wake up while unlocked with a" + + " secure auth method", + ) + } } } else -> Unit diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt index b34004397520..23894a3f7835 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt @@ -50,22 +50,20 @@ open class BaseScreenSharePermissionDialog( public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - window.apply { - addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS) - setGravity(Gravity.CENTER) - } + window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS) + window?.setGravity(Gravity.CENTER) setContentView(R.layout.screen_share_dialog) - dialogTitle = findViewById(R.id.screen_share_dialog_title) - warning = findViewById(R.id.text_warning) - startButton = findViewById(android.R.id.button1) - cancelButton = findViewById(android.R.id.button2) + dialogTitle = requireViewById(R.id.screen_share_dialog_title) + warning = requireViewById(R.id.text_warning) + startButton = requireViewById(android.R.id.button1) + cancelButton = requireViewById(android.R.id.button2) updateIcon() initScreenShareOptions() createOptionsView(getOptionsViewLayoutId()) } private fun updateIcon() { - val icon = findViewById<ImageView>(R.id.screen_share_dialog_icon) + val icon = requireViewById<ImageView>(R.id.screen_share_dialog_icon) if (dialogIconTint != null) { icon.setColorFilter(context.getColor(dialogIconTint)) } @@ -92,7 +90,7 @@ open class BaseScreenSharePermissionDialog( options ) adapter.setDropDownViewResource(R.layout.screen_share_dialog_spinner_item_text) - screenShareModeSpinner = findViewById(R.id.screen_share_mode_spinner) + screenShareModeSpinner = requireViewById(R.id.screen_share_mode_spinner) screenShareModeSpinner.adapter = adapter screenShareModeSpinner.onItemSelectedListener = this } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt index 604d44967e68..e8683fbea735 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt @@ -100,11 +100,11 @@ class ScreenRecordPermissionDialog( @LayoutRes override fun getOptionsViewLayoutId(): Int = R.layout.screen_record_options private fun initRecordOptionsView() { - audioSwitch = findViewById(R.id.screenrecord_audio_switch) - tapsSwitch = findViewById(R.id.screenrecord_taps_switch) - tapsView = findViewById(R.id.show_taps) + audioSwitch = requireViewById(R.id.screenrecord_audio_switch) + tapsSwitch = requireViewById(R.id.screenrecord_taps_switch) + tapsView = requireViewById(R.id.show_taps) updateTapsViewVisibility() - options = findViewById(R.id.screen_recording_options) + options = requireViewById(R.id.screen_recording_options) val a: ArrayAdapter<*> = ScreenRecordingAdapter(context, android.R.layout.simple_spinner_dropdown_item, MODES) a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt index ecd456887fa4..aa6bfc3473d2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt @@ -83,7 +83,7 @@ constructor( if (overrideTransition) { val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0) try { - WindowManagerGlobal.getWindowManagerService() + checkNotNull(WindowManagerGlobal.getWindowManagerService()) .overridePendingAppTransitionRemote(runner, displayTracker.defaultDisplayId) } catch (e: Exception) { Log.e(TAG, "Error overriding screenshot app transition", e) diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java index fc89a9e637ec..f4d19dc2691d 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java @@ -30,6 +30,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Looper; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; @@ -38,6 +39,7 @@ import androidx.core.graphics.ColorUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor; +import com.android.systemui.shade.TouchLogger; import com.android.systemui.util.LargeScreenUtils; import java.util.concurrent.Executor; @@ -59,6 +61,7 @@ public class ScrimView extends View { private float mViewAlpha = 1.0f; private Drawable mDrawable; private PorterDuffColorFilter mColorFilter; + private String mScrimName; private int mTintColor; private boolean mBlendWithMainColor = true; private Runnable mChangeRunnable; @@ -336,6 +339,15 @@ public class ScrimView extends View { } } + public void setScrimName(String scrimName) { + mScrimName = scrimName; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch(mScrimName, ev, super.dispatchTouchEvent(ev)); + } + /** * The position of the bottom of the scrim, used for clipping. * @see #enableBottomEdgeConcave(boolean) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt index 6143308a7cbf..4644d415ea72 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt @@ -108,7 +108,7 @@ class NPVCDownEventState private constructor( * @see NPVCDownEventState.asStringList */ fun toList(): List<Row> { - return buffer.asSequence().map { it.asStringList }.toList() + return buffer.map { it.asStringList } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelView.java index af3cc860df57..c501d88b77ce 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelView.java @@ -106,6 +106,11 @@ public final class NotificationPanelView extends FrameLayout { } @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch("NPV", ev, super.dispatchTouchEvent(ev)); + } + + @Override public void dispatchConfigurationChanged(Configuration newConfig) { super.dispatchConfigurationChanged(newConfig); mOnConfigurationChangedListener.onConfigurationChanged(newConfig); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 35fd98cf34c6..132cd6115bc7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1117,7 +1117,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), mDreamingToLockscreenTransition, mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(), - setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); + setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY( mDreamingToLockscreenTransitionTranslationY), setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher); @@ -1153,7 +1154,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(), mLockscreenToDreamingTransition, mMainDispatcher); collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(), - setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); + setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); collectFlow(mView, mLockscreenToDreamingTransitionViewModel.lockscreenTranslationY( mLockscreenToDreamingTransitionTranslationY), setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher); @@ -2129,6 +2131,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } updateExpansionAndVisibility(); mNotificationStackScrollLayoutController.setPanelFlinging(false); + mShadeLog.d("onFlingEnd called"); // TODO(b/277909752): remove log when bug is fixed // expandImmediate should be always reset at the end of animation mQsController.setExpandImmediate(false); } @@ -2664,6 +2667,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump setListening(true); } if (mBarState != SHADE) { + // TODO(b/277909752): remove below logs when bug is fixed + mShadeLog.d("onExpandingFinished called"); + if (mSplitShadeEnabled && !mQsController.getExpanded()) { + mShadeLog.d("onExpandingFinished called before QS got expanded"); + } // updating qsExpandImmediate is done in onPanelStateChanged for unlocked shade but // on keyguard panel state is always OPEN so we need to have that extra update mQsController.setExpandImmediate(false); @@ -3447,11 +3455,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ipw.print("mIgnoreXTouchSlop="); ipw.println(mIgnoreXTouchSlop); ipw.print("mExpandLatencyTracking="); ipw.println(mExpandLatencyTracking); ipw.println("gestureExclusionRect:" + calculateGestureExclusionRect()); + Trace.beginSection("Table<DownEvents>"); new DumpsysTableLogger( TAG, NPVCDownEventState.TABLE_HEADERS, mLastDownEvents.toList() ).printTableData(ipw); + Trace.endSection(); } @Override @@ -4714,6 +4724,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void onPanelStateChanged(@PanelState int state) { + mShadeLog.logPanelStateChanged(state); mQsController.updateExpansionEnabledAmbient(); if (state == STATE_OPEN && mCurrentPanelState != state) { @@ -4740,6 +4751,16 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mCurrentPanelState = state; } + private Consumer<Float> setDreamLockscreenTransitionAlpha( + NotificationStackScrollLayoutController stackScroller) { + return (Float alpha) -> { + // Also animate the status bar's alpha during transitions between the lockscreen and + // dreams. + mKeyguardStatusBarViewController.setAlpha(alpha); + setTransitionAlpha(stackScroller).accept(alpha); + }; + } + private Consumer<Float> setTransitionAlpha( NotificationStackScrollLayoutController stackScroller) { return (Float alpha) -> { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 1f401fbbe6c1..d2b62eb3779d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -840,13 +840,17 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW pw.println(" mDeferWindowLayoutParams=" + mDeferWindowLayoutParams); pw.println(mCurrentState); if (mWindowRootView != null && mWindowRootView.getViewRootImpl() != null) { + Trace.beginSection("mWindowRootView.dump()"); mWindowRootView.getViewRootImpl().dump(" ", pw); + Trace.endSection(); } + Trace.beginSection("Table<State>"); new DumpsysTableLogger( TAG, NotificationShadeWindowState.TABLE_HEADERS, mStateBuffer.toList() ).printTableData(pw); + Trace.endSection(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index d25294343d2f..e3010ca72194 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -170,7 +170,7 @@ class NotificationShadeWindowState( * @see [NotificationShadeWindowState.asStringList] */ fun toList(): List<Row> { - return buffer.asSequence().map { it.asStringList }.toList() + return buffer.map { it.asStringList } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java index a9c4aebe7ed6..f9b4e671f373 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java @@ -107,6 +107,8 @@ public class NotificationShadeWindowView extends WindowRootView { result = result != null ? result : super.dispatchTouchEvent(ev); + TouchLogger.logDispatchTouch(TAG, ev, result); + mInteractionEventHandler.dispatchTouchEventComplete(); return result; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 18e964462189..832a25bfbc41 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -91,6 +91,7 @@ public class NotificationShadeWindowViewController { private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; private final LockIconViewController mLockIconViewController; + private final ShadeLogger mShadeLogger; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final StatusBarWindowStateController mStatusBarWindowStateController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; @@ -151,6 +152,7 @@ public class NotificationShadeWindowViewController { KeyguardUnlockAnimationController keyguardUnlockAnimationController, NotificationInsetsController notificationInsetsController, AmbientState ambientState, + ShadeLogger shadeLogger, PulsingGestureListener pulsingGestureListener, LockscreenHostedDreamGestureListener lockscreenHostedDreamGestureListener, KeyguardBouncerViewModel keyguardBouncerViewModel, @@ -176,6 +178,7 @@ public class NotificationShadeWindowViewController { mStatusBarWindowStateController = statusBarWindowStateController; mLockIconViewController = lockIconViewController; mBackActionInteractor = backActionInteractor; + mShadeLogger = shadeLogger; mLockIconViewController.init(); mService = centralSurfaces; mPowerInteractor = powerInteractor; @@ -223,6 +226,13 @@ public class NotificationShadeWindowViewController { return mView.findViewById(R.id.keyguard_message_area); } + private Boolean logDownDispatch(MotionEvent ev, String msg, Boolean result) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mShadeLogger.logShadeWindowDispatch(ev, msg, result); + } + return result; + } + /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */ public void setupExpandedStatusBar() { mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller); @@ -237,8 +247,8 @@ public class NotificationShadeWindowViewController { @Override public Boolean handleDispatchTouchEvent(MotionEvent ev) { if (mStatusBarViewController == null) { // Fix for b/192490822 - Log.w(TAG, "Ignoring touch while statusBarView not yet set."); - return false; + return logDownDispatch(ev, + "Ignoring touch while statusBarView not yet set", false); } boolean isDown = ev.getActionMasked() == MotionEvent.ACTION_DOWN; boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP; @@ -250,10 +260,9 @@ public class NotificationShadeWindowViewController { } // Reset manual touch dispatch state here but make sure the UP/CANCEL event still - // gets - // delivered. + // gets delivered. if (!isCancel && mService.shouldIgnoreTouch()) { - return false; + return logDownDispatch(ev, "touch ignored by CS", false); } if (isDown) { @@ -265,8 +274,11 @@ public class NotificationShadeWindowViewController { mTouchActive = false; mDownEvent = null; } - if (mTouchCancelled || mExpandAnimationRunning) { - return false; + if (mTouchCancelled) { + return logDownDispatch(ev, "touch cancelled", false); + } + if (mExpandAnimationRunning) { + return logDownDispatch(ev, "expand animation running", false); } if (mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) { @@ -280,17 +292,17 @@ public class NotificationShadeWindowViewController { } if (mIsOcclusionTransitionRunning) { - return false; + return logDownDispatch(ev, "occlusion transition running", false); } mFalsingCollector.onTouchEvent(ev); mPulsingWakeupGestureHandler.onTouchEvent(ev); if (mDreamingWakeupGestureHandler != null && mDreamingWakeupGestureHandler.onTouchEvent(ev)) { - return true; + return logDownDispatch(ev, "dream wakeup gesture handled", true); } if (mStatusBarKeyguardViewManager.dispatchTouchEvent(ev)) { - return true; + return logDownDispatch(ev, "dispatched to Keyguard", true); } if (mBrightnessMirror != null && mBrightnessMirror.getVisibility() == View.VISIBLE) { @@ -298,7 +310,7 @@ public class NotificationShadeWindowViewController { // you can't touch anything other than the brightness slider while the mirror is // showing and the rest of the panel is transparent. if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { - return false; + return logDownDispatch(ev, "disallowed new pointer", false); } } if (isDown) { @@ -329,7 +341,9 @@ public class NotificationShadeWindowViewController { expandingBelowNotch = true; } if (expandingBelowNotch) { - return mStatusBarViewController.sendTouchToView(ev); + return logDownDispatch(ev, + "expand below notch. sending touch to status bar", + mStatusBarViewController.sendTouchToView(ev)); } if (!mIsTrackingBarGesture && isDown @@ -339,9 +353,10 @@ public class NotificationShadeWindowViewController { if (mStatusBarViewController.touchIsWithinView(x, y)) { if (mStatusBarWindowStateController.windowIsShowing()) { mIsTrackingBarGesture = true; - return mStatusBarViewController.sendTouchToView(ev); - } else { // it's hidden or hiding, don't send to notification shade. - return true; + return logDownDispatch(ev, "sending touch to status bar", + mStatusBarViewController.sendTouchToView(ev)); + } else { + return logDownDispatch(ev, "hidden or hiding", true); } } } else if (mIsTrackingBarGesture) { @@ -349,10 +364,10 @@ public class NotificationShadeWindowViewController { if (isUp || isCancel) { mIsTrackingBarGesture = false; } - return sendToStatusBar; + return logDownDispatch(ev, "sending bar gesture to status bar", + sendToStatusBar); } - - return null; + return logDownDispatch(ev, "no custom touch dispatch of down event", null); } @Override @@ -364,18 +379,26 @@ public class NotificationShadeWindowViewController { public boolean shouldInterceptTouchEvent(MotionEvent ev) { if (mStatusBarStateController.isDozing() && !mService.isPulsing() && !mDockManager.isDocked()) { - // Capture all touch events in always-on. + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mShadeLogger.d("NSWVC: capture all touch events in always-on"); + } return true; } if (mStatusBarKeyguardViewManager.shouldInterceptTouchEvent(ev)) { // Don't allow touches to proceed to underlying views if alternate // bouncer is showing + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mShadeLogger.d("NSWVC: alt bouncer showing"); + } return true; } if (mLockIconViewController.onInterceptTouchEvent(ev)) { // immediately return true; don't send the touch to the drag down helper + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mShadeLogger.d("NSWVC: don't send touch to drag down helper"); + } return true; } @@ -383,7 +406,13 @@ public class NotificationShadeWindowViewController { && mDragDownHelper.isDragDownEnabled() && !mService.isBouncerShowing() && !mStatusBarStateController.isDozing()) { - return mDragDownHelper.onInterceptTouchEvent(ev); + boolean result = mDragDownHelper.onInterceptTouchEvent(ev); + if (result) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mShadeLogger.d("NSWVC: drag down helper intercepted"); + } + } + return result; } else { return false; } @@ -495,6 +524,7 @@ public class NotificationShadeWindowViewController { } public void cancelCurrentTouch() { + mShadeLogger.d("NSWVC: cancelling current touch"); if (mTouchActive) { final long now = mClock.uptimeMillis(); final MotionEvent event; @@ -508,6 +538,7 @@ public class NotificationShadeWindowViewController { MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); event.setSource(InputDevice.SOURCE_TOUCHSCREEN); } + Log.w(TAG, "Canceling current touch event (should be very rare)"); mView.dispatchTouchEvent(event); event.recycle(); mTouchCancelled = true; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index 3b3df50f1520..a4e439b04fc0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -22,6 +22,7 @@ import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; import android.view.WindowInsets; @@ -183,6 +184,12 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout } @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch("NotificationsQuickSettingsContainer", ev, + super.dispatchTouchEvent(ev)); + } + + @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { if (mIsMigratingNSSL) { return super.drawChild(canvas, child, drawingTime); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index baac57ca44ba..ac7aeae43713 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -787,6 +787,12 @@ public class QuickSettingsController implements Dumpable { /** update Qs height state */ public void setExpansionHeight(float height) { + // TODO(b/277909752): remove below log when bug is fixed + if (mSplitShadeEnabled && mShadeExpandedFraction == 1.0f && height == 0) { + Log.wtf(TAG, + "setting QS height to 0 in split shade while shade is open(ing). " + + "Value of mExpandImmediate = " + mExpandImmediate); + } int maxHeight = getMaxExpansionHeight(); height = Math.min(Math.max( height, getMinExpansionHeight()), maxHeight); @@ -933,7 +939,6 @@ public class QuickSettingsController implements Dumpable { return mShadeExpandedHeight; } - @VisibleForTesting void setExpandImmediate(boolean expandImmediate) { if (expandImmediate != mExpandImmediate) { mShadeLog.logQsExpandImmediateChanged(expandImmediate); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index 22c638177a48..d7a339210412 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -27,6 +27,8 @@ import android.view.WindowManagerGlobal; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.log.LogBuffer; +import com.android.systemui.log.dagger.ShadeTouchLog; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationPresenter; @@ -56,6 +58,7 @@ public final class ShadeControllerImpl implements ShadeController { private final CommandQueue mCommandQueue; private final Executor mMainExecutor; + private final LogBuffer mTouchLog; private final KeyguardStateController mKeyguardStateController; private final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarStateController mStatusBarStateController; @@ -79,6 +82,7 @@ public final class ShadeControllerImpl implements ShadeController { public ShadeControllerImpl( CommandQueue commandQueue, @Main Executor mainExecutor, + @ShadeTouchLog LogBuffer touchLog, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, @@ -92,6 +96,7 @@ public final class ShadeControllerImpl implements ShadeController { ) { mCommandQueue = commandQueue; mMainExecutor = mainExecutor; + mTouchLog = touchLog; mShadeViewControllerLazy = shadeViewControllerLazy; mStatusBarStateController = statusBarStateController; mStatusBarWindowController = statusBarWindowController; @@ -413,6 +418,7 @@ public final class ShadeControllerImpl implements ShadeController { @Override public void start() { + TouchLogger.logTouchesTo(mTouchLog); getShadeViewController().setTrackingStartedListener(this::runPostCollapseRunnables); getShadeViewController().setOpenCloseListener( new OpenCloseListener() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt index c6cb9c4d347c..bea12de5ab49 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt @@ -130,12 +130,12 @@ constructor( private lateinit var carrierIconSlots: List<String> private lateinit var mShadeCarrierGroupController: ShadeCarrierGroupController - private val batteryIcon: BatteryMeterView = header.findViewById(R.id.batteryRemainingIcon) - private val clock: Clock = header.findViewById(R.id.clock) - private val date: TextView = header.findViewById(R.id.date) - private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons) - private val mShadeCarrierGroup: ShadeCarrierGroup = header.findViewById(R.id.carrier_group) - private val systemIcons: View = header.findViewById(R.id.shade_header_system_icons) + private val batteryIcon: BatteryMeterView = header.requireViewById(R.id.batteryRemainingIcon) + private val clock: Clock = header.requireViewById(R.id.clock) + private val date: TextView = header.requireViewById(R.id.date) + private val iconContainer: StatusIconContainer = header.requireViewById(R.id.statusIcons) + private val mShadeCarrierGroup: ShadeCarrierGroup = header.requireViewById(R.id.carrier_group) + private val systemIcons: View = header.requireViewById(R.id.shade_header_system_icons) private var roundedCorners = 0 private var cutout: DisplayCutout? = null @@ -582,7 +582,7 @@ constructor( inner class CustomizerAnimationListener( private val enteringCustomizing: Boolean, ) : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) header.animate().setListener(null) if (enteringCustomizing) { @@ -590,7 +590,7 @@ constructor( } } - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { super.onAnimationStart(animation) if (!enteringCustomizing) { customizing = false diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 1c30bddfe859..8d23f5d71681 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -79,19 +79,39 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { fun logMotionEvent(event: MotionEvent, message: String) { buffer.log( - TAG, - LogLevel.VERBOSE, - { - str1 = message - long1 = event.eventTime - long2 = event.downTime - int1 = event.action - int2 = event.classification - double1 = event.y.toDouble() - }, - { - "$str1: eventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2" - } + TAG, + LogLevel.VERBOSE, + { + str1 = message + long1 = event.eventTime + long2 = event.downTime + int1 = event.action + int2 = event.classification + }, + { + "$str1: eventTime=$long1,downTime=$long2,action=$int1,class=$int2" + } + ) + } + + /** Logs motion event dispatch results from NotificationShadeWindowViewController. */ + fun logShadeWindowDispatch(event: MotionEvent, message: String, result: Boolean?) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = message + long1 = event.eventTime + long2 = event.downTime + }, + { + val prefix = when (result) { + true -> "SHADE TOUCH REROUTED" + false -> "SHADE TOUCH BLOCKED" + null -> "SHADE TOUCH DISPATCHED" + } + "$prefix: eventTime=$long1,downTime=$long2, reason=$str1" + } ) } @@ -316,6 +336,17 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { ) } + fun logPanelStateChanged(@PanelState panelState: Int) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = panelState.panelStateToString() + }, + { "New panel State: $str1" } + ) + } + fun flingQs(flingType: Int, isClick: Boolean) { buffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index 6e7678407805..05b1ac64a5f6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -106,7 +106,7 @@ abstract class ShadeViewProviderModule { featureFlags: FeatureFlags, ): NotificationShadeWindowView { if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { - return root.findViewById(R.id.legacy_window_root) + return root.requireViewById(R.id.legacy_window_root) } return root as NotificationShadeWindowView? ?: throw IllegalStateException("root view not a NotificationShadeWindowView") @@ -118,7 +118,7 @@ abstract class ShadeViewProviderModule { fun providesNotificationStackScrollLayout( notificationShadeWindowView: NotificationShadeWindowView, ): NotificationStackScrollLayout { - return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller) + return notificationShadeWindowView.requireViewById(R.id.notification_stack_scroller) } @Provides @@ -153,7 +153,7 @@ abstract class ShadeViewProviderModule { fun providesNotificationPanelView( notificationShadeWindowView: NotificationShadeWindowView, ): NotificationPanelView { - return notificationShadeWindowView.findViewById(R.id.notification_panel) + return notificationShadeWindowView.requireViewById(R.id.notification_panel) } /** @@ -175,7 +175,7 @@ abstract class ShadeViewProviderModule { fun providesLightRevealScrim( notificationShadeWindowView: NotificationShadeWindowView, ): LightRevealScrim { - return notificationShadeWindowView.findViewById(R.id.light_reveal_scrim) + return notificationShadeWindowView.requireViewById(R.id.light_reveal_scrim) } @Provides @@ -183,7 +183,7 @@ abstract class ShadeViewProviderModule { fun providesKeyguardRootView( notificationShadeWindowView: NotificationShadeWindowView, ): KeyguardRootView { - return notificationShadeWindowView.findViewById(R.id.keyguard_root_view) + return notificationShadeWindowView.requireViewById(R.id.keyguard_root_view) } @Provides @@ -191,7 +191,7 @@ abstract class ShadeViewProviderModule { fun providesSharedNotificationContainer( notificationShadeWindowView: NotificationShadeWindowView, ): SharedNotificationContainer { - return notificationShadeWindowView.findViewById(R.id.shared_notification_container) + return notificationShadeWindowView.requireViewById(R.id.shared_notification_container) } // TODO(b/277762009): Only allow this view's controller to inject the view. See above. @@ -200,7 +200,7 @@ abstract class ShadeViewProviderModule { fun providesAuthRippleView( notificationShadeWindowView: NotificationShadeWindowView, ): AuthRippleView? { - return notificationShadeWindowView.findViewById(R.id.auth_ripple) + return notificationShadeWindowView.requireViewById(R.id.auth_ripple) } // TODO(b/277762009): Only allow this view's controller to inject the view. See above. @@ -212,9 +212,9 @@ abstract class ShadeViewProviderModule { featureFlags: FeatureFlags ): LockIconView { if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) { - return keyguardRootView.findViewById(R.id.lock_icon_view) + return keyguardRootView.requireViewById(R.id.lock_icon_view) } else { - return notificationPanelView.findViewById(R.id.lock_icon_view) + return notificationPanelView.requireViewById(R.id.lock_icon_view) } } @@ -224,7 +224,7 @@ abstract class ShadeViewProviderModule { fun providesTapAgainView( notificationPanelView: NotificationPanelView, ): TapAgainView { - return notificationPanelView.findViewById(R.id.shade_falsing_tap_again) + return notificationPanelView.requireViewById(R.id.shade_falsing_tap_again) } // TODO(b/277762009): Only allow this view's controller to inject the view. See above. @@ -233,7 +233,7 @@ abstract class ShadeViewProviderModule { fun providesNotificationsQuickSettingsContainer( notificationShadeWindowView: NotificationShadeWindowView, ): NotificationsQuickSettingsContainer { - return notificationShadeWindowView.findViewById(R.id.notification_container_parent) + return notificationShadeWindowView.requireViewById(R.id.notification_container_parent) } // TODO(b/277762009): Only allow this view's controller to inject the view. See above. @@ -243,7 +243,7 @@ abstract class ShadeViewProviderModule { fun providesShadeHeaderView( notificationShadeWindowView: NotificationShadeWindowView, ): MotionLayout { - val stub = notificationShadeWindowView.findViewById<ViewStub>(R.id.qs_header_stub) + val stub = notificationShadeWindowView.requireViewById<ViewStub>(R.id.qs_header_stub) val layoutId = R.layout.combined_qs_header stub.layoutResource = layoutId return stub.inflate() as MotionLayout @@ -260,7 +260,7 @@ abstract class ShadeViewProviderModule { @SysUISingleton @Named(SHADE_HEADER) fun providesBatteryMeterView(@Named(SHADE_HEADER) view: MotionLayout): BatteryMeterView { - return view.findViewById(R.id.batteryRemainingIcon) + return view.requireViewById(R.id.batteryRemainingIcon) } @Provides @@ -295,7 +295,7 @@ abstract class ShadeViewProviderModule { fun providesOngoingPrivacyChip( @Named(SHADE_HEADER) header: MotionLayout, ): OngoingPrivacyChip { - return header.findViewById(R.id.privacy_chip) + return header.requireViewById(R.id.privacy_chip) } @Provides @@ -304,7 +304,7 @@ abstract class ShadeViewProviderModule { fun providesStatusIconContainer( @Named(SHADE_HEADER) header: MotionLayout, ): StatusIconContainer { - return header.findViewById(R.id.statusIcons) + return header.requireViewById(R.id.statusIcons) } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/TouchLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/TouchLogger.kt new file mode 100644 index 000000000000..58704bfbbcbf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/TouchLogger.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.view.MotionEvent +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel + +private const val TAG = "systemui.shade.touch" + +/** + * A logger for tracking touch dispatching in the shade view hierarchy. The purpose of this logger + * is to passively observe dispatchTouchEvent calls in order to see which subtrees of the shade are + * handling touches. Additionally, some touches may be passively observed for views near the top of + * the shade hierarchy that cannot intercept touches, i.e. scrims. The usage of static methods for + * logging is sub-optimal in many ways, but it was selected in this case to make usage of this + * non-function diagnostic code as low friction as possible. + */ +class TouchLogger { + companion object { + private var touchLogger: DispatchTouchLogger? = null + + @JvmStatic + fun logTouchesTo(buffer: LogBuffer) { + touchLogger = DispatchTouchLogger(buffer) + } + + @JvmStatic + fun logDispatchTouch(viewTag: String, ev: MotionEvent, result: Boolean): Boolean { + touchLogger?.logDispatchTouch(viewTag, ev, result) + return result + } + } +} + +/** Logs touches. */ +private class DispatchTouchLogger(private val buffer: LogBuffer) { + fun logDispatchTouch(viewTag: String, ev: MotionEvent, result: Boolean) { + // NOTE: never log position of touches for security purposes + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = viewTag + int1 = ev.action + long1 = ev.downTime + bool1 = result + }, + { "Touch: view=$str1, type=${typeToString(int1)}, downtime=$long1, result=$bool1" } + ) + } + + private fun typeToString(type: Int): String { + return when (type) { + MotionEvent.ACTION_DOWN -> "DOWN" + MotionEvent.ACTION_UP -> "UP" + MotionEvent.ACTION_MOVE -> "MOVE" + MotionEvent.ACTION_CANCEL -> "CANCEL" + MotionEvent.ACTION_POINTER_DOWN -> "POINTER_DOWN" + MotionEvent.ACTION_POINTER_UP -> "POINTER_UP" + else -> "OTHER" + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index 87abc9208d45..8edc26d01d71 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -25,7 +25,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn /** Models UI state and handles user input for the shade scene. */ @@ -39,14 +39,22 @@ constructor( ) { /** The key of the scene we should switch to when swiping up. */ val upDestinationSceneKey: StateFlow<SceneKey> = - authenticationInteractor.isUnlocked - .map { isUnlocked -> upDestinationSceneKey(isUnlocked = isUnlocked) } + combine( + authenticationInteractor.isUnlocked, + authenticationInteractor.canSwipeToDismiss, + ) { isUnlocked, canSwipeToDismiss -> + upDestinationSceneKey( + isUnlocked = isUnlocked, + canSwipeToDismiss = canSwipeToDismiss, + ) + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = upDestinationSceneKey( isUnlocked = authenticationInteractor.isUnlocked.value, + canSwipeToDismiss = authenticationInteractor.canSwipeToDismiss.value, ), ) @@ -57,7 +65,12 @@ constructor( private fun upDestinationSceneKey( isUnlocked: Boolean, + canSwipeToDismiss: Boolean, ): SceneKey { - return if (isUnlocked) SceneKey.Gone else SceneKey.Lockscreen + return when { + canSwipeToDismiss -> SceneKey.Lockscreen + isUnlocked -> SceneKey.Gone + else -> SceneKey.Lockscreen + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt index 37140ec2aa32..520976746785 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt @@ -37,8 +37,8 @@ class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: Attri init { inflate(context, R.layout.battery_status_chip, this) - roundedContainer = findViewById(R.id.rounded_container) - batteryMeterView = findViewById(R.id.battery_meter_view) + roundedContainer = requireViewById(R.id.rounded_container) + batteryMeterView = requireViewById(R.id.battery_meter_view) updateResources() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index 823bb355a307..3120128c7967 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt @@ -14,9 +14,11 @@ import android.graphics.Shader import android.os.Trace import android.util.AttributeSet import android.util.MathUtils.lerp +import android.view.MotionEvent import android.view.View import android.view.animation.PathInterpolator import com.android.app.animation.Interpolators +import com.android.systemui.shade.TouchLogger import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold import com.android.systemui.util.getColorWithAlpha import com.android.systemui.util.leak.RotationUtils @@ -234,6 +236,8 @@ class PowerButtonReveal( } } +private const val TAG = "LightRevealScrim" + /** * Scrim view that partially reveals the content underneath it using a [RadialGradient] with a * transparent center. The center position, size, and stops of the gradient can be manipulated to @@ -419,15 +423,14 @@ constructor( revealGradientCenter.y = top + (revealGradientHeight / 2f) } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { if ( - canvas == null || - revealGradientWidth <= 0 || - revealGradientHeight <= 0 || - revealAmount == 0f + revealGradientWidth <= 0 || + revealGradientHeight <= 0 || + revealAmount == 0f ) { if (revealAmount < 1f) { - canvas?.drawColor(revealGradientEndColor) + canvas.drawColor(revealGradientEndColor) } return } @@ -447,6 +450,10 @@ constructor( canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint) } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return TouchLogger.logDispatchTouch(TAG, event, super.dispatchTouchEvent(event)) + } + private fun setPaintColorFilter() { gradientPaint.colorFilter = PorterDuffColorFilter( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 4710574ac20d..672796a513e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -474,7 +474,7 @@ class LockscreenShadeTransitionController @Inject constructor( } if (endlistener != null) { dragDownAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { endlistener.invoke() } }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt index 750272d65659..17b4e3baef13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt @@ -66,7 +66,7 @@ class MediaArtworkProcessor @Inject constructor() { inBitmap = oldIn.copy(Bitmap.Config.ARGB_8888, false /* isMutable */) oldIn.recycle() } - val outBitmap = Bitmap.createBitmap(inBitmap.width, inBitmap.height, + val outBitmap = Bitmap.createBitmap(inBitmap?.width ?: 0, inBitmap?.height ?: 0, Bitmap.Config.ARGB_8888) input = Allocation.createFromBitmap(renderScript, inBitmap, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 0e20df6ecfba..6ad1395964a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -272,7 +272,7 @@ class NotificationShadeDepthController @Inject constructor( blurUtils.blurRadiusOfRatio(animation.animatedValue as Float) } addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { keyguardAnimator = null wakeAndUnlockBlurRadius = 0f } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index eddb6835318d..d1e0a7193948 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -234,7 +234,7 @@ open class PrivacyDotViewController @Inject constructor( } // Set the dot's view gravity to hug the status bar - (corner.findViewById<View>(R.id.privacy_dot) + (corner.requireViewById<View>(R.id.privacy_dot) .layoutParams as FrameLayout.LayoutParams) .gravity = rotatedCorner.innerGravity() } @@ -255,7 +255,7 @@ open class PrivacyDotViewController @Inject constructor( // in every rotation. The only thing we need to check is rtl val rtl = state.layoutRtl val size = Point() - tl.context.display.getRealSize(size) + tl.context.display?.getRealSize(size) val currentRotation = RotationUtils.getExactRotation(tl.context) val displayWidth: Int diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt index 6e8b8bdebbe3..1ad4620f9dfa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -168,10 +168,8 @@ class SystemEventChipAnimationController @Inject constructor( } val keyFrame1Height = dotSize * 2 - val v = currentAnimatedView!!.view - val chipVerticalCenter = v.top + v.measuredHeight / 2 - val height1 = ValueAnimator.ofInt( - currentAnimatedView!!.view.measuredHeight, keyFrame1Height).apply { + val chipVerticalCenter = chipBounds.top + chipBounds.height() / 2 + val height1 = ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply { startDelay = 8.frames duration = 6.frames interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt index 23edf1787649..2403920e69f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt @@ -164,7 +164,9 @@ constructor( } private fun isChipAnimationEnabled(): Boolean { - return DeviceConfig.getBoolean(NAMESPACE_PRIVACY, CHIP_ANIMATION_ENABLED, true) + val defaultValue = + context.resources.getBoolean(R.bool.config_enablePrivacyChipAnimation) + return DeviceConfig.getBoolean(NAMESPACE_PRIVACY, CHIP_ANIMATION_ENABLED, defaultValue) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt index e0183971141d..f40f57099110 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt @@ -243,7 +243,7 @@ constructor( if (!event.showAnimation && event.forceVisible) { // If animations are turned off, we'll transition directly to the dot animationState.value = SHOWING_PERSISTENT_DOT - notifyTransitionToPersistentDot() + notifyTransitionToPersistentDot(event) return } @@ -335,7 +335,7 @@ constructor( } animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot)) if (hasPersistentDot) { - val dotAnim = notifyTransitionToPersistentDot() + val dotAnim = notifyTransitionToPersistentDot(currentlyDisplayedEvent) if (dotAnim != null) { animators.add(dotAnim) } @@ -344,12 +344,12 @@ constructor( return AnimatorSet().also { it.playTogether(animators) } } - private fun notifyTransitionToPersistentDot(): Animator? { + private fun notifyTransitionToPersistentDot(event: StatusEvent?): Animator? { logger?.logTransitionToPersistentDotCallbackInvoked() val anims: List<Animator> = listeners.mapNotNull { it.onSystemStatusAnimationTransitionToPersistentDot( - currentlyDisplayedEvent?.contentDescription + event?.contentDescription ) } if (anims.isNotEmpty()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 903d48536946..c5de16511aef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -179,15 +179,20 @@ constructor( } if (weatherTarget != null) { val clickIntent = weatherTarget.headerAction?.intent - val weatherData = WeatherData.fromBundle(weatherTarget.baseAction.extras, { v -> - if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { - activityStarter.startActivity( - clickIntent, - true, /* dismissShade */ - null, - false) + val weatherData = weatherTarget.baseAction?.extras?.let { extras -> + WeatherData.fromBundle( + extras, + ) { _ -> + if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + activityStarter.startActivity( + clickIntent, + true, /* dismissShade */ + null, + false) + } } - }) + } + if (weatherData != null) { keyguardUpdateMonitor.sendWeatherData(weatherData) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt index 16f1a45ba83f..1b43922f652a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt @@ -74,7 +74,7 @@ class ViewGroupFadeHelper { root.setTag(R.id.view_group_fade_helper_previous_value_tag, newAlpha) } addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { endRunnable?.run() } }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java deleted file mode 100644 index f04b24ebfb42..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.collection.coordinator; - -import static android.app.NotificationManager.IMPORTANCE_MIN; - -import android.app.Notification; -import android.service.notification.StatusBarNotification; - -import com.android.systemui.ForegroundServiceController; -import com.android.systemui.appops.AppOpsController; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.statusbar.notification.collection.ListEntry; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; -import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import javax.inject.Inject; - -/** - * Handles ForegroundService and AppOp interactions with notifications. - * Tags notifications with appOps - * Lifetime extends notifications associated with an ongoing ForegroundService. - * Filters out notifications that represent foreground services that are no longer running - * Puts foreground service notifications into the FGS section. See {@link NotifCoordinators} for - * section ordering priority. - * - * Previously this logic lived in - * frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceController - * frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener - * frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender - */ -@CoordinatorScope -public class AppOpsCoordinator implements Coordinator { - private static final String TAG = "AppOpsCoordinator"; - - private final ForegroundServiceController mForegroundServiceController; - private final AppOpsController mAppOpsController; - private final DelayableExecutor mMainExecutor; - - private NotifPipeline mNotifPipeline; - - @Inject - public AppOpsCoordinator( - ForegroundServiceController foregroundServiceController, - AppOpsController appOpsController, - @Main DelayableExecutor mainExecutor) { - mForegroundServiceController = foregroundServiceController; - mAppOpsController = appOpsController; - mMainExecutor = mainExecutor; - } - - @Override - public void attach(NotifPipeline pipeline) { - mNotifPipeline = pipeline; - - // filter out foreground service notifications that aren't necessary anymore - mNotifPipeline.addPreGroupFilter(mNotifFilter); - - } - - public NotifSectioner getSectioner() { - return mNotifSectioner; - } - - /** - * Filters out notifications that represent foreground services that are no longer running or - * that already have an app notification with the appOps tagged to - */ - private final NotifFilter mNotifFilter = new NotifFilter(TAG) { - @Override - public boolean shouldFilterOut(NotificationEntry entry, long now) { - StatusBarNotification sbn = entry.getSbn(); - - // Filters out system-posted disclosure notifications when unneeded - if (mForegroundServiceController.isDisclosureNotification(sbn) - && !mForegroundServiceController.isDisclosureNeededForUser( - sbn.getUser().getIdentifier())) { - return true; - } - return false; - } - }; - - /** - * Puts colorized foreground service and call notifications into its own section. - */ - private final NotifSectioner mNotifSectioner = new NotifSectioner("ForegroundService", - NotificationPriorityBucketKt.BUCKET_FOREGROUND_SERVICE) { - @Override - public boolean isInSection(ListEntry entry) { - NotificationEntry notificationEntry = entry.getRepresentativeEntry(); - if (notificationEntry != null) { - return isColorizedForegroundService(notificationEntry) || isCall(notificationEntry); - } - return false; - } - - private boolean isColorizedForegroundService(NotificationEntry entry) { - Notification notification = entry.getSbn().getNotification(); - return notification.isForegroundService() - && notification.isColorized() - && entry.getImportance() > IMPORTANCE_MIN; - } - - private boolean isCall(NotificationEntry entry) { - Notification notification = entry.getSbn().getNotification(); - return entry.getImportance() > IMPORTANCE_MIN - && notification.isStyle(Notification.CallStyle.class); - } - }; -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java new file mode 100644 index 000000000000..63997f8f2457 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.coordinator; + +import static android.app.NotificationManager.IMPORTANCE_MIN; + +import android.app.Notification; + +import com.android.systemui.statusbar.notification.collection.ListEntry; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; +import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; + +import javax.inject.Inject; + +/** + * Handles sectioning for foreground service notifications. + * Puts non-min colorized foreground service notifications into the FGS section. See + * {@link NotifCoordinators} for section ordering priority. + */ +@CoordinatorScope +public class ColorizedFgsCoordinator implements Coordinator { + private static final String TAG = "ColorizedCoordinator"; + + @Inject + public ColorizedFgsCoordinator() { + } + + @Override + public void attach(NotifPipeline pipeline) { + } + + public NotifSectioner getSectioner() { + return mNotifSectioner; + } + + + /** + * Puts colorized foreground service and call notifications into its own section. + */ + private final NotifSectioner mNotifSectioner = new NotifSectioner("ColorizedSectioner", + NotificationPriorityBucketKt.BUCKET_FOREGROUND_SERVICE) { + @Override + public boolean isInSection(ListEntry entry) { + NotificationEntry notificationEntry = entry.getRepresentativeEntry(); + if (notificationEntry != null) { + return isColorizedForegroundService(notificationEntry) || isCall(notificationEntry); + } + return false; + } + + private boolean isColorizedForegroundService(NotificationEntry entry) { + Notification notification = entry.getSbn().getNotification(); + return notification.isForegroundService() + && notification.isColorized() + && entry.getImportance() > IMPORTANCE_MIN; + } + + private boolean isCall(NotificationEntry entry) { + Notification notification = entry.getSbn().getNotification(); + return entry.getImportance() > IMPORTANCE_MIN + && notification.isStyle(Notification.CallStyle.class); + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index 0ccab9e46b72..226a9577c820 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -33,34 +33,34 @@ interface NotifCoordinators : Coordinator, PipelineDumpable @CoordinatorScope class NotifCoordinatorsImpl @Inject constructor( - sectionStyleProvider: SectionStyleProvider, - featureFlags: FeatureFlags, - dataStoreCoordinator: DataStoreCoordinator, - hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, - hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, - keyguardCoordinator: KeyguardCoordinator, - rankingCoordinator: RankingCoordinator, - appOpsCoordinator: AppOpsCoordinator, - deviceProvisionedCoordinator: DeviceProvisionedCoordinator, - bubbleCoordinator: BubbleCoordinator, - headsUpCoordinator: HeadsUpCoordinator, - gutsCoordinator: GutsCoordinator, - conversationCoordinator: ConversationCoordinator, - debugModeCoordinator: DebugModeCoordinator, - groupCountCoordinator: GroupCountCoordinator, - groupWhenCoordinator: GroupWhenCoordinator, - mediaCoordinator: MediaCoordinator, - preparationCoordinator: PreparationCoordinator, - remoteInputCoordinator: RemoteInputCoordinator, - rowAppearanceCoordinator: RowAppearanceCoordinator, - stackCoordinator: StackCoordinator, - shadeEventCoordinator: ShadeEventCoordinator, - smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, - viewConfigCoordinator: ViewConfigCoordinator, - visualStabilityCoordinator: VisualStabilityCoordinator, - sensitiveContentCoordinator: SensitiveContentCoordinator, - dismissibilityCoordinator: DismissibilityCoordinator, - dreamCoordinator: DreamCoordinator, + sectionStyleProvider: SectionStyleProvider, + featureFlags: FeatureFlags, + dataStoreCoordinator: DataStoreCoordinator, + hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, + hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, + keyguardCoordinator: KeyguardCoordinator, + rankingCoordinator: RankingCoordinator, + colorizedFgsCoordinator: ColorizedFgsCoordinator, + deviceProvisionedCoordinator: DeviceProvisionedCoordinator, + bubbleCoordinator: BubbleCoordinator, + headsUpCoordinator: HeadsUpCoordinator, + gutsCoordinator: GutsCoordinator, + conversationCoordinator: ConversationCoordinator, + debugModeCoordinator: DebugModeCoordinator, + groupCountCoordinator: GroupCountCoordinator, + groupWhenCoordinator: GroupWhenCoordinator, + mediaCoordinator: MediaCoordinator, + preparationCoordinator: PreparationCoordinator, + remoteInputCoordinator: RemoteInputCoordinator, + rowAppearanceCoordinator: RowAppearanceCoordinator, + stackCoordinator: StackCoordinator, + shadeEventCoordinator: ShadeEventCoordinator, + smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, + viewConfigCoordinator: ViewConfigCoordinator, + visualStabilityCoordinator: VisualStabilityCoordinator, + sensitiveContentCoordinator: SensitiveContentCoordinator, + dismissibilityCoordinator: DismissibilityCoordinator, + dreamCoordinator: DreamCoordinator, ) : NotifCoordinators { private val mCoreCoordinators: MutableList<CoreCoordinator> = ArrayList() @@ -79,7 +79,7 @@ class NotifCoordinatorsImpl @Inject constructor( mCoordinators.add(hideNotifsForOtherUsersCoordinator) mCoordinators.add(keyguardCoordinator) mCoordinators.add(rankingCoordinator) - mCoordinators.add(appOpsCoordinator) + mCoordinators.add(colorizedFgsCoordinator) mCoordinators.add(deviceProvisionedCoordinator) mCoordinators.add(bubbleCoordinator) mCoordinators.add(debugModeCoordinator) @@ -106,7 +106,7 @@ class NotifCoordinatorsImpl @Inject constructor( // Manually add Ordered Sections mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp - mOrderedSections.add(appOpsCoordinator.sectioner) // ForegroundService + mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService mOrderedSections.add(conversationCoordinator.peopleAlertingSectioner) // People Alerting mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 106d11f6bcc1..7d1cca81e614 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification -import com.android.systemui.ForegroundServiceNotificationListener import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.people.widget.PeopleSpaceWidgetManager @@ -70,7 +69,6 @@ class NotificationsControllerImpl @Inject constructor( private val animatedImageNotificationManager: AnimatedImageNotificationManager, private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager, private val bubblesOptional: Optional<Bubbles>, - private val fgsNotifListener: ForegroundServiceNotificationListener, private val featureFlags: FeatureFlags ) : NotificationsController { @@ -105,7 +103,6 @@ class NotificationsControllerImpl @Inject constructor( notificationsMediaManager.setUpWithPresenter(presenter) notificationLogger.setUpWithContainer(listContainer) peopleSpaceWidgetManager.attach(notificationListener) - fgsNotifListener.init() } // TODO: Convert all functions below this line into listeners instead of public methods diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt index 38a1579222b1..9c4aa072a83d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt @@ -60,7 +60,7 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a override fun onFinishInflate() { super.onFinishInflate() - appControlRow = findViewById(R.id.app_control) + appControlRow = requireViewById(R.id.app_control) } /** @@ -143,9 +143,9 @@ class AppControlView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { lateinit var switch: Switch override fun onFinishInflate() { - iconView = findViewById(R.id.icon) - channelName = findViewById(R.id.app_name) - switch = findViewById(R.id.toggle) + iconView = requireViewById(R.id.icon) + channelName = requireViewById(R.id.app_name) + switch = requireViewById(R.id.toggle) setOnClickListener { switch.toggle() } } @@ -174,9 +174,9 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { override fun onFinishInflate() { super.onFinishInflate() - channelName = findViewById(R.id.channel_name) - channelDescription = findViewById(R.id.channel_description) - switch = findViewById(R.id.toggle) + channelName = requireViewById(R.id.channel_name) + channelDescription = requireViewById(R.id.channel_description) + switch = requireViewById(R.id.toggle) switch.setOnCheckedChangeListener { _, b -> channel?.let { controller.proposeEditForChannel(it, if (b) it.importance else IMPORTANCE_NONE) 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 ed489a6c5343..d92d11b18d74 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 @@ -1894,6 +1894,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return traceTag; } + if (isSummaryWithChildren()) { + return traceTag + "(summary)"; + } Class<? extends Notification.Style> style = getEntry().getSbn().getNotification().getNotificationStyle(); if (style == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index d71bc2fd6470..5e3a67ece4a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -93,6 +93,7 @@ import com.android.systemui.flags.ViewRefactorFlag; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; @@ -3480,6 +3481,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return super.onTouchEvent(ev); } + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch(TAG, ev, super.dispatchTouchEvent(ev)); + } + void dispatchDownEventToScroller(MotionEvent ev) { MotionEvent downEvent = MotionEvent.obtain(ev); downEvent.setAction(MotionEvent.ACTION_DOWN); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 2ccbc9f2f017..baeae79f4d7d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -914,8 +914,8 @@ constructor( val packages: Array<String> = context.resources.getStringArray(R.array.system_ui_packages) for (pkg in packages) { - if (intent.component == null) break - if (pkg == intent.component.packageName) { + val componentName = intent.component ?: break + if (pkg == componentName.packageName) { return UserHandle(UserHandle.myUserId()) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 632f241ddfff..127569d179fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -634,7 +634,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final ActivityIntentHelper mActivityIntentHelper; - private final NotificationStackScrollLayoutController mStackScrollerController; + public final NotificationStackScrollLayoutController mStackScrollerController; private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener = (extractor, which) -> updateTheme(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index 7dcdc0bdb383..97cb45aebd13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -22,8 +22,6 @@ import android.os.LocaleList import android.view.View.LAYOUT_DIRECTION_RTL import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.policy.ConfigurationController - -import java.util.ArrayList import javax.inject.Inject @SysUISingleton @@ -40,6 +38,7 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config private var localeList: LocaleList? = null private val context: Context private var layoutDirection: Int + private var orientation = Configuration.ORIENTATION_UNDEFINED init { val currentConfig = context.resources.configuration @@ -134,8 +133,18 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config it.onThemeChanged() } } + + val newOrientation = newConfig.orientation + if (orientation != newOrientation) { + orientation = newOrientation + listeners.filterForEach({ this.listeners.contains(it) }) { + it.onOrientationChanged(orientation) + } + } } + + override fun addCallback(listener: ConfigurationController.ConfigurationListener) { listeners.add(listener) listener.onDensityOrFontScaleChanged() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index f15dcc310837..a1f12b896d3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -81,6 +81,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction; private final KeyguardBypassController mBypassController; private final StatusBarStateController mStatusBarStateController; + private final PhoneStatusBarTransitions mPhoneStatusBarTransitions; private final CommandQueue mCommandQueue; private final NotificationWakeUpCoordinator mWakeUpCoordinator; @@ -109,6 +110,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, StatusBarStateController stateController, + PhoneStatusBarTransitions phoneStatusBarTransitions, KeyguardBypassController bypassController, NotificationWakeUpCoordinator wakeUpCoordinator, DarkIconDispatcher darkIconDispatcher, @@ -156,6 +158,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar }); mBypassController = bypassController; mStatusBarStateController = stateController; + mPhoneStatusBarTransitions = phoneStatusBarTransitions; mWakeUpCoordinator = wakeUpCoordinator; mCommandQueue = commandQueue; mKeyguardStateController = keyguardStateController; @@ -203,6 +206,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @Override public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) { updateHeadsUpAndPulsingRoundness(entry); + mPhoneStatusBarTransitions.onHeadsUpStateChanged(isHeadsUp); } private void updateTopEntry() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt index 34bbd1359df7..cdd410e766a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.animation.requiresRemeasuring /** * Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI @@ -98,7 +99,7 @@ constructor( ambientIndicationArea?.let { nonNullAmbientIndicationArea -> // remove old ambient indication from its parent val originalAmbientIndicationView = - oldBottomArea.findViewById<View>(R.id.ambient_indication_container) + oldBottomArea.requireViewById<View>(R.id.ambient_indication_container) (originalAmbientIndicationView.parent as ViewGroup).removeView( originalAmbientIndicationView ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 5c1f824bc687..38c3815ac7f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -87,6 +87,7 @@ public class KeyguardStatusBarView extends RelativeLayout { private int mStatusBarPaddingEnd; private int mMinDotWidth; private View mSystemIconsContainer; + private View mSystemIcons; private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow( DarkChange.EMPTY); @@ -119,6 +120,7 @@ public class KeyguardStatusBarView extends RelativeLayout { protected void onFinishInflate() { super.onFinishInflate(); mSystemIconsContainer = findViewById(R.id.system_icons_container); + mSystemIcons = findViewById(R.id.system_icons); mMultiUserAvatar = findViewById(R.id.multi_user_avatar); mCarrierLabel = findViewById(R.id.keyguard_carrier_text); mBatteryView = mSystemIconsContainer.findViewById(R.id.battery); @@ -167,6 +169,13 @@ public class KeyguardStatusBarView extends RelativeLayout { mStatusIconContainer.getPaddingBottom() ); + mSystemIcons.setPaddingRelative( + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_start), + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_top), + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_end), + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_bottom) + ); + // Respect font size setting. mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java index 15c6dcfd7889..cc38405701f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java @@ -31,6 +31,8 @@ public final class PhoneStatusBarTransitions extends BarTransitions { private final float mIconAlphaWhenOpaque; + private boolean mIsHeadsUp; + private View mStartSide, mStatusIcons, mBattery; private Animator mCurrentAnimation; @@ -52,15 +54,32 @@ public final class PhoneStatusBarTransitions extends BarTransitions { return ObjectAnimator.ofFloat(v, "alpha", v.getAlpha(), toAlpha); } - private float getNonBatteryClockAlphaFor(int mode) { - return isLightsOut(mode) ? ICON_ALPHA_WHEN_LIGHTS_OUT_NON_BATTERY_CLOCK - : !isOpaque(mode) ? ICON_ALPHA_WHEN_NOT_OPAQUE - : mIconAlphaWhenOpaque; + private float getStatusIconsAlphaFor(int mode) { + return getDefaultAlphaFor(mode); + } + + private float getStartSideAlphaFor(int mode) { + // When there's a heads up notification, we need the start side icons to show regardless of + // lights out mode. + if (mIsHeadsUp) { + return getIconAlphaBasedOnOpacity(mode); + } + return getDefaultAlphaFor(mode); } private float getBatteryClockAlpha(int mode) { return isLightsOut(mode) ? ICON_ALPHA_WHEN_LIGHTS_OUT_BATTERY_CLOCK - : getNonBatteryClockAlphaFor(mode); + : getIconAlphaBasedOnOpacity(mode); + } + + private float getDefaultAlphaFor(int mode) { + return isLightsOut(mode) ? ICON_ALPHA_WHEN_LIGHTS_OUT_NON_BATTERY_CLOCK + : getIconAlphaBasedOnOpacity(mode); + } + + private float getIconAlphaBasedOnOpacity(int mode) { + return !isOpaque(mode) ? ICON_ALPHA_WHEN_NOT_OPAQUE + : mIconAlphaWhenOpaque; } private boolean isOpaque(int mode) { @@ -74,19 +93,28 @@ public final class PhoneStatusBarTransitions extends BarTransitions { applyMode(newMode, animate); } + /** Informs this controller that the heads up notification state has changed. */ + public void onHeadsUpStateChanged(boolean isHeadsUp) { + mIsHeadsUp = isHeadsUp; + // We want the icon to be fully visible when the HUN appears, so just immediately change the + // icon visibility and don't animate. + applyMode(getMode(), /* animate= */ false); + } + private void applyMode(int mode, boolean animate) { if (mStartSide == null) return; // pre-init - float newAlpha = getNonBatteryClockAlphaFor(mode); - float newAlphaBC = getBatteryClockAlpha(mode); + float newStartSideAlpha = getStartSideAlphaFor(mode); + float newStatusIconsAlpha = getStatusIconsAlphaFor(mode); + float newBatteryAlpha = getBatteryClockAlpha(mode); if (mCurrentAnimation != null) { mCurrentAnimation.cancel(); } if (animate) { AnimatorSet anims = new AnimatorSet(); anims.playTogether( - animateTransitionTo(mStartSide, newAlpha), - animateTransitionTo(mStatusIcons, newAlpha), - animateTransitionTo(mBattery, newAlphaBC) + animateTransitionTo(mStartSide, newStartSideAlpha), + animateTransitionTo(mStatusIcons, newStatusIconsAlpha), + animateTransitionTo(mBattery, newBatteryAlpha) ); if (isLightsOut(mode)) { anims.setDuration(LIGHTS_OUT_DURATION); @@ -94,9 +122,9 @@ public final class PhoneStatusBarTransitions extends BarTransitions { anims.start(); mCurrentAnimation = anims; } else { - mStartSide.setAlpha(newAlpha); - mStatusIcons.setAlpha(newAlpha); - mBattery.setAlpha(newAlphaBC); + mStartSide.setAlpha(newStartSideAlpha); + mStatusIcons.setAlpha(newStatusIconsAlpha); + mBattery.setAlpha(newBatteryAlpha); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index d546a84791fc..83a040cc17b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -208,25 +208,29 @@ public class PhoneStatusBarView extends FrameLayout { ViewGroup.LayoutParams layoutParams = getLayoutParams(); mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext); layoutParams.height = mStatusBarHeight - waterfallTopInset; + updatePaddings(); + setLayoutParams(layoutParams); + } - int statusBarPaddingTop = getResources().getDimensionPixelSize( - R.dimen.status_bar_padding_top); + private void updatePaddings() { int statusBarPaddingStart = getResources().getDimensionPixelSize( R.dimen.status_bar_padding_start); - int statusBarPaddingEnd = getResources().getDimensionPixelSize( - R.dimen.status_bar_padding_end); - View sbContents = findViewById(R.id.status_bar_contents); - sbContents.setPaddingRelative( + findViewById(R.id.status_bar_contents).setPaddingRelative( statusBarPaddingStart, - statusBarPaddingTop, - statusBarPaddingEnd, + getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top), + getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0); findViewById(R.id.notification_lights_out) .setPaddingRelative(0, statusBarPaddingStart, 0, 0); - setLayoutParams(layoutParams); + findViewById(R.id.system_icons).setPaddingRelative( + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_start), + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_top), + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_end), + getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_bottom) + ); } private void updateLayoutForCutout() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 2affb8173a01..931aedd90542 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -75,13 +75,13 @@ class PhoneStatusBarViewController private constructor( } override fun onViewAttached() { - statusContainer = mView.findViewById(R.id.system_icons) + statusContainer = mView.requireViewById(R.id.system_icons) statusContainer.setOnHoverListener( statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer)) if (moveFromCenterAnimationController == null) return - val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up) - val systemIconArea: ViewGroup = mView.findViewById(R.id.status_bar_end_side_content) + val statusBarLeftSide: View = mView.requireViewById(R.id.status_bar_start_side_except_heads_up) + val systemIconArea: ViewGroup = mView.requireViewById(R.id.status_bar_end_side_content) val viewsToAnimate = arrayOf( statusBarLeftSide, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index e82ac59b6746..fc661384146c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -370,6 +370,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mScrimBehind = behindScrim; mScrimInFront = scrimInFront; updateThemeColors(); + mNotificationsScrim.setScrimName(getScrimName(mNotificationsScrim)); + mScrimBehind.setScrimName(getScrimName(mScrimBehind)); + mScrimInFront.setScrimName(getScrimName(mScrimInFront)); behindScrim.enableBottomEdgeConcave(mClipsQsScrim); mNotificationsScrim.enableRoundedCorners(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt index c850d4f9c56b..ad1817019284 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -117,11 +117,11 @@ class StatusBarContentInsetsProvider @Inject constructor( * status bar area is contiguous. */ fun currentRotationHasCornerCutout(): Boolean { - val cutout = context.display.cutout ?: return false + val cutout = checkNotNull(context.display).cutout ?: return false val topBounds = cutout.boundingRectTop val point = Point() - context.display.getRealSize(point) + checkNotNull(context.display).getRealSize(point) return topBounds.left <= 0 || topBounds.right >= point.x } @@ -161,7 +161,7 @@ class StatusBarContentInsetsProvider @Inject constructor( */ fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Pair<Int, Int> = traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") { - val displayCutout = context.display.cutout + val displayCutout = checkNotNull(context.display).cutout val key = getCacheKey(rotation, displayCutout) val screenBounds = context.resources.configuration.windowConfiguration.maxBounds @@ -198,7 +198,7 @@ class StatusBarContentInsetsProvider @Inject constructor( fun getStatusBarContentAreaForRotation( @Rotation rotation: Int ): Rect { - val displayCutout = context.display.cutout + val displayCutout = checkNotNull(context.display).cutout val key = getCacheKey(rotation, displayCutout) return insetsCache[key] ?: getAndSetCalculatedAreaForRotation( rotation, displayCutout, getResourcesForRotation(rotation, context), key) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 5c1dfbe42cf9..ea57eb46bab4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -22,6 +22,8 @@ import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConst import static com.android.systemui.plugins.ActivityStarter.OnDismissAction; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows; import android.content.Context; import android.content.res.ColorStateList; @@ -60,10 +62,14 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInte import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.bouncer.ui.BouncerView; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.TaskbarDelegate; @@ -86,8 +92,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; -import dagger.Lazy; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashSet; @@ -97,6 +101,9 @@ import java.util.Set; import javax.inject.Inject; +import dagger.Lazy; +import kotlinx.coroutines.CoroutineDispatcher; + /** * Manages creating, showing, hiding and resetting the keyguard within the status bar. Calls back * via {@link ViewMediatorCallback} to poke the wake lock and report that the keyguard is done, @@ -281,6 +288,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private int mLastBiometricMode; private boolean mLastScreenOffAnimationPlaying; private float mQsExpansion; + + private FeatureFlags mFlags; + final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); private boolean mIsBackAnimationEnabled; private final boolean mUdfpsNewTouchDetectionEnabled; @@ -326,6 +336,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } }; + private Lazy<WindowManagerLockscreenVisibilityInteractor> mWmLockscreenVisibilityInteractor; @Inject public StatusBarKeyguardViewManager( @@ -352,7 +363,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb BouncerView primaryBouncerView, AlternateBouncerInteractor alternateBouncerInteractor, UdfpsOverlayInteractor udfpsOverlayInteractor, - ActivityStarter activityStarter + ActivityStarter activityStarter, + KeyguardTransitionInteractor keyguardTransitionInteractor, + @Main CoroutineDispatcher mainDispatcher, + Lazy<WindowManagerLockscreenVisibilityInteractor> wmLockscreenVisibilityInteractor ) { mContext = context; mViewMediatorCallback = callback; @@ -370,6 +384,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mShadeController = shadeController; mLatencyTracker = latencyTracker; mKeyguardSecurityModel = keyguardSecurityModel; + mFlags = featureFlags; mPrimaryBouncerCallbackInteractor = primaryBouncerCallbackInteractor; mPrimaryBouncerInteractor = primaryBouncerInteractor; mPrimaryBouncerView = primaryBouncerView; @@ -381,8 +396,14 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mUdfpsNewTouchDetectionEnabled = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION); mUdfpsOverlayInteractor = udfpsOverlayInteractor; mActivityStarter = activityStarter; + mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mMainDispatcher = mainDispatcher; + mWmLockscreenVisibilityInteractor = wmLockscreenVisibilityInteractor; } + KeyguardTransitionInteractor mKeyguardTransitionInteractor; + CoroutineDispatcher mMainDispatcher; + @Override public void registerCentralSurfaces(CentralSurfaces centralSurfaces, ShadeViewController shadeViewController, @@ -429,6 +450,14 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } + private KeyguardStateController.Callback mKeyguardStateControllerCallback = + new KeyguardStateController.Callback() { + @Override + public void onUnlockedChanged() { + updateAlternateBouncerShowing(mAlternateBouncerInteractor.maybeHide()); + } + }; + private void registerListeners() { mKeyguardUpdateManager.registerCallback(mUpdateMonitorCallback); mStatusBarStateController.addCallback(this); @@ -442,6 +471,32 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mDockManager.addListener(mDockEventListener); mIsDocked = mDockManager.isDocked(); } + mKeyguardStateController.addCallback(mKeyguardStateControllerCallback); + + if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + mShadeViewController.postToView(() -> + collectFlow( + getViewRootImpl().getView(), + combineFlows( + mKeyguardTransitionInteractor.getFinishedKeyguardState(), + mWmLockscreenVisibilityInteractor.get() + .getUsingKeyguardGoingAwayAnimation(), + (finishedState, animating) -> + KeyguardInteractor.Companion.isKeyguardVisibleInState( + finishedState) + || animating), + this::consumeShowStatusBarKeyguardView)); + } + } + + private void consumeShowStatusBarKeyguardView(boolean show) { + if (show != mLastShowing) { + if (show) { + show(null); + } else { + hide(0, 0); + } + } } /** Register a callback, to be invoked by the Predictive Back system. */ @@ -1313,6 +1368,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb hideAlternateBouncer(false); executeAfterKeyguardGoneAction(); } + + if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + mKeyguardTransitionInteractor.startDismissKeyguardTransition(); + } } /** Display security message to relevant KeyguardMessageArea. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index e8da9519c7ef..1bceb29dc85d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -105,18 +105,18 @@ class UnlockedScreenOffAnimationController @Inject constructor( } } addListener(object : AnimatorListenerAdapter() { - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { if (lightRevealScrim.revealEffect !is CircleReveal) { lightRevealScrim.revealAmount = 1f } } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { lightRevealAnimationPlaying = false interactionJankMonitor.end(CUJ_SCREEN_OFF) } - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { interactionJankMonitor.begin( notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF) } @@ -345,7 +345,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( // portrait. If we're in another orientation, disable the screen off animation so we don't // animate in the keyguard AOD UI sideways or upside down. if (!keyguardStateController.isKeyguardScreenRotationAllowed && - context.display.rotation != Surface.ROTATION_0) { + context.display?.rotation != Surface.ROTATION_0) { return false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt index 270c592ae4fd..12594771a0df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt @@ -34,7 +34,7 @@ class StatusBarUserSwitcherContainer( override fun onFinishInflate() { super.onFinishInflate() - text = findViewById(R.id.current_user_name) - avatar = findViewById(R.id.current_user_avatar) + text = requireViewById(R.id.current_user_name) + avatar = requireViewById(R.id.current_user_avatar) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java index 6b80a9dab7cf..b2ef818d3282 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java @@ -42,5 +42,6 @@ public interface ConfigurationController extends CallbackController<Configuratio default void onThemeChanged() {} default void onLocaleListChanged() {} default void onLayoutDirectionChanged(boolean isLayoutRtl) {} + default void onOrientationChanged(int orientation) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt index 49504827e073..ffb743ff926c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -128,7 +128,8 @@ public class DeviceControlsControllerImpl @Inject constructor( val prefs = userContextProvider.userContext.getSharedPreferences( PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) - val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) + val seededPackages = + prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet() val controlsController = controlsComponent.getControlsController().get() val componentsToSeed = mutableListOf<ComponentName>() @@ -174,7 +175,8 @@ public class DeviceControlsControllerImpl @Inject constructor( } private fun addPackageToSeededSet(prefs: SharedPreferences, pkg: String) { - val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) + val seededPackages = + prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet() val updatedPkgs = seededPackages.toMutableSet() updatedPkgs.add(pkg) prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, updatedPkgs).apply() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java index 710588c82a4a..63dcad98f056 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy; import static android.hardware.biometrics.BiometricSourceType.FACE; +import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; + import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.Context; @@ -38,6 +40,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import dagger.Lazy; @@ -49,6 +52,7 @@ import java.util.Objects; import javax.inject.Inject; /** + * */ @SysUISingleton public class KeyguardStateControllerImpl implements KeyguardStateController, Dumpable { @@ -103,7 +107,10 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum */ private boolean mSnappingKeyguardBackAfterSwipe = false; + private FeatureFlags mFeatureFlags; + /** + * */ @Inject public KeyguardStateControllerImpl( @@ -112,13 +119,15 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum LockPatternUtils lockPatternUtils, Lazy<KeyguardUnlockAnimationController> keyguardUnlockAnimationController, KeyguardUpdateMonitorLogger logger, - DumpManager dumpManager) { + DumpManager dumpManager, + FeatureFlags featureFlags) { mContext = context; mLogger = logger; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); mUnlockAnimationControllerLazy = keyguardUnlockAnimationController; + mFeatureFlags = featureFlags; dumpManager.registerDumpable(getClass().getSimpleName(), this); @@ -272,7 +281,8 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum @Override public boolean isKeyguardScreenRotationAllowed() { return SystemProperties.getBoolean("lockscreen.rot_override", false) - || mContext.getResources().getBoolean(R.bool.config_enableLockScreenRotation); + || mContext.getResources().getBoolean(R.bool.config_enableLockScreenRotation) + || mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 27aaa6828036..eb7d339f5b42 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -353,8 +353,8 @@ constructor( // before CoreStartables run, and will not be removed. // In many cases, it reports the battery level of the stylus. registerBatteryListener(deviceId) - } else if (device.bluetoothAddress != null) { - onStylusBluetoothConnected(deviceId, device.bluetoothAddress) + } else { + device.bluetoothAddress?.let { onStylusBluetoothConnected(deviceId, it) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt index 72786efc416d..5ad963035e36 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt @@ -60,7 +60,7 @@ class UserSwitchFullscreenDialog( override fun getWidth(): Int { val displayMetrics = context.resources.displayMetrics.apply { - context.display.getRealMetrics(this) + checkNotNull(context.display).getRealMetrics(this) } return displayMetrics.widthPixels } diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt index 088cd93bdf7e..ee84580ac4ec 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt @@ -52,22 +52,22 @@ class UserSwitcherPopupMenu( override fun show() { // need to call show() first in order to construct the listView super.show() - val listView = getListView() + listView?.apply { + isVerticalScrollBarEnabled = false + isHorizontalScrollBarEnabled = false - listView.setVerticalScrollBarEnabled(false) - listView.setHorizontalScrollBarEnabled(false) + // Creates a transparent spacer between items + val shape = ShapeDrawable() + shape.alpha = 0 + divider = shape + dividerHeight = res.getDimensionPixelSize( + R.dimen.bouncer_user_switcher_popup_divider_height) - // Creates a transparent spacer between items - val shape = ShapeDrawable() - shape.setAlpha(0) - listView.setDivider(shape) - listView.setDividerHeight(res.getDimensionPixelSize( - R.dimen.bouncer_user_switcher_popup_divider_height)) - - val height = res.getDimensionPixelSize(R.dimen.bouncer_user_switcher_popup_header_height) - listView.addHeaderView(createSpacer(height), null, false) - listView.addFooterView(createSpacer(height), null, false) - setWidth(findMaxWidth(listView)) + val height = res.getDimensionPixelSize(R.dimen.bouncer_user_switcher_popup_header_height) + addHeaderView(createSpacer(height), null, false) + addFooterView(createSpacer(height), null, false) + setWidth(findMaxWidth(this)) + } super.show() } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt index f026f0f9de37..bfed0c44f6b7 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt @@ -227,7 +227,8 @@ constructor( ) } try { - WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null) + checkNotNull(WindowManagerGlobal.getWindowManagerService()) + .lockNow(/* options= */ null) } catch (e: RemoteException) { Log.e( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt index e74232df3ac3..7f16e47abce1 100644 --- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt @@ -67,7 +67,7 @@ object LegacyUserUiHelper { val resourceId: Int? = getGuestUserRecordNameResourceId(record) return when { resourceId != null -> context.getString(resourceId) - record.info != null -> record.info.name + record.info != null -> checkNotNull(record.info.name) else -> context.getString( getUserSwitcherActionTextResourceId( diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt index 56c5d3b433ff..7866d7673937 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt @@ -223,11 +223,11 @@ class TransitionLayout @JvmOverloads constructor( } } - override fun dispatchDraw(canvas: Canvas?) { - canvas?.save() - canvas?.clipRect(boundsRect) + override fun dispatchDraw(canvas: Canvas) { + canvas.save() + canvas.clipRect(boundsRect) super.dispatchDraw(canvas) - canvas?.restore() + canvas.restore() } private fun updateBounds() { diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 12d7b4d372e8..0d0a6469d674 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -29,7 +29,7 @@ import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch /** A class allowing Java classes to collect on Kotlin flows. */ @@ -75,3 +75,7 @@ fun <T> collectFlow( repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } } + +fun <A, B, R> combineFlows(flow1: Flow<A>, flow2: Flow<B>, bifunction: (A, B) -> R): Flow<R> { + return combine(flow1, flow2, bifunction) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index d39a53d311c6..9cc3cdbf5c34 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -44,6 +44,7 @@ import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession.Token; import android.net.Uri; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -57,6 +58,7 @@ import android.util.Slog; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.CaptioningManager; +import androidx.annotation.NonNull; import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; @@ -81,6 +83,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; @@ -131,7 +134,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final Receiver mReceiver = new Receiver(); private final RingerModeObservers mRingerModeObservers; private final MediaSessions mMediaSessions; - private final CaptioningManager mCaptioningManager; + private final AtomicReference<CaptioningManager> mCaptioningManager = new AtomicReference<>(); private final KeyguardManager mKeyguardManager; private final ActivityManager mActivityManager; private final UserTracker mUserTracker; @@ -155,16 +158,16 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final WakefulnessLifecycle.Observer mWakefullnessLifecycleObserver = new WakefulnessLifecycle.Observer() { - @Override - public void onStartedWakingUp() { - mDeviceInteractive = true; - } + @Override + public void onStartedWakingUp() { + mDeviceInteractive = true; + } - @Override - public void onFinishedGoingToSleep() { - mDeviceInteractive = false; - } - }; + @Override + public void onFinishedGoingToSleep() { + mDeviceInteractive = false; + } + }; @Inject public VolumeDialogControllerImpl( @@ -179,7 +182,6 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa AccessibilityManager accessibilityManager, PackageManager packageManager, WakefulnessLifecycle wakefulnessLifecycle, - CaptioningManager captioningManager, KeyguardManager keyguardManager, ActivityManager activityManager, UserTracker userTracker, @@ -209,17 +211,19 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mVibrator = vibrator; mHasVibrator = mVibrator.hasVibrator(); mAudioService = iAudioService; - mCaptioningManager = captioningManager; mKeyguardManager = keyguardManager; mActivityManager = activityManager; mUserTracker = userTracker; + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mWorker)); + createCaptioningManagerServiceByUserContext(mUserTracker.getUserContext()); + dumpManager.registerDumpable("VolumeDialogControllerImpl", this); boolean accessibilityVolumeStreamActive = accessibilityManager .isAccessibilityVolumeStreamActive(); mVolumeController.setA11yMode(accessibilityVolumeStreamActive ? - VolumePolicy.A11Y_MODE_INDEPENDENT_A11Y_VOLUME : - VolumePolicy.A11Y_MODE_MEDIA_A11Y_VOLUME); + VolumePolicy.A11Y_MODE_INDEPENDENT_A11Y_VOLUME : + VolumePolicy.A11Y_MODE_MEDIA_A11Y_VOLUME); mWakefulnessLifecycle.addObserver(mWakefullnessLifecycleObserver); } @@ -316,12 +320,31 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mWorker.sendEmptyMessage(W.GET_STATE); } - public boolean areCaptionsEnabled() { - return mCaptioningManager.isSystemAudioCaptioningEnabled(); + /** + * We met issues about the wrong state of System Caption in multi-user mode. + * It happened in the usage of CaptioningManager Service from SysUI process + * that is a global system process of User 0. + * Therefore, we have to add callback on UserTracker that allows us to get the Context of + * active User and then get the corresponding CaptioningManager Service for further usages. + */ + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + createCaptioningManagerServiceByUserContext(userContext); + } + }; + + private void createCaptioningManagerServiceByUserContext(@NonNull Context userContext) { + mCaptioningManager.set(userContext.getSystemService(CaptioningManager.class)); + } + + public void getCaptionsEnabledState(boolean checkForSwitchState) { + mWorker.obtainMessage(W.GET_CAPTIONS_ENABLED_STATE, checkForSwitchState).sendToTarget(); } - public void setCaptionsEnabled(boolean isEnabled) { - mCaptioningManager.setSystemAudioCaptioningEnabled(isEnabled); + public void setCaptionsEnabledState(boolean enabled) { + mWorker.obtainMessage(W.SET_CAPTIONS_ENABLED_STATE, enabled).sendToTarget(); } public void getCaptionsComponentState(boolean fromTooltip) { @@ -362,8 +385,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } public void setEnableDialogs(boolean volumeUi, boolean safetyWarning) { - mShowVolumeDialog = volumeUi; - mShowSafetyWarning = safetyWarning; + mShowVolumeDialog = volumeUi; + mShowSafetyWarning = safetyWarning; } @Override @@ -414,12 +437,38 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } private void onShowCsdWarningW(@AudioManager.CsdWarning int csdWarning, int durationMs) { - mCallbacks.onShowCsdWarning(csdWarning, durationMs); + mCallbacks.onShowCsdWarning(csdWarning, durationMs); } private void onGetCaptionsComponentStateW(boolean fromTooltip) { - mCallbacks.onCaptionComponentStateChanged( - mCaptioningManager.isSystemAudioCaptioningUiEnabled(), fromTooltip); + CaptioningManager captioningManager = mCaptioningManager.get(); + if (null != captioningManager) { + mCallbacks.onCaptionComponentStateChanged( + captioningManager.isSystemAudioCaptioningUiEnabled(), fromTooltip); + } else { + Log.e(TAG, "onGetCaptionsComponentStateW(), null captioningManager"); + } + } + + private void onGetCaptionsEnabledStateW(boolean checkForSwitchState) { + CaptioningManager captioningManager = mCaptioningManager.get(); + if (null != captioningManager) { + mCallbacks.onCaptionEnabledStateChanged( + captioningManager.isSystemAudioCaptioningEnabled(), checkForSwitchState); + } else { + Log.e(TAG, "onGetCaptionsEnabledStateW(), null captioningManager"); + } + } + + private void onSetCaptionsEnabledStateW(boolean enabled) { + CaptioningManager captioningManager = mCaptioningManager.get(); + if (null != captioningManager) { + captioningManager.setSystemAudioCaptioningEnabled(enabled); + mCallbacks.onCaptionEnabledStateChanged( + captioningManager.isSystemAudioCaptioningEnabled(), false); + } else { + Log.e(TAG, "onGetCaptionsEnabledStateW(), null captioningManager"); + } } private void onAccessibilityModeChanged(Boolean showA11yStream) { @@ -719,7 +768,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa * This method will never be called if the CSD (Computed Sound Dose) feature is * not enabled. See com.android.android.server.audio.SoundDoseHelper for the state of * the feature. - * @param warning the type of warning to display, values are one of + * @param csdWarning the type of warning to display, values are one of * {@link android.media.AudioManager#CSD_WARNING_DOSE_REACHED_1X}, * {@link android.media.AudioManager#CSD_WARNING_DOSE_REPEATED_5X}, * {@link android.media.AudioManager#CSD_WARNING_MOMENTARY_EXPOSURE}, @@ -798,6 +847,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private static final int ACCESSIBILITY_MODE_CHANGED = 15; private static final int GET_CAPTIONS_COMPONENT_STATE = 16; private static final int SHOW_CSD_WARNING = 17; + private static final int GET_CAPTIONS_ENABLED_STATE = 18; + private static final int SET_CAPTIONS_ENABLED_STATE = 19; W(Looper looper) { super(looper); @@ -825,6 +876,10 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa case ACCESSIBILITY_MODE_CHANGED: onAccessibilityModeChanged((Boolean) msg.obj); break; case SHOW_CSD_WARNING: onShowCsdWarningW(msg.arg1, msg.arg2); break; + case GET_CAPTIONS_ENABLED_STATE: + onGetCaptionsEnabledStateW((Boolean) msg.obj); break; + case SET_CAPTIONS_ENABLED_STATE: + onSetCaptionsEnabledStateW((Boolean) msg.obj); break; } } } @@ -993,6 +1048,17 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa componentEnabled, fromTooltip)); } } + + @Override + public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch) { + boolean captionsEnabled = isEnabled != null && isEnabled; + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post( + () -> entry.getKey().onCaptionEnabledStateChanged( + captionsEnabled, checkBeforeSwitch)); + } + } + } private final class RingerModeObservers { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 6219e4d09b43..aafa16f4a5ed 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1333,21 +1333,30 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, if (!isServiceComponentEnabled) return; - updateCaptionsIcon(); + checkEnabledStateForCaptionsIconUpdate(); if (fromTooltip) showCaptionsTooltip(); } - private void updateCaptionsIcon() { - boolean captionsEnabled = mController.areCaptionsEnabled(); - if (mODICaptionsIcon.getCaptionsEnabled() != captionsEnabled) { - mHandler.post(mODICaptionsIcon.setCaptionsEnabled(captionsEnabled)); + private void updateCaptionsEnabledH(boolean isCaptionsEnabled, boolean checkForSwitchState) { + if (checkForSwitchState) { + mController.setCaptionsEnabledState(!isCaptionsEnabled); + } else { + updateCaptionsIcon(isCaptionsEnabled); + } + } + + private void checkEnabledStateForCaptionsIconUpdate() { + mController.getCaptionsEnabledState(false); + } + + private void updateCaptionsIcon(boolean isCaptionsEnabled) { + if (mODICaptionsIcon.getCaptionsEnabled() != isCaptionsEnabled) { + mHandler.post(mODICaptionsIcon.setCaptionsEnabled(isCaptionsEnabled)); } } private void onCaptionIconClicked() { - boolean isEnabled = mController.areCaptionsEnabled(); - mController.setCaptionsEnabled(!isEnabled); - updateCaptionsIcon(); + mController.getCaptionsEnabledState(true); } private void incrementManualToggleCount() { @@ -2363,7 +2372,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } else { updateRowsH(activeRow); } - } @Override @@ -2371,6 +2379,11 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, Boolean isComponentEnabled, Boolean fromTooltip) { updateODICaptionsH(isComponentEnabled, fromTooltip); } + + @Override + public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkForSwitchState) { + updateCaptionsEnabledH(isEnabled, checkForSwitchState); + } }; @VisibleForTesting void onPostureChanged(int posture) { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java index 677d3ff3df69..c894d914bfa3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java @@ -180,8 +180,9 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { } @Test - public void testResume() { - mKeyguardAbsKeyInputViewController.onResume(KeyguardSecurityView.VIEW_REVEALED); + public void testOnViewAttached() { + reset(mLockPatternUtils); + mKeyguardAbsKeyInputViewController.onViewAttached(); verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index 1a9260c2ede6..3a9473085583 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -20,6 +20,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.inputmethod.InputMethodManager import android.widget.EditText +import android.widget.ImageView import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.internal.widget.LockPatternUtils @@ -29,6 +30,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.whenever import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -36,6 +38,7 @@ import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` @@ -45,104 +48,109 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardPasswordViewControllerTest : SysuiTestCase() { - @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView - @Mock private lateinit var passwordEntry: EditText - @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode - @Mock lateinit var lockPatternUtils: LockPatternUtils - @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback - @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory - @Mock lateinit var latencyTracker: LatencyTracker - @Mock lateinit var inputMethodManager: InputMethodManager - @Mock lateinit var emergencyButtonController: EmergencyButtonController - @Mock lateinit var mainExecutor: DelayableExecutor - @Mock lateinit var falsingCollector: FalsingCollector - @Mock lateinit var keyguardViewController: KeyguardViewController - @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea - @Mock - private lateinit var mKeyguardMessageAreaController: - KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView + @Mock private lateinit var passwordEntry: EditText + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode + @Mock lateinit var lockPatternUtils: LockPatternUtils + @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback + @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory + @Mock lateinit var latencyTracker: LatencyTracker + @Mock lateinit var inputMethodManager: InputMethodManager + @Mock lateinit var emergencyButtonController: EmergencyButtonController + @Mock lateinit var mainExecutor: DelayableExecutor + @Mock lateinit var falsingCollector: FalsingCollector + @Mock lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea + @Mock + private lateinit var mKeyguardMessageAreaController: + KeyguardMessageAreaController<BouncerKeyguardMessageArea> - private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController + private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - Mockito.`when`( - keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>( - R.id.bouncer_message_area)) - .thenReturn(mKeyguardMessageArea) - Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea)) - .thenReturn(mKeyguardMessageAreaController) - Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry) - Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)) - .thenReturn(passwordEntry) - `when`(keyguardPasswordView.resources).thenReturn(context.resources) - val fakeFeatureFlags = FakeFeatureFlags() - fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) - keyguardPasswordViewController = - KeyguardPasswordViewController( - keyguardPasswordView, - keyguardUpdateMonitor, - securityMode, - lockPatternUtils, - keyguardSecurityCallback, - messageAreaControllerFactory, - latencyTracker, - inputMethodManager, - emergencyButtonController, - mainExecutor, - mContext.resources, - falsingCollector, - keyguardViewController, - fakeFeatureFlags) - } + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + Mockito.`when`( + keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>( + R.id.bouncer_message_area + ) + ) + .thenReturn(mKeyguardMessageArea) + Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea)) + .thenReturn(mKeyguardMessageAreaController) + Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry) + Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)) + .thenReturn(passwordEntry) + whenever(keyguardPasswordView.findViewById<ImageView>(R.id.switch_ime_button)) + .thenReturn(mock(ImageView::class.java)) + `when`(keyguardPasswordView.resources).thenReturn(context.resources) + val fakeFeatureFlags = FakeFeatureFlags() + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + keyguardPasswordViewController = + KeyguardPasswordViewController( + keyguardPasswordView, + keyguardUpdateMonitor, + securityMode, + lockPatternUtils, + keyguardSecurityCallback, + messageAreaControllerFactory, + latencyTracker, + inputMethodManager, + emergencyButtonController, + mainExecutor, + mContext.resources, + falsingCollector, + keyguardViewController, + fakeFeatureFlags + ) + } - @Test - fun testFocusWhenBouncerIsShown() { - Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true) - Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) - keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - keyguardPasswordView.post { - verify(keyguardPasswordView).requestFocus() - verify(keyguardPasswordView).showKeyboard() + @Test + fun testFocusWhenBouncerIsShown() { + Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true) + Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) + keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) + keyguardPasswordView.post { + verify(keyguardPasswordView).requestFocus() + verify(keyguardPasswordView).showKeyboard() + } } - } - @Test - fun testDoNotFocusWhenBouncerIsHidden() { - Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false) - Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) - keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - verify(keyguardPasswordView, never()).requestFocus() - } + @Test + fun testDoNotFocusWhenBouncerIsHidden() { + Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false) + Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) + keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) + verify(keyguardPasswordView, never()).requestFocus() + } - @Test - fun testHideKeyboardWhenOnPause() { - keyguardPasswordViewController.onPause() - keyguardPasswordView.post { - verify(keyguardPasswordView).clearFocus() - verify(keyguardPasswordView).hideKeyboard() + @Test + fun testHideKeyboardWhenOnPause() { + keyguardPasswordViewController.onPause() + keyguardPasswordView.post { + verify(keyguardPasswordView).clearFocus() + verify(keyguardPasswordView).hideKeyboard() + } } - } - @Test - fun startAppearAnimation() { - keyguardPasswordViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false) - } + @Test + fun testOnViewAttached() { + keyguardPasswordViewController.onViewAttached() + verify(mKeyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false) + } - @Test - fun startAppearAnimation_withExistingMessage() { - `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") - keyguardPasswordViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) - } + @Test + fun testOnViewAttached_withExistingMessage() { + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + keyguardPasswordViewController.onViewAttached() + verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) + } - @Test - fun testMessageIsSetWhenReset() { - keyguardPasswordViewController.resetState() - verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) - } + @Test + fun testMessageIsSetWhenReset() { + keyguardPasswordViewController.resetState() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index cb182297eae1..1acd676f02cd 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -46,6 +46,7 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.never +import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -54,35 +55,35 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardPatternViewControllerTest : SysuiTestCase() { - private lateinit var mKeyguardPatternView: KeyguardPatternView + private lateinit var mKeyguardPatternView: KeyguardPatternView - @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode + @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode - @Mock private lateinit var mLockPatternUtils: LockPatternUtils + @Mock private lateinit var mLockPatternUtils: LockPatternUtils - @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback + @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback - @Mock private lateinit var mLatencyTracker: LatencyTracker - private var mFalsingCollector: FalsingCollector = FalsingCollectorFake() + @Mock private lateinit var mLatencyTracker: LatencyTracker + private var mFalsingCollector: FalsingCollector = FalsingCollectorFake() - @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController + @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController - @Mock - private lateinit var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory + @Mock + private lateinit var mKeyguardMessageAreaControllerFactory: + KeyguardMessageAreaController.Factory - @Mock - private lateinit var mKeyguardMessageAreaController: - KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock + private lateinit var mKeyguardMessageAreaController: + KeyguardMessageAreaController<BouncerKeyguardMessageArea> - @Mock private lateinit var mPostureController: DevicePostureController + @Mock private lateinit var mPostureController: DevicePostureController - private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController - private lateinit var fakeFeatureFlags: FakeFeatureFlags + private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController + private lateinit var fakeFeatureFlags: FakeFeatureFlags - @Captor - lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> + @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> @Before fun setup() { @@ -91,9 +92,8 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { .thenReturn(mKeyguardMessageAreaController) fakeFeatureFlags = FakeFeatureFlags() fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false) - mKeyguardPatternView = View.inflate(mContext, R.layout.keyguard_pattern_view, null) - as KeyguardPatternView - + mKeyguardPatternView = + View.inflate(mContext, R.layout.keyguard_pattern_view, null) as KeyguardPatternView mKeyguardPatternViewController = KeyguardPatternViewController( @@ -125,8 +125,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { @Test fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() { overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f) - whenever(mPostureController.devicePosture) - .thenReturn(DEVICE_POSTURE_HALF_OPENED) + whenever(mPostureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED) mKeyguardPatternViewController.onViewAttached() @@ -150,7 +149,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { private fun getPatternTopGuideline(): Float { val cs = ConstraintSet() val container = - mKeyguardPatternView.findViewById(R.id.pattern_container) as ConstraintLayout + mKeyguardPatternView.requireViewById(R.id.pattern_container) as ConstraintLayout cs.clone(container) return cs.getConstraint(R.id.pattern_top_guideline).layout.guidePercent } @@ -159,39 +158,37 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio) } - @Test - fun withFeatureFlagOn_oldMessage_isHidden() { - fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) - - mKeyguardPatternViewController.onViewAttached() - - verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable() - } - - @Test - fun onPause_resetsText() { - mKeyguardPatternViewController.init() - mKeyguardPatternViewController.onPause() - verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) - } - - @Test - fun startAppearAnimation() { - mKeyguardPatternViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false) - } - - @Test - fun startAppearAnimation_withExistingMessage() { - `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") - mKeyguardPatternViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) - } - - @Test - fun resume() { - mKeyguardPatternViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()) - } + @Test + fun withFeatureFlagOn_oldMessage_isHidden() { + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + + mKeyguardPatternViewController.onViewAttached() + + verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable() + } + + @Test + fun onPause_resetsText() { + mKeyguardPatternViewController.init() + mKeyguardPatternViewController.onPause() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) + } + + @Test + fun testOnViewAttached() { + reset(mKeyguardMessageAreaController) + reset(mLockPatternUtils) + mKeyguardPatternViewController.onViewAttached() + verify(mKeyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false) + verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()) + } + + @Test + fun testOnViewAttached_withExistingMessage() { + reset(mKeyguardMessageAreaController) + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + mKeyguardPatternViewController.onViewAttached() + verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index cf86c2192352..efe1955595ca 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -100,6 +100,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { .thenReturn(mDeleteButton); when(mPinBasedInputView.findViewById(R.id.key_enter)) .thenReturn(mOkButton); + + when(mPinBasedInputView.getResources()).thenReturn(getContext().getResources()); FakeFeatureFlags featureFlags = new FakeFeatureFlags(); featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 4dc7652f83cf..80fd7213778e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -114,7 +114,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { objectKeyguardPINView = View.inflate(mContext, R.layout.keyguard_pin_view, null) - .findViewById(R.id.keyguard_pin_view) as KeyguardPINView + .requireViewById(R.id.keyguard_pin_view) as KeyguardPINView } private fun constructPinViewController( @@ -175,7 +175,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { private fun getPinTopGuideline(): Float { val cs = ConstraintSet() - val container = objectKeyguardPINView.findViewById(R.id.pin_container) as ConstraintLayout + val container = objectKeyguardPINView.requireViewById(R.id.pin_container) as ConstraintLayout cs.clone(container) return cs.getConstraint(R.id.pin_pad_top_guideline).layout.guidePercent } @@ -185,27 +185,27 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { } @Test - fun startAppearAnimation() { + fun testOnViewAttached() { val pinViewController = constructPinViewController(mockKeyguardPinView) - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(keyguardMessageAreaController) .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test - fun startAppearAnimation_withExistingMessage() { + fun testOnViewAttached_withExistingMessage() { val pinViewController = constructPinViewController(mockKeyguardPinView) Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.") - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean()) } @Test - fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() { + fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() { val pinViewController = constructPinViewController(mockKeyguardPinView) `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) @@ -213,7 +213,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3) `when`(passwordTextView.text).thenReturn("") - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(deleteButton).visibility = View.INVISIBLE verify(enterButton).visibility = View.INVISIBLE @@ -222,7 +222,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { } @Test - fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() { + fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() { val pinViewController = constructPinViewController(mockKeyguardPinView) `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) @@ -230,7 +230,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6) `when`(passwordTextView.text).thenReturn("") - pinViewController.startAppearAnimation() + pinViewController.onViewAttached() verify(deleteButton).visibility = View.VISIBLE verify(enterButton).visibility = View.VISIBLE diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 9ba21da59361..63c51e46551e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -38,6 +38,7 @@ import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserS import com.android.keyguard.KeyguardSecurityModel.SecurityMode import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate import com.android.systemui.biometrics.SideFpsController import com.android.systemui.biometrics.SideFpsUiRequestSource @@ -46,6 +47,8 @@ import com.android.systemui.classifier.FalsingA11yDelegate import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.log.SessionTracker import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.FalsingManager @@ -61,6 +64,8 @@ import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argThat +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.GlobalSettings @@ -144,6 +149,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { private lateinit var testableResources: TestableResources private lateinit var sceneTestUtils: SceneTestUtils private lateinit var sceneInteractor: SceneInteractor + private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor + private lateinit var authenticationInteractor: AuthenticationInteractor private lateinit var sceneTransitionStateFlow: MutableStateFlow<ObservableTransitionState> private lateinit var underTest: KeyguardSecurityContainerController @@ -182,6 +189,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) featureFlags.set(Flags.SCENE_CONTAINER, false) featureFlags.set(Flags.BOUNCER_USER_SWITCHER, false) + featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) keyguardPasswordViewController = KeyguardPasswordViewController( @@ -204,9 +212,17 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { whenever(userInteractor.getSelectedUserId()).thenReturn(TARGET_USER_ID) sceneTestUtils = SceneTestUtils(this) sceneInteractor = sceneTestUtils.sceneInteractor() + keyguardTransitionInteractor = + KeyguardTransitionInteractorFactory.create(sceneTestUtils.testScope.backgroundScope) + .keyguardTransitionInteractor sceneTransitionStateFlow = MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Lockscreen)) sceneInteractor.setTransitionState(sceneTransitionStateFlow) + authenticationInteractor = + sceneTestUtils.authenticationInteractor( + repository = sceneTestUtils.authenticationRepository(), + sceneInteractor = sceneInteractor + ) underTest = KeyguardSecurityContainerController( @@ -236,9 +252,9 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, userInteractor, faceAuthAccessibilityDelegate, - ) { - sceneInteractor - } + keyguardTransitionInteractor, + { authenticationInteractor }, + ) } @Test @@ -493,30 +509,6 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { } @Test - fun showNextSecurityScreenOrFinish_SimPinToAnotherSimPin_None() { - // GIVEN the current security method is SimPin - whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) - whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) - .thenReturn(false) - underTest.showSecurityScreen(SecurityMode.SimPin) - - // WHEN a request is made from the SimPin screens to show the next security method - whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) - .thenReturn(SecurityMode.SimPin) - whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true) - - underTest.showNextSecurityScreenOrFinish( - /* authenticated= */ true, - TARGET_USER_ID, - /* bypassSecondaryLockScreen= */ true, - SecurityMode.SimPin - ) - - // THEN the next security method of None will dismiss keyguard. - verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) - } - - @Test fun onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() { val registeredSwipeListener = registeredSwipeListener whenever(keyguardUpdateMonitor.isFaceDetectionRunning).thenReturn(false) @@ -587,18 +579,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) underTest.onViewAttached() verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) - clearInvocations(viewFlipperController) configurationListenerArgumentCaptor.value.onThemeChanged() - verify(viewFlipperController).clearViews() - verify(viewFlipperController) - .asynchronouslyInflateView( - eq(SecurityMode.PIN), - any(), - onViewInflatedCallbackArgumentCaptor.capture() - ) - onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) - verify(view).reset() - verify(viewFlipperController).reset() verify(view).reloadColors() } @@ -608,16 +589,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) underTest.onViewAttached() verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) - clearInvocations(viewFlipperController) configurationListenerArgumentCaptor.value.onUiModeChanged() - verify(viewFlipperController).clearViews() - verify(viewFlipperController) - .asynchronouslyInflateView( - eq(SecurityMode.PIN), - any(), - onViewInflatedCallbackArgumentCaptor.capture() - ) - onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) verify(view).reloadColors() } @@ -753,7 +725,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { } @Test - fun dismissesKeyguard_whenSceneChangesFromBouncerToGone() = + fun dismissesKeyguard_whenSceneChangesToGone() = sceneTestUtils.testScope.runTest { featureFlags.set(Flags.SCENE_CONTAINER, true) @@ -790,12 +762,32 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { runCurrent() verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt()) + // While listening, moving back to the lockscreen scene does not dismiss the keyguard + // again. + clearInvocations(viewMediatorCallback) + sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen, null), "reason") + sceneTransitionStateFlow.value = + ObservableTransitionState.Transition( + SceneKey.Gone, + SceneKey.Lockscreen, + flowOf(.5f) + ) + runCurrent() + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen, null), "reason") + sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Lockscreen) + runCurrent() + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + // While listening, moving back to the bouncer scene does not dismiss the keyguard // again. clearInvocations(viewMediatorCallback) sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer, null), "reason") sceneTransitionStateFlow.value = - ObservableTransitionState.Transition(SceneKey.Gone, SceneKey.Bouncer, flowOf(.5f)) + ObservableTransitionState.Transition( + SceneKey.Lockscreen, + SceneKey.Bouncer, + flowOf(.5f) + ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer, null), "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Bouncer) @@ -815,7 +807,21 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { runCurrent() verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) - // While not listening, moving back to the bouncer does not dismiss the keyguard. + // While not listening, moving to the lockscreen does not dismiss the keyguard. + sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen, null), "reason") + sceneTransitionStateFlow.value = + ObservableTransitionState.Transition( + SceneKey.Gone, + SceneKey.Lockscreen, + flowOf(.5f) + ) + runCurrent() + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen, null), "reason") + sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Lockscreen) + runCurrent() + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + + // While not listening, moving to the bouncer does not dismiss the keyguard. sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer, null), "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition(SceneKey.Gone, SceneKey.Bouncer, flowOf(.5f)) @@ -826,12 +832,15 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) // Reattaching the view starts listening again so moving from the bouncer scene to the - // gone - // scene now does dismiss the keyguard again. + // gone scene now does dismiss the keyguard again, this time from lockscreen. underTest.onViewAttached() sceneInteractor.changeScene(SceneModel(SceneKey.Gone, null), "reason") sceneTransitionStateFlow.value = - ObservableTransitionState.Transition(SceneKey.Bouncer, SceneKey.Gone, flowOf(.5f)) + ObservableTransitionState.Transition( + SceneKey.Lockscreen, + SceneKey.Gone, + flowOf(.5f) + ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone, null), "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone) @@ -849,6 +858,17 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { verify(userSwitcher).setAlpha(0f) } + @Test + fun testOnUserSwitched() { + val userSwitchCallbackArgumentCaptor = + argumentCaptor<UserSwitcherController.UserSwitchCallback>() + underTest.onViewAttached() + verify(userSwitcherController) + .addUserSwitchCallback(capture(userSwitchCallbackArgumentCaptor)) + userSwitchCallbackArgumentCaptor.value.onUserSwitched() + verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any()) + } + private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener get() { underTest.onViewAttached() diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index a3acc781f2a7..291dda256c4f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -97,6 +97,8 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { @Test fun onViewAttached() { underTest.onViewAttached() + verify(keyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test @@ -120,8 +122,6 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { underTest.startAppearAnimation() - verify(keyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index efcf4ddb5c71..626faa601970 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -98,6 +98,8 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { underTest.onViewAttached() Mockito.verify(keyguardUpdateMonitor) .registerCallback(any(KeyguardUpdateMonitorCallback::class.java)) + Mockito.verify(keyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test @@ -120,8 +122,6 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { underTest.startAppearAnimation() - Mockito.verify(keyguardMessageAreaController) - .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt index f8262d43cdbb..210f3cb65ac0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt @@ -21,9 +21,9 @@ class KeyguardStatusViewTest : SysuiTestCase() { private lateinit var keyguardStatusView: KeyguardStatusView private val mediaView: View - get() = keyguardStatusView.findViewById(R.id.status_view_media_container) + get() = keyguardStatusView.requireViewById(R.id.status_view_media_container) private val statusViewContainer: ViewGroup - get() = keyguardStatusView.findViewById(R.id.status_view_container) + get() = keyguardStatusView.requireViewById(R.id.status_view_container) private val childrenExcludingMedia get() = statusViewContainer.children.filter { it != mediaView } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java index 956e0b81e6ce..b18137c47f5c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -49,7 +49,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -164,8 +164,9 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { mVibrator, mAuthRippleController, mResources, - new KeyguardTransitionInteractor(mTransitionRepository, - TestScopeProvider.getTestScope().getBackgroundScope()), + KeyguardTransitionInteractorFactory.create( + TestScopeProvider.getTestScope().getBackgroundScope(), + mTransitionRepository).getKeyguardTransitionInteractor(), KeyguardInteractorFactory.create(mFeatureFlags).getKeyguardInteractor(), mFeatureFlags, mPrimaryBouncerInteractor diff --git a/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt index 9fcb9c8f1662..7c2550f465f6 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/NumPadAnimatorTest.kt @@ -47,10 +47,10 @@ class NumPadAnimatorTest : SysuiTestCase() { @Test fun testOnLayout() { - underTest.onLayout(100) + underTest.onLayout(100, 100) verify(background).cornerRadius = 50f reset(background) - underTest.onLayout(100) + underTest.onLayout(100, 100) verify(background, never()).cornerRadius = anyFloat() } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt index 6fe889270d65..9f9b9a478561 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt @@ -19,9 +19,11 @@ import android.animation.Animator import android.testing.AndroidTestingRunner import android.transition.TransitionValues import android.view.View +import android.view.ViewGroup import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardStatusViewController.SplitShadeTransitionAdapter import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -84,5 +86,5 @@ private fun SplitShadeTransitionAdapter.createAnimator( startValues: TransitionValues?, endValues: TransitionValues? ): Animator? { - return createAnimator(/* sceneRoot= */ null, startValues, endValues) + return createAnimator(/* sceneRoot= */ mock(), startValues, endValues) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt index 01d3a3931052..ea7cc3dcd119 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt @@ -27,6 +27,8 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.biometrics.AuthController import com.android.systemui.decor.FaceScanningProviderFactory import com.android.systemui.dump.logcatLogBuffer +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.log.ScreenDecorationsLogger import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.util.mockito.whenever @@ -80,6 +82,8 @@ class FaceScanningProviderFactoryTest : SysuiTestCase() { R.bool.config_fillMainBuiltInDisplayCutout, true ) + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.STOP_PULSING_FACE_SCANNING_ANIMATION, true) underTest = FaceScanningProviderFactory( authController, @@ -87,7 +91,8 @@ class FaceScanningProviderFactoryTest : SysuiTestCase() { statusBarStateController, keyguardUpdateMonitor, mock(Executor::class.java), - ScreenDecorationsLogger(logcatLogBuffer("FaceScanningProviderFactoryTest")) + ScreenDecorationsLogger(logcatLogBuffer("FaceScanningProviderFactoryTest")), + featureFlags, ) whenever(authController.faceSensorLocation).thenReturn(Point(10, 10)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java deleted file mode 100644 index b47b08c2d14c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui; - -import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNull; -import static junit.framework.TestCase.fail; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.annotation.UserIdInt; -import android.app.AppOpsManager; -import android.app.Notification; -import android.app.NotificationManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.UserHandle; -import android.service.notification.StatusBarNotification; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.widget.RemoteViews; - -import androidx.test.filters.SmallTest; - -import com.android.internal.messages.nano.SystemMessageProto; -import com.android.systemui.appops.AppOpsController; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class ForegroundServiceControllerTest extends SysuiTestCase { - private ForegroundServiceController mFsc; - private ForegroundServiceNotificationListener mListener; - private NotifCollectionListener mCollectionListener; - @Mock private AppOpsController mAppOpsController; - @Mock private Handler mMainHandler; - @Mock private NotifPipeline mNotifPipeline; - - @Before - public void setUp() throws Exception { - // allow the TestLooper to be asserted as the main thread these tests - allowTestableLooperAsMainThread(); - - MockitoAnnotations.initMocks(this); - mFsc = new ForegroundServiceController(mAppOpsController, mMainHandler); - mListener = new ForegroundServiceNotificationListener( - mContext, mFsc, mNotifPipeline); - mListener.init(); - ArgumentCaptor<NotifCollectionListener> entryListenerCaptor = - ArgumentCaptor.forClass(NotifCollectionListener.class); - verify(mNotifPipeline).addCollectionListener( - entryListenerCaptor.capture()); - mCollectionListener = entryListenerCaptor.getValue(); - } - - @Test - public void testAppOpsChangedCalledFromBgThread() { - try { - // WHEN onAppOpChanged is called from a different thread than the MainLooper - disallowTestableLooperAsMainThread(); - NotificationEntry entry = createFgEntry(); - mFsc.onAppOpChanged( - AppOpsManager.OP_CAMERA, - entry.getSbn().getUid(), - entry.getSbn().getPackageName(), - true); - - // This test is run on the TestableLooper, which is not the MainLooper, so - // we expect an exception to be thrown - fail("onAppOpChanged shouldn't be allowed to be called from a bg thread."); - } catch (IllegalStateException e) { - // THEN expect an exception - } - } - - @Test - public void testAppOpsCRUD() { - // no crash on remove that doesn't exist - mFsc.onAppOpChanged(9, 1000, "pkg1", false); - assertNull(mFsc.getAppOps(0, "pkg1")); - - // multiuser & multipackage - mFsc.onAppOpChanged(8, 50, "pkg1", true); - mFsc.onAppOpChanged(1, 60, "pkg3", true); - mFsc.onAppOpChanged(7, 500000, "pkg2", true); - - assertEquals(1, mFsc.getAppOps(0, "pkg1").size()); - assertTrue(mFsc.getAppOps(0, "pkg1").contains(8)); - - assertEquals(1, mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size()); - assertTrue(mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7)); - - assertEquals(1, mFsc.getAppOps(0, "pkg3").size()); - assertTrue(mFsc.getAppOps(0, "pkg3").contains(1)); - - // multiple ops for the same package - mFsc.onAppOpChanged(9, 50, "pkg1", true); - mFsc.onAppOpChanged(5, 50, "pkg1", true); - - assertEquals(3, mFsc.getAppOps(0, "pkg1").size()); - assertTrue(mFsc.getAppOps(0, "pkg1").contains(8)); - assertTrue(mFsc.getAppOps(0, "pkg1").contains(9)); - assertTrue(mFsc.getAppOps(0, "pkg1").contains(5)); - - assertEquals(1, mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size()); - assertTrue(mFsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7)); - - // remove one of the multiples - mFsc.onAppOpChanged(9, 50, "pkg1", false); - assertEquals(2, mFsc.getAppOps(0, "pkg1").size()); - assertTrue(mFsc.getAppOps(0, "pkg1").contains(8)); - assertTrue(mFsc.getAppOps(0, "pkg1").contains(5)); - - // remove last op - mFsc.onAppOpChanged(1, 60, "pkg3", false); - assertNull(mFsc.getAppOps(0, "pkg3")); - } - - @Test - public void testDisclosurePredicate() { - StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1", - 5000, "monkeys", Notification.FLAG_AUTO_CANCEL); - StatusBarNotification sbn_user1_disclosure = makeMockSBN(USERID_ONE, "android", - SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES, - null, Notification.FLAG_NO_CLEAR); - - assertTrue(mFsc.isDisclosureNotification(sbn_user1_disclosure)); - assertFalse(mFsc.isDisclosureNotification(sbn_user1_app1)); - } - - @Test - public void testNeedsDisclosureAfterRemovingUnrelatedNotification() { - final String PKG1 = "com.example.app100"; - - StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1, - 5000, "monkeys", Notification.FLAG_AUTO_CANCEL); - StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1); - - // first add a normal notification - entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT); - // nothing required yet - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); - // now the app starts a fg service - entryAdded(makeMockDisclosure(USERID_ONE, new String[]{PKG1}), - NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required! - // add the fg notification - entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has got it covered - // remove the boring notification - entryRemoved(sbn_user1_app1); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has STILL got it covered - entryRemoved(sbn_user1_app1_fg); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required! - } - - @Test - public void testSimpleAddRemove() { - final String PKG1 = "com.example.app1"; - final String PKG2 = "com.example.app2"; - - StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1, - 5000, "monkeys", Notification.FLAG_AUTO_CANCEL); - entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT); - - // no services are "running" - entryAdded(makeMockDisclosure(USERID_ONE, null), - NotificationManager.IMPORTANCE_DEFAULT); - - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG1}), - NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required! - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - // switch to different package - entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG2}), - NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - entryUpdated(makeMockDisclosure(USERID_TWO, new String[]{PKG1}), - NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO)); // finally user2 needs one too - - entryUpdated(makeMockDisclosure(USERID_ONE, new String[]{PKG2, PKG1}), - NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - entryRemoved(makeMockDisclosure(USERID_ONE, null /*unused*/)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - entryRemoved(makeMockDisclosure(USERID_TWO, null /*unused*/)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - } - - @Test - public void testDisclosureBasic() { - final String PKG1 = "com.example.app0"; - - StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1, - 5000, "monkeys", Notification.FLAG_AUTO_CANCEL); - StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1); - - entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT); // not fg - entryAdded(makeMockDisclosure(USERID_ONE, new String[]{PKG1}), - NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required! - entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); // app1 has got it covered - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - // let's take out the other notification and see what happens. - - entryRemoved(sbn_user1_app1); - assertFalse( - mFsc.isDisclosureNeededForUser(USERID_ONE)); // still covered by sbn_user1_app1_fg - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - // let's attempt to downgrade the notification from FLAG_FOREGROUND and see what we get - StatusBarNotification sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1); - sbn_user1_app1_fg_sneaky.getNotification().flags = 0; - entryUpdated(sbn_user1_app1_fg_sneaky, - NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required! - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - // ok, ok, we'll put it back - sbn_user1_app1_fg_sneaky.getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE; - entryUpdated(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - entryRemoved(sbn_user1_app1_fg_sneaky); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); // should be required! - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - - // now let's test an upgrade - entryAdded(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; - entryUpdated(sbn_user1_app1, - NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification - - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); - - // remove it, make sure we're out of compliance again - entryRemoved(sbn_user1_app1); // was fg, should return true - entryRemoved(sbn_user1_app1); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); - - // importance upgrade - entryAdded(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN); - assertTrue(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; - entryUpdated(sbn_user1_app1_fg, - NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification - - // finally, let's turn off the service - entryAdded(makeMockDisclosure(USERID_ONE, null), - NotificationManager.IMPORTANCE_DEFAULT); - - assertFalse(mFsc.isDisclosureNeededForUser(USERID_ONE)); - assertFalse(mFsc.isDisclosureNeededForUser(USERID_TWO)); - } - - @Test - public void testNoNotifsNorAppOps_noSystemAlertWarningRequired() { - // no notifications nor app op signals that this package/userId requires system alert - // warning - assertFalse(mFsc.isSystemAlertWarningNeeded(USERID_ONE, "any")); - } - - @Test - public void testCustomLayouts_systemAlertWarningRequired() { - // GIVEN a notification with a custom layout - final String pkg = "com.example.app0"; - StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0, - false); - - // WHEN the custom layout entry is added - entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN); - - // THEN a system alert warning is required since there aren't any notifications that can - // display the app ops - assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg)); - } - - @Test - public void testStandardLayoutExists_noSystemAlertWarningRequired() { - // GIVEN two notifications (one with a custom layout, the other with a standard layout) - final String pkg = "com.example.app0"; - StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0, - false); - StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true); - - // WHEN the entries are added - entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN); - entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN); - - // THEN no system alert warning is required, since there is at least one notification - // with a standard layout that can display the app ops on the notification - assertFalse(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg)); - } - - @Test - public void testStandardLayoutRemoved_systemAlertWarningRequired() { - // GIVEN two notifications (one with a custom layout, the other with a standard layout) - final String pkg = "com.example.app0"; - StatusBarNotification customLayoutNotif = makeMockSBN(USERID_ONE, pkg, 0, - false); - StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true); - - // WHEN the entries are added and then the standard layout notification is removed - entryAdded(customLayoutNotif, NotificationManager.IMPORTANCE_MIN); - entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN); - entryRemoved(standardLayoutNotif); - - // THEN a system alert warning is required since there aren't any notifications that can - // display the app ops - assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg)); - } - - @Test - public void testStandardLayoutUpdatedToCustomLayout_systemAlertWarningRequired() { - // GIVEN a standard layout notification and then an updated version with a customLayout - final String pkg = "com.example.app0"; - StatusBarNotification standardLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, true); - StatusBarNotification updatedToCustomLayoutNotif = makeMockSBN(USERID_ONE, pkg, 1, false); - - // WHEN the entries is added and then updated to a custom layout - entryAdded(standardLayoutNotif, NotificationManager.IMPORTANCE_MIN); - entryUpdated(updatedToCustomLayoutNotif, NotificationManager.IMPORTANCE_MIN); - - // THEN a system alert warning is required since there aren't any notifications that can - // display the app ops - assertTrue(mFsc.isSystemAlertWarningNeeded(USERID_ONE, pkg)); - } - - private StatusBarNotification makeMockSBN(int userId, String pkg, int id, String tag, - int flags) { - final Notification n = mock(Notification.class); - n.extras = new Bundle(); - n.flags = flags; - return makeMockSBN(userId, pkg, id, tag, n); - } - - private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag, - Notification n) { - final StatusBarNotification sbn = mock(StatusBarNotification.class); - when(sbn.getNotification()).thenReturn(n); - when(sbn.getId()).thenReturn(id); - when(sbn.getPackageName()).thenReturn(pkg); - when(sbn.getTag()).thenReturn(tag); - when(sbn.getUserId()).thenReturn(userid); - when(sbn.getUser()).thenReturn(new UserHandle(userid)); - when(sbn.getKey()).thenReturn("MOCK:"+userid+"|"+pkg+"|"+id+"|"+tag); - return sbn; - } - - private StatusBarNotification makeMockSBN(int uid, String pkg, int id, - boolean usesStdLayout) { - StatusBarNotification sbn = makeMockSBN(uid, pkg, id, "foo", 0); - if (usesStdLayout) { - sbn.getNotification().contentView = null; - sbn.getNotification().headsUpContentView = null; - sbn.getNotification().bigContentView = null; - } else { - sbn.getNotification().contentView = mock(RemoteViews.class); - } - return sbn; - } - - private StatusBarNotification makeMockFgSBN(int uid, String pkg, int id, - boolean usesStdLayout) { - StatusBarNotification sbn = - makeMockSBN(uid, pkg, id, "foo", Notification.FLAG_FOREGROUND_SERVICE); - if (usesStdLayout) { - sbn.getNotification().contentView = null; - sbn.getNotification().headsUpContentView = null; - sbn.getNotification().bigContentView = null; - } else { - sbn.getNotification().contentView = mock(RemoteViews.class); - } - return sbn; - } - - private StatusBarNotification makeMockFgSBN(int uid, String pkg) { - return makeMockSBN(uid, pkg, 1000, "foo", Notification.FLAG_FOREGROUND_SERVICE); - } - - private StatusBarNotification makeMockDisclosure(int userid, String[] pkgs) { - final Notification n = mock(Notification.class); - n.flags = Notification.FLAG_ONGOING_EVENT; - final Bundle extras = new Bundle(); - if (pkgs != null) extras.putStringArray(Notification.EXTRA_FOREGROUND_APPS, pkgs); - n.extras = extras; - n.when = System.currentTimeMillis() - 10000; // ten seconds ago - final StatusBarNotification sbn = makeMockSBN(userid, "android", - SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES, - null, n); - sbn.getNotification().extras = extras; - return sbn; - } - - private NotificationEntry addFgEntry() { - NotificationEntry entry = createFgEntry(); - mCollectionListener.onEntryAdded(entry); - return entry; - } - - private NotificationEntry createFgEntry() { - return new NotificationEntryBuilder() - .setSbn(makeMockFgSBN(0, TEST_PACKAGE_NAME, 1000, true)) - .setImportance(NotificationManager.IMPORTANCE_DEFAULT) - .build(); - } - - private void entryRemoved(StatusBarNotification notification) { - mCollectionListener.onEntryRemoved( - new NotificationEntryBuilder() - .setSbn(notification) - .build(), - REASON_APP_CANCEL); - } - - private void entryAdded(StatusBarNotification notification, int importance) { - NotificationEntry entry = new NotificationEntryBuilder() - .setSbn(notification) - .setImportance(importance) - .build(); - mCollectionListener.onEntryAdded(entry); - } - - private void entryUpdated(StatusBarNotification notification, int importance) { - NotificationEntry entry = new NotificationEntryBuilder() - .setSbn(notification) - .setImportance(importance) - .build(); - mCollectionListener.onEntryUpdated(entry); - } - - @UserIdInt private static final int USERID_ONE = 10; // UserManagerService.MIN_USER_ID; - @UserIdInt private static final int USERID_TWO = USERID_ONE + 1; - private static final String TEST_PACKAGE_NAME = "test"; -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 796e66514afe..f81ef10e6b14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -92,6 +92,8 @@ import com.android.systemui.decor.OverlayWindow; import com.android.systemui.decor.PrivacyDotCornerDecorProviderImpl; import com.android.systemui.decor.PrivacyDotDecorProviderFactory; import com.android.systemui.decor.RoundedCornerResDelegate; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.log.ScreenDecorationsLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.FakeDisplayTracker; @@ -226,13 +228,16 @@ public class ScreenDecorationsTest extends SysuiTestCase { doAnswer(it -> !(mMockCutoutList.isEmpty())).when(mCutoutFactory).getHasProviders(); doReturn(mMockCutoutList).when(mCutoutFactory).getProviders(); + FakeFeatureFlags featureFlags = new FakeFeatureFlags(); + featureFlags.set(Flags.STOP_PULSING_FACE_SCANNING_ANIMATION, true); mFaceScanningDecorProvider = spy(new FaceScanningOverlayProviderImpl( BOUNDS_POSITION_TOP, mAuthController, mStatusBarStateController, mKeyguardUpdateMonitor, mExecutor, - new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")))); + new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), + featureFlags)); mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 56f81606a282..e96ad876db3a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -682,6 +682,36 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } @Test + public void windowMagnifierEditMode_performA11yClickAction_exitEditMode() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + View closeButton = getInternalView(R.id.close_button); + View bottomRightCorner = getInternalView(R.id.bottom_right_corner); + View bottomLeftCorner = getInternalView(R.id.bottom_left_corner); + View topRightCorner = getInternalView(R.id.top_right_corner); + View topLeftCorner = getInternalView(R.id.top_left_corner); + + assertEquals(View.VISIBLE, closeButton.getVisibility()); + assertEquals(View.VISIBLE, bottomRightCorner.getVisibility()); + assertEquals(View.VISIBLE, bottomLeftCorner.getVisibility()); + assertEquals(View.VISIBLE, topRightCorner.getVisibility()); + assertEquals(View.VISIBLE, topLeftCorner.getVisibility()); + + final View mirrorView = mWindowManager.getAttachedView(); + mirrorView.performAccessibilityAction(AccessibilityAction.ACTION_CLICK.getId(), null); + + assertEquals(View.GONE, closeButton.getVisibility()); + assertEquals(View.GONE, bottomRightCorner.getVisibility()); + assertEquals(View.GONE, bottomLeftCorner.getVisibility()); + assertEquals(View.GONE, topRightCorner.getVisibility()); + assertEquals(View.GONE, topLeftCorner.getVisibility()); + } + + @Test public void enableWindowMagnification_hasA11yWindowTitle() { mInstrumentation.runOnMainSync(() -> { mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt index 316de59692f9..2233e3226fd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -70,7 +70,7 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { assertTrue(dialog.isShowing) // The dialog is now fullscreen. - val window = dialog.window + val window = checkNotNull(dialog.window) val decorView = window.decorView as DecorView assertEquals(MATCH_PARENT, window.attributes.width) assertEquals(MATCH_PARENT, window.attributes.height) @@ -172,14 +172,15 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { // Important: the power menu animation relies on this behavior to know when to animate (see // http://ag/16774605). val dialog = runOnMainThreadAndWaitForIdleSync { TestDialog(context) } - dialog.window.setWindowAnimations(0) - assertEquals(0, dialog.window.attributes.windowAnimations) + val window = checkNotNull(dialog.window) + window.setWindowAnimations(0) + assertEquals(0, window.attributes.windowAnimations) val touchSurface = createTouchSurface() runOnMainThreadAndWaitForIdleSync { dialogLaunchAnimator.showFromView(dialog, touchSurface) } - assertNotEquals(0, dialog.window.attributes.windowAnimations) + assertNotEquals(0, window.attributes.windowAnimations) } @Test @@ -351,13 +352,14 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { init { // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. - window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + checkNotNull(window).setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(contentView) + val window = checkNotNull(window) window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT) window.setBackgroundDrawable(windowBackground) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index fc7d20aeb356..707b1b37afd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -33,6 +33,7 @@ import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -605,7 +606,68 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertThat(hintedPinLength).isNull() } - private fun switchToScene(sceneKey: SceneKey) { - sceneInteractor.changeScene(SceneModel(sceneKey), "reason") + @Test + fun isLockscreenDismissed() = + testScope.runTest { + val isLockscreenDismissed by collectLastValue(underTest.isLockscreenDismissed) + // Start on lockscreen. + switchToScene(SceneKey.Lockscreen) + assertThat(isLockscreenDismissed).isFalse() + + // The user swipes down to reveal shade. + switchToScene(SceneKey.Shade) + assertThat(isLockscreenDismissed).isFalse() + + // The user swipes down to reveal quick settings. + switchToScene(SceneKey.QuickSettings) + assertThat(isLockscreenDismissed).isFalse() + + // The user swipes up to go back to shade. + switchToScene(SceneKey.Shade) + assertThat(isLockscreenDismissed).isFalse() + + // The user swipes up to reveal bouncer. + switchToScene(SceneKey.Bouncer) + assertThat(isLockscreenDismissed).isFalse() + + // The user hits back to return to lockscreen. + switchToScene(SceneKey.Lockscreen) + assertThat(isLockscreenDismissed).isFalse() + + // The user swipes up to reveal bouncer. + switchToScene(SceneKey.Bouncer) + assertThat(isLockscreenDismissed).isFalse() + + // The user enters correct credentials and goes to gone. + switchToScene(SceneKey.Gone) + assertThat(isLockscreenDismissed).isTrue() + + // The user swipes down to reveal shade. + switchToScene(SceneKey.Shade) + assertThat(isLockscreenDismissed).isTrue() + + // The user swipes down to reveal quick settings. + switchToScene(SceneKey.QuickSettings) + assertThat(isLockscreenDismissed).isTrue() + + // The user swipes up to go back to shade. + switchToScene(SceneKey.Shade) + assertThat(isLockscreenDismissed).isTrue() + + // The user swipes up to go back to gone. + switchToScene(SceneKey.Gone) + assertThat(isLockscreenDismissed).isTrue() + + // The device goes to sleep, returning to the lockscreen. + switchToScene(SceneKey.Lockscreen) + assertThat(isLockscreenDismissed).isFalse() + } + + private fun TestScope.switchToScene(sceneKey: SceneKey) { + val model = SceneModel(sceneKey) + val loggingReason = "reason" + sceneInteractor.changeScene(model, loggingReason) + sceneInteractor.onSceneChanged(model, loggingReason) + runCurrent() } } 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 e3e61306bcd7..4e52e64a8af1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -139,6 +139,7 @@ open class AuthContainerViewTest : SysuiTestCase() { @Before fun setup() { featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt) + featureFlags.set(Flags.ONE_WAY_HAPTICS_API_MIGRATION, false) } @After @@ -151,7 +152,10 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testNotifiesAnimatedIn() { initializeFingerprintContainer() - verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */) + verify(callback).onDialogAnimatedIn( + authContainer?.requestId ?: 0L, + true /* startFingerprintNow */ + ) } @Test @@ -196,7 +200,10 @@ open class AuthContainerViewTest : SysuiTestCase() { waitForIdleSync() // attaching the view resets the state and allows this to happen again - verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */) + verify(callback).onDialogAnimatedIn( + authContainer?.requestId ?: 0L, + true /* startFingerprintNow */ + ) } @Test @@ -211,7 +218,10 @@ open class AuthContainerViewTest : SysuiTestCase() { // the first time is triggered by initializeFingerprintContainer() // the second time was triggered by dismissWithoutCallback() - verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */) + verify(callback, times(2)).onDialogAnimatedIn( + authContainer?.requestId ?: 0L, + true /* startFingerprintNow */ + ) } @Test @@ -517,10 +527,11 @@ open class AuthContainerViewTest : SysuiTestCase() { { authBiometricFingerprintViewModel }, { promptSelectorInteractor }, { bpCredentialInteractor }, - PromptViewModel(promptSelectorInteractor, vibrator), + PromptViewModel(promptSelectorInteractor, vibrator, featureFlags), { credentialViewModel }, Handler(TestableLooper.get(this).looper), - fakeExecutor + fakeExecutor, + vibrator ) { 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 3d4171ff9cf3..48e513140c3a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -197,6 +197,8 @@ public class AuthControllerTest extends SysuiTestCase { private ArgumentCaptor<String> mMessageCaptor; @Mock private Resources mResources; + @Mock + private VibratorHelper mVibratorHelper; private TestableContext mContextSpy; private Execution mExecution; @@ -515,7 +517,7 @@ public class AuthControllerTest extends SysuiTestCase { assertThat(mModalityCaptor.getValue().intValue()).isEqualTo(modality); assertThat(mMessageCaptor.getValue()).isEqualTo( - mContext.getString(R.string.biometric_face_not_recognized)); + mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)); } @Test @@ -1097,7 +1099,7 @@ public class AuthControllerTest extends SysuiTestCase { () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor, () -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor, mHandler, - mBackgroundExecutor, mUdfpsUtils); + mBackgroundExecutor, mUdfpsUtils, mVibratorHelper); } @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 2b08c66c96fa..994db460cdbb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt @@ -164,7 +164,7 @@ class SideFpsControllerTest : SysuiTestCase() { context.addMockSystemService(WindowManager::class.java, windowManager) whenEver(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView) - whenEver(sideFpsView.findViewById<LottieAnimationView>(eq(R.id.sidefps_animation))) + whenEver(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation))) .thenReturn(mock(LottieAnimationView::class.java)) with(mock(ViewPropertyAnimator::class.java)) { whenEver(sideFpsView.animate()).thenReturn(this) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 40b1f207894a..4d19543d41ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.hardware.biometrics.PromptInfo import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.view.HapticFeedbackConstants import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase @@ -33,6 +34,8 @@ import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat @@ -71,13 +74,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private lateinit var selector: PromptSelectorInteractor private lateinit var viewModel: PromptViewModel + private val featureFlags = FakeFeatureFlags() @Before fun setup() { selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils) selector.resetPrompt() - viewModel = PromptViewModel(selector, vibrator) + viewModel = PromptViewModel(selector, vibrator, featureFlags) + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) } @Test @@ -149,6 +154,29 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa verify(vibrator, never()).vibrateAuthError(any()) } + @Test + fun playSuccessHaptic_onwayHapticsEnabled_SetsConfirmConstant() = runGenericTest { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) + viewModel.showAuthenticated(testCase.authenticatedModality, 1_000L) + + if (expectConfirmation) { + viewModel.confirmAuthenticated() + } + + val currentConstant by collectLastValue(viewModel.hapticsToPlay) + assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.CONFIRM) + } + + @Test + fun playErrorHaptic_onwayHapticsEnabled_SetsRejectConstant() = runGenericTest { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + viewModel.showTemporaryError("test", "messageAfterError", false) + + val currentConstant by collectLastValue(viewModel.hapticsToPlay) + assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.REJECT) + } + private suspend fun TestScope.showAuthenticated( authenticatedModality: BiometricModality, expectConfirmation: Boolean, @@ -499,6 +527,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) val legacyState by collectLastValue(viewModel.legacyState) + val confirmationRequired by collectLastValue(viewModel.isConfirmationRequired) if (testCase.isCoex && testCase.authenticatedByFingerprint) { viewModel.ensureFingerprintHasStarted(isDelayed = true) @@ -507,7 +536,11 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.showHelp(helpMessage) assertThat(size).isEqualTo(PromptSize.MEDIUM) - assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) + if (confirmationRequired == true) { + assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) + } else { + assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED) + } assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(messageVisible).isTrue() assertThat(authenticating).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt index d6cafcb6b5ae..5a5c058793b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt @@ -211,7 +211,7 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_y) val expectedCenterX: Float val expectedCenterY: Float - when (context.display.rotation) { + when (checkNotNull(context.display).rotation) { Surface.ROTATION_90 -> { expectedCenterX = width * normalizedPortPosY expectedCenterY = height * (1 - normalizedPortPosX) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt index 42f28c8c6043..2ae342a5cfa5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt @@ -125,7 +125,7 @@ class ControlViewHolderTest : SysuiTestCase() { control ) cvh.bindData(cws, false) - val chevronIcon = baseLayout.findViewById<View>(R.id.chevron_icon) + val chevronIcon = baseLayout.requireViewById<View>(R.id.chevron_icon) assertThat(chevronIcon.visibility).isEqualTo(View.VISIBLE) } @@ -138,4 +138,4 @@ private const val TINT_COLOR = 0x00ff00 // Should be different from [COLOR] private val DRAWABLE = GradientDrawable() private val COLOR = ColorStateList.valueOf(0xffff00) private val DEFAULT_CONTROL = Control.StatelessBuilder( - CONTROL_ID, mock(PendingIntent::class.java)).build()
\ No newline at end of file + CONTROL_ID, mock(PendingIntent::class.java)).build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index fcd6568de9f3..a400ff963026 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -365,7 +365,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { val selectedItems = listOf( SelectedItem.StructureItem( - StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) + StructureInfo(checkNotNull(ComponentName.unflattenFromString("pkg/.cls1")), + "a", ArrayList()) ), ) preferredPanelRepository.setSelectedComponent( diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java index 49cdfa72f344..7311f4a5ef71 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java @@ -33,15 +33,14 @@ import static org.mockito.Mockito.when; import android.app.AlarmManager; import android.os.Handler; import android.os.HandlerThread; +import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.DejankUtils; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.DozeParameters; -import com.android.systemui.tuner.TunerService; import com.android.systemui.util.wakelock.WakeLockFake; import org.junit.After; @@ -53,6 +52,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) @SmallTest +@TestableLooper.RunWithLooper public class DozeUiTest extends SysuiTestCase { @Mock @@ -62,23 +62,19 @@ public class DozeUiTest extends SysuiTestCase { @Mock private DozeParameters mDozeParameters; @Mock - private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock private DozeHost mHost; @Mock private DozeLog mDozeLog; - @Mock - private TunerService mTunerService; private WakeLockFake mWakeLock; private Handler mHandler; private HandlerThread mHandlerThread; private DozeUi mDozeUi; - @Mock - private StatusBarStateController mStatusBarStateController; @Before public void setUp() throws Exception { + allowTestableLooperAsMainThread(); MockitoAnnotations.initMocks(this); + DejankUtils.setImmediate(true); mHandlerThread = new HandlerThread("DozeUiTest"); mHandlerThread.start(); @@ -86,12 +82,13 @@ public class DozeUiTest extends SysuiTestCase { mHandler = mHandlerThread.getThreadHandler(); mDozeUi = new DozeUi(mContext, mAlarmManager, mWakeLock, mHost, mHandler, - mDozeParameters, mStatusBarStateController, mDozeLog); + mDozeParameters, mDozeLog); mDozeUi.setDozeMachine(mMachine); } @After public void tearDown() throws Exception { + DejankUtils.setImmediate(false); mHandlerThread.quit(); mHandler = null; mHandlerThread = null; diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java index 07cb5d88a515..6a178895839b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java @@ -22,17 +22,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import android.os.RemoteException; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; -import com.android.internal.app.AssistUtils; -import com.android.internal.app.IVisualQueryDetectionAttentionListener; import com.android.systemui.SysuiTestCase; -import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.assist.AssistManager; +import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener; import com.android.systemui.shared.condition.Condition; import org.junit.Before; @@ -50,9 +47,7 @@ public class AssistantAttentionConditionTest extends SysuiTestCase { @Mock Condition.Callback mCallback; @Mock - AssistUtils mAssistUtils; - @Mock - DreamOverlayStateController mDreamOverlayStateController; + AssistManager mAssistManager; @Mock CoroutineScope mScope; @@ -62,55 +57,34 @@ public class AssistantAttentionConditionTest extends SysuiTestCase { public void setup() { MockitoAnnotations.initMocks(this); - mAssistantAttentionCondition = - new AssistantAttentionCondition(mScope, mDreamOverlayStateController, mAssistUtils); + mAssistantAttentionCondition = new AssistantAttentionCondition(mScope, mAssistManager); // Adding a callback also starts the condition. mAssistantAttentionCondition.addCallback(mCallback); } @Test public void testEnableVisualQueryDetection() { - final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor = - ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); - verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture()); - - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); - argumentCaptor.getValue().onStateChanged(); - - verify(mAssistUtils).enableVisualQueryDetection(any()); + verify(mAssistManager).addVisualQueryAttentionListener( + any(VisualQueryAttentionListener.class)); } @Test public void testDisableVisualQueryDetection() { - final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor = - ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); - verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture()); - - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); - argumentCaptor.getValue().onStateChanged(); - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(false); - argumentCaptor.getValue().onStateChanged(); - - verify(mAssistUtils).disableVisualQueryDetection(); + mAssistantAttentionCondition.stop(); + verify(mAssistManager).removeVisualQueryAttentionListener( + any(VisualQueryAttentionListener.class)); } @Test - public void testAttentionChangedTriggersCondition() throws RemoteException { - final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor = - ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); - verify(mDreamOverlayStateController).addCallback(callbackCaptor.capture()); - - when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); - callbackCaptor.getValue().onStateChanged(); - - final ArgumentCaptor<IVisualQueryDetectionAttentionListener> listenerCaptor = - ArgumentCaptor.forClass(IVisualQueryDetectionAttentionListener.class); - verify(mAssistUtils).enableVisualQueryDetection(listenerCaptor.capture()); + public void testAttentionChangedTriggersCondition() { + final ArgumentCaptor<VisualQueryAttentionListener> argumentCaptor = + ArgumentCaptor.forClass(VisualQueryAttentionListener.class); + verify(mAssistManager).addVisualQueryAttentionListener(argumentCaptor.capture()); - listenerCaptor.getValue().onAttentionGained(); + argumentCaptor.getValue().onAttentionGained(); assertThat(mAssistantAttentionCondition.isConditionMet()).isTrue(); - listenerCaptor.getValue().onAttentionLost(); + argumentCaptor.getValue().onAttentionLost(); assertThat(mAssistantAttentionCondition.isConditionMet()).isFalse(); verify(mCallback, times(2)).onConditionChanged(eq(mAssistantAttentionCondition)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index d8d3f92911ea..b05de48ae224 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -224,7 +224,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager, mShadeWindowLogger); mFeatureFlags = new FakeFeatureFlags(); - + mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); DejankUtils.setImmediate(true); @@ -593,6 +593,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); assertFalse(mViewMediator.isShowingAndNotOccluded()); + verify(mKeyguardUnlockAnimationController).notifyFinishedKeyguardExitAnimation(false); } @Test @@ -609,6 +610,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mKeyguardUnlockAnimationController).notifyFinishedKeyguardExitAnimation(true); } @Test @@ -617,6 +619,9 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { startMockKeyguardExitAnimation(); cancelMockKeyguardExitAnimation(); + // Calling cancel above results in keyguard not visible, as there is no pending lock + verify(mKeyguardUnlockAnimationController).notifyFinishedKeyguardExitAnimation(false); + mViewMediator.maybeHandlePendingLock(); TestableLooper.get(this).processAllMessages(); @@ -631,9 +636,15 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) - public void testStartKeyguardExitAnimation_expectSurfaceBehindRemoteAnimation() { + public void testStartKeyguardExitAnimation_expectSurfaceBehindRemoteAnimationAndExits() { startMockKeyguardExitAnimation(); assertTrue(mViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind()); + + mViewMediator.mViewMediatorCallback.keyguardDonePending(true, + mUpdateMonitor.getCurrentUser()); + mViewMediator.mViewMediatorCallback.readyForKeyguardDone(); + TestableLooper.get(this).processAllMessages(); + verify(mKeyguardUnlockAnimationController).notifyFinishedKeyguardExitAnimation(false); } /** @@ -946,7 +957,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mSystemClock, mDispatcher, () -> mDreamingToLockscreenTransitionViewModel, - mSystemPropertiesHelper); + mSystemPropertiesHelper, + () -> mock(WindowManagerLockscreenVisibilityManager.class)); mViewMediator.start(); mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null); 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 85ee0e4d6ec3..fe5b8120428d 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 @@ -52,6 +52,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR +import com.android.systemui.flags.Flags.KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory @@ -168,7 +169,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { biometricSettingsRepository = FakeBiometricSettingsRepository() deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() trustRepository = FakeTrustRepository() - featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) } + featureFlags = + FakeFeatureFlags().apply { + set(FACE_AUTH_REFACTOR, true) + set(KEYGUARD_WM_STATE_REFACTOR, false) + } val withDeps = KeyguardInteractorFactory.create( featureFlags = featureFlags, @@ -332,9 +337,6 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { ) .isFalse() - whenever(faceManager.sensorPropertiesInternal).thenReturn(null) - assertThat(createDeviceEntryFaceAuthRepositoryImpl().isDetectionSupported).isFalse() - whenever(faceManager.sensorPropertiesInternal).thenReturn(listOf()) assertThat(createDeviceEntryFaceAuthRepositoryImpl().isDetectionSupported).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 5e3376a45488..5ead16bdc10f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -63,6 +63,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -193,7 +194,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { assertThat(underTest.isKeyguardShowing()).isFalse() val captor = argumentCaptor<KeyguardStateController.Callback>() - verify(keyguardStateController).addCallback(captor.capture()) + verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture()) whenever(keyguardStateController.isShowing).thenReturn(true) captor.value.onKeyguardShowingChanged() @@ -255,7 +256,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { assertThat(latest).isFalse() val captor = argumentCaptor<KeyguardStateController.Callback>() - verify(keyguardStateController).addCallback(captor.capture()) + verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture()) whenever(keyguardStateController.isOccluded).thenReturn(true) captor.value.onKeyguardShowingChanged() @@ -280,7 +281,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { assertThat(isKeyguardUnlocked).isFalse() val captor = argumentCaptor<KeyguardStateController.Callback>() - verify(keyguardStateController).addCallback(captor.capture()) + verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture()) whenever(keyguardStateController.isUnlocked).thenReturn(true) captor.value.onUnlockedChanged() @@ -454,7 +455,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { assertThat(latest).isFalse() val captor = argumentCaptor<KeyguardStateController.Callback>() - verify(keyguardStateController).addCallback(captor.capture()) + verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture()) whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(true) captor.value.onKeyguardGoingAwayChanged() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepositoryImplTest.kt new file mode 100644 index 000000000000..bed959f1a3f0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepositoryImplTest.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.keyguard.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +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 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardSurfaceBehindRepositoryImplTest : SysuiTestCase() { + private val testScope = TestScope() + + private lateinit var underTest: KeyguardSurfaceBehindRepositoryImpl + + @Before + fun setUp() { + underTest = KeyguardSurfaceBehindRepositoryImpl() + } + + @Test + fun testSetAnimatingSurface() { + testScope.runTest { + val values by collectValues(underTest.isAnimatingSurface) + + runCurrent() + underTest.setAnimatingSurface(true) + runCurrent() + underTest.setAnimatingSurface(false) + runCurrent() + + // Default (first) value should be false. + assertThat(values).isEqualTo(listOf(false, true, false)) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt new file mode 100644 index 000000000000..e2bf2f82b1c4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.shade.data.repository.FakeShadeRepository +import dagger.Lazy +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import junit.framework.Assert.fail +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FromLockscreenTransitionInteractorTest : KeyguardTransitionInteractorTestCase() { + private lateinit var underTest: FromLockscreenTransitionInteractor + + // Override the fromLockscreenTransitionInteractor provider from the superclass so our underTest + // interactor is provided to any classes that need it. + override var fromLockscreenTransitionInteractorLazy: Lazy<FromLockscreenTransitionInteractor>? = + Lazy { + underTest + } + + @Before + override fun setUp() { + super.setUp() + + underTest = + FromLockscreenTransitionInteractor( + transitionRepository = super.transitionRepository, + transitionInteractor = super.transitionInteractor, + scope = super.testScope.backgroundScope, + keyguardInteractor = super.keyguardInteractor, + flags = FakeFeatureFlags(), + shadeRepository = FakeShadeRepository(), + ) + } + + @Test + fun testSurfaceBehindVisibility_nonNullOnlyForRelevantTransitions() = + testScope.runTest { + val values by collectValues(underTest.surfaceBehindVisibility) + runCurrent() + + // Transition-specific surface visibility should be null ("don't care") initially. + assertEquals( + listOf( + null, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + ) + ) + + runCurrent() + + assertEquals( + listOf( + null, // LOCKSCREEN -> AOD does not have any specific surface visibility. + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + + assertEquals( + listOf( + null, + true, // Surface is made visible immediately during LOCKSCREEN -> GONE + ), + values + ) + } + + @Test + fun testSurfaceBehindModel() = + testScope.runTest { + val values by collectValues(underTest.surfaceBehindModel) + runCurrent() + + assertEquals( + values, + listOf( + null, // We should start null ("don't care"). + ) + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + ) + ) + runCurrent() + + assertEquals( + listOf( + null, // LOCKSCREEN -> AOD does not have specific view params. + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + value = 0.01f, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + value = 0.99f, + ) + ) + runCurrent() + + assertEquals(3, values.size) + val model1percent = values[1] + val model99percent = values[2] + + try { + // We should initially have an alpha of 0f when unlocking, so the surface is not + // visible + // while lockscreen UI animates out. + assertEquals(0f, model1percent!!.alpha) + + // By the end it should probably be visible. + assertTrue(model99percent!!.alpha > 0f) + } catch (e: NullPointerException) { + fail("surfaceBehindModel was unexpectedly null.") + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt new file mode 100644 index 000000000000..85bc374651e0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt @@ -0,0 +1,214 @@ +/* + * 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.coroutines.collectValues +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.mockito.mock +import dagger.Lazy +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import junit.framework.Assert.fail +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FromPrimaryBouncerTransitionInteractorTest : KeyguardTransitionInteractorTestCase() { + private lateinit var underTest: FromPrimaryBouncerTransitionInteractor + + // Override the fromPrimaryBouncerTransitionInteractor provider from the superclass so our + // underTest interactor is provided to any classes that need it. + override var fromPrimaryBouncerTransitionInteractorLazy: + Lazy<FromPrimaryBouncerTransitionInteractor>? = + Lazy { + underTest + } + + @Before + override fun setUp() { + super.setUp() + + underTest = + FromPrimaryBouncerTransitionInteractor( + transitionRepository = super.transitionRepository, + transitionInteractor = super.transitionInteractor, + scope = super.testScope.backgroundScope, + keyguardInteractor = super.keyguardInteractor, + flags = FakeFeatureFlags(), + keyguardSecurityModel = mock(), + ) + } + + @Test + fun testSurfaceBehindVisibility() = + testScope.runTest { + val values by collectValues(underTest.surfaceBehindVisibility) + runCurrent() + + // Transition-specific surface visibility should be null ("don't care") initially. + assertEquals( + listOf( + null, + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.LOCKSCREEN, + ) + ) + + runCurrent() + + assertEquals( + listOf( + null, // PRIMARY_BOUNCER -> LOCKSCREEN does not have any specific visibility. + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + value = 0.01f, + ) + ) + + runCurrent() + + assertEquals( + listOf( + null, + false, // Surface is only made visible once the bouncer UI animates out. + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + value = 0.99f, + ) + ) + + runCurrent() + + assertEquals( + listOf( + null, + false, + true, // Surface should eventually be visible. + ), + values + ) + } + + @Test + fun testSurfaceBehindModel() = + testScope.runTest { + val values by collectValues(underTest.surfaceBehindModel) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + assertEquals( + listOf( + null, // PRIMARY_BOUNCER -> LOCKSCREEN does not have specific view params. + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + value = 0.01f, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + value = 0.99f, + ) + ) + runCurrent() + + assertEquals(3, values.size) + val model1percent = values[1] + val model99percent = values[2] + + try { + // We should initially have an alpha of 0f when unlocking, so the surface is not + // visible + // while lockscreen UI animates out. + assertEquals(0f, model1percent!!.alpha) + + // By the end it should probably be visible. + assertTrue(model99percent!!.alpha > 0f) + } catch (e: NullPointerException) { + fail("surfaceBehindModel was unexpectedly null.") + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt new file mode 100644 index 000000000000..fdcc66b6c974 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt @@ -0,0 +1,169 @@ +/* + * 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.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import kotlinx.coroutines.flow.flowOf +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.Mock +import org.mockito.MockitoAnnotations.initMocks + +@SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class KeyguardSurfaceBehindInteractorTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardSurfaceBehindInteractor + private lateinit var repository: FakeKeyguardSurfaceBehindRepository + + @Mock + private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor + @Mock + private lateinit var fromPrimaryBouncerTransitionInteractor: + FromPrimaryBouncerTransitionInteractor + + private val lockscreenSurfaceBehindModel = KeyguardSurfaceBehindModel(alpha = 0.33f) + private val primaryBouncerSurfaceBehindModel = KeyguardSurfaceBehindModel(alpha = 0.66f) + + private val testScope = TestScope() + + private lateinit var transitionRepository: FakeKeyguardTransitionRepository + private lateinit var transitionInteractor: KeyguardTransitionInteractor + + @Before + fun setUp() { + initMocks(this) + + whenever(fromLockscreenTransitionInteractor.surfaceBehindModel) + .thenReturn(flowOf(lockscreenSurfaceBehindModel)) + whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindModel) + .thenReturn(flowOf(primaryBouncerSurfaceBehindModel)) + + transitionRepository = FakeKeyguardTransitionRepository() + + transitionInteractor = + KeyguardTransitionInteractorFactory.create( + scope = testScope.backgroundScope, + repository = transitionRepository, + ) + .keyguardTransitionInteractor + + repository = FakeKeyguardSurfaceBehindRepository() + underTest = + KeyguardSurfaceBehindInteractor( + repository = repository, + fromLockscreenInteractor = fromLockscreenTransitionInteractor, + fromPrimaryBouncerInteractor = fromPrimaryBouncerTransitionInteractor, + transitionInteractor = transitionInteractor, + ) + } + + @Test + fun viewParamsSwitchToCorrectFlow() = + testScope.runTest { + val values by collectValues(underTest.viewParams) + + // Start on the LOCKSCREEN. + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + + runCurrent() + + // We're on LOCKSCREEN; we should be using the default params. + assertEquals(1, values.size) + assertTrue(values[0].alpha == 0f) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + + // We're going from LOCKSCREEN -> GONE, we should be using the lockscreen interactor's + // surface behind model. + assertEquals(2, values.size) + assertEquals(values[1], lockscreenSurfaceBehindModel) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + + // We're going from PRIMARY_BOUNCER -> GONE, we should be using the bouncer interactor's + // surface behind model. + assertEquals(3, values.size) + assertEquals(values[2], primaryBouncerSurfaceBehindModel) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + + // Once PRIMARY_BOUNCER -> GONE finishes, we should be using default params, which is + // alpha=1f when we're GONE. + assertEquals(4, values.size) + assertEquals(1f, values[3].alpha) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt new file mode 100644 index 000000000000..8db19aef1aa6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.util.mockito.mock +import dagger.Lazy +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope + +open class KeyguardTransitionInteractorTestCase : SysuiTestCase() { + val testDispatcher = StandardTestDispatcher() + val testScope = TestScope(testDispatcher) + + lateinit var keyguardRepository: FakeKeyguardRepository + lateinit var transitionRepository: FakeKeyguardTransitionRepository + + lateinit var keyguardInteractor: KeyguardInteractor + lateinit var transitionInteractor: KeyguardTransitionInteractor + + /** + * Replace these lazy providers with non-null ones if you want test dependencies to use a real + * instance of the interactor for the test. + */ + open var fromLockscreenTransitionInteractorLazy: Lazy<FromLockscreenTransitionInteractor>? = + null + open var fromPrimaryBouncerTransitionInteractorLazy: + Lazy<FromPrimaryBouncerTransitionInteractor>? = + null + + open fun setUp() { + keyguardRepository = FakeKeyguardRepository() + transitionRepository = FakeKeyguardTransitionRepository() + + keyguardInteractor = + KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor + + transitionInteractor = + KeyguardTransitionInteractorFactory.create( + repository = transitionRepository, + keyguardInteractor = keyguardInteractor, + scope = testScope.backgroundScope, + fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractorLazy + ?: Lazy { mock() }, + fromPrimaryBouncerTransitionInteractor = + fromPrimaryBouncerTransitionInteractorLazy ?: Lazy { mock() }, + ) + .also { + fromLockscreenTransitionInteractorLazy = it.fromLockscreenTransitionInteractor + fromPrimaryBouncerTransitionInteractorLazy = + it.fromPrimaryBouncerTransitionInteractor + } + .keyguardTransitionInteractor + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index aa6bd4e85e24..4b221a0e81bf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -104,12 +104,21 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN) - featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, true) } + featureFlags = + FakeFeatureFlags().apply { + set(Flags.FACE_AUTH_REFACTOR, true) + set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) + } transitionInteractor = KeyguardTransitionInteractorFactory.create( scope = testScope, repository = transitionRepository, + keyguardInteractor = createKeyguardInteractor(), + fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor }, + fromPrimaryBouncerTransitionInteractor = { + fromPrimaryBouncerTransitionInteractor + }, ) .keyguardTransitionInteractor @@ -119,6 +128,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardInteractor = createKeyguardInteractor(), transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, + flags = featureFlags, shadeRepository = shadeRepository, ) .apply { start() } @@ -129,6 +139,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardInteractor = createKeyguardInteractor(), transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, + flags = featureFlags, keyguardSecurityModel = keyguardSecurityModel, ) .apply { start() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt index baa5ee81cc4a..1dcb55d781f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt @@ -44,6 +44,8 @@ import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticati import com.android.systemui.keyguard.util.IndicationHelper import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter.OnDismissAction +import com.android.systemui.power.data.repository.FakePowerRepository +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -80,6 +82,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { private lateinit var configurationRepository: FakeConfigurationRepository private lateinit var featureFlags: FakeFeatureFlags private lateinit var trustRepository: FakeTrustRepository + private lateinit var powerRepository: FakePowerRepository @Mock private lateinit var indicationHelper: IndicationHelper @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @@ -102,6 +105,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { set(Flags.DELAY_BOUNCER, false) } trustRepository = FakeTrustRepository() + powerRepository = FakePowerRepository() underTest = OccludingAppDeviceEntryInteractor( BiometricMessageInteractor( @@ -145,6 +149,14 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { testScope.backgroundScope, mockedContext, activityStarter, + PowerInteractor( + powerRepository, + keyguardRepository, + falsingCollector = mock(), + screenOffAnimationController = mock(), + statusBarStateController = mock(), + ), + FakeFeatureFlags().apply { set(Flags.FP_LISTEN_OCCLUDING_APPS, true) }, ) } @@ -160,6 +172,18 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { } @Test + fun fingerprintSuccess_notInteractive_doesNotGoToHomeScreen() = + testScope.runTest { + givenOnOccludingApp(true) + powerRepository.setInteractive(false) + fingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + verifyNeverGoToHomeScreen() + } + + @Test fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() = testScope.runTest { givenOnOccludingApp(false) @@ -291,6 +315,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { } private fun givenOnOccludingApp(isOnOccludingApp: Boolean) { + powerRepository.setInteractive(true) keyguardRepository.setKeyguardOccluded(isOnOccludingApp) keyguardRepository.setKeyguardShowing(isOnOccludingApp) bouncerRepository.setPrimaryShow(!isOnOccludingApp) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt new file mode 100644 index 000000000000..73ecae5af9b3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt @@ -0,0 +1,412 @@ +/* + * 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.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.flow.MutableStateFlow +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.Mock +import org.mockito.MockitoAnnotations.initMocks + +@SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { + + private lateinit var underTest: WindowManagerLockscreenVisibilityInteractor + + @Mock private lateinit var surfaceBehindInteractor: KeyguardSurfaceBehindInteractor + @Mock + private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor + @Mock + private lateinit var fromPrimaryBouncerTransitionInteractor: + FromPrimaryBouncerTransitionInteractor + + private val lockscreenSurfaceVisibilityFlow = MutableStateFlow<Boolean?>(false) + private val primaryBouncerSurfaceVisibilityFlow = MutableStateFlow<Boolean?>(false) + private val surfaceBehindIsAnimatingFlow = MutableStateFlow(false) + + private val testScope = TestScope() + + private lateinit var keyguardInteractor: KeyguardInteractor + private lateinit var transitionRepository: FakeKeyguardTransitionRepository + private lateinit var transitionInteractor: KeyguardTransitionInteractor + + @Before + fun setUp() { + initMocks(this) + + whenever(fromLockscreenTransitionInteractor.surfaceBehindVisibility) + .thenReturn(lockscreenSurfaceVisibilityFlow) + whenever(fromPrimaryBouncerTransitionInteractor.surfaceBehindVisibility) + .thenReturn(primaryBouncerSurfaceVisibilityFlow) + whenever(surfaceBehindInteractor.isAnimatingSurface) + .thenReturn(surfaceBehindIsAnimatingFlow) + + transitionRepository = FakeKeyguardTransitionRepository() + + transitionInteractor = + KeyguardTransitionInteractorFactory.create( + scope = testScope.backgroundScope, + repository = transitionRepository, + ) + .also { keyguardInteractor = it.keyguardInteractor } + .keyguardTransitionInteractor + + underTest = + WindowManagerLockscreenVisibilityInteractor( + keyguardInteractor = keyguardInteractor, + transitionInteractor = transitionInteractor, + surfaceBehindInteractor = surfaceBehindInteractor, + fromLockscreenTransitionInteractor, + fromPrimaryBouncerTransitionInteractor, + ) + } + + @Test + fun surfaceBehindVisibility_switchesToCorrectFlow() = + testScope.runTest { + val values by collectValues(underTest.surfaceBehindVisibility) + + // Start on LOCKSCREEN. + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + + runCurrent() + + assertEquals( + listOf( + false, // We should start with the surface invisible on LOCKSCREEN. + ), + values + ) + + val lockscreenSpecificSurfaceVisibility = true + lockscreenSurfaceVisibilityFlow.emit(lockscreenSpecificSurfaceVisibility) + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + + // We started a transition from LOCKSCREEN, we should be using the value emitted by the + // lockscreenSurfaceVisibilityFlow. + assertEquals( + listOf( + false, + lockscreenSpecificSurfaceVisibility, + ), + values + ) + + // Go back to LOCKSCREEN, since we won't emit 'true' twice in a row. + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + assertEquals( + listOf( + false, + lockscreenSpecificSurfaceVisibility, + false, // FINISHED (LOCKSCREEN) + ), + values + ) + + val bouncerSpecificVisibility = true + primaryBouncerSurfaceVisibilityFlow.emit(bouncerSpecificVisibility) + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + ) + ) + + runCurrent() + + // We started a transition from PRIMARY_BOUNCER, we should be using the value emitted by + // the + // primaryBouncerSurfaceVisibilityFlow. + assertEquals( + listOf( + false, + lockscreenSpecificSurfaceVisibility, + false, + bouncerSpecificVisibility, + ), + values + ) + } + + @Test + fun testUsingGoingAwayAnimation_duringTransitionToGone() = + testScope.runTest { + val values by collectValues(underTest.usingKeyguardGoingAwayAnimation) + + // Start on LOCKSCREEN. + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + assertEquals( + listOf( + false, // Not using the animation when we're just sitting on LOCKSCREEN. + ), + values + ) + + surfaceBehindIsAnimatingFlow.emit(true) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + assertEquals( + listOf( + false, + true, // Still true when we're FINISHED -> GONE, since we're still animating. + ), + values + ) + + surfaceBehindIsAnimatingFlow.emit(false) + runCurrent() + + assertEquals( + listOf( + false, + true, + false, // False once the animation ends. + ), + values + ) + } + + @Test + fun testNotUsingGoingAwayAnimation_evenWhenAnimating_ifStateIsNotGone() = + testScope.runTest { + val values by collectValues(underTest.usingKeyguardGoingAwayAnimation) + + // Start on LOCKSCREEN. + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + assertEquals( + listOf( + false, // Not using the animation when we're just sitting on LOCKSCREEN. + ), + values + ) + + surfaceBehindIsAnimatingFlow.emit(true) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + assertEquals( + listOf( + false, + true, // We're happily animating while transitioning to gone. + ), + values + ) + + // Oh no, we're still surfaceBehindAnimating=true, but no longer transitioning to GONE. + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + ) + ) + runCurrent() + + assertEquals( + listOf( + false, + true, + false, // Despite the animator still running, this should be false. + ), + values + ) + + surfaceBehindIsAnimatingFlow.emit(false) + runCurrent() + + assertEquals( + listOf( + false, + true, + false, // The animator ending should have no effect. + ), + values + ) + } + + @Test + fun lockscreenVisibility_visibleWhenGone() = + testScope.runTest { + val values by collectValues(underTest.lockscreenVisibility) + + // Start on LOCKSCREEN. + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, // Unsurprisingly, we should start with the lockscreen visible on + // LOCKSCREEN. + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, // Lockscreen remains visible while we're transitioning to GONE. + ), + values + ) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + + assertEquals( + listOf( + true, + false, // Once we're fully GONE, the lockscreen should not be visible. + ), + values + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt new file mode 100644 index 000000000000..a22f603ae92a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.binder + +import android.testing.TestableLooper.RunWithLooper +import android.view.RemoteAnimationTarget +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardViewController +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.AnimatorTestRule +import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor +import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertNull +import junit.framework.Assert.assertTrue +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.doAnswer +import org.mockito.MockitoAnnotations + +@SmallTest +@RoboPilotTest +@RunWithLooper(setAsMainLooper = true) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class KeyguardSurfaceBehindParamsApplierTest : SysuiTestCase() { + @get:Rule val animatorTestRule = AnimatorTestRule() + + private lateinit var underTest: KeyguardSurfaceBehindParamsApplier + private lateinit var executor: FakeExecutor + + @Mock private lateinit var keyguardViewController: KeyguardViewController + + @Mock private lateinit var interactor: KeyguardSurfaceBehindInteractor + + @Mock private lateinit var remoteAnimationTarget: RemoteAnimationTarget + + private var isAnimatingSurface: Boolean? = null + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + executor = FakeExecutor(FakeSystemClock()) + underTest = + KeyguardSurfaceBehindParamsApplier( + executor = executor, + keyguardViewController = keyguardViewController, + interactor = interactor, + ) + + doAnswer { + (it.arguments[0] as Boolean).let { animating -> isAnimatingSurface = animating } + } + .whenever(interactor) + .setAnimatingSurface(anyBoolean()) + } + + @After + fun tearDown() { + animatorTestRule.advanceTimeBy(1000.toLong()) + } + + @Test + fun testNotAnimating_setParamsWithNoAnimation() { + underTest.viewParams = + KeyguardSurfaceBehindModel( + alpha = 0.3f, + translationY = 300f, + ) + + // A surface has not yet been provided, so we shouldn't have set animating to false OR true + // just yet. + assertNull(isAnimatingSurface) + + underTest.applyParamsToSurface(remoteAnimationTarget) + + // We should now explicitly not be animating the surface. + assertFalse(checkNotNull(isAnimatingSurface)) + } + + @Test + fun testAnimating_paramsThenSurfaceProvided() { + underTest.viewParams = + KeyguardSurfaceBehindModel( + animateFromAlpha = 0f, + alpha = 0.3f, + animateFromTranslationY = 0f, + translationY = 300f, + ) + + // A surface has not yet been provided, so we shouldn't have set animating to false OR true + // just yet. + assertNull(isAnimatingSurface) + + underTest.applyParamsToSurface(remoteAnimationTarget) + + // We should now be animating the surface. + assertTrue(checkNotNull(isAnimatingSurface)) + } + + @Test + fun testAnimating_surfaceThenParamsProvided() { + underTest.applyParamsToSurface(remoteAnimationTarget) + + // The default params (which do not animate) should have been applied, so we're explicitly + // NOT animating yet. + assertFalse(checkNotNull(isAnimatingSurface)) + + underTest.viewParams = + KeyguardSurfaceBehindModel( + animateFromAlpha = 0f, + alpha = 0.3f, + animateFromTranslationY = 0f, + translationY = 300f, + ) + + // We should now be animating the surface. + assertTrue(checkNotNull(isAnimatingSurface)) + } + + @Test + fun testAnimating_thenReleased_animatingIsFalse() { + underTest.viewParams = + KeyguardSurfaceBehindModel( + animateFromAlpha = 0f, + alpha = 0.3f, + animateFromTranslationY = 0f, + translationY = 300f, + ) + underTest.applyParamsToSurface(remoteAnimationTarget) + + assertTrue(checkNotNull(isAnimatingSurface)) + + underTest.notifySurfaceReleased() + + // Releasing the surface should immediately cancel animators. + assertFalse(checkNotNull(isAnimatingSurface)) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt new file mode 100644 index 000000000000..623c8771b3d2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.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.ui.binder + +import android.app.IActivityTaskManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { + private lateinit var underTest: WindowManagerLockscreenVisibilityManager + private lateinit var executor: FakeExecutor + + @Mock private lateinit var activityTaskManagerService: IActivityTaskManager + + @Mock private lateinit var keyguardStateController: KeyguardStateController + + @Mock private lateinit var keyguardSurfaceBehindAnimator: KeyguardSurfaceBehindParamsApplier + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + executor = FakeExecutor(FakeSystemClock()) + + underTest = + WindowManagerLockscreenVisibilityManager( + executor = executor, + activityTaskManagerService = activityTaskManagerService, + keyguardStateController = keyguardStateController, + keyguardSurfaceBehindAnimator = keyguardSurfaceBehindAnimator, + ) + } + + @Test + fun testLockscreenVisible_andAodVisible() { + underTest.setLockscreenShown(true) + underTest.setAodVisible(true) + + verify(activityTaskManagerService).setLockScreenShown(true, true) + verifyNoMoreInteractions(activityTaskManagerService) + } + + @Test + fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible() { + underTest.setLockscreenShown(true) + underTest.setAodVisible(true) + + verify(activityTaskManagerService).setLockScreenShown(true, true) + verifyNoMoreInteractions(activityTaskManagerService) + + underTest.setSurfaceBehindVisibility(true) + + verify(activityTaskManagerService).keyguardGoingAway(anyInt()) + verifyNoMoreInteractions(activityTaskManagerService) + } + + @Test + fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway() { + underTest.setLockscreenShown(false) + underTest.setAodVisible(false) + + verify(activityTaskManagerService).setLockScreenShown(false, false) + verifyNoMoreInteractions(activityTaskManagerService) + + underTest.setSurfaceBehindVisibility(true) + + verifyNoMoreInteractions(activityTaskManagerService) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt index c67f53519957..bfc6f31c57db 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -21,9 +21,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt index 80ab418fbd30..0ad14d029e40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt @@ -31,7 +31,7 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.phone.SystemUIDialogManager @@ -83,16 +83,21 @@ class UdfpsFingerprintViewModelTest : SysuiTestCase() { bouncerRepository = FakeKeyguardBouncerRepository() transitionRepository = FakeKeyguardTransitionRepository() shadeRepository = FakeShadeRepository() - val transitionInteractor = - KeyguardTransitionInteractor( - transitionRepository, - testScope.backgroundScope, - ) val keyguardInteractor = KeyguardInteractorFactory.create( + repository = keyguardRepository, featureFlags = featureFlags, ) .keyguardInteractor + + val transitionInteractor = + KeyguardTransitionInteractorFactory.create( + scope = testScope.backgroundScope, + repository = transitionRepository, + keyguardInteractor = keyguardInteractor, + ) + .keyguardTransitionInteractor + underTest = FingerprintViewModel( context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt index 0456824abfbc..edcaa1d65f49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt @@ -30,7 +30,7 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepos import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState @@ -98,15 +98,20 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() { bouncerRepository = it.bouncerRepository } + val transitionInteractor = + KeyguardTransitionInteractorFactory.create( + scope = testScope.backgroundScope, + repository = transitionRepository, + keyguardInteractor = keyguardInteractor, + ) + .keyguardTransitionInteractor + underTest = UdfpsLockscreenViewModel( context, lockscreenColorResId, alternateBouncerResId, - KeyguardTransitionInteractor( - transitionRepository, - testScope.backgroundScope, - ), + transitionInteractor, UdfpsKeyguardInteractor( configRepository, BurnInInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt index 5b8272b04bfb..ef51e474bf71 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.keyguard.TestScopeProvider import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector @@ -37,6 +38,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -66,7 +68,6 @@ import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before @@ -132,7 +133,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) - context.resources.configuration.locales = LocaleList(Locale.US, Locale.UK) + context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK)) transitionRepository = FakeKeyguardTransitionRepository() mediaCarouselController = MediaCarouselController( @@ -152,7 +153,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { debugLogger, mediaFlags, keyguardUpdateMonitor, - KeyguardTransitionInteractor(transitionRepository, TestScope().backgroundScope), + KeyguardTransitionInteractorFactory.create( + scope = TestScopeProvider.getTestScope().backgroundScope, + repository = transitionRepository, + ) + .keyguardTransitionInteractor, globalSettings ) verify(configurationController).addCallback(capture(configListener)) @@ -730,13 +735,13 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Test fun testOnLocaleListChanged_playersAreAddedBack() { - context.resources.configuration.locales = LocaleList(Locale.US, Locale.UK, Locale.CANADA) + context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK, Locale.CANADA)) testConfigurationChange(configListener.value::onLocaleListChanged) verify(pageIndicator, never()).tintList = ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) - context.resources.configuration.locales = LocaleList(Locale.UK, Locale.US, Locale.CANADA) + context.resources.configuration.setLocales(LocaleList(Locale.UK, Locale.US, Locale.CANADA)) testConfigurationChange(configListener.value::onLocaleListChanged) verify(pageIndicator).tintList = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index ab24c46825e4..db00e0927886 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -278,6 +278,21 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { } @Test + public void + whenNotBroadcasting_verifyLeBroadcastServiceCallBackIsUnregisteredIfProfileEnabled() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + mIsBroadcasting = true; + + mMediaOutputBaseDialogImpl.start(); + verify(mLocalBluetoothLeBroadcast).registerServiceCallBack(any(), any()); + + mIsBroadcasting = false; + mMediaOutputBaseDialogImpl.stop(); + verify(mLocalBluetoothLeBroadcast).unregisterServiceCallBack(any()); + } + + @Test public void refresh_checkStopText() { mStopText = "test_string"; mMediaOutputBaseDialogImpl.refresh(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index 3e69a29bd963..bfc8c83c1c2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -256,6 +256,30 @@ public class MediaOutputDialogTest extends SysuiTestCase { } @Test + public void isBroadcastSupported_noBleDeviceAndEnabledBroadcast_returnsTrue() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + when(mLocalBluetoothLeBroadcast.isEnabled(any())).thenReturn(true); + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true); + when(mMediaDevice.isBLEDevice()).thenReturn(false); + + assertThat(mMediaOutputDialog.isBroadcastSupported()).isTrue(); + } + + @Test + public void isBroadcastSupported_noBleDeviceAndDisabledBroadcast_returnsFalse() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + when(mLocalBluetoothLeBroadcast.isEnabled(any())).thenReturn(false); + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true); + when(mMediaDevice.isBLEDevice()).thenReturn(false); + + assertThat(mMediaOutputDialog.isBroadcastSupported()).isFalse(); + } + + @Test public void getBroadcastIconVisibility_isBroadcasting_returnVisible() { when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( mLocalBluetoothLeBroadcast); diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt index ee3b80ac932e..906420d03bed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt @@ -123,7 +123,7 @@ class TaskPreviewSizeProviderTest : SysuiTestCase() { private fun givenDisplay(width: Int, height: Int, isTablet: Boolean = false) { val bounds = Rect(0, 0, width, height) - val windowMetrics = WindowMetrics(bounds, null) + val windowMetrics = WindowMetrics(bounds, { null }, 1.0f) whenever(windowManager.maximumWindowMetrics).thenReturn(windowMetrics) whenever(windowManager.currentWindowMetrics).thenReturn(windowMetrics) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt index 3a74c7255cbb..7bd97ce2670c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -108,20 +108,6 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { } @Test - fun mediaProjectionState_onSessionSet_tokenNull_emitsEntireScreen() = - testScope.runTest { - val state by collectLastValue(repo.mediaProjectionState) - runCurrent() - - fakeMediaProjectionManager.dispatchOnSessionSet( - session = - ContentRecordingSession.createTaskSession(/* taskWindowContainerToken= */ null) - ) - - assertThat(state).isEqualTo(MediaProjectionState.EntireScreen) - } - - @Test fun mediaProjectionState_sessionSet_taskWithToken_noMatchingRunningTask_emitsEntireScreen() = testScope.runTest { val state by collectLastValue(repo.mediaProjectionState) diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index fab1de00dcbc..2d3dc585ac70 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -73,7 +73,7 @@ class BackPanelControllerTest : SysuiTestCase() { context, windowManager, ViewConfiguration.get(context), - Handler.createAsync(Looper.myLooper()), + Handler.createAsync(checkNotNull(Looper.myLooper())), vibratorHelper, configurationController, latencyTracker, 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 d933b57e8e15..1536c1737de6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -69,6 +69,7 @@ import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.Bubbles import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlin.test.assertNotNull import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -672,7 +673,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } iconCaptor.value?.let { icon -> - assertThat(icon).isNotNull() + assertNotNull(icon) assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) } } @@ -755,7 +756,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { assertThat(shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL) assertThat(longLabel).isEqualTo(NOTE_TASK_LONG_LABEL) assertThat(isLongLived).isEqualTo(true) - assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) + assertThat(icon?.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) assertThat(extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE)) .isEqualTo(NOTE_TASK_PACKAGE_NAME) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt new file mode 100644 index 000000000000..53c04ccbdb38 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -0,0 +1,538 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel +import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel +import com.android.systemui.model.SysUiState +import com.android.systemui.scene.SceneTestUtils.Companion.toDataLayer +import com.android.systemui.scene.domain.startable.SceneContainerStartable +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.settings.FakeDisplayTracker +import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +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 + +/** + * Integration test cases for the Scene Framework. + * + * **Principles** + * * All test cases here should be done from the perspective of the view-models of the system. + * * Focus on happy paths, let smaller unit tests focus on failure cases. + * * These are _integration_ tests and, as such, are larger and harder to maintain than unit tests. + * Therefore, when adding or modifying test cases, consider whether what you're testing is better + * covered by a more granular unit test. + * * Please reuse the helper methods in this class (for example, [putDeviceToSleep] or + * [emulateUserDrivenTransition]). + * * All tests start with the device locked and with a PIN auth method. The class offers useful + * methods like [setAuthMethod], [unlockDevice], [lockDevice], etc. to help you set up a starting + * state that makes more sense for your test case. + * * All helper methods in this class make assertions that are meant to make sure that they're only + * being used when the state is as required (e.g. cannot unlock an already unlocked device, cannot + * put to sleep a device that's already asleep, etc.). + */ +@SmallTest +@RunWith(JUnit4::class) +class SceneFrameworkIntegrationTest : SysuiTestCase() { + + private val utils = SceneTestUtils(this) + private val testScope = utils.testScope + + private val sceneContainerConfig = utils.fakeSceneContainerConfig() + private val sceneRepository = + utils.fakeSceneContainerRepository( + containerConfig = sceneContainerConfig, + ) + private val sceneInteractor = + utils.sceneInteractor( + repository = sceneRepository, + ) + + private val authenticationRepository = utils.authenticationRepository() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = authenticationRepository, + sceneInteractor = sceneInteractor, + ) + + private val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(sceneContainerConfig.initialSceneKey) + ) + private val sceneContainerViewModel = + SceneContainerViewModel( + interactor = sceneInteractor, + ) + .apply { setTransitionState(transitionState) } + + private val bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ) + private val bouncerViewModel = + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, + authenticationInteractor = authenticationInteractor, + ) + + private val lockscreenSceneViewModel = + LockscreenSceneViewModel( + applicationScope = testScope.backgroundScope, + authenticationInteractor = authenticationInteractor, + bouncerInteractor = bouncerInteractor, + ) + + private val shadeSceneViewModel = + ShadeSceneViewModel( + applicationScope = testScope.backgroundScope, + authenticationInteractor = authenticationInteractor, + bouncerInteractor = bouncerInteractor, + ) + + private val keyguardRepository = utils.keyguardRepository() + private val keyguardInteractor = + utils.keyguardInteractor( + repository = keyguardRepository, + ) + + @Before + fun setUp() { + val featureFlags = FakeFeatureFlags().apply { set(Flags.SCENE_CONTAINER, true) } + + authenticationRepository.setUnlocked(false) + + val displayTracker = FakeDisplayTracker(context) + val sysUiState = SysUiState(displayTracker) + val startable = + SceneContainerStartable( + applicationScope = testScope.backgroundScope, + sceneInteractor = sceneInteractor, + authenticationInteractor = authenticationInteractor, + keyguardInteractor = keyguardInteractor, + featureFlags = featureFlags, + sysUiState = sysUiState, + displayId = displayTracker.defaultDisplayId, + sceneLogger = mock(), + ) + startable.start() + + assertWithMessage("Initial scene key mismatch!") + .that(sceneContainerViewModel.currentScene.value.key) + .isEqualTo(sceneContainerConfig.initialSceneKey) + assertWithMessage("Initial scene container visibility mismatch!") + .that(sceneContainerViewModel.isVisible.value) + .isTrue() + } + + @Test + fun clickLockButtonAndEnterCorrectPin_unlocksDevice() = + testScope.runTest { + lockscreenSceneViewModel.onLockButtonClicked() + assertCurrentScene(SceneKey.Bouncer) + emulateUiSceneTransition() + + enterPin() + assertCurrentScene(SceneKey.Gone) + emulateUiSceneTransition( + expectedVisible = false, + ) + } + + @Test + fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() = + testScope.runTest { + val upDestinationSceneKey by + collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Bouncer) + emulateUserDrivenTransition( + to = upDestinationSceneKey, + ) + + enterPin() + assertCurrentScene(SceneKey.Gone) + emulateUiSceneTransition( + expectedVisible = false, + ) + } + + @Test + fun swipeUpOnLockscreen_withAuthMethodSwipe_dismissesLockscreen() = + testScope.runTest { + setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe) + + val upDestinationSceneKey by + collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Gone) + emulateUserDrivenTransition( + to = upDestinationSceneKey, + ) + } + + @Test + fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = + testScope.runTest { + val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey) + setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe) + assertCurrentScene(SceneKey.Lockscreen) + + // Emulate a user swipe to the shade scene. + emulateUserDrivenTransition(to = SceneKey.Shade) + assertCurrentScene(SceneKey.Shade) + + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Lockscreen) + emulateUserDrivenTransition( + to = upDestinationSceneKey, + ) + } + + @Test + fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() = + testScope.runTest { + val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey) + setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe) + assertCurrentScene(SceneKey.Lockscreen) + + // Emulate a user swipe to dismiss the lockscreen. + emulateUserDrivenTransition(to = SceneKey.Gone) + assertCurrentScene(SceneKey.Gone) + + // Emulate a user swipe to the shade scene. + emulateUserDrivenTransition(to = SceneKey.Shade) + assertCurrentScene(SceneKey.Shade) + + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Gone) + emulateUserDrivenTransition( + to = upDestinationSceneKey, + ) + } + + @Test + fun withAuthMethodNone_deviceWakeUp_skipsLockscreen() = + testScope.runTest { + setAuthMethod(AuthenticationMethodModel.None) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + + wakeUpDevice() + assertCurrentScene(SceneKey.Gone) + } + + @Test + fun withAuthMethodSwipe_deviceWakeUp_doesNotSkipLockscreen() = + testScope.runTest { + setAuthMethod(AuthenticationMethodModel.Swipe) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + + wakeUpDevice() + assertCurrentScene(SceneKey.Lockscreen) + } + + @Test + fun deviceGoesToSleep_switchesToLockscreen() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + + putDeviceToSleep() + assertCurrentScene(SceneKey.Lockscreen) + } + + @Test + fun deviceGoesToSleep_wakeUp_unlock() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + putDeviceToSleep() + assertCurrentScene(SceneKey.Lockscreen) + wakeUpDevice() + assertCurrentScene(SceneKey.Lockscreen) + + unlockDevice() + assertCurrentScene(SceneKey.Gone) + } + + @Test + fun deviceWakesUpWhileUnlocked_dismissesLockscreen() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + wakeUpDevice() + assertCurrentScene(SceneKey.Gone) + } + + @Test + fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() = + testScope.runTest { + unlockDevice() + val upDestinationSceneKey by + collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test + fun deviceGoesToSleep_withLockTimeout_staysOnLockscreen() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + + // Pretend like the timeout elapsed and now lock the device. + lockDevice() + assertCurrentScene(SceneKey.Lockscreen) + } + + /** + * Asserts that the current scene in the view-model matches what's expected. + * + * Note that this doesn't assert what the current scene is in the UI. + */ + private fun TestScope.assertCurrentScene(expected: SceneKey) { + runCurrent() + assertWithMessage("Current scene mismatch!") + .that(sceneContainerViewModel.currentScene.value.key) + .isEqualTo(expected) + } + + /** + * Returns the [SceneKey] of the current scene as displayed in the UI. + * + * This can be different than the value in [SceneContainerViewModel.currentScene], by design, as + * the UI must gradually transition between scenes. + */ + private fun getCurrentSceneInUi(): SceneKey { + return when (val state = transitionState.value) { + is ObservableTransitionState.Idle -> state.scene + is ObservableTransitionState.Transition -> state.fromScene + } + } + + /** Updates the current authentication method and related states in the data layer. */ + private fun TestScope.setAuthMethod( + authMethod: DomainLayerAuthenticationMethodModel, + ) { + // Set the lockscreen enabled bit _before_ set the auth method as the code picks up on the + // lockscreen enabled bit _after_ the auth method is changed and the lockscreen enabled bit + // is not an observable that can trigger a new evaluation. + authenticationRepository.setLockscreenEnabled(authMethod !is AuthenticationMethodModel.None) + authenticationRepository.setAuthenticationMethod(authMethod.toDataLayer()) + if (!authMethod.isSecure) { + // When the auth method is not secure, the device is never considered locked. + authenticationRepository.setUnlocked(true) + } + runCurrent() + } + + /** + * Emulates a complete transition in the UI from whatever the current scene is in the UI to + * whatever the current scene should be, based on the value in + * [SceneContainerViewModel.onSceneChanged]. + * + * This should post a series of values into [transitionState] to emulate a gradual scene + * transition and culminate with a call to [SceneContainerViewModel.onSceneChanged]. + * + * The method asserts that a transition is actually required. E.g. it will fail if the current + * scene in [transitionState] is already caught up with the scene in + * [SceneContainerViewModel.currentScene]. + * + * @param expectedVisible Whether [SceneContainerViewModel.isVisible] should be set at the end + * of the UI transition. + */ + private fun TestScope.emulateUiSceneTransition( + expectedVisible: Boolean = true, + ) { + val to = sceneContainerViewModel.currentScene.value + val from = getCurrentSceneInUi() + assertWithMessage("Cannot transition to ${to.key} as the UI is already on that scene!") + .that(to.key) + .isNotEqualTo(from) + + // Begin to transition. + val progressFlow = MutableStateFlow(0f) + transitionState.value = + ObservableTransitionState.Transition( + fromScene = getCurrentSceneInUi(), + toScene = to.key, + progress = progressFlow, + ) + runCurrent() + + // Report progress of transition. + while (progressFlow.value < 1f) { + progressFlow.value += 0.2f + runCurrent() + } + + // End the transition and report the change. + transitionState.value = ObservableTransitionState.Idle(to.key) + + sceneContainerViewModel.onSceneChanged(to) + runCurrent() + + assertWithMessage("Visibility mismatch after scene transition from $from to ${to.key}!") + .that(sceneContainerViewModel.isVisible.value) + .isEqualTo(expectedVisible) + } + + /** + * Emulates a fire-and-forget user action (a fling or back, not a pointer-tracking swipe) that + * causes a scene change to the [to] scene. + * + * This also includes the emulation of the resulting UI transition that culminates with the UI + * catching up with the requested scene change (see [emulateUiSceneTransition]). + * + * @param to The scene to transition to. + */ + private fun TestScope.emulateUserDrivenTransition( + to: SceneKey?, + ) { + checkNotNull(to) + + sceneInteractor.changeScene(SceneModel(to), "reason") + assertThat(sceneContainerViewModel.currentScene.value.key).isEqualTo(to) + + emulateUiSceneTransition( + expectedVisible = to != SceneKey.Gone, + ) + } + + /** + * Locks the device immediately (without delay). + * + * Asserts the device to be lockable (e.g. that the current authentication is secure). + * + * Not to be confused with [putDeviceToSleep], which may also instantly lock the device. + */ + private suspend fun TestScope.lockDevice() { + val authMethod = authenticationInteractor.getAuthenticationMethod() + assertWithMessage("The authentication method of $authMethod is not secure, cannot lock!") + .that(authMethod.isSecure) + .isTrue() + + authenticationRepository.setUnlocked(false) + runCurrent() + } + + /** Unlocks the device by entering the correct PIN. Ends up in the Gone scene. */ + private fun TestScope.unlockDevice() { + assertWithMessage("Cannot unlock a device that's already unlocked!") + .that(authenticationInteractor.isUnlocked.value) + .isFalse() + + lockscreenSceneViewModel.onLockButtonClicked() + runCurrent() + emulateUiSceneTransition() + + enterPin() + emulateUiSceneTransition( + expectedVisible = false, + ) + } + + /** + * Enters the correct PIN in the bouncer UI. + * + * Asserts that the current scene is [SceneKey.Bouncer] and that the current bouncer UI is a PIN + * before proceeding. + * + * Does not assert that the device is locked or unlocked. + */ + private fun TestScope.enterPin() { + assertWithMessage("Cannot enter PIN when not on the Bouncer scene!") + .that(getCurrentSceneInUi()) + .isEqualTo(SceneKey.Bouncer) + val authMethodViewModel by collectLastValue(bouncerViewModel.authMethod) + assertWithMessage("Cannot enter PIN when not using a PIN authentication method!") + .that(authMethodViewModel) + .isInstanceOf(PinBouncerViewModel::class.java) + + val pinBouncerViewModel = authMethodViewModel as PinBouncerViewModel + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + pinBouncerViewModel.onPinButtonClicked(digit) + } + pinBouncerViewModel.onAuthenticateButtonClicked() + runCurrent() + } + + /** Changes device wakefulness state from asleep to awake, going through intermediary states. */ + private fun TestScope.wakeUpDevice() { + val wakefulnessModel = keyguardRepository.wakefulness.value + assertWithMessage("Cannot wake up device as it's already awake!") + .that(wakefulnessModel.isStartingToWakeOrAwake()) + .isFalse() + + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.STARTING_TO_WAKE) + ) + runCurrent() + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.AWAKE) + ) + runCurrent() + } + + /** Changes device wakefulness state from awake to asleep, going through intermediary states. */ + private suspend fun TestScope.putDeviceToSleep( + instantlyLockDevice: Boolean = true, + ) { + val wakefulnessModel = keyguardRepository.wakefulness.value + assertWithMessage("Cannot put device to sleep as it's already asleep!") + .that(wakefulnessModel.isStartingToWakeOrAwake()) + .isTrue() + + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.STARTING_TO_SLEEP) + ) + runCurrent() + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.ASLEEP) + ) + runCurrent() + + if (instantlyLockDevice) { + lockDevice() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 0a93a7ca465f..16cc924b5754 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -28,9 +28,6 @@ import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -105,91 +102,50 @@ class SceneInteractorTest : SysuiTestCase() { } @Test - fun isVisible() = - testScope.runTest { - val isVisible by collectLastValue(underTest.isVisible) - assertThat(isVisible).isTrue() - - underTest.setVisible(false, "reason") - assertThat(isVisible).isFalse() - - underTest.setVisible(true, "reason") - assertThat(isVisible).isTrue() - } - - @Test - fun finishedSceneTransitions() = + fun transitioningTo() = testScope.runTest { val transitionState = MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(SceneKey.Lockscreen) + ObservableTransitionState.Idle(underTest.desiredScene.value.key) ) underTest.setTransitionState(transitionState) - var transitionCount = 0 - val job = launch { - underTest - .finishedSceneTransitions( - from = SceneKey.Shade, - to = SceneKey.QuickSettings, - ) - .collect { transitionCount++ } - } - assertThat(transitionCount).isEqualTo(0) + val transitionTo by collectLastValue(underTest.transitioningTo) + assertThat(transitionTo).isNull() underTest.changeScene(SceneModel(SceneKey.Shade), "reason") + assertThat(transitionTo).isNull() + + val progress = MutableStateFlow(0f) transitionState.value = ObservableTransitionState.Transition( - fromScene = SceneKey.Lockscreen, + fromScene = underTest.desiredScene.value.key, toScene = SceneKey.Shade, - progress = flowOf(0.5f), + progress = progress, ) - runCurrent() - underTest.onSceneChanged(SceneModel(SceneKey.Shade), "reason") - transitionState.value = ObservableTransitionState.Idle(SceneKey.Shade) - runCurrent() - assertThat(transitionCount).isEqualTo(0) + assertThat(transitionTo).isEqualTo(SceneKey.Shade) - underTest.changeScene(SceneModel(SceneKey.QuickSettings), "reason") - transitionState.value = - ObservableTransitionState.Transition( - fromScene = SceneKey.Shade, - toScene = SceneKey.QuickSettings, - progress = flowOf(0.5f), - ) - runCurrent() - underTest.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason") - transitionState.value = ObservableTransitionState.Idle(SceneKey.QuickSettings) - runCurrent() - assertThat(transitionCount).isEqualTo(1) + progress.value = 0.5f + assertThat(transitionTo).isEqualTo(SceneKey.Shade) + + progress.value = 1f + assertThat(transitionTo).isEqualTo(SceneKey.Shade) - underTest.changeScene(SceneModel(SceneKey.Shade), "reason") - transitionState.value = - ObservableTransitionState.Transition( - fromScene = SceneKey.QuickSettings, - toScene = SceneKey.Shade, - progress = flowOf(0.5f), - ) - runCurrent() - underTest.onSceneChanged(SceneModel(SceneKey.Shade), "reason") transitionState.value = ObservableTransitionState.Idle(SceneKey.Shade) - runCurrent() - assertThat(transitionCount).isEqualTo(1) + assertThat(transitionTo).isNull() + } - underTest.changeScene(SceneModel(SceneKey.QuickSettings), "reason") - transitionState.value = - ObservableTransitionState.Transition( - fromScene = SceneKey.Shade, - toScene = SceneKey.QuickSettings, - progress = flowOf(0.5f), - ) - runCurrent() - underTest.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason") - transitionState.value = ObservableTransitionState.Idle(SceneKey.QuickSettings) - runCurrent() - assertThat(transitionCount).isEqualTo(2) + @Test + fun isVisible() = + testScope.runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() - job.cancel() + underTest.setVisible(false, "reason") + assertThat(isVisible).isFalse() + + underTest.setVisible(true, "reason") + assertThat(isVisible).isTrue() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 45db7a0b17f1..951cadd7664e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -21,7 +21,7 @@ package com.android.systemui.scene.domain.startable import android.view.Display import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.model.AuthenticationMethodModel +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.WakeSleepReason @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.model.SysUiState import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.toDataLayer import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -80,14 +81,13 @@ class SceneContainerStartableTest : SysuiTestCase() { ) @Test - fun hydrateVisibility_featureEnabled() = + fun hydrateVisibility() = testScope.runTest { val currentDesiredSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) val isVisible by collectLastValue(sceneInteractor.isVisible) val transitionStateFlow = prepareState( - isFeatureEnabled = true, isDeviceUnlocked = true, initialSceneKey = SceneKey.Gone, ) @@ -123,44 +123,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun hydrateVisibility_featureDisabled() = - testScope.runTest { - val currentDesiredSceneKey by - collectLastValue(sceneInteractor.desiredScene.map { it.key }) - val isVisible by collectLastValue(sceneInteractor.isVisible) - val transitionStateFlow = - prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = true, - initialSceneKey = SceneKey.Gone, - ) - assertThat(currentDesiredSceneKey).isEqualTo(SceneKey.Gone) - assertThat(isVisible).isTrue() - - underTest.start() - - assertThat(isVisible).isTrue() - - sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason") - transitionStateFlow.value = - ObservableTransitionState.Transition( - fromScene = SceneKey.Gone, - toScene = SceneKey.Shade, - progress = flowOf(0.5f), - ) - assertThat(isVisible).isTrue() - - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") - transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Shade) - assertThat(isVisible).isTrue() - } - - @Test - fun switchToLockscreenWhenDeviceLocks_featureEnabled() = + fun switchToLockscreenWhenDeviceLocks() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, isDeviceUnlocked = true, initialSceneKey = SceneKey.Gone, ) @@ -173,28 +139,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToLockscreenWhenDeviceLocks_featureDisabled() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = false, - initialSceneKey = SceneKey.Gone, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) - underTest.start() - - authenticationRepository.setUnlocked(false) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) - } - - @Test - fun switchFromBouncerToGoneWhenDeviceUnlocked_featureEnabled() = + fun switchFromBouncerToGoneWhenDeviceUnlocked() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, isDeviceUnlocked = false, initialSceneKey = SceneKey.Bouncer, ) @@ -207,28 +155,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchFromBouncerToGoneWhenDeviceUnlocked_featureDisabled() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = false, - initialSceneKey = SceneKey.Bouncer, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) - underTest.start() - - authenticationRepository.setUnlocked(true) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) - } - - @Test - fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOn_bypassOn() = + fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, isBypassEnabled = true, initialSceneKey = SceneKey.Lockscreen, ) @@ -241,11 +171,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOn_bypassOff() = + fun stayOnLockscreenWhenDeviceUnlocksWithBypassOff() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, isBypassEnabled = false, initialSceneKey = SceneKey.Lockscreen, ) @@ -258,28 +187,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOff_bypassOn() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = false, - isBypassEnabled = true, - initialSceneKey = SceneKey.Lockscreen, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) - underTest.start() - - authenticationRepository.setUnlocked(true) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) - } - - @Test - fun switchToLockscreenWhenDeviceSleepsLocked_featureEnabled() = + fun switchToLockscreenWhenDeviceSleepsLocked() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, isDeviceUnlocked = false, initialSceneKey = SceneKey.Shade, ) @@ -292,23 +203,6 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToLockscreenWhenDeviceSleepsLocked_featureDisabled() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = false, - initialSceneKey = SceneKey.Shade, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) - underTest.start() - - keyguardRepository.setWakefulnessModel(STARTING_TO_SLEEP) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) - } - - @Test fun hydrateSystemUiState() = testScope.runTest { val transitionStateFlow = prepareState() @@ -339,11 +233,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone_featureEnabled() = + fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.None, ) @@ -356,13 +249,12 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNotNone_featureEnabled() = + fun stayOnLockscreenWhenDeviceStartsToWakeUp_authMethodSwipe() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, initialSceneKey = SceneKey.Lockscreen, - authenticationMethod = AuthenticationMethodModel.Pin, + authenticationMethod = AuthenticationMethodModel.Swipe, ) assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) underTest.start() @@ -373,13 +265,12 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone_featureDisabled() = + fun doesNotSwitchToGoneWhenDeviceStartsToWakeUp_authMethodSecure() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = false, initialSceneKey = SceneKey.Lockscreen, - authenticationMethod = AuthenticationMethodModel.None, + authenticationMethod = AuthenticationMethodModel.Pin, ) assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) underTest.start() @@ -389,14 +280,32 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) } + @Test + fun switchToGoneWhenDeviceStartsToWakeUp_authMethodSecure_deviceUnlocked() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + prepareState( + initialSceneKey = SceneKey.Lockscreen, + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + underTest.start() + + authenticationRepository.setUnlocked(true) + runCurrent() + keyguardRepository.setWakefulnessModel(STARTING_TO_WAKE) + + assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) + } + private fun prepareState( - isFeatureEnabled: Boolean = true, isDeviceUnlocked: Boolean = false, isBypassEnabled: Boolean = false, initialSceneKey: SceneKey? = null, authenticationMethod: AuthenticationMethodModel? = null, ): MutableStateFlow<ObservableTransitionState> { - featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled) + featureFlags.set(Flags.SCENE_CONTAINER, true) authenticationRepository.setUnlocked(isDeviceUnlocked) keyguardRepository.setBypassEnabled(isBypassEnabled) val transitionStateFlow = @@ -410,7 +319,7 @@ class SceneContainerStartableTest : SysuiTestCase() { sceneInteractor.onSceneChanged(SceneModel(it), "reason") } authenticationMethod?.let { - authenticationRepository.setAuthenticationMethod(authenticationMethod) + authenticationRepository.setAuthenticationMethod(authenticationMethod.toDataLayer()) authenticationRepository.setLockscreenEnabled( authenticationMethod != AuthenticationMethodModel.None ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt index 07feedf1d654..ad6909d71ddd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt @@ -126,6 +126,7 @@ class ScreenRecordPermissionDialogTest : SysuiTestCase() { private fun onSpinnerItemSelected(position: Int) { val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) - spinner.onItemSelectedListener.onItemSelected(spinner, mock(), position, /* id= */ 0) + checkNotNull(spinner.onItemSelectedListener) + .onItemSelected(spinner, mock(), position, /* id= */ 0) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 7b3e89dc0017..1edeeffe5217 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -95,7 +95,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock private lateinit var notificationShadeDepthController: NotificationShadeDepthController @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController - @Mock private lateinit var shadeController: ShadeController + @Mock private lateinit var shadeLogger: ShadeLogger @Mock private lateinit var ambientState: AmbientState @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController @@ -150,44 +150,45 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { testScope = TestScope() underTest = NotificationShadeWindowViewController( - lockscreenShadeTransitionController, - FalsingCollectorFake(), - sysuiStatusBarStateController, - dockManager, - notificationShadeDepthController, - view, - notificationPanelViewController, - ShadeExpansionStateManager(), - stackScrollLayoutController, - statusBarKeyguardViewManager, - statusBarWindowStateController, - lockIconViewController, - centralSurfaces, - backActionInteractor, - powerInteractor, - notificationShadeWindowController, - unfoldTransitionProgressProvider, - keyguardUnlockAnimationController, - notificationInsetsController, - ambientState, - pulsingGestureListener, - mLockscreenHostedDreamGestureListener, - keyguardBouncerViewModel, - keyguardBouncerComponentFactory, - mock(KeyguardMessageAreaController.Factory::class.java), - keyguardTransitionInteractor, - primaryBouncerToGoneTransitionViewModel, - notificationExpansionRepository, - featureFlags, - FakeSystemClock(), - BouncerMessageInteractor( - FakeBouncerMessageRepository(), - mock(BouncerMessageFactory::class.java), - FakeUserRepository(), - CountDownTimerUtil(), - featureFlags - ), - BouncerLogger(logcatLogBuffer("BouncerLog")) + lockscreenShadeTransitionController, + FalsingCollectorFake(), + sysuiStatusBarStateController, + dockManager, + notificationShadeDepthController, + view, + notificationPanelViewController, + ShadeExpansionStateManager(), + stackScrollLayoutController, + statusBarKeyguardViewManager, + statusBarWindowStateController, + lockIconViewController, + centralSurfaces, + backActionInteractor, + powerInteractor, + notificationShadeWindowController, + unfoldTransitionProgressProvider, + keyguardUnlockAnimationController, + notificationInsetsController, + ambientState, + shadeLogger, + pulsingGestureListener, + mLockscreenHostedDreamGestureListener, + keyguardBouncerViewModel, + keyguardBouncerComponentFactory, + mock(KeyguardMessageAreaController.Factory::class.java), + keyguardTransitionInteractor, + primaryBouncerToGoneTransitionViewModel, + notificationExpansionRepository, + featureFlags, + FakeSystemClock(), + BouncerMessageInteractor( + FakeBouncerMessageRepository(), + mock(BouncerMessageFactory::class.java), + FakeUserRepository(), + CountDownTimerUtil(), + featureFlags + ), + BouncerLogger(logcatLogBuffer("BouncerLog")) ) underTest.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 5c3ce71e33d2..829184c4f05a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -105,6 +105,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { @Mock private lateinit var lockIconViewController: LockIconViewController @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController @Mock private lateinit var ambientState: AmbientState + @Mock private lateinit var shadeLogger: ShadeLogger @Mock private lateinit var pulsingGestureListener: PulsingGestureListener @Mock private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener @@ -179,6 +180,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { keyguardUnlockAnimationController, notificationInsetsController, ambientState, + shadeLogger, pulsingGestureListener, mLockscreenHostedDreamGestureListener, keyguardBouncerViewModel, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 112a09bcfe62..577b6e0bc58a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -584,7 +584,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { private fun emptyInsets() = mock(WindowInsets::class.java) private fun WindowInsets.withCutout(): WindowInsets { - whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT) + whenever(checkNotNull(displayCutout).safeInsetBottom).thenReturn(CUTOUT_HEIGHT) return this } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index 8d3c4b21aa26..405199ed4ecb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -567,7 +567,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { private fun emptyInsets() = mock(WindowInsets::class.java) private fun WindowInsets.withCutout(): WindowInsets { - whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT) + whenever(checkNotNull(displayCutout).safeInsetBottom).thenReturn(CUTOUT_HEIGHT) return this } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt index 52e0c9c9936b..6a14a005b0f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt @@ -22,6 +22,7 @@ import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.assist.AssistManager +import com.android.systemui.log.LogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController @@ -59,6 +60,7 @@ class ShadeControllerImplTest : SysuiTestCase() { @Mock private lateinit var shadeViewController: ShadeViewController @Mock private lateinit var nswvc: NotificationShadeWindowViewController @Mock private lateinit var display: Display + @Mock private lateinit var touchLog: LogBuffer private lateinit var shadeController: ShadeControllerImpl @@ -71,6 +73,7 @@ class ShadeControllerImplTest : SysuiTestCase() { ShadeControllerImpl( commandQueue, FakeExecutor(FakeSystemClock()), + touchLog, keyguardStateController, statusBarStateController, statusBarKeyguardViewManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt index 2501f85798aa..8f8b840b03c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt @@ -138,19 +138,19 @@ class ShadeHeaderControllerTest : SysuiTestCase() { @Before fun setup() { - whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock) + whenever<Clock>(view.requireViewById(R.id.clock)).thenReturn(clock) whenever(clock.context).thenReturn(mockedContext) - whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date) + whenever<TextView>(view.requireViewById(R.id.date)).thenReturn(date) whenever(date.context).thenReturn(mockedContext) - whenever<ShadeCarrierGroup>(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup) + whenever<ShadeCarrierGroup>(view.requireViewById(R.id.carrier_group)).thenReturn(carrierGroup) - whenever<BatteryMeterView>(view.findViewById(R.id.batteryRemainingIcon)) + whenever<BatteryMeterView>(view.requireViewById(R.id.batteryRemainingIcon)) .thenReturn(batteryMeterView) - whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons) - whenever<View>(view.findViewById(R.id.shade_header_system_icons)).thenReturn(systemIcons) + whenever<StatusIconContainer>(view.requireViewById(R.id.statusIcons)).thenReturn(statusIcons) + whenever<View>(view.requireViewById(R.id.shade_header_system_icons)).thenReturn(systemIcons) viewContext = Mockito.spy(context) whenever(view.context).thenReturn(viewContext) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 7443097a2628..69b952542c29 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -42,6 +42,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { private val authenticationInteractor = utils.authenticationInteractor( repository = utils.authenticationRepository(), + sceneInteractor = sceneInteractor, ) private val underTest = @@ -76,6 +77,30 @@ class ShadeSceneViewModelTest : SysuiTestCase() { } @Test + fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + utils.authenticationRepository.setLockscreenEnabled(true) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen) + } + + @Test + fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + utils.authenticationRepository.setLockscreenEnabled(true) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.desiredScene) diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt index 58b44ae5bcbd..19dc72d6c2e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -236,7 +236,8 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { `when`(precondition.conditionsMet()).thenReturn(true) // Given a session is created - val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView) + val weatherView = + checkNotNull(controller.buildAndConnectWeatherView(fakeParent, customView)) controller.stateChangeListener.onViewAttachedToWindow(weatherView) verify(smartspaceManager).createSmartspaceSession(any()) @@ -258,7 +259,8 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { // Given a session is created val customView = Mockito.mock(TestView::class.java) - val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView) + val weatherView = + checkNotNull(controller.buildAndConnectWeatherView(fakeParent, customView)) controller.stateChangeListener.onViewAttachedToWindow(weatherView) verify(smartspaceManager).createSmartspaceSession(any()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java index b6da20fed380..280897d8bd75 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java @@ -17,21 +17,19 @@ package com.android.systemui.statusbar; -import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED; - import static com.android.systemui.dump.LogBufferHelperKt.logcatLogBuffer; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import android.app.ActivityManager; import android.app.Notification; -import android.app.PendingIntent; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; @@ -67,17 +65,15 @@ public class AlertingNotificationManagerTest extends SysuiTestCase { private static final String TEST_PACKAGE_NAME = "test"; private static final int TEST_UID = 0; - protected static final int TEST_MINIMUM_DISPLAY_TIME = 200; - protected static final int TEST_STICKY_DISPLAY_TIME = 1000; - protected static final int TEST_AUTO_DISMISS_TIME = 500; + protected static final int TEST_MINIMUM_DISPLAY_TIME = 400; + protected static final int TEST_AUTO_DISMISS_TIME = 600; + protected static final int TEST_STICKY_AUTO_DISMISS_TIME = 800; // Number of notifications to use in tests requiring multiple notifications private static final int TEST_NUM_NOTIFICATIONS = 4; - protected static final int TEST_TIMEOUT_TIME = 15000; + protected static final int TEST_TIMEOUT_TIME = 2_000; protected final Runnable mTestTimeoutRunnable = () -> mTimedOut = true; - protected NotificationEntry mEntry; protected Handler mTestHandler; - private StatusBarNotification mSbn; protected boolean mTimedOut = false; @Mock protected ExpandableNotificationRow mRow; @@ -88,8 +84,8 @@ public class AlertingNotificationManagerTest extends SysuiTestCase { private TestableAlertingNotificationManager(Handler handler) { super(new HeadsUpManagerLogger(logcatLogBuffer()), handler); mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME; - mStickyDisplayTime = TEST_STICKY_DISPLAY_TIME; mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME; + mStickyDisplayTime = TEST_STICKY_AUTO_DISMISS_TIME; } @Override @@ -114,7 +110,7 @@ public class AlertingNotificationManagerTest extends SysuiTestCase { return new TestableAlertingNotificationManager(mTestHandler); } - protected StatusBarNotification createNewSbn(int id, Notification n) { + protected StatusBarNotification createSbn(int id, Notification n) { return new StatusBarNotification( TEST_PACKAGE_NAME /* pkg */, TEST_PACKAGE_NAME, @@ -128,45 +124,53 @@ public class AlertingNotificationManagerTest extends SysuiTestCase { 0 /* postTime */); } - protected StatusBarNotification createNewSbn(int id, Notification.Builder n) { - return new StatusBarNotification( - TEST_PACKAGE_NAME /* pkg */, - TEST_PACKAGE_NAME, - id, - null /* tag */, - TEST_UID, - 0 /* initialPid */, - n.build(), - new UserHandle(ActivityManager.getCurrentUser()), - null /* overrideGroupKey */, - 0 /* postTime */); + protected StatusBarNotification createSbn(int id, Notification.Builder n) { + return createSbn(id, n.build()); } - protected StatusBarNotification createNewNotification(int id) { - Notification.Builder n = new Notification.Builder(mContext, "") + protected StatusBarNotification createSbn(int id) { + final Notification.Builder b = new Notification.Builder(mContext, "") .setSmallIcon(R.drawable.ic_person) .setContentTitle("Title") .setContentText("Text"); - return createNewSbn(id, n); + return createSbn(id, b); } - protected StatusBarNotification createStickySbn(int id) { - Notification stickyHun = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setFullScreenIntent(mock(PendingIntent.class), /* highPriority */ true) - .build(); - stickyHun.flags |= FLAG_FSI_REQUESTED_BUT_DENIED; - return createNewSbn(id, stickyHun); + protected NotificationEntry createEntry(int id, Notification n) { + return new NotificationEntryBuilder().setSbn(createSbn(id, n)).build(); + } + + protected NotificationEntry createEntry(int id) { + return new NotificationEntryBuilder().setSbn(createSbn(id)).build(); + } + + protected void verifyAlertingAtTime(AlertingNotificationManager anm, NotificationEntry entry, + boolean shouldBeAlerting, int whenToCheckAlertingMillis, String whenCondition) { + final Boolean[] wasAlerting = {null}; + final Runnable checkAlerting = + () -> wasAlerting[0] = anm.isAlerting(entry.getKey()); + + mTestHandler.postDelayed(checkAlerting, whenToCheckAlertingMillis); + mTestHandler.postDelayed(mTestTimeoutRunnable, TEST_TIMEOUT_TIME); + TestableLooper.get(this).processMessages(2); + + assertFalse("Test timed out", mTimedOut); + if (shouldBeAlerting) { + assertTrue("Should still be alerting after " + whenCondition, wasAlerting[0]); + } else { + assertFalse("Should not still be alerting after " + whenCondition, wasAlerting[0]); + } + assertFalse("Should not still be alerting after processing", + anm.isAlerting(entry.getKey())); } @Before public void setUp() { mTestHandler = Handler.createAsync(Looper.myLooper()); - mSbn = createNewNotification(0 /* id */); - mEntry = new NotificationEntryBuilder() - .setSbn(mSbn) - .build(); - mEntry.setRow(mRow); + + assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME); + assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME); + assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_TIMEOUT_TIME); } @After @@ -176,59 +180,64 @@ public class AlertingNotificationManagerTest extends SysuiTestCase { @Test public void testShowNotification_addsEntry() { - AlertingNotificationManager alm = createAlertingNotificationManager(); + final AlertingNotificationManager alm = createAlertingNotificationManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); - alm.showNotification(mEntry); + alm.showNotification(entry); - assertTrue(alm.isAlerting(mEntry.getKey())); + assertTrue(alm.isAlerting(entry.getKey())); assertTrue(alm.hasNotifications()); - assertEquals(mEntry, alm.getEntry(mEntry.getKey())); + assertEquals(entry, alm.getEntry(entry.getKey())); } @Test public void testShowNotification_autoDismisses() { - AlertingNotificationManager alm = createAlertingNotificationManager(); + final AlertingNotificationManager alm = createAlertingNotificationManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); - alm.showNotification(mEntry); - mTestHandler.postDelayed(mTestTimeoutRunnable, TEST_TIMEOUT_TIME); + alm.showNotification(entry); - // Wait for remove runnable and then process it immediately - TestableLooper.get(this).processMessages(1); + verifyAlertingAtTime(alm, entry, false, TEST_AUTO_DISMISS_TIME * 3 / 2, + "auto dismiss time"); - assertFalse("Test timed out", mTimedOut); - assertFalse(alm.isAlerting(mEntry.getKey())); + assertFalse(alm.isAlerting(entry.getKey())); } @Test public void testRemoveNotification_removeDeferred() { - AlertingNotificationManager alm = createAlertingNotificationManager(); - alm.showNotification(mEntry); + final AlertingNotificationManager alm = createAlertingNotificationManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); // Try to remove but defer, since the notification has not been shown long enough. - alm.removeNotification(mEntry.getKey(), false /* releaseImmediately */); + final boolean removedImmediately = alm.removeNotification(entry.getKey(), + false /* releaseImmediately */); - assertTrue(alm.isAlerting(mEntry.getKey())); + assertFalse(removedImmediately); + assertTrue(alm.isAlerting(entry.getKey())); } @Test public void testRemoveNotification_forceRemove() { - AlertingNotificationManager alm = createAlertingNotificationManager(); - alm.showNotification(mEntry); + final AlertingNotificationManager alm = createAlertingNotificationManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); // Remove forcibly with releaseImmediately = true. - alm.removeNotification(mEntry.getKey(), true /* releaseImmediately */); + final boolean removedImmediately = alm.removeNotification(entry.getKey(), + true /* releaseImmediately */); - assertFalse(alm.isAlerting(mEntry.getKey())); + assertTrue(removedImmediately); + assertFalse(alm.isAlerting(entry.getKey())); } @Test public void testReleaseAllImmediately() { - AlertingNotificationManager alm = createAlertingNotificationManager(); + final AlertingNotificationManager alm = createAlertingNotificationManager(); for (int i = 0; i < TEST_NUM_NOTIFICATIONS; i++) { - StatusBarNotification sbn = createNewNotification(i); - NotificationEntry entry = new NotificationEntryBuilder() - .setSbn(sbn) - .build(); + final NotificationEntry entry = createEntry(i); entry.setRow(mRow); alm.showNotification(entry); } @@ -240,10 +249,12 @@ public class AlertingNotificationManagerTest extends SysuiTestCase { @Test public void testCanRemoveImmediately_notShownLongEnough() { - AlertingNotificationManager alm = createAlertingNotificationManager(); - alm.showNotification(mEntry); + final AlertingNotificationManager alm = createAlertingNotificationManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); // The entry has just been added so we should not remove immediately. - assertFalse(alm.canRemoveImmediately(mEntry.getKey())); + assertFalse(alm.canRemoveImmediately(entry.getKey())); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt index 724ea023f2bd..e4da53a1a0a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt @@ -47,7 +47,7 @@ class MediaArtworkProcessorTest : SysuiTestCase() { processor = MediaArtworkProcessor() val point = Point() - context.display.getSize(point) + checkNotNull(context.display).getSize(point) screenWidth = point.x screenHeight = point.y } @@ -106,4 +106,4 @@ class MediaArtworkProcessorTest : SysuiTestCase() { // THEN the processed bitmap is null assertThat(background).isNull() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt index 2de57051d4f2..9036f22a792a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt @@ -278,7 +278,7 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(false) // WHEN a connection attempt is made and view is attached - val view = controller.buildAndConnectView(fakeParent) + val view = controller.buildAndConnectView(fakeParent)!! controller.stateChangeListener.onViewAttachedToWindow(view) // THEN no session is created diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java index bc32759a9938..f2207afeda10 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java @@ -24,97 +24,53 @@ import static android.app.NotificationManager.IMPORTANCE_MIN; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import android.app.Notification; import android.app.PendingIntent; import android.app.Person; import android.content.Intent; import android.graphics.Color; import android.os.UserHandle; -import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import androidx.test.filters.SmallTest; -import com.android.systemui.ForegroundServiceController; import com.android.systemui.SysuiTestCase; -import com.android.systemui.appops.AppOpsController; import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class AppOpsCoordinatorTest extends SysuiTestCase { - private static final String TEST_PKG = "test_pkg"; - private static final int NOTIF_USER_ID = 0; +public class ColorizedFgsCoordinatorTest extends SysuiTestCase { - @Mock private ForegroundServiceController mForegroundServiceController; - @Mock private AppOpsController mAppOpsController; + private static final int NOTIF_USER_ID = 0; @Mock private NotifPipeline mNotifPipeline; private NotificationEntryBuilder mEntryBuilder; - private AppOpsCoordinator mAppOpsCoordinator; - private NotifFilter mForegroundFilter; + private ColorizedFgsCoordinator mColorizedFgsCoordinator; private NotifSectioner mFgsSection; - private FakeSystemClock mClock = new FakeSystemClock(); - private FakeExecutor mExecutor = new FakeExecutor(mClock); - @Before public void setup() { MockitoAnnotations.initMocks(this); allowTestableLooperAsMainThread(); - mAppOpsCoordinator = - new AppOpsCoordinator( - mForegroundServiceController, - mAppOpsController, - mExecutor); + mColorizedFgsCoordinator = new ColorizedFgsCoordinator(); mEntryBuilder = new NotificationEntryBuilder() .setUser(new UserHandle(NOTIF_USER_ID)); - mAppOpsCoordinator.attach(mNotifPipeline); - - // capture filter - ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class); - verify(mNotifPipeline, times(1)).addPreGroupFilter(filterCaptor.capture()); - mForegroundFilter = filterCaptor.getValue(); - - mFgsSection = mAppOpsCoordinator.getSectioner(); - } - - @Test - public void filterTest_disclosureUnnecessary() { - NotificationEntry entry = mEntryBuilder.build(); - StatusBarNotification sbn = entry.getSbn(); - - // GIVEN the notification is a disclosure notification - when(mForegroundServiceController.isDisclosureNotification(sbn)).thenReturn(true); - - // GIVEN the disclosure isn't needed for this user - when(mForegroundServiceController.isDisclosureNeededForUser(sbn.getUserId())) - .thenReturn(false); + mColorizedFgsCoordinator.attach(mNotifPipeline); - // THEN filter out the notification - assertTrue(mForegroundFilter.shouldFilterOut(entry, 0)); + mFgsSection = mColorizedFgsCoordinator.getSectioner(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 61da90165c82..39b294879450 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -115,6 +115,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; +import com.android.systemui.log.LogBuffer; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.notetask.NoteTaskController; import com.android.systemui.plugins.ActivityStarter; @@ -441,6 +442,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mShadeController = spy(new ShadeControllerImpl( mCommandQueue, mMainExecutor, + mock(LogBuffer.class), mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt index 6155e3c16996..56d23978a5c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt @@ -212,13 +212,13 @@ class ConfigurationControllerImplTest : SysuiTestCase() { @Test fun localeListChanged_listenerNotified() { val config = mContext.resources.configuration - config.locales = LocaleList(Locale.CANADA, Locale.GERMANY) + config.setLocales(LocaleList(Locale.CANADA, Locale.GERMANY)) mConfigurationController.onConfigurationChanged(config) val listener = createAndAddListener() // WHEN the locales are updated - config.locales = LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE) + config.setLocales(LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE)) mConfigurationController.onConfigurationChanged(config) // THEN the listener is notified @@ -274,6 +274,23 @@ class ConfigurationControllerImplTest : SysuiTestCase() { } @Test + fun orientationUpdated_listenerNotified() { + val config = mContext.resources.configuration + config.orientation = Configuration.ORIENTATION_LANDSCAPE + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the orientation is updated + config.orientation = Configuration.ORIENTATION_PORTRAIT + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.orientationChanged).isTrue() + } + + + @Test fun multipleUpdates_listenerNotifiedOfAll() { val config = mContext.resources.configuration config.densityDpi = 14 @@ -325,6 +342,7 @@ class ConfigurationControllerImplTest : SysuiTestCase() { var themeChanged = false var localeListChanged = false var layoutDirectionChanged = false + var orientationChanged = false override fun onConfigChanged(newConfig: Configuration?) { changedConfig = newConfig @@ -350,6 +368,9 @@ class ConfigurationControllerImplTest : SysuiTestCase() { override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { layoutDirectionChanged = true } + override fun onOrientationChanged(orientation: Int) { + orientationChanged = true + } fun assertNoMethodsCalled() { assertThat(densityOrFontScaleChanged).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java index 2f1e372eb33d..ec6286b66ed2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java @@ -75,6 +75,7 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { private HeadsUpManagerPhone mHeadsUpManager; private View mOperatorNameView; private StatusBarStateController mStatusbarStateController; + private PhoneStatusBarTransitions mPhoneStatusBarTransitions; private KeyguardBypassController mBypassController; private NotificationWakeUpCoordinator mWakeUpCoordinator; private KeyguardStateController mKeyguardStateController; @@ -95,6 +96,7 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mHeadsUpManager = mock(HeadsUpManagerPhone.class); mOperatorNameView = new View(mContext); mStatusbarStateController = mock(StatusBarStateController.class); + mPhoneStatusBarTransitions = mock(PhoneStatusBarTransitions.class); mBypassController = mock(KeyguardBypassController.class); mWakeUpCoordinator = mock(NotificationWakeUpCoordinator.class); mKeyguardStateController = mock(KeyguardStateController.class); @@ -105,6 +107,7 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mock(NotificationIconAreaController.class), mHeadsUpManager, mStatusbarStateController, + mPhoneStatusBarTransitions, mBypassController, mWakeUpCoordinator, mDarkIconDispatcher, @@ -188,6 +191,7 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mock(NotificationIconAreaController.class), mHeadsUpManager, mStatusbarStateController, + mPhoneStatusBarTransitions, mBypassController, mWakeUpCoordinator, mDarkIconDispatcher, @@ -283,4 +287,18 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { /* delta = */ 0.001 ); } + + @Test + public void onHeadsUpStateChanged_true_transitionsNotified() { + mHeadsUpAppearanceController.onHeadsUpStateChanged(mEntry, true); + + verify(mPhoneStatusBarTransitions).onHeadsUpStateChanged(true); + } + + @Test + public void onHeadsUpStateChanged_false_transitionsNotified() { + mHeadsUpAppearanceController.onHeadsUpStateChanged(mEntry, false); + + verify(mPhoneStatusBarTransitions).onHeadsUpStateChanged(false); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java index 72522caf21b4..bb20d1806bce 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java @@ -39,7 +39,6 @@ import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.AlertingNotificationManagerTest; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; @@ -62,8 +61,6 @@ import org.mockito.junit.MockitoRule; public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); - private HeadsUpManagerPhone mHeadsUpManager; - private final HeadsUpManagerLogger mHeadsUpManagerLogger = new HeadsUpManagerLogger( logcatLogBuffer()); @Mock private GroupMembershipManager mGroupManager; @@ -108,15 +105,31 @@ public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { } } + private HeadsUpManagerPhone createHeadsUpManagerPhone() { + return new TestableHeadsUpManagerPhone( + mContext, + mHeadsUpManagerLogger, + mGroupManager, + mVSProvider, + mStatusBarStateController, + mBypassController, + mConfigurationController, + mTestHandler, + mAccessibilityManagerWrapper, + mUiEventLogger, + mShadeExpansionStateManager + ); + } + @Override protected AlertingNotificationManager createAlertingNotificationManager() { - return mHeadsUpManager; + return createHeadsUpManagerPhone(); } @Before @Override public void setUp() { - AccessibilityManagerWrapper accessibilityMgr = + final AccessibilityManagerWrapper accessibilityMgr = mDependency.injectMockDependency(AccessibilityManagerWrapper.class); when(accessibilityMgr.getRecommendedTimeoutMillis(anyInt(), anyInt())) .thenReturn(TEST_AUTO_DISMISS_TIME); @@ -126,19 +139,6 @@ public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { R.integer.ambient_notification_extension_time, 500); super.setUp(); - mHeadsUpManager = new TestableHeadsUpManagerPhone( - mContext, - mHeadsUpManagerLogger, - mGroupManager, - mVSProvider, - mStatusBarStateController, - mBypassController, - mConfigurationController, - mTestHandler, - mAccessibilityManagerWrapper, - mUiEventLogger, - mShadeExpansionStateManager - ); } @After @@ -149,63 +149,67 @@ public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { @Test public void testSnooze() { - mHeadsUpManager.showNotification(mEntry); + final HeadsUpManagerPhone hmp = createHeadsUpManagerPhone(); + final NotificationEntry entry = createEntry(/* id = */ 0); - mHeadsUpManager.snooze(); + hmp.showNotification(entry); + hmp.snooze(); - assertTrue(mHeadsUpManager.isSnoozed(mEntry.getSbn().getPackageName())); + assertTrue(hmp.isSnoozed(entry.getSbn().getPackageName())); } @Test public void testSwipedOutNotification() { - mHeadsUpManager.showNotification(mEntry); - mHeadsUpManager.addSwipedOutNotification(mEntry.getKey()); + final HeadsUpManagerPhone hmp = createHeadsUpManagerPhone(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + hmp.showNotification(entry); + hmp.addSwipedOutNotification(entry.getKey()); // Remove should succeed because the notification is swiped out - mHeadsUpManager.removeNotification(mEntry.getKey(), false /* releaseImmediately */); + final boolean removedImmediately = hmp.removeNotification(entry.getKey(), + /* releaseImmediately = */ false); - assertFalse(mHeadsUpManager.isAlerting(mEntry.getKey())); + assertTrue(removedImmediately); + assertFalse(hmp.isAlerting(entry.getKey())); } @Test public void testCanRemoveImmediately_swipedOut() { - mHeadsUpManager.showNotification(mEntry); - mHeadsUpManager.addSwipedOutNotification(mEntry.getKey()); + final HeadsUpManagerPhone hmp = createHeadsUpManagerPhone(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + hmp.showNotification(entry); + hmp.addSwipedOutNotification(entry.getKey()); // Notification is swiped so it can be immediately removed. - assertTrue(mHeadsUpManager.canRemoveImmediately(mEntry.getKey())); + assertTrue(hmp.canRemoveImmediately(entry.getKey())); } @Ignore("b/141538055") @Test public void testCanRemoveImmediately_notTopEntry() { - NotificationEntry laterEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(1)) - .build(); + final HeadsUpManagerPhone hmp = createHeadsUpManagerPhone(); + final NotificationEntry earlierEntry = createEntry(/* id = */ 0); + final NotificationEntry laterEntry = createEntry(/* id = */ 1); laterEntry.setRow(mRow); - mHeadsUpManager.showNotification(mEntry); - mHeadsUpManager.showNotification(laterEntry); + + hmp.showNotification(earlierEntry); + hmp.showNotification(laterEntry); // Notification is "behind" a higher priority notification so we can remove it immediately. - assertTrue(mHeadsUpManager.canRemoveImmediately(mEntry.getKey())); + assertTrue(hmp.canRemoveImmediately(earlierEntry.getKey())); } @Test public void testExtendHeadsUp() { - mHeadsUpManager.showNotification(mEntry); - Runnable pastNormalTimeRunnable = - () -> mLivesPastNormalTime = mHeadsUpManager.isAlerting(mEntry.getKey()); - mTestHandler.postDelayed(pastNormalTimeRunnable, - TEST_AUTO_DISMISS_TIME + mHeadsUpManager.mExtensionTime / 2); - mTestHandler.postDelayed(mTestTimeoutRunnable, TEST_TIMEOUT_TIME); - - mHeadsUpManager.extendHeadsUp(); + final HeadsUpManagerPhone hmp = createHeadsUpManagerPhone(); + final NotificationEntry entry = createEntry(/* id = */ 0); - // Wait for normal time runnable and extended remove runnable and process them on arrival. - TestableLooper.get(this).processMessages(2); + hmp.showNotification(entry); + hmp.extendHeadsUp(); - assertFalse("Test timed out", mTimedOut); - assertTrue("Pulse was not extended", mLivesPastNormalTime); - assertFalse(mHeadsUpManager.isAlerting(mEntry.getKey())); + final int pastNormalTimeMillis = TEST_AUTO_DISMISS_TIME + hmp.mExtensionTime / 2; + verifyAlertingAtTime(hmp, entry, true, pastNormalTimeMillis, "normal time"); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitionsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitionsTest.kt new file mode 100644 index 000000000000..4af1b24ba96c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitionsTest.kt @@ -0,0 +1,237 @@ +/* + * 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 + +import android.testing.TestableLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT +import com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT +import com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE +import com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT +import com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT +import com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class PhoneStatusBarTransitionsTest : SysuiTestCase() { + + // PhoneStatusBarView does a lot of non-standard things when inflating, so just use mocks. + private val batteryView = mock<View>() + private val statusIcons = mock<View>() + private val startIcons = mock<View>() + private val statusBarView = + mock<PhoneStatusBarView>().apply { + whenever(this.context).thenReturn(mContext) + whenever(this.findViewById<View>(R.id.battery)).thenReturn(batteryView) + whenever(this.findViewById<View>(R.id.statusIcons)).thenReturn(statusIcons) + whenever(this.findViewById<View>(R.id.status_bar_start_side_except_heads_up)) + .thenReturn(startIcons) + } + private val backgroundView = mock<View>().apply { whenever(this.context).thenReturn(mContext) } + + private val underTest: PhoneStatusBarTransitions by lazy { + PhoneStatusBarTransitions(statusBarView, backgroundView).also { + // The views' alphas will be set when PhoneStatusBarTransitions is created and we want + // to ignore those in the tests, so clear those verifications here. + reset(batteryView) + reset(statusIcons) + reset(startIcons) + } + } + + @Before + fun setUp() { + context.orCreateTestableResources.addOverride( + R.dimen.status_bar_icon_drawing_alpha, + RESOURCE_ALPHA, + ) + } + + @Test + fun transitionTo_lightsOutMode_batteryTranslucent() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT, /* animate= */ false) + + val alpha = batteryView.capturedAlpha() + assertThat(alpha).isGreaterThan(0) + assertThat(alpha).isLessThan(1) + } + + @Test + fun transitionTo_lightsOutMode_statusIconsHidden() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT, /* animate= */ false) + + assertThat(statusIcons.capturedAlpha()).isEqualTo(0) + } + + @Test + fun transitionTo_lightsOutMode_startIconsHidden() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT, /* animate= */ false) + + assertThat(startIcons.capturedAlpha()).isEqualTo(0) + } + + @Test + fun transitionTo_lightsOutTransparentMode_batteryTranslucent() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT_TRANSPARENT, /* animate= */ false) + + val alpha = batteryView.capturedAlpha() + assertThat(alpha).isGreaterThan(0) + assertThat(alpha).isLessThan(1) + } + + @Test + fun transitionTo_lightsOutTransparentMode_statusIconsHidden() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT_TRANSPARENT, /* animate= */ false) + + assertThat(statusIcons.capturedAlpha()).isEqualTo(0) + } + + @Test + fun transitionTo_lightsOutTransparentMode_startIconsHidden() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT_TRANSPARENT, /* animate= */ false) + + assertThat(startIcons.capturedAlpha()).isEqualTo(0) + } + + @Test + fun transitionTo_translucentMode_batteryIconShown() { + underTest.transitionTo(/* mode= */ MODE_TRANSLUCENT, /* animate= */ false) + + assertThat(batteryView.capturedAlpha()).isEqualTo(1) + } + + @Test + fun transitionTo_semiTransparentMode_statusIconsShown() { + underTest.transitionTo(/* mode= */ MODE_SEMI_TRANSPARENT, /* animate= */ false) + + assertThat(statusIcons.capturedAlpha()).isEqualTo(1) + } + + @Test + fun transitionTo_transparentMode_startIconsShown() { + // Transparent is the default, so we need to switch to a different mode first + underTest.transitionTo(/* mode= */ MODE_OPAQUE, /* animate= */ false) + reset(startIcons) + + underTest.transitionTo(/* mode= */ MODE_TRANSPARENT, /* animate= */ false) + + assertThat(startIcons.capturedAlpha()).isEqualTo(1) + } + + @Test + fun transitionTo_opaqueMode_batteryIconUsesResourceAlpha() { + underTest.transitionTo(/* mode= */ MODE_OPAQUE, /* animate= */ false) + + assertThat(batteryView.capturedAlpha()).isEqualTo(RESOURCE_ALPHA) + } + + @Test + fun transitionTo_opaqueMode_statusIconsUseResourceAlpha() { + underTest.transitionTo(/* mode= */ MODE_OPAQUE, /* animate= */ false) + + assertThat(statusIcons.capturedAlpha()).isEqualTo(RESOURCE_ALPHA) + } + + @Test + fun transitionTo_opaqueMode_startIconsUseResourceAlpha() { + underTest.transitionTo(/* mode= */ MODE_OPAQUE, /* animate= */ false) + + assertThat(startIcons.capturedAlpha()).isEqualTo(RESOURCE_ALPHA) + } + + @Test + fun onHeadsUpStateChanged_true_semiTransparentMode_startIconsShown() { + underTest.transitionTo(/* mode= */ MODE_SEMI_TRANSPARENT, /* animate= */ false) + reset(startIcons) + + underTest.onHeadsUpStateChanged(true) + + assertThat(startIcons.capturedAlpha()).isEqualTo(1) + } + + @Test + fun onHeadsUpStateChanged_true_opaqueMode_startIconsUseResourceAlpha() { + underTest.transitionTo(/* mode= */ MODE_OPAQUE, /* animate= */ false) + reset(startIcons) + + underTest.onHeadsUpStateChanged(true) + + assertThat(startIcons.capturedAlpha()).isEqualTo(RESOURCE_ALPHA) + } + + /** Regression test for b/291173113. */ + @Test + fun onHeadsUpStateChanged_true_lightsOutMode_startIconsUseResourceAlpha() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT, /* animate= */ false) + reset(startIcons) + + underTest.onHeadsUpStateChanged(true) + + assertThat(startIcons.capturedAlpha()).isEqualTo(RESOURCE_ALPHA) + } + + @Test + fun onHeadsUpStateChanged_false_semiTransparentMode_startIconsShown() { + underTest.transitionTo(/* mode= */ MODE_SEMI_TRANSPARENT, /* animate= */ false) + reset(startIcons) + + underTest.onHeadsUpStateChanged(false) + + assertThat(startIcons.capturedAlpha()).isEqualTo(1) + } + + @Test + fun onHeadsUpStateChanged_false_opaqueMode_startIconsUseResourceAlpha() { + underTest.transitionTo(/* mode= */ MODE_OPAQUE, /* animate= */ false) + reset(startIcons) + + underTest.onHeadsUpStateChanged(false) + + assertThat(startIcons.capturedAlpha()).isEqualTo(RESOURCE_ALPHA) + } + + @Test + fun onHeadsUpStateChanged_false_lightsOutMode_startIconsHidden() { + underTest.transitionTo(/* mode= */ MODE_LIGHTS_OUT, /* animate= */ false) + reset(startIcons) + + underTest.onHeadsUpStateChanged(false) + + assertThat(startIcons.capturedAlpha()).isEqualTo(0) + } + + private fun View.capturedAlpha(): Float { + val captor = argumentCaptor<Float>() + verify(this).alpha = captor.capture() + return captor.value + } + + private companion object { + const val RESOURCE_ALPHA = 0.34f + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt index 1759fb794bd6..210c5ab28c42 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -463,10 +463,10 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val provider = StatusBarContentInsetsProvider(contextMock, configurationController, mock(DumpManager::class.java)) - configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600) + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) // WHEN: get insets on the second display val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) @@ -482,14 +482,14 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val provider = StatusBarContentInsetsProvider(contextMock, configurationController, mock(DumpManager::class.java)) - configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) val firstDisplayInsetsFirstCall = provider .getStatusBarContentAreaForRotation(ROTATION_NONE) - configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600) + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) + configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) // WHEN: get insets on the first display again val firstDisplayInsetsSecondCall = provider @@ -577,4 +577,4 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { " expected=$expected actual=$actual", expected.equals(actual)) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index ed9cf3f2f158..0da736003e22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -35,6 +35,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; + import android.service.trust.TrustAgentService; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -73,6 +75,8 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.TaskbarDelegate; import com.android.systemui.plugins.ActivityStarter; @@ -201,7 +205,10 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mBouncerView, mAlternateBouncerInteractor, mUdfpsOverlayInteractor, - mActivityStarter) { + mActivityStarter, + mock(KeyguardTransitionInteractor.class), + StandardTestDispatcher(null, null), + () -> mock(WindowManagerLockscreenVisibilityInteractor.class)) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -701,7 +708,10 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mBouncerView, mAlternateBouncerInteractor, mUdfpsOverlayInteractor, - mActivityStarter) { + mActivityStarter, + mock(KeyguardTransitionInteractor.class), + StandardTestDispatcher(null, null), + () -> mock(WindowManagerLockscreenVisibilityInteractor.class)) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java index 9c52788dc2eb..34c4ac1f4b67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java @@ -32,7 +32,6 @@ import android.testing.TestableLooper.RunWithLooper; import androidx.test.filters.SmallTest; import com.android.internal.logging.testing.FakeMetricsLogger; -import com.android.systemui.ForegroundServiceNotificationListener; import com.android.systemui.InitController; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.ActivityStarter; @@ -94,7 +93,6 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase { mDependency.injectTestDependency(ShadeController.class, mShadeController); mDependency.injectMockDependency(NotificationRemoteInputManager.Callback.class); mDependency.injectMockDependency(NotificationShadeWindowController.class); - mDependency.injectMockDependency(ForegroundServiceNotificationListener.class); NotificationShadeWindowView notificationShadeWindowView = mock(NotificationShadeWindowView.class); 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 823155b0d7e6..b8f2cab3019e 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 @@ -81,7 +81,7 @@ import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; -import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -136,8 +136,8 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { private StatusBarWindowStateController mStatusBarWindowStateController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @ClassRule - public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java index 14edf3dba6c7..e5bbead520f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.policy; +import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED; + import static com.android.systemui.dump.LogBufferHelperKt.logcatLogBuffer; import static com.google.common.truth.Truth.assertThat; @@ -40,7 +42,6 @@ import android.app.Person; import android.content.Context; import android.content.Intent; import android.os.Handler; -import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -48,6 +49,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.systemui.R; import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.AlertingNotificationManagerTest; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -63,16 +65,11 @@ import org.mockito.Mock; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class HeadsUpManagerTest extends AlertingNotificationManagerTest { - private static final int TEST_A11Y_AUTO_DISMISS_TIME = 600; - private static final int TEST_A11Y_TIMEOUT_TIME = 5_000; + private static final int TEST_TOUCH_ACCEPTANCE_TIME = 200; + private static final int TEST_A11Y_AUTO_DISMISS_TIME = 1_000; + private static final int TEST_A11Y_TIMEOUT_TIME = 3_000; - private HeadsUpManager mHeadsUpManager; - private boolean mLivesPastNormalTime; private UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); - @Mock private HeadsUpManager.HeadsUpEntry mAlertEntry; - @Mock private NotificationEntry mEntry; - @Mock private StatusBarNotification mSbn; - @Mock private Notification mNotification; private final HeadsUpManagerLogger mLogger = spy(new HeadsUpManagerLogger(logcatLogBuffer())); @Mock private AccessibilityManagerWrapper mAccessibilityMgr; @@ -83,27 +80,81 @@ public class HeadsUpManagerTest extends AlertingNotificationManagerTest { AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger) { super(context, logger, handler, accessibilityManagerWrapper, uiEventLogger); + mTouchAcceptanceDelay = TEST_TOUCH_ACCEPTANCE_TIME; mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME; - mStickyDisplayTime = TEST_STICKY_DISPLAY_TIME; mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME; + mStickyDisplayTime = TEST_STICKY_AUTO_DISMISS_TIME; } } + private HeadsUpManager createHeadsUpManager() { + return new TestableHeadsUpManager(mContext, mLogger, mTestHandler, mAccessibilityMgr, + mUiEventLoggerFake); + } + @Override protected AlertingNotificationManager createAlertingNotificationManager() { - return mHeadsUpManager; + return createHeadsUpManager(); + } + + private NotificationEntry createStickyEntry(int id) { + final Notification notif = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setFullScreenIntent(mock(PendingIntent.class), /* highPriority */ true) + .build(); + return createEntry(id, notif); + } + + private NotificationEntry createStickyForSomeTimeEntry(int id) { + final Notification notif = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setFlag(FLAG_FSI_REQUESTED_BUT_DENIED, true) + .build(); + return createEntry(id, notif); } + private PendingIntent createFullScreenIntent() { + return PendingIntent.getActivity( + getContext(), 0, new Intent(getContext(), this.getClass()), + PendingIntent.FLAG_MUTABLE_UNAUDITED); + } + + private NotificationEntry createFullScreenIntentEntry(int id) { + final Notification notif = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setFullScreenIntent(createFullScreenIntent(), /* highPriority */ true) + .build(); + return createEntry(id, notif); + } + + + private void useAccessibilityTimeout(boolean use) { + if (use) { + doReturn(TEST_A11Y_AUTO_DISMISS_TIME).when(mAccessibilityMgr) + .getRecommendedTimeoutMillis(anyInt(), anyInt()); + } else { + when(mAccessibilityMgr.getRecommendedTimeoutMillis(anyInt(), anyInt())).then( + i -> i.getArgument(0)); + } + } + + @Before @Override public void setUp() { initMocks(this); - when(mEntry.getSbn()).thenReturn(mSbn); - when(mEntry.getKey()).thenReturn("entryKey"); - when(mSbn.getNotification()).thenReturn(mNotification); super.setUp(); - mHeadsUpManager = new TestableHeadsUpManager(mContext, mLogger, mTestHandler, - mAccessibilityMgr, mUiEventLoggerFake); + + assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME); + assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME); + assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME); + + assertThat(TEST_TOUCH_ACCEPTANCE_TIME + TEST_AUTO_DISMISS_TIME).isLessThan( + TEST_TIMEOUT_TIME); + assertThat(TEST_TOUCH_ACCEPTANCE_TIME + TEST_STICKY_AUTO_DISMISS_TIME).isLessThan( + TEST_TIMEOUT_TIME); + assertThat(TEST_TOUCH_ACCEPTANCE_TIME + TEST_A11Y_AUTO_DISMISS_TIME).isLessThan( + TEST_A11Y_TIMEOUT_TIME); } @After @@ -114,193 +165,327 @@ public class HeadsUpManagerTest extends AlertingNotificationManagerTest { @Test public void testHunRemovedLogging() { - mAlertEntry.mEntry = mEntry; - mHeadsUpManager.onAlertEntryRemoved(mAlertEntry); - verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(mEntry)); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createEntry(/* id = */ 0); + final HeadsUpManager.HeadsUpEntry headsUpEntry = mock(HeadsUpManager.HeadsUpEntry.class); + headsUpEntry.mEntry = notifEntry; + + hum.onAlertEntryRemoved(headsUpEntry); + + verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(notifEntry)); } @Test public void testShouldHeadsUpBecomePinned_hasFSI_notUnpinned_true() { - // Set up NotifEntry with FSI - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); - notifEntry.getSbn().getNotification().fullScreenIntent = PendingIntent.getActivity( - getContext(), 0, new Intent(getContext(), this.getClass()), - PendingIntent.FLAG_MUTABLE_UNAUDITED); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createFullScreenIntentEntry(/* id = */ 0); // Add notifEntry to ANM mAlertEntries map and make it NOT unpinned - mHeadsUpManager.showNotification(notifEntry); - HeadsUpManager.HeadsUpEntry headsUpEntry = - mHeadsUpManager.getHeadsUpEntry(notifEntry.getKey()); + hum.showNotification(notifEntry); + + final HeadsUpManager.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(notifEntry.getKey()); headsUpEntry.wasUnpinned = false; - assertTrue(mHeadsUpManager.shouldHeadsUpBecomePinned(notifEntry)); + assertTrue(hum.shouldHeadsUpBecomePinned(notifEntry)); } @Test public void testShouldHeadsUpBecomePinned_wasUnpinned_false() { - // Set up NotifEntry with FSI - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); - notifEntry.getSbn().getNotification().fullScreenIntent = PendingIntent.getActivity( - getContext(), 0, new Intent(getContext(), this.getClass()), - PendingIntent.FLAG_MUTABLE_UNAUDITED); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createFullScreenIntentEntry(/* id = */ 0); // Add notifEntry to ANM mAlertEntries map and make it unpinned - mHeadsUpManager.showNotification(notifEntry); - HeadsUpManager.HeadsUpEntry headsUpEntry = - mHeadsUpManager.getHeadsUpEntry(notifEntry.getKey()); + hum.showNotification(notifEntry); + + final HeadsUpManager.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(notifEntry.getKey()); headsUpEntry.wasUnpinned = true; - assertFalse(mHeadsUpManager.shouldHeadsUpBecomePinned(notifEntry)); + assertFalse(hum.shouldHeadsUpBecomePinned(notifEntry)); } @Test public void testShouldHeadsUpBecomePinned_noFSI_false() { - // Set up NotifEntry with no FSI - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); - assertFalse(mHeadsUpManager.shouldHeadsUpBecomePinned(notifEntry)); + assertFalse(hum.shouldHeadsUpBecomePinned(entry)); } + + @Test + public void testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + useAccessibilityTimeout(false); + + hum.showNotification(entry); + + final int pastJustAutoDismissMillis = + TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME; + verifyAlertingAtTime(hum, entry, true, pastJustAutoDismissMillis, "just auto dismiss"); + } + + + @Test + public void testShowNotification_autoDismissesWithDefaultTimeout() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + useAccessibilityTimeout(false); + + hum.showNotification(entry); + + final int pastDefaultTimeoutMillis = TEST_TOUCH_ACCEPTANCE_TIME + + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2; + verifyAlertingAtTime(hum, entry, false, pastDefaultTimeoutMillis, "default timeout"); + } + + + @Test + public void testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0); + useAccessibilityTimeout(false); + + hum.showNotification(entry); + + final int pastDefaultTimeoutMillis = TEST_TOUCH_ACCEPTANCE_TIME + + (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2; + verifyAlertingAtTime(hum, entry, true, pastDefaultTimeoutMillis, "default timeout"); + } + + + @Test + public void testShowNotification_sticky_neverAutoDismisses() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createStickyEntry(/* id = */ 0); + useAccessibilityTimeout(false); + + hum.showNotification(entry); + + final int pastLongestAutoDismissMillis = + TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME; + final Boolean[] wasAlerting = {null}; + final Runnable checkAlerting = + () -> wasAlerting[0] = hum.isAlerting(entry.getKey()); + mTestHandler.postDelayed(checkAlerting, pastLongestAutoDismissMillis); + TestableLooper.get(this).processMessages(1); + + assertTrue("Should still be alerting past longest auto-dismiss", wasAlerting[0]); + assertTrue("Should still be alerting after processing", + hum.isAlerting(entry.getKey())); + } + + @Test public void testShowNotification_autoDismissesWithAccessibilityTimeout() { - doReturn(TEST_A11Y_AUTO_DISMISS_TIME).when(mAccessibilityMgr) - .getRecommendedTimeoutMillis(anyInt(), anyInt()); - mHeadsUpManager.showNotification(mEntry); - Runnable pastNormalTimeRunnable = - () -> mLivesPastNormalTime = mHeadsUpManager.isAlerting(mEntry.getKey()); - mTestHandler.postDelayed(pastNormalTimeRunnable, - (TEST_A11Y_AUTO_DISMISS_TIME + TEST_AUTO_DISMISS_TIME) / 2); - mTestHandler.postDelayed(mTestTimeoutRunnable, TEST_A11Y_TIMEOUT_TIME); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + useAccessibilityTimeout(true); - TestableLooper.get(this).processMessages(2); + hum.showNotification(entry); - assertFalse("Test timed out", mTimedOut); - assertTrue("Heads up should live long enough", mLivesPastNormalTime); - assertFalse(mHeadsUpManager.isAlerting(mEntry.getKey())); + final int pastDefaultTimeoutMillis = TEST_TOUCH_ACCEPTANCE_TIME + + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2; + verifyAlertingAtTime(hum, entry, true, pastDefaultTimeoutMillis, "default timeout"); } @Test - public void testIsSticky_rowPinnedAndExpanded_true() { - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); + public void testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0); + useAccessibilityTimeout(true); + + hum.showNotification(entry); + + final int pastStickyTimeoutMillis = TEST_TOUCH_ACCEPTANCE_TIME + + (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2; + verifyAlertingAtTime(hum, entry, true, pastStickyTimeoutMillis, "sticky timeout"); + } + + + @Test + public void testRemoveNotification_beforeMinimumDisplayTime() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + useAccessibilityTimeout(false); + + hum.showNotification(entry); + + // Try to remove but defer, since the notification has not been shown long enough. + final boolean removedImmediately = hum.removeNotification( + entry.getKey(), false /* releaseImmediately */); + + assertFalse("HUN should not be removed before minimum display time", removedImmediately); + assertTrue("HUN should still be alerting before minimum display time", + hum.isAlerting(entry.getKey())); + + final int pastMinimumDisplayTimeMillis = + (TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2; + verifyAlertingAtTime(hum, entry, false, pastMinimumDisplayTimeMillis, + "minimum display time"); + } + + + @Test + public void testRemoveNotification_afterMinimumDisplayTime() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + useAccessibilityTimeout(false); + + hum.showNotification(entry); + + // After the minimum display time: + // 1. Check whether the notification is still alerting. + // 2. Try to remove it and check whether the remove succeeded. + // 3. Check whether it is still alerting after trying to remove it. + final Boolean[] livedPastMinimumDisplayTime = {null}; + final Boolean[] removedAfterMinimumDisplayTime = {null}; + final Boolean[] livedPastRemoveAfterMinimumDisplayTime = {null}; + final Runnable pastMinimumDisplayTimeRunnable = () -> { + livedPastMinimumDisplayTime[0] = hum.isAlerting(entry.getKey()); + removedAfterMinimumDisplayTime[0] = hum.removeNotification( + entry.getKey(), /* releaseImmediately = */ false); + livedPastRemoveAfterMinimumDisplayTime[0] = hum.isAlerting(entry.getKey()); + }; + final int pastMinimumDisplayTimeMillis = + (TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2; + mTestHandler.postDelayed(pastMinimumDisplayTimeRunnable, pastMinimumDisplayTimeMillis); + // Wait until the minimum display time has passed before attempting removal. + TestableLooper.get(this).processMessages(1); + + assertTrue("HUN should live past minimum display time", + livedPastMinimumDisplayTime[0]); + assertTrue("HUN should be removed immediately past minimum display time", + removedAfterMinimumDisplayTime[0]); + assertFalse("HUN should not live after being removed past minimum display time", + livedPastRemoveAfterMinimumDisplayTime[0]); + assertFalse(hum.isAlerting(entry.getKey())); + } + + + @Test + public void testRemoveNotification_releaseImmediately() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + hum.showNotification(entry); + + // Remove forcibly with releaseImmediately = true. + final boolean removedImmediately = hum.removeNotification( + entry.getKey(), /* releaseImmediately = */ true); + + assertTrue(removedImmediately); + assertFalse(hum.isAlerting(entry.getKey())); + } + + @Test + public void testIsSticky_rowPinnedAndExpanded_true() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createEntry(/* id = */ 0); when(mRow.isPinned()).thenReturn(true); notifEntry.setRow(mRow); - mHeadsUpManager.showNotification(notifEntry); + hum.showNotification(notifEntry); - HeadsUpManager.HeadsUpEntry headsUpEntry = - mHeadsUpManager.getHeadsUpEntry(notifEntry.getKey()); + final HeadsUpManager.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(notifEntry.getKey()); headsUpEntry.setExpanded(true); - assertTrue(mHeadsUpManager.isSticky(notifEntry.getKey())); + assertTrue(hum.isSticky(notifEntry.getKey())); } @Test public void testIsSticky_remoteInputActive_true() { - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createEntry(/* id = */ 0); - mHeadsUpManager.showNotification(notifEntry); - - HeadsUpManager.HeadsUpEntry headsUpEntry = - mHeadsUpManager.getHeadsUpEntry(notifEntry.getKey()); + hum.showNotification(notifEntry); + final HeadsUpManager.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(notifEntry.getKey()); headsUpEntry.remoteInputActive = true; - assertTrue(mHeadsUpManager.isSticky(notifEntry.getKey())); + assertTrue(hum.isSticky(notifEntry.getKey())); } @Test public void testIsSticky_hasFullScreenIntent_true() { - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); - - mHeadsUpManager.showNotification(notifEntry); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createFullScreenIntentEntry(/* id = */ 0); - HeadsUpManager.HeadsUpEntry headsUpEntry = - mHeadsUpManager.getHeadsUpEntry(notifEntry.getKey()); + hum.showNotification(notifEntry); - notifEntry.getSbn().getNotification().fullScreenIntent = PendingIntent.getActivity( - getContext(), 0, new Intent(getContext(), this.getClass()), - PendingIntent.FLAG_MUTABLE_UNAUDITED); - - assertTrue(mHeadsUpManager.isSticky(notifEntry.getKey())); + assertTrue(hum.isSticky(notifEntry.getKey())); } + @Test - public void testIsSticky_stickyAndNotDemoted_true() { - NotificationEntry alertEntry = new NotificationEntryBuilder() - .setSbn(createStickySbn(0)) - .build(); + public void testIsSticky_stickyForSomeTime_false() { + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0); - mHeadsUpManager.showNotification(alertEntry); + hum.showNotification(entry); - assertTrue(mHeadsUpManager.isSticky(alertEntry.getKey())); + assertFalse(hum.isSticky(entry.getKey())); } + @Test public void testIsSticky_false() { - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createEntry(/* id = */ 0); - mHeadsUpManager.showNotification(notifEntry); + hum.showNotification(notifEntry); - HeadsUpManager.HeadsUpEntry headsUpEntry = - mHeadsUpManager.getHeadsUpEntry(notifEntry.getKey()); + final HeadsUpManager.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(notifEntry.getKey()); headsUpEntry.setExpanded(false); headsUpEntry.remoteInputActive = false; - assertFalse(mHeadsUpManager.isSticky(notifEntry.getKey())); + assertFalse(hum.isSticky(notifEntry.getKey())); } @Test public void testCompareTo_withNullEntries() { - NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build(); - mHeadsUpManager.showNotification(alertEntry); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build(); + + hum.showNotification(alertEntry); - assertThat(mHeadsUpManager.compare(alertEntry, null)).isLessThan(0); - assertThat(mHeadsUpManager.compare(null, alertEntry)).isGreaterThan(0); - assertThat(mHeadsUpManager.compare(null, null)).isEqualTo(0); + assertThat(hum.compare(alertEntry, null)).isLessThan(0); + assertThat(hum.compare(null, alertEntry)).isGreaterThan(0); + assertThat(hum.compare(null, null)).isEqualTo(0); } @Test public void testCompareTo_withNonAlertEntries() { - NotificationEntry nonAlertEntry1 = new NotificationEntryBuilder().setTag("nae1").build(); - NotificationEntry nonAlertEntry2 = new NotificationEntryBuilder().setTag("nae2").build(); - NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build(); - mHeadsUpManager.showNotification(alertEntry); - - assertThat(mHeadsUpManager.compare(alertEntry, nonAlertEntry1)).isLessThan(0); - assertThat(mHeadsUpManager.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0); - assertThat(mHeadsUpManager.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0); + final HeadsUpManager hum = createHeadsUpManager(); + + final NotificationEntry nonAlertEntry1 = new NotificationEntryBuilder().setTag( + "nae1").build(); + final NotificationEntry nonAlertEntry2 = new NotificationEntryBuilder().setTag( + "nae2").build(); + final NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build(); + hum.showNotification(alertEntry); + + assertThat(hum.compare(alertEntry, nonAlertEntry1)).isLessThan(0); + assertThat(hum.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0); + assertThat(hum.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0); } @Test public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() { - HeadsUpManager.HeadsUpEntry ongoingCall = mHeadsUpManager.new HeadsUpEntry(); + final HeadsUpManager hum = createHeadsUpManager(); + + final HeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry(); ongoingCall.setEntry(new NotificationEntryBuilder() - .setSbn(createNewSbn(0, + .setSbn(createSbn(/* id = */ 0, new Notification.Builder(mContext, "") .setCategory(Notification.CATEGORY_CALL) .setOngoing(true))) .build()); - HeadsUpManager.HeadsUpEntry activeRemoteInput = mHeadsUpManager.new HeadsUpEntry(); - activeRemoteInput.setEntry(new NotificationEntryBuilder() - .setSbn(createNewNotification(1)) - .build()); + final HeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(); + activeRemoteInput.setEntry(createEntry(/* id = */ 1)); activeRemoteInput.remoteInputActive = true; assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0); @@ -309,20 +494,20 @@ public class HeadsUpManagerTest extends AlertingNotificationManagerTest { @Test public void testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() { - HeadsUpManager.HeadsUpEntry incomingCall = mHeadsUpManager.new HeadsUpEntry(); - Person person = new Person.Builder().setName("person").build(); - PendingIntent intent = mock(PendingIntent.class); + final HeadsUpManager hum = createHeadsUpManager(); + + final HeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry(); + final Person person = new Person.Builder().setName("person").build(); + final PendingIntent intent = mock(PendingIntent.class); incomingCall.setEntry(new NotificationEntryBuilder() - .setSbn(createNewSbn(0, + .setSbn(createSbn(/* id = */ 0, new Notification.Builder(mContext, "") .setStyle(Notification.CallStyle .forIncomingCall(person, intent, intent)))) .build()); - HeadsUpManager.HeadsUpEntry activeRemoteInput = mHeadsUpManager.new HeadsUpEntry(); - activeRemoteInput.setEntry(new NotificationEntryBuilder() - .setSbn(createNewNotification(1)) - .build()); + final HeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(); + activeRemoteInput.setEntry(createEntry(/* id = */ 1)); activeRemoteInput.remoteInputActive = true; assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0); @@ -331,22 +516,18 @@ public class HeadsUpManagerTest extends AlertingNotificationManagerTest { @Test public void testPinEntry_logsPeek() { + final HeadsUpManager hum = createHeadsUpManager(); + // Needs full screen intent in order to be pinned - final PendingIntent fullScreenIntent = PendingIntent.getActivity(mContext, 0, - new Intent().setPackage(mContext.getPackageName()), PendingIntent.FLAG_MUTABLE); + final HeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(); + entryToPin.setEntry(createFullScreenIntentEntry(/* id = */ 0)); - HeadsUpManager.HeadsUpEntry entryToPin = mHeadsUpManager.new HeadsUpEntry(); - entryToPin.setEntry(new NotificationEntryBuilder() - .setSbn(createNewSbn(0, - new Notification.Builder(mContext, "") - .setFullScreenIntent(fullScreenIntent, true))) - .build()); // Note: the standard way to show a notification would be calling showNotification rather // than onAlertEntryAdded. However, in practice showNotification in effect adds // the notification and then updates it; in order to not log twice, the entry needs // to have a functional ExpandableNotificationRow that can keep track of whether it's // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit. - mHeadsUpManager.onAlertEntryAdded(entryToPin); + hum.onAlertEntryAdded(entryToPin); assertEquals(1, mUiEventLoggerFake.numLogs()); assertEquals(HeadsUpManager.NotificationPeekEvent.NOTIFICATION_PEEK.getId(), @@ -355,14 +536,15 @@ public class HeadsUpManagerTest extends AlertingNotificationManagerTest { @Test public void testSetUserActionMayIndirectlyRemove() { - NotificationEntry notifEntry = new NotificationEntryBuilder() - .setSbn(createNewNotification(/* id= */ 0)) - .build(); + final HeadsUpManager hum = createHeadsUpManager(); + final NotificationEntry notifEntry = createEntry(/* id = */ 0); + + hum.showNotification(notifEntry); + + assertFalse(hum.canRemoveImmediately(notifEntry.getKey())); - mHeadsUpManager.showNotification(notifEntry); - assertFalse(mHeadsUpManager.canRemoveImmediately(notifEntry.getKey())); + hum.setUserActionMayIndirectlyRemove(notifEntry); - mHeadsUpManager.setUserActionMayIndirectlyRemove(notifEntry); - assertTrue(mHeadsUpManager.canRemoveImmediately(notifEntry.getKey())); + assertTrue(hum.canRemoveImmediately(notifEntry.getKey())); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java index 5cabcd4163b2..cae892fc2213 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java @@ -36,6 +36,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.logging.KeyguardUpdateMonitorLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import dagger.Lazy; @@ -67,6 +68,8 @@ public class KeyguardStateControllerTest extends SysuiTestCase { private Lazy<KeyguardUnlockAnimationController> mKeyguardUnlockAnimationControllerLazy; @Mock private KeyguardUpdateMonitorLogger mLogger; + @Mock + private FeatureFlags mFeatureFlags; @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mUpdateCallbackCaptor; @@ -80,7 +83,8 @@ public class KeyguardStateControllerTest extends SysuiTestCase { mLockPatternUtils, mKeyguardUnlockAnimationControllerLazy, mLogger, - mDumpManager); + mDumpManager, + mFeatureFlags); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index ef39ff8ed521..79feb417a062 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -82,7 +82,7 @@ import com.android.systemui.statusbar.phone.LightBarController; import org.junit.After; import org.junit.Before; -import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -111,8 +111,8 @@ public class RemoteInputViewTest extends SysuiTestCase { private BlockingQueueIntentReceiver mReceiver; private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); - @ClassRule - public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); @Before public void setUp() throws Exception { diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index 066300445a94..69d7586e6ab4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -40,7 +40,6 @@ import android.os.Process; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.CaptioningManager; import androidx.test.filters.SmallTest; @@ -64,6 +63,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.concurrent.Executor; + @RunWith(AndroidTestingRunner.class) @SmallTest @TestableLooper.RunWithLooper @@ -96,8 +97,6 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { @Mock private WakefulnessLifecycle mWakefullnessLifcycle; @Mock - private CaptioningManager mCaptioningManager; - @Mock private KeyguardManager mKeyguardManager; @Mock private ActivityManager mActivityManager; @@ -117,6 +116,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { when(mRingerModeLiveData.getValue()).thenReturn(-1); when(mRingerModeInternalLiveData.getValue()).thenReturn(-1); when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser()); + when(mUserTracker.getUserContext()).thenReturn(mContext); // Enable group volume adjustments mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions, @@ -127,7 +127,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { mVolumeController = new TestableVolumeDialogControllerImpl(mContext, mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager, mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager, - mPackageManager, mWakefullnessLifcycle, mCaptioningManager, mKeyguardManager, + mPackageManager, mWakefullnessLifcycle, mKeyguardManager, mActivityManager, mUserTracker, mDumpManager, mCallback); mVolumeController.setEnableDialogs(true, true); } @@ -219,6 +219,11 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { verify(mRingerModeInternalLiveData).observeForever(any()); } + @Test + public void testAddCallbackWithUserTracker() { + verify(mUserTracker).addCallback(any(UserTracker.Callback.class), any(Executor.class)); + } + static class TestableVolumeDialogControllerImpl extends VolumeDialogControllerImpl { private final WakefulnessLifecycle.Observer mWakefullessLifecycleObserver; @@ -234,7 +239,6 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { AccessibilityManager accessibilityManager, PackageManager packageManager, WakefulnessLifecycle wakefulnessLifecycle, - CaptioningManager captioningManager, KeyguardManager keyguardManager, ActivityManager activityManager, UserTracker userTracker, @@ -242,7 +246,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { C callback) { super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager, notificationManager, optionalVibrator, iAudioService, accessibilityManager, - packageManager, wakefulnessLifecycle, captioningManager, keyguardManager, + packageManager, wakefulnessLifecycle, keyguardManager, activityManager, userTracker, dumpManager); mCallbacks = callback; diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index fa18e575220c..21e4f5ad589a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -31,6 +31,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assume.assumeNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -38,7 +39,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.animation.AnimatorTestRule; import android.app.KeyguardManager; import android.content.res.Configuration; import android.media.AudioManager; @@ -85,14 +85,14 @@ import java.util.function.Predicate; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class VolumeDialogImplTest extends SysuiTestCase { - private static final AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule(); - VolumeDialogImpl mDialog; View mActiveRinger; View mDrawerContainer; View mDrawerVibrate; View mDrawerMute; View mDrawerNormal; + CaptionsToggleImageButton mODICaptionsIcon; + private TestableLooper mTestableLooper; private ConfigurationController mConfigurationController; private int mOriginalOrientation; @@ -180,6 +180,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { mDrawerVibrate = mDrawerContainer.findViewById(R.id.volume_drawer_vibrate); mDrawerMute = mDrawerContainer.findViewById(R.id.volume_drawer_mute); mDrawerNormal = mDrawerContainer.findViewById(R.id.volume_drawer_normal); + mODICaptionsIcon = mDialog.getDialogView().findViewById(R.id.odi_captions_icon); Prefs.putInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, @@ -688,6 +689,28 @@ public class VolumeDialogImplTest extends SysuiTestCase { assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.CLOSE); } + @Test + public void testOnCaptionEnabledStateChanged_checkBeforeSwitchTrue_setCaptionsEnabledState() { + ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture = + ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class); + verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any()); + VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue(); + + callbacks.onCaptionEnabledStateChanged(true, true); + verify(mVolumeDialogController).setCaptionsEnabledState(eq(false)); + } + + @Test + public void testOnCaptionEnabledStateChanged_checkBeforeSwitchFalse_getCaptionsEnabledTrue() { + ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture = + ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class); + verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any()); + VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue(); + + callbacks.onCaptionEnabledStateChanged(true, false); + assertTrue(mODICaptionsIcon.getCaptionsEnabled()); + } + /** * The content description should include ringer state, and the correct one. */ @@ -727,7 +750,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { public void teardown() { cleanUp(mDialog); setOrientation(mOriginalOrientation); - sAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration); mTestableLooper.moveTimeForward(mLongestHideShowAnimationDuration); mTestableLooper.processAllMessages(); reset(mPostureController); diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java index 19c68e86e5dc..41dbc147dfc5 100644 --- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java +++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java @@ -49,7 +49,7 @@ import java.util.function.Consumer; * public class SampleAnimatorTest { * * {@literal @}Rule - * public AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule(); + * public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); * * {@literal @}UiThreadTest * {@literal @}Test @@ -58,7 +58,7 @@ import java.util.function.Consumer; * animator.setDuration(1000L); * assertThat(animator.getAnimatedValue(), is(0)); * animator.start(); - * sAnimatorTestRule.advanceTimeBy(500L); + * mAnimatorTestRule.advanceTimeBy(500L); * assertThat(animator.getAnimatedValue(), is(500)); * } * } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt new file mode 100644 index 000000000000..823f29a910d1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeKeyguardSurfaceBehindRepository : KeyguardSurfaceBehindRepository { + private val _isAnimatingSurface = MutableStateFlow(false) + override val isAnimatingSurface = _isAnimatingSurface.asStateFlow() + + override fun setAnimatingSurface(animating: Boolean) { + _isAnimatingSurface.value = animating + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt index 312ade510784..23faaf3ef957 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.util.mockito.mock +import dagger.Lazy import kotlinx.coroutines.CoroutineScope /** @@ -30,18 +32,36 @@ object KeyguardTransitionInteractorFactory { fun create( scope: CoroutineScope, repository: KeyguardTransitionRepository = FakeKeyguardTransitionRepository(), + keyguardInteractor: KeyguardInteractor = + KeyguardInteractorFactory.create().keyguardInteractor, + fromLockscreenTransitionInteractor: Lazy<FromLockscreenTransitionInteractor> = Lazy { + mock<FromLockscreenTransitionInteractor>() + }, + fromPrimaryBouncerTransitionInteractor: Lazy<FromPrimaryBouncerTransitionInteractor> = + Lazy { + mock<FromPrimaryBouncerTransitionInteractor>() + }, ): WithDependencies { return WithDependencies( repository = repository, + keyguardInteractor = keyguardInteractor, + fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractor, + fromPrimaryBouncerTransitionInteractor = fromPrimaryBouncerTransitionInteractor, KeyguardTransitionInteractor( scope = scope, repository = repository, + keyguardInteractor = { keyguardInteractor }, + fromLockscreenTransitionInteractor = fromLockscreenTransitionInteractor, + fromPrimaryBouncerTransitionInteractor = fromPrimaryBouncerTransitionInteractor, ) ) } data class WithDependencies( val repository: KeyguardTransitionRepository, + val keyguardInteractor: KeyguardInteractor, + val fromLockscreenTransitionInteractor: Lazy<FromLockscreenTransitionInteractor>, + val fromPrimaryBouncerTransitionInteractor: Lazy<FromPrimaryBouncerTransitionInteractor>, val keyguardTransitionInteractor: KeyguardTransitionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index 0829f31e3890..dd45331df4b3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -18,9 +18,11 @@ package com.android.systemui.scene import android.content.pm.UserInfo import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel import com.android.systemui.authentication.data.repository.AuthenticationRepository import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.BouncerInteractor @@ -123,6 +125,7 @@ class SceneTestUtils( repository: SceneContainerRepository = fakeSceneContainerRepository() ): SceneInteractor { return SceneInteractor( + applicationScope = applicationScope(), repository = repository, logger = mock(), ) @@ -201,5 +204,18 @@ class SceneTestUtils( RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE), RemoteUserInput(10f, 40f, RemoteUserInputAction.UP), ) + + fun DomainLayerAuthenticationMethodModel.toDataLayer(): DataLayerAuthenticationMethodModel { + return when (this) { + DomainLayerAuthenticationMethodModel.None -> DataLayerAuthenticationMethodModel.None + DomainLayerAuthenticationMethodModel.Swipe -> + DataLayerAuthenticationMethodModel.None + DomainLayerAuthenticationMethodModel.Pin -> DataLayerAuthenticationMethodModel.Pin + DomainLayerAuthenticationMethodModel.Password -> + DataLayerAuthenticationMethodModel.Password + DomainLayerAuthenticationMethodModel.Pattern -> + DataLayerAuthenticationMethodModel.Pattern + } + } } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 70aff057c9ab..d6702b7ecf9d 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -134,6 +134,7 @@ import android.service.autofill.AutofillService; import android.service.autofill.CompositeUserData; import android.service.autofill.Dataset; import android.service.autofill.Dataset.DatasetEligibleReason; +import android.service.autofill.Field; import android.service.autofill.FieldClassification; import android.service.autofill.FieldClassification.Match; import android.service.autofill.FieldClassificationUserData; @@ -189,6 +190,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -1780,11 +1782,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState */ private static class DatasetComputationContainer { // List of all autofill ids that have a corresponding datasets - Set<AutofillId> mAutofillIds = new ArraySet<>(); + Set<AutofillId> mAutofillIds = new LinkedHashSet<>(); // Set of datasets. Kept separately, to be able to be used directly for composing // FillResponse. Set<Dataset> mDatasets = new LinkedHashSet<>(); - ArrayMap<AutofillId, Set<Dataset>> mAutofillIdToDatasetMap = new ArrayMap<>(); + Map<AutofillId, Set<Dataset>> mAutofillIdToDatasetMap = new LinkedHashMap<>(); public String toString() { final StringBuilder builder = new StringBuilder("DatasetComputationContainer["); @@ -1822,7 +1824,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // for now to keep safe. TODO(b/266379948): Revisit this logic. Set<Dataset> datasets = c2.mAutofillIdToDatasetMap.get(id); - Set<Dataset> copyDatasets = new ArraySet<>(datasets); + Set<Dataset> copyDatasets = new LinkedHashSet<>(datasets); c1.mAutofillIds.add(id); c1.mAutofillIdToDatasetMap.put(id, copyDatasets); c1.mDatasets.addAll(copyDatasets); @@ -1838,6 +1840,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + /** + * Computes datasets that are eligible to be shown based on provider detections. + * Datasets are populated in the provided container for them to be later merged with the + * PCC eligible datasets based on preference strategy. + * @param response + * @param container + */ private void computeDatasetsForProviderAndUpdateContainer( FillResponse response, DatasetComputationContainer container) { @DatasetEligibleReason int globalPickReason = PICK_REASON_UNKNOWN; @@ -1849,9 +1858,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } List<Dataset> datasets = response.getDatasets(); if (datasets == null) return; - ArrayMap<AutofillId, Set<Dataset>> autofillIdToDatasetMap = new ArrayMap<>(); + Map<AutofillId, Set<Dataset>> autofillIdToDatasetMap = new LinkedHashMap<>(); Set<Dataset> eligibleDatasets = new LinkedHashSet<>(); - Set<AutofillId> eligibleAutofillIds = new ArraySet<>(); + Set<AutofillId> eligibleAutofillIds = new LinkedHashSet<>(); for (Dataset dataset : response.getDatasets()) { if (dataset.getFieldIds() == null || dataset.getFieldIds().isEmpty()) continue; @DatasetEligibleReason int pickReason = globalPickReason; @@ -1927,7 +1936,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState eligibleAutofillIds.add(id); Set<Dataset> datasetForIds = autofillIdToDatasetMap.get(id); if (datasetForIds == null) { - datasetForIds = new ArraySet<>(); + datasetForIds = new LinkedHashSet<>(); } datasetForIds.add(dataset); autofillIdToDatasetMap.put(id, datasetForIds); @@ -1938,23 +1947,30 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState container.mAutofillIds = eligibleAutofillIds; } + /** + * Computes datasets that are eligible to be shown based on PCC detections. + * Datasets are populated in the provided container for them to be later merged with the + * provider eligible datasets based on preference strategy. + * @param response + * @param container + */ private void computeDatasetsForPccAndUpdateContainer( FillResponse response, DatasetComputationContainer container) { List<Dataset> datasets = response.getDatasets(); if (datasets == null) return; synchronized (mLock) { - ArrayMap<String, Set<AutofillId>> hintsToAutofillIdMap = + Map<String, Set<AutofillId>> hintsToAutofillIdMap = mClassificationState.mHintsToAutofillIdMap; // TODO(266379948): Handle group hints too. - ArrayMap<String, Set<AutofillId>> groupHintsToAutofillIdMap = + Map<String, Set<AutofillId>> groupHintsToAutofillIdMap = mClassificationState.mGroupHintsToAutofillIdMap; - ArrayMap<AutofillId, Set<Dataset>> map = new ArrayMap<>(); + Map<AutofillId, Set<Dataset>> map = new LinkedHashMap<>(); Set<Dataset> eligibleDatasets = new LinkedHashSet<>(); - Set<AutofillId> eligibleAutofillIds = new ArraySet<>(); + Set<AutofillId> eligibleAutofillIds = new LinkedHashSet<>(); for (int i = 0; i < datasets.size(); i++) { @@ -1970,13 +1986,35 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState ArrayList<InlinePresentation> fieldInlinePresentations = new ArrayList<>(); ArrayList<InlinePresentation> fieldInlineTooltipPresentations = new ArrayList<>(); ArrayList<Dataset.DatasetFieldFilter> fieldFilters = new ArrayList<>(); - Set<AutofillId> datasetAutofillIds = new ArraySet<>(); + Set<AutofillId> datasetAutofillIds = new LinkedHashSet<>(); + + boolean isDatasetAvailable = false; + Set<AutofillId> additionalDatasetAutofillIds = new LinkedHashSet<>(); + Set<AutofillId> additionalEligibleAutofillIds = new LinkedHashSet<>(); for (int j = 0; j < dataset.getAutofillDatatypes().size(); j++) { if (dataset.getAutofillDatatypes().get(j) == null) { + // TODO : revisit pickReason logic if (dataset.getFieldIds() != null && dataset.getFieldIds().get(j) != null) { pickReason = PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER; } + // Check if the autofill id at this index is detected by PCC. + // If not, add that id here, otherwise, we can have duplicates when later + // merging with provider datasets. + // Howover, this doesn't make datasetAvailable for PCC on its own. + // For that, there has to be a datatype detected by PCC, and the dataset + // for that datatype provided by the provider. + AutofillId autofillId = dataset.getFieldIds().get(j); + if (!mClassificationState.mClassificationCombinedHintsMap + .containsKey(autofillId)) { + additionalEligibleAutofillIds.add(autofillId); + additionalDatasetAutofillIds.add(autofillId); + // For each of the field, copy over values. + copyFieldsFromDataset(dataset, j, autofillId, fieldIds, fieldValues, + fieldPresentations, fieldDialogPresentations, + fieldInlinePresentations, fieldInlineTooltipPresentations, + fieldFilters); + } continue; } String hint = dataset.getAutofillDatatypes().get(j); @@ -1984,22 +2022,18 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (hintsToAutofillIdMap.containsKey(hint)) { ArrayList<AutofillId> tempIds = new ArrayList<>(hintsToAutofillIdMap.get(hint)); - + if (tempIds.isEmpty()) { + continue; + } + isDatasetAvailable = true; for (AutofillId autofillId : tempIds) { eligibleAutofillIds.add(autofillId); datasetAutofillIds.add(autofillId); // For each of the field, copy over values. - fieldIds.add(autofillId); - fieldValues.add(dataset.getFieldValues().get(j)); - // TODO(b/266379948): might need to make it more efficient by not - // copying over value if it didn't exist. This would require creating - // a getter for the presentations arraylist. - fieldPresentations.add(dataset.getFieldPresentation(j)); - fieldDialogPresentations.add(dataset.getFieldDialogPresentation(j)); - fieldInlinePresentations.add(dataset.getFieldInlinePresentation(j)); - fieldInlineTooltipPresentations.add( - dataset.getFieldInlineTooltipPresentation(j)); - fieldFilters.add(dataset.getFilter(j)); + copyFieldsFromDataset(dataset, j, autofillId, fieldIds, fieldValues, + fieldPresentations, fieldDialogPresentations, + fieldInlinePresentations, fieldInlineTooltipPresentations, + fieldFilters); } } // TODO(b/266379948): handle the case: @@ -2008,34 +2042,38 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // TODO(b/266379948): also handle the case where there could be more types in // the dataset, provided by the provider, however, they aren't applicable. } - Dataset newDataset = - new Dataset( - fieldIds, - fieldValues, - fieldPresentations, - fieldDialogPresentations, - fieldInlinePresentations, - fieldInlineTooltipPresentations, - fieldFilters, - new ArrayList<>(), - dataset.getFieldContent(), - null, - null, - null, - null, - dataset.getId(), - dataset.getAuthentication()); - newDataset.setEligibleReasonReason(pickReason); - eligibleDatasets.add(newDataset); - Set<Dataset> newDatasets; - for (AutofillId autofillId : datasetAutofillIds) { - if (map.containsKey(autofillId)) { - newDatasets = map.get(autofillId); - } else { - newDatasets = new ArraySet<>(); + if (isDatasetAvailable) { + datasetAutofillIds.addAll(additionalDatasetAutofillIds); + eligibleAutofillIds.addAll(additionalEligibleAutofillIds); + Dataset newDataset = + new Dataset( + fieldIds, + fieldValues, + fieldPresentations, + fieldDialogPresentations, + fieldInlinePresentations, + fieldInlineTooltipPresentations, + fieldFilters, + new ArrayList<>(), + dataset.getFieldContent(), + null, + null, + null, + null, + dataset.getId(), + dataset.getAuthentication()); + newDataset.setEligibleReasonReason(pickReason); + eligibleDatasets.add(newDataset); + Set<Dataset> newDatasets; + for (AutofillId autofillId : datasetAutofillIds) { + if (map.containsKey(autofillId)) { + newDatasets = map.get(autofillId); + } else { + newDatasets = new LinkedHashSet<>(); + } + newDatasets.add(newDataset); + map.put(autofillId, newDatasets); } - newDatasets.add(newDataset); - map.put(autofillId, newDatasets); } } container.mAutofillIds = eligibleAutofillIds; @@ -2044,6 +2082,31 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + private void copyFieldsFromDataset( + Dataset dataset, + int index, + AutofillId autofillId, + ArrayList<AutofillId> fieldIds, + ArrayList<AutofillValue> fieldValues, + ArrayList<RemoteViews> fieldPresentations, + ArrayList<RemoteViews> fieldDialogPresentations, + ArrayList<InlinePresentation> fieldInlinePresentations, + ArrayList<InlinePresentation> fieldInlineTooltipPresentations, + ArrayList<Dataset.DatasetFieldFilter> fieldFilters) { + // copy over values + fieldIds.add(autofillId); + fieldValues.add(dataset.getFieldValues().get(index)); + // TODO(b/266379948): might need to make it more efficient by not + // copying over value if it didn't exist. This would require creating + // a getter for the presentations arraylist. + fieldPresentations.add(dataset.getFieldPresentation(index)); + fieldDialogPresentations.add(dataset.getFieldDialogPresentation(index)); + fieldInlinePresentations.add(dataset.getFieldInlinePresentation(index)); + fieldInlineTooltipPresentations.add( + dataset.getFieldInlineTooltipPresentation(index)); + fieldFilters.add(dataset.getFilter(index)); + } + // FillServiceCallbacks @Override @SuppressWarnings("GuardedBy") @@ -2577,10 +2640,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (sDebug) Slog.d(TAG, "Updating client state from auth dataset"); mClientState = newClientState; } - Dataset dataset = (Dataset) result; - FillResponse temp = new FillResponse.Builder().addDataset(dataset).build(); - temp = getEffectiveFillResponse(temp); - dataset = temp.getDatasets().get(0); + Dataset dataset = getEffectiveDatasetForAuthentication((Dataset) result); final Dataset oldDataset = authenticatedResponse.getDatasets().get(datasetIdx); if (!isAuthResultDatasetEphemeral(oldDataset, data)) { authenticatedResponse.getDatasets().set(datasetIdx, dataset); @@ -2606,6 +2666,39 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + Dataset getEffectiveDatasetForAuthentication(Dataset authenticatedDataset) { + FillResponse response = new FillResponse.Builder().addDataset(authenticatedDataset).build(); + response = getEffectiveFillResponse(response); + if (DBG) { + Slog.d(TAG, "DBG: authenticated effective response: " + response); + } + if (response == null || response.getDatasets().size() == 0) { + Log.wtf(TAG, "No datasets in fill response on authentication. response = " + + (response == null ? "null" : response.toString())); + return authenticatedDataset; + } + List<Dataset> datasets = response.getDatasets(); + Dataset result = response.getDatasets().get(0); + if (datasets.size() > 1) { + Dataset.Builder builder = new Dataset.Builder(); + for (Dataset dataset : datasets) { + if (!dataset.getFieldIds().isEmpty()) { + for (int i = 0; i < dataset.getFieldIds().size(); i++) { + builder.setField(dataset.getFieldIds().get(i), + new Field.Builder().setValue(dataset.getFieldValues().get(i)) + .build()); + } + } + } + result = builder.setId(authenticatedDataset.getId()).build(); + } + + if (DBG) { + Slog.d(TAG, "DBG: authenticated effective dataset after auth: " + result); + } + return result; + } + /** * Returns whether the dataset returned from the authentication result is ephemeral or not. * See {@link AutofillManager#EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET} for more diff --git a/services/core/java/com/android/server/PendingIntentUtils.java b/services/core/java/com/android/server/PendingIntentUtils.java index 1600101b20f4..a72a4d254a2a 100644 --- a/services/core/java/com/android/server/PendingIntentUtils.java +++ b/services/core/java/com/android/server/PendingIntentUtils.java @@ -34,6 +34,7 @@ public class PendingIntentUtils { public static Bundle createDontSendToRestrictedAppsBundle(@Nullable Bundle bundle) { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setDontSendToRestrictedApps(true); + options.setPendingIntentBackgroundActivityLaunchAllowed(false); if (bundle == null) { return options.toBundle(); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 992c18a3fd65..66ea4d06d2b7 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -2501,12 +2501,12 @@ public final class ActiveServices { FGS_STOP_REASON_STOP_FOREGROUND, FGS_TYPE_POLICY_CHECK_UNKNOWN); - // foregroundServiceType is used in logFGSStateChangeLocked(), so we can't clear it - // earlier. - r.foregroundServiceType = 0; synchronized (mFGSLogger) { mFGSLogger.logForegroundServiceStop(r.appInfo.uid, r); } + // foregroundServiceType is used in logFGSStateChangeLocked(), so we can't clear it + // earlier. + r.foregroundServiceType = 0; r.mFgsNotificationWasDeferred = false; signalForegroundServiceObserversLocked(r); resetFgsRestrictionLocked(r); diff --git a/services/core/java/com/android/server/am/CoreSettingsObserver.java b/services/core/java/com/android/server/am/CoreSettingsObserver.java index 7482e64f1d26..182205a86a7d 100644 --- a/services/core/java/com/android/server/am/CoreSettingsObserver.java +++ b/services/core/java/com/android/server/am/CoreSettingsObserver.java @@ -30,6 +30,7 @@ import android.widget.WidgetFlags; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import java.util.ArrayList; import java.util.HashMap; @@ -161,6 +162,12 @@ final class CoreSettingsObserver extends ContentObserver { WidgetFlags.MAGNIFIER_ASPECT_RATIO_DEFAULT)); sDeviceConfigEntries.add(new DeviceConfigEntry<Boolean>( + DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION, + SystemUiDeviceConfigFlags.KEY_REMOTEVIEWS_ADAPTER_CONVERSION, boolean.class, + SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION_DEFAULT)); + + sDeviceConfigEntries.add(new DeviceConfigEntry<Boolean>( TextFlags.NAMESPACE, TextFlags.ENABLE_NEW_CONTEXT_MENU, TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, boolean.class, TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT)); diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java index 786e1cc7075f..f6859d1f027e 100644 --- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java +++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java @@ -141,6 +141,10 @@ public class ForegroundServiceTypeLoggerModule { // grab the appropriate types final IntArray apiTypes = convertFgsTypeToApiTypes(record.foregroundServiceType); + if (apiTypes.size() == 0) { + Slog.w(TAG, "Foreground service start for UID: " + + uid + " does not have any types"); + } // now we need to iterate through the types // and insert the new record as needed final IntArray apiTypesFound = new IntArray(); @@ -201,6 +205,9 @@ public class ForegroundServiceTypeLoggerModule { // and also clean up the start calls stack by UID final IntArray apiTypes = convertFgsTypeToApiTypes(record.foregroundServiceType); final UidState uidState = mUids.get(uid); + if (apiTypes.size() == 0) { + Slog.w(TAG, "FGS stop call for: " + uid + " has no types!"); + } if (uidState == null) { Slog.w(TAG, "FGS stop call being logged with no start call for UID for UID " + uid @@ -460,16 +467,17 @@ public class ForegroundServiceTypeLoggerModule { public void logFgsApiEvent(ServiceRecord r, int fgsState, @FgsApiState int apiState, @ForegroundServiceApiType int apiType, long timestamp) { - long apiDurationBeforeFgsStart = r.createRealTime - timestamp; - long apiDurationAfterFgsEnd = timestamp - r.mFgsExitTime; + long apiDurationBeforeFgsStart = 0; + long apiDurationAfterFgsEnd = 0; UidState uidState = mUids.get(r.appInfo.uid); - if (uidState != null) { - if (uidState.mFirstFgsTimeStamp.contains(apiType)) { - apiDurationBeforeFgsStart = uidState.mFirstFgsTimeStamp.get(apiType) - timestamp; - } - if (uidState.mLastFgsTimeStamp.contains(apiType)) { - apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType); - } + if (uidState == null) { + return; + } + if (uidState.mFirstFgsTimeStamp.contains(apiType)) { + apiDurationBeforeFgsStart = uidState.mFirstFgsTimeStamp.get(apiType) - timestamp; + } + if (uidState.mLastFgsTimeStamp.contains(apiType)) { + apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType); } final int[] apiTypes = new int[1]; apiTypes[0] = apiType; @@ -525,10 +533,11 @@ public class ForegroundServiceTypeLoggerModule { @ForegroundServiceApiType int apiType, long timestamp) { long apiDurationAfterFgsEnd = 0; UidState uidState = mUids.get(uid); - if (uidState != null) { - if (uidState.mLastFgsTimeStamp.contains(apiType)) { - apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType); - } + if (uidState == null) { + return; + } + if (uidState.mLastFgsTimeStamp.contains(apiType)) { + apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType); } final int[] apiTypes = new int[1]; apiTypes[0] = apiType; diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index f7bbc8b50bc0..8a8e2af2a547 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -286,6 +286,12 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN + ")"); } + private String getFgsInfoForWtf() { + return " cmp: " + this.getComponentName().toShortString() + + " sdk: " + this.appInfo.targetSdkVersion + ; + } + void maybeLogFgsLogicChange() { final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding, mAllowWIUInBindService); @@ -311,7 +317,8 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN + " OS:" // Orig-start + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService) + " NS:" // New-start - + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings); + + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings) + + getFgsInfoForWtf(); Slog.wtf(TAG_SERVICE, message); } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index d8266ec55833..9f9e2eb38288 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -435,7 +435,7 @@ public class AudioDeviceBroker { // LE Audio it stays the same and we must trigger the proper stream volume alignment, if // LE Audio communication device is activated after the audio system has already switched to // MODE_IN_CALL mode. - if (isBluetoothLeAudioRequested()) { + if (isBluetoothLeAudioRequested() && device != null) { final int streamType = mAudioService.getBluetoothContextualVolumeStream(); final int leAudioVolIndex = getVssVolumeForDevice(streamType, device.getInternalType()); final int leAudioMaxVolIndex = getMaxVssVolumeForStream(streamType); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 5a92cb464950..13e3fc7852e9 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -2070,12 +2070,18 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeLeAudioDeviceAvailable( AudioDeviceBroker.BtDeviceInfo btInfo, int streamType, String eventSource) { - final String address = btInfo.mDevice.getAddress(); - final String name = BtHelper.getName(btInfo.mDevice); final int volumeIndex = btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10; final int device = btInfo.mAudioSystemDevice; if (device != AudioSystem.DEVICE_NONE) { + final String address = btInfo.mDevice.getAddress(); + String name = BtHelper.getName(btInfo.mDevice); + + // The BT Stack does not provide a name for LE Broadcast devices + if (device == AudioSystem.DEVICE_OUT_BLE_BROADCAST && name.equals("")) { + name = "Broadcast"; + } + /* Audio Policy sees Le Audio similar to A2DP. Let's make sure * AUDIO_POLICY_FORCE_NO_BT_A2DP is not set */ diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index cb5e7f1be571..2ae3118d7bfa 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -150,6 +150,10 @@ public final class AuthSession implements IBinder.DeathRecipient { // Timestamp when hardware authentication occurred private long mAuthenticatedTimeMs; + @NonNull + private final OperationContextExt mOperationContext; + + AuthSession(@NonNull Context context, @NonNull BiometricContext biometricContext, @NonNull IStatusBarService statusBarService, @@ -215,6 +219,7 @@ public final class AuthSession implements IBinder.DeathRecipient { mFingerprintSensorProperties = fingerprintSensorProperties; mCancelled = false; mBiometricFrameworkStatsLogger = logger; + mOperationContext = new OperationContextExt(true /* isBP */); try { mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */); @@ -581,6 +586,8 @@ public final class AuthSession implements IBinder.DeathRecipient { } else { Slog.d(TAG, "delaying fingerprint sensor start"); } + + mBiometricContext.updateContext(mOperationContext, isCrypto()); } // call once anytime after onDialogAnimatedIn() to indicate it's appropriate to start the @@ -743,12 +750,12 @@ public final class AuthSession implements IBinder.DeathRecipient { + ", Client: " + BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT + ", RequireConfirmation: " + mPreAuthInfo.confirmationRequested + ", State: " + FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__CONFIRMED - + ", Latency: " + latency); + + ", Latency: " + latency + + ", SessionId: " + mOperationContext.getId()); } mBiometricFrameworkStatsLogger.authenticate( - mBiometricContext.updateContext(new OperationContextExt(true /* isBP */), - isCrypto()), + mOperationContext, statsModality(), BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT, @@ -780,13 +787,13 @@ public final class AuthSession implements IBinder.DeathRecipient { + ", Client: " + BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT + ", Reason: " + reason + ", Error: " + error - + ", Latency: " + latency); + + ", Latency: " + latency + + ", SessionId: " + mOperationContext.getId()); } // Auth canceled if (error != 0) { mBiometricFrameworkStatsLogger.error( - mBiometricContext.updateContext(new OperationContextExt(true /* isBP */), - isCrypto()), + mOperationContext, statsModality(), BiometricsProtoEnums.ACTION_AUTHENTICATE, BiometricsProtoEnums.CLIENT_BIOMETRIC_PROMPT, diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStats.java b/services/core/java/com/android/server/biometrics/AuthenticationStats.java new file mode 100644 index 000000000000..137a418e31ab --- /dev/null +++ b/services/core/java/com/android/server/biometrics/AuthenticationStats.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +/** + * Utility class for on-device biometric authentication data, including total authentication, + * rejections, and the number of sent enrollment notifications. + */ +public class AuthenticationStats { + + private final int mUserId; + private int mTotalAttempts; + private int mRejectedAttempts; + private int mEnrollmentNotifications; + private final int mModality; + + public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts, + int enrollmentNotifications, final int modality) { + mUserId = userId; + mTotalAttempts = totalAttempts; + mRejectedAttempts = rejectedAttempts; + mEnrollmentNotifications = enrollmentNotifications; + mModality = modality; + } + + public AuthenticationStats(final int userId, final int modality) { + mUserId = userId; + mTotalAttempts = 0; + mRejectedAttempts = 0; + mEnrollmentNotifications = 0; + mModality = modality; + } + + public int getUserId() { + return mUserId; + } + + public int getTotalAttempts() { + return mTotalAttempts; + } + + public int getRejectedAttempts() { + return mRejectedAttempts; + } + + public int getEnrollmentNotifications() { + return mEnrollmentNotifications; + } + + public int getModality() { + return mModality; + } + + /** Calculate FRR. */ + public float getFrr() { + if (mTotalAttempts > 0) { + return mRejectedAttempts / (float) mTotalAttempts; + } else { + return -1.0f; + } + } + + /** Update total authentication attempts and rejections. */ + public void authenticate(boolean authenticated) { + if (!authenticated) { + mRejectedAttempts++; + } + mTotalAttempts++; + } + + /** Reset total authentication attempts and rejections. */ + public void resetData() { + mTotalAttempts = 0; + mRejectedAttempts = 0; + } +} diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java new file mode 100644 index 000000000000..c9cd8148c0f1 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java @@ -0,0 +1,106 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.HashMap; +import java.util.Map; + +/** + * Calculate and collect on-device False Rejection Rates (FRR). + * FRR = All [given biometric modality] unlock failures / all [given biometric modality] unlock + * attempts. + */ +public class AuthenticationStatsCollector { + + private static final String TAG = "AuthenticationStatsCollector"; + + // The minimum number of attempts that will calculate the FRR and trigger the notification. + private static final int MINIMUM_ATTEMPTS = 500; + // The maximum number of eligible biometric enrollment notification can be sent. + private static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 2; + + private final float mThreshold; + private final int mModality; + + @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap; + + public AuthenticationStatsCollector(@NonNull Context context, int modality) { + mThreshold = context.getResources() + .getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1); + mUserAuthenticationStatsMap = new HashMap<>(); + mModality = modality; + } + + /** Update total authentication and rejected attempts. */ + public void authenticate(int userId, boolean authenticated) { + // Check if this is a new user. + if (!mUserAuthenticationStatsMap.containsKey(userId)) { + mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality)); + } + + AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); + + authenticationStats.authenticate(authenticated); + + persistDataIfNeeded(userId); + sendNotificationIfNeeded(userId); + } + + private void sendNotificationIfNeeded(int userId) { + AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); + if (authenticationStats.getTotalAttempts() >= MINIMUM_ATTEMPTS) { + // Send notification if FRR exceeds the threshold + if (authenticationStats.getEnrollmentNotifications() < MAXIMUM_ENROLLMENT_NOTIFICATIONS + && authenticationStats.getFrr() >= mThreshold) { + // TODO(wenhuiy): Send notifications. + } + + authenticationStats.resetData(); + } + } + + private void persistDataIfNeeded(int userId) { + AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); + if (authenticationStats.getTotalAttempts() % 50 == 0) { + // TODO(wenhuiy): Persist data. + } + } + + /** + * Only being used in tests. Callers should not make any changes to the returned + * authentication stats. + * + * @return AuthenticationStats of the user, or null if the stats doesn't exist. + */ + @Nullable + @VisibleForTesting + AuthenticationStats getAuthenticationStatsForUser(int userId) { + return mUserAuthenticationStatsMap.getOrDefault(userId, null); + } + + @VisibleForTesting + void setAuthenticationStatsForUser(int userId, AuthenticationStats authenticationStats) { + mUserAuthenticationStatsMap.put(userId, authenticationStats); + } +} diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java index c76a2e38aabc..87037af11a84 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java @@ -27,6 +27,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.Utils; /** @@ -41,6 +42,7 @@ public class BiometricLogger { private final int mStatsAction; private final int mStatsClient; private final BiometricFrameworkStatsLogger mSink; + @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector; @NonNull private final ALSProbe mALSProbe; private long mFirstAcquireTimeMs; @@ -49,7 +51,8 @@ public class BiometricLogger { /** Get a new logger with all unknown fields (for operations that do not require logs). */ public static BiometricLogger ofUnknown(@NonNull Context context) { return new BiometricLogger(context, BiometricsProtoEnums.MODALITY_UNKNOWN, - BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN); + BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN, + null /* AuthenticationStatsCollector */); } /** @@ -64,26 +67,32 @@ public class BiometricLogger { * @param statsClient One of {@link BiometricsProtoEnums} CLIENT_* constants. */ public BiometricLogger( - @NonNull Context context, int statsModality, int statsAction, int statsClient) { + @NonNull Context context, int statsModality, int statsAction, int statsClient, + AuthenticationStatsCollector authenticationStatsCollector) { this(statsModality, statsAction, statsClient, BiometricFrameworkStatsLogger.getInstance(), + authenticationStatsCollector, context.getSystemService(SensorManager.class)); } @VisibleForTesting BiometricLogger( int statsModality, int statsAction, int statsClient, - BiometricFrameworkStatsLogger logSink, SensorManager sensorManager) { + BiometricFrameworkStatsLogger logSink, + @NonNull AuthenticationStatsCollector statsCollector, + SensorManager sensorManager) { mStatsModality = statsModality; mStatsAction = statsAction; mStatsClient = statsClient; mSink = logSink; + mAuthenticationStatsCollector = statsCollector; mALSProbe = new ALSProbe(sensorManager); } /** Creates a new logger with the action replaced with the new action. */ public BiometricLogger swapAction(@NonNull Context context, int statsAction) { - return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient); + return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient, + null /* AuthenticationStatsCollector */); } /** Disable logging metrics and only log critical events, such as system health issues. */ @@ -192,10 +201,13 @@ public class BiometricLogger { public void logOnAuthenticated(Context context, OperationContextExt operationContext, boolean authenticated, boolean requireConfirmation, int targetUserId, boolean isBiometricPrompt) { + // Do not log metrics when fingerprint enrollment reason is ENROLL_FIND_SENSOR if (!mShouldLogMetrics) { return; } + mAuthenticationStatsCollector.authenticate(targetUserId, authenticated); + int authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__UNKNOWN; if (!authenticated) { authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__REJECTED; 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 33ed63ce07e0..a7d160c4fa60 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 @@ -49,6 +49,7 @@ import android.util.proto.ProtoOutputStream; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -112,6 +113,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private final BiometricContext mBiometricContext; @NonNull private final AuthSessionCoordinator mAuthSessionCoordinator; + @NonNull + private final AuthenticationStatsCollector mAuthenticationStatsCollector; @Nullable private IFace mDaemon; @@ -173,6 +176,9 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator(); mDaemon = daemon; + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FACE); + for (SensorProps prop : props) { final int sensorId = prop.commonProps.sensorId; @@ -283,7 +289,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mContext, mFaceSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mFaceSensors.get(sensorId).getAuthenticatorIds()); @@ -341,7 +348,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { final FaceInvalidationClient client = new FaceInvalidationClient(mContext, mFaceSensors.get(sensorId).getLazySession(), userId, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mFaceSensors.get(sensorId).getAuthenticatorIds(), callback); scheduleForSensor(sensorId, client); @@ -372,7 +380,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext); scheduleForSensor(sensorId, client); }); @@ -386,7 +395,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mFaceSensors.get(sensorId).getLazySession(), token, userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, challenge); scheduleForSensor(sensorId, client); }); @@ -407,7 +417,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { opPackageName, id, FaceUtils.getInstance(sensorId), disabledFeatures, ENROLL_TIMEOUT_SEC, previewSurface, sensorId, createLogger(BiometricsProtoEnums.ACTION_ENROLL, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, maxTemplatesPerUser, debugConsent); scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( mBiometricStateCallback, new ClientMonitorCallback() { @@ -443,7 +454,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { final FaceDetectClient client = new FaceDetectClient(mContext, mFaceSensors.get(sensorId).getLazySession(), token, id, callback, options, - createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, + mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); @@ -471,7 +483,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mContext, mFaceSensors.get(sensorId).getLazySession(), token, requestId, callback, operationId, restricted, options, cookie, false /* requireConfirmation */, - createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, + mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, mUsageStats, mFaceSensors.get(sensorId).getLockoutCache(), allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId)); @@ -540,7 +553,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { new ClientMonitorCallbackConverter(receiver), faceIds, userId, opPackageName, FaceUtils.getInstance(sensorId), sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mFaceSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client, mBiometricStateCallback); @@ -554,7 +568,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mContext, mFaceSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, hardwareAuthToken, mFaceSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId)); @@ -624,7 +639,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mFaceSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_ENUMERATE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, FaceUtils.getInstance(sensorId), mFaceSensors.get(sensorId).getAuthenticatorIds()); @@ -636,9 +652,10 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { }); } - private BiometricLogger createLogger(int statsAction, int statsClient) { + private BiometricLogger createLogger(int statsAction, int statsClient, + AuthenticationStatsCollector authenticationStatsCollector) { return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FACE, - statsAction, statsClient); + statsAction, statsClient, authenticationStatsCollector); } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index 1e33c96d50ad..10991d5f9133 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -52,6 +52,7 @@ import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.SensorServiceStateProto; import com.android.server.biometrics.SensorStateProto; import com.android.server.biometrics.UserStateProto; @@ -124,6 +125,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @Nullable private IBiometricsFace mDaemon; @NonNull private final HalResultController mHalResultController; @NonNull private final BiometricContext mBiometricContext; + @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector; // for requests that do not use biometric prompt @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0); private int mCurrentUserId = UserHandle.USER_NULL; @@ -364,6 +366,9 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { mCurrentUserId = UserHandle.USER_NULL; }); + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FACE); + try { ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG); } catch (RemoteException e) { @@ -554,7 +559,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId, opPackageName, mSensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, sSystemClock.millis()); mGeneratedChallengeCache = client; mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @@ -586,7 +591,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { final FaceRevokeChallengeClient client = new FaceRevokeChallengeClient(mContext, mLazyDaemon, token, userId, opPackageName, mSensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext); mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @Override @@ -617,7 +622,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { opPackageName, id, FaceUtils.getLegacyInstance(mSensorId), disabledFeatures, ENROLL_TIMEOUT_SEC, previewSurface, mSensorId, createLogger(BiometricsProtoEnums.ACTION_ENROLL, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext); mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @@ -677,7 +682,8 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { final FaceAuthenticationClient client = new FaceAuthenticationClient(mContext, mLazyDaemon, token, requestId, receiver, operationId, restricted, options, cookie, false /* requireConfirmation */, - createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, + mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, mLockoutTracker, mUsageStats, allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId)); @@ -713,7 +719,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { new ClientMonitorCallbackConverter(receiver), faceId, userId, opPackageName, FaceUtils.getLegacyInstance(mSensorId), mSensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, mAuthenticatorIds); mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); @@ -731,7 +737,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { opPackageName, FaceUtils.getLegacyInstance(mSensorId), mSensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, mAuthenticatorIds); mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); @@ -750,7 +756,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { final FaceResetLockoutClient client = new FaceResetLockoutClient(mContext, mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, hardwareAuthToken); mScheduler.scheduleClientMonitor(client); }); @@ -821,7 +827,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { final FaceInternalCleanupClient client = new FaceInternalCleanupClient(mContext, mLazyDaemon, userId, mContext.getOpPackageName(), mSensorId, createLogger(BiometricsProtoEnums.ACTION_ENUMERATE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds); mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback, @@ -953,7 +959,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { final FaceUpdateActiveUserClient client = new FaceUpdateActiveUserClient(mContext, mLazyDaemon, targetUserId, mContext.getOpPackageName(), mSensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, hasEnrolled, mAuthenticatorIds); mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @Override @@ -968,9 +974,10 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { }); } - private BiometricLogger createLogger(int statsAction, int statsClient) { + private BiometricLogger createLogger(int statsAction, int statsClient, + AuthenticationStatsCollector authenticationStatsCollector) { return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FACE, - statsAction, statsClient); + statsAction, statsClient, authenticationStatsCollector); } /** 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 0421d78b73b9..2d062db12cdc 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 @@ -57,6 +57,7 @@ import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -122,6 +123,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Nullable private IUdfpsOverlayController mUdfpsOverlayController; @Nullable private ISidefpsController mSidefpsController; private AuthSessionCoordinator mAuthSessionCoordinator; + @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector; private final class BiometricTaskStackListener extends TaskStackListener { @Override @@ -181,6 +183,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator(); mDaemon = daemon; + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FINGERPRINT); + final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context); for (SensorProps prop : props) { @@ -338,7 +343,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mFingerprintSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mFingerprintSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client); @@ -363,7 +369,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mContext, mFingerprintSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, hardwareAuthToken, mFingerprintSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId)); @@ -380,7 +387,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mFingerprintSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext); scheduleForSensor(sensorId, client); }); @@ -395,7 +402,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mFingerprintSensors.get(sensorId).getLazySession(), token, userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, challenge); scheduleForSensor(sensorId, client); }); @@ -415,7 +423,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, opPackageName, FingerprintUtils.getInstance(sensorId), sensorId, createLogger(BiometricsProtoEnums.ACTION_ENROLL, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, mFingerprintSensors.get(sensorId).getSensorProperties(), mUdfpsOverlayController, mSidefpsController, @@ -455,7 +463,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi final FingerprintDetectClient client = new FingerprintDetectClient(mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, id, callback, options, - createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, + mAuthenticationStatsCollector), mBiometricContext, mUdfpsOverlayController, isStrongBiometric); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); @@ -477,7 +486,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, requestId, callback, operationId, restricted, options, cookie, false /* requireConfirmation */, - createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, + mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, mTaskStackListener, mFingerprintSensors.get(sensorId).getLockoutCache(), mUdfpsOverlayController, mSidefpsController, @@ -566,7 +576,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi new ClientMonitorCallbackConverter(receiver), fingerprintIds, userId, opPackageName, FingerprintUtils.getInstance(sensorId), sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mFingerprintSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client, mBiometricStateCallback); @@ -588,7 +599,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mFingerprintSensors.get(sensorId).getLazySession(), userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_ENUMERATE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, FingerprintUtils.getInstance(sensorId), mFingerprintSensors.get(sensorId).getAuthenticatorIds()); @@ -600,9 +612,10 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi }); } - private BiometricLogger createLogger(int statsAction, int statsClient) { + private BiometricLogger createLogger(int statsAction, int statsClient, + AuthenticationStatsCollector authenticationStatsCollector) { return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT, - statsAction, statsClient); + statsAction, statsClient, authenticationStatsCollector); } @Override @@ -635,7 +648,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi new FingerprintInvalidationClient(mContext, mFingerprintSensors.get(sensorId).getLazySession(), userId, sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mFingerprintSensors.get(sensorId).getAuthenticatorIds(), callback); scheduleForSensor(sensorId, client); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index 92b216df2fdd..4b07dca75e9e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -52,6 +52,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.SensorServiceStateProto; import com.android.server.biometrics.SensorStateProto; import com.android.server.biometrics.UserStateProto; @@ -123,6 +124,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider @Nullable private IUdfpsOverlayController mUdfpsOverlayController; @Nullable private ISidefpsController mSidefpsController; @NonNull private final BiometricContext mBiometricContext; + @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector; // for requests that do not use biometric prompt @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0); private int mCurrentUserId = UserHandle.USER_NULL; @@ -351,6 +353,9 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider mCurrentUserId = UserHandle.USER_NULL; }); + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + BiometricsProtoEnums.MODALITY_FINGERPRINT); + try { ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG); } catch (RemoteException e) { @@ -497,7 +502,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider new FingerprintUpdateActiveUserClient(mContext, mLazyDaemon, targetUserId, mContext.getOpPackageName(), mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, this::getCurrentUser, hasEnrolled, mAuthenticatorIds, force); mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @@ -544,7 +550,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider final FingerprintResetLockoutClient client = new FingerprintResetLockoutClient(mContext, userId, mContext.getOpPackageName(), sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mLockoutTracker); mScheduler.scheduleClientMonitor(client); }); @@ -559,7 +566,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider new ClientMonitorCallbackConverter(receiver), userId, opPackageName, mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext); mScheduler.scheduleClientMonitor(client); }); @@ -573,7 +581,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider mContext, mLazyDaemon, token, userId, opPackageName, mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext); mScheduler.scheduleClientMonitor(client); }); @@ -594,7 +603,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider FingerprintUtils.getLegacyInstance(mSensorId), ENROLL_TIMEOUT_SEC, mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_ENROLL, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, mUdfpsOverlayController, mSidefpsController, enrollReason); mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @Override @@ -639,7 +648,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider final boolean isStrongBiometric = Utils.isStrongBiometric(mSensorProperties.sensorId); final FingerprintDetectClient client = new FingerprintDetectClient(mContext, mLazyDaemon, token, id, listener, options, - createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, + mAuthenticationStatsCollector), mBiometricContext, mUdfpsOverlayController, isStrongBiometric); mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); @@ -660,7 +670,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient( mContext, mLazyDaemon, token, requestId, listener, operationId, restricted, options, cookie, false /* requireConfirmation */, - createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), + createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, + mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, mTaskStackListener, mLockoutTracker, mUdfpsOverlayController, mSidefpsController, @@ -706,7 +717,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider userId, opPackageName, FingerprintUtils.getLegacyInstance(mSensorId), mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, mAuthenticatorIds); mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); @@ -726,7 +737,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider FingerprintUtils.getLegacyInstance(mSensorId), mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, mAuthenticatorIds); mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); @@ -741,7 +752,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider mContext, mLazyDaemon, userId, mContext.getOpPackageName(), mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_ENUMERATE, - BiometricsProtoEnums.CLIENT_UNKNOWN), + BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, FingerprintUtils.getLegacyInstance(mSensorId), mAuthenticatorIds); mScheduler.scheduleClientMonitor(client, callback); @@ -762,9 +773,10 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider mBiometricStateCallback)); } - private BiometricLogger createLogger(int statsAction, int statsClient) { + private BiometricLogger createLogger(int statsAction, int statsClient, + AuthenticationStatsCollector authenticationStatsCollector) { return new BiometricLogger(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT, - statsAction, statsClient); + statsAction, statsClient, authenticationStatsCollector); } @Override diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java index da51569ee5cc..1c1b69b64723 100644 --- a/services/core/java/com/android/server/display/DisplayBrightnessState.java +++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java @@ -134,6 +134,13 @@ public final class DisplayBrightnessState { } /** + * Helper methods to create builder + */ + public static Builder builder() { + return new Builder(); + } + + /** * A DisplayBrightnessState's builder class. */ public static class Builder { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 626502ef07b4..898a3c416083 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1546,6 +1546,7 @@ public final class DisplayManagerService extends SystemService { if (displayId != Display.INVALID_DISPLAY && virtualDevice != null && dwpc != null) { mDisplayWindowPolicyControllers.put( displayId, Pair.create(virtualDevice, dwpc)); + Slog.d(TAG, "Virtual Display: successfully created virtual display"); } } @@ -1592,19 +1593,25 @@ public final class DisplayManagerService extends SystemService { if (!getProjectionService().setContentRecordingSession(session, projection)) { // Unable to start mirroring, so release VirtualDisplay. Projection service // handles stopping the projection. + Slog.w(TAG, "Content Recording: failed to start mirroring - " + + "releasing virtual display " + displayId); releaseVirtualDisplayInternal(callback.asBinder()); return Display.INVALID_DISPLAY; } else if (projection != null) { // Indicate that this projection has been used to record, and can't be used // again. + Slog.d(TAG, "Content Recording: notifying MediaProjection of successful" + + " VirtualDisplay creation."); projection.notifyVirtualDisplayCreated(displayId); } } catch (RemoteException e) { Slog.e(TAG, "Unable to tell MediaProjectionManagerService to set the " + "content recording session", e); + return displayId; } + Slog.d(TAG, "Virtual Display: successfully set up virtual display " + + displayId); } - return displayId; } finally { Binder.restoreCallingIdentity(secondToken); @@ -1628,10 +1635,13 @@ public final class DisplayManagerService extends SystemService { return -1; } + + Slog.d(TAG, "Virtual Display: creating DisplayDevice with VirtualDisplayAdapter"); DisplayDevice device = mVirtualDisplayAdapter.createVirtualDisplayLocked( callback, projection, callingUid, packageName, surface, flags, virtualDisplayConfig); if (device == null) { + Slog.w(TAG, "Virtual Display: VirtualDisplayAdapter failed to create DisplayDevice"); return -1; } @@ -1709,6 +1719,7 @@ public final class DisplayManagerService extends SystemService { DisplayDevice device = mVirtualDisplayAdapter.releaseVirtualDisplayLocked(appToken); + Slog.d(TAG, "Virtual Display: Display Device released"); if (device != null) { // TODO: multi-display - handle virtual displays the same as other display adapters. mDisplayDeviceRepo.onDisplayDeviceEvent(device, diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 32c37b03d591..4db8777ba605 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -347,7 +347,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal private boolean mDozing; private boolean mAppliedDimming; - private boolean mAppliedLowPower; + private boolean mAppliedThrottling; // Reason for which the brightness was last changed. See {@link BrightnessReason} for more @@ -1465,24 +1465,13 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal slowChange = false; mAppliedDimming = false; } - // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor - // as long as it is above the minimum threshold. - if (mPowerRequest.lowPowerMode) { - if (brightnessState > PowerManager.BRIGHTNESS_MIN) { - final float brightnessFactor = - Math.min(mPowerRequest.screenLowPowerBrightnessFactor, 1); - final float lowPowerBrightnessFloat = (brightnessState * brightnessFactor); - brightnessState = Math.max(lowPowerBrightnessFloat, PowerManager.BRIGHTNESS_MIN); - mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_LOW_POWER); - } - if (!mAppliedLowPower) { - slowChange = false; - } - mAppliedLowPower = true; - } else if (mAppliedLowPower) { - slowChange = false; - mAppliedLowPower = false; - } + + DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest, + brightnessState, slowChange); + + brightnessState = clampedState.getBrightness(); + slowChange = clampedState.isSlowChange(); + mBrightnessReasonTemp.addModifier(clampedState.getBrightnessReason().getModifier()); // The current brightness to use has been calculated at this point, and HbmController should // be notified so that it can accurately calculate HDR or HBM levels. We specifically do it @@ -1540,8 +1529,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // allowed range. float animateValue = clampScreenBrightness(brightnessState); - animateValue = mBrightnessClamperController.clamp(animateValue); - // If there are any HDR layers on the screen, we have a special brightness value that we // use instead. We still preserve the calculated brightness for Standard Dynamic Range // (SDR) layers, but the main brightness value will be the one for HDR. @@ -2408,7 +2395,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal pw.println(" mPowerRequest=" + mPowerRequest); pw.println(" mBrightnessReason=" + mBrightnessReason); pw.println(" mAppliedDimming=" + mAppliedDimming); - pw.println(" mAppliedLowPower=" + mAppliedLowPower); pw.println(" mAppliedThrottling=" + mAppliedThrottling); pw.println(" mDozing=" + mDozing); pw.println(" mSkipRampState=" + skipRampStateToString(mSkipRampState)); diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index 4f7a2ba58570..9f480b6f5e21 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -141,9 +141,12 @@ public class VirtualDisplayAdapter extends DisplayAdapter { try { if (projection != null) { projection.registerCallback(mediaProjectionCallback); + Slog.d(TAG, "Virtual Display: registered media projection callback for new " + + "VirtualDisplayDevice"); } appToken.linkToDeath(device, 0); } catch (RemoteException ex) { + Slog.e(TAG, "Virtual Display: error while setting up VirtualDisplayDevice", ex); mVirtualDisplayDevices.remove(appToken); device.destroyLocked(false); return null; @@ -439,6 +442,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { } public void stopLocked() { + Slog.d(TAG, "Virtual Display: stopping device " + mName); setSurfaceLocked(null); mStopped = true; } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java index 9345a3d97122..54a280fddb6c 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java @@ -21,6 +21,9 @@ import android.os.PowerManager; import java.io.PrintWriter; +/** + * Provides max allowed brightness + */ abstract class BrightnessClamper<T> { protected float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index d0f28c3bea81..f19d00b82aab 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -20,6 +20,7 @@ import static com.android.server.display.brightness.clamper.BrightnessClamper.Ty import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.display.DisplayManagerInternal; import android.os.Handler; import android.os.HandlerExecutor; import android.os.PowerManager; @@ -28,6 +29,7 @@ import android.provider.DeviceConfigInterface; import android.util.IndentingPrintWriter; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.DisplayBrightnessState; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.feature.DeviceConfigParameterProvider; @@ -42,7 +44,7 @@ import java.util.concurrent.Executor; */ public class BrightnessClamperController { - private static final boolean ENABLED = false; + private static final boolean THERMAL_ENABLED = false; private final DeviceConfigParameterProvider mDeviceConfigParameterProvider; private final Handler mHandler; @@ -50,6 +52,8 @@ public class BrightnessClamperController { private final Executor mExecutor; private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers = new ArrayList<>(); + + private final List<BrightnessModifier> mModifiers = new ArrayList<>(); private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener = properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; @@ -77,11 +81,12 @@ public class BrightnessClamperController { } }; - if (ENABLED) { + if (THERMAL_ENABLED) { mClampers.add( new BrightnessThermalClamper(handler, clamperChangeListenerInternal, data)); - start(); } + mModifiers.add(new BrightnessLowPowerModeModifier()); + start(); } /** @@ -95,8 +100,18 @@ public class BrightnessClamperController { * Applies clamping * Called in DisplayControllerHandler */ - public float clamp(float value) { - return Math.min(value, mBrightnessCap); + public DisplayBrightnessState clamp(DisplayManagerInternal.DisplayPowerRequest request, + float brightnessValue, boolean slowChange) { + float cappedBrightness = Math.min(brightnessValue, mBrightnessCap); + + DisplayBrightnessState.Builder builder = DisplayBrightnessState.builder(); + builder.setIsSlowChange(slowChange); + builder.setBrightness(cappedBrightness); + + for (int i = 0; i < mModifiers.size(); i++) { + mModifiers.get(i).apply(request, builder); + } + return builder.build(); } /** @@ -108,6 +123,7 @@ public class BrightnessClamperController { writer.println(" mClamperType: " + mClamperType); IndentingPrintWriter ipw = new IndentingPrintWriter(writer, " "); mClampers.forEach(clamper -> clamper.dump(ipw)); + mModifiers.forEach(modifier -> modifier.dump(ipw)); } /** @@ -144,8 +160,10 @@ public class BrightnessClamperController { } private void start() { - mDeviceConfigParameterProvider.addOnPropertiesChangedListener( - mExecutor, mOnPropertiesChangedListener); + if (!mClampers.isEmpty()) { + mDeviceConfigParameterProvider.addOnPropertiesChangedListener( + mExecutor, mOnPropertiesChangedListener); + } } /** diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowPowerModeModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowPowerModeModifier.java new file mode 100644 index 000000000000..f48ad2f19d6b --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowPowerModeModifier.java @@ -0,0 +1,61 @@ +/* + * 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.brightness.clamper; + +import android.hardware.display.DisplayManagerInternal; +import android.os.PowerManager; + +import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.brightness.BrightnessReason; + +import java.io.PrintWriter; + +class BrightnessLowPowerModeModifier implements BrightnessModifier { + + private boolean mAppliedLowPower = false; + + @Override + public void apply(DisplayManagerInternal.DisplayPowerRequest request, + DisplayBrightnessState.Builder stateBuilder) { + // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor + // as long as it is above the minimum threshold. + if (request.lowPowerMode) { + float value = stateBuilder.getBrightness(); + if (value > PowerManager.BRIGHTNESS_MIN) { + final float brightnessFactor = + Math.min(request.screenLowPowerBrightnessFactor, 1); + final float lowPowerBrightnessFloat = Math.max((value * brightnessFactor), + PowerManager.BRIGHTNESS_MIN); + stateBuilder.setBrightness(lowPowerBrightnessFloat); + stateBuilder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_LOW_POWER); + } + if (!mAppliedLowPower) { + stateBuilder.setIsSlowChange(false); + } + mAppliedLowPower = true; + } else if (mAppliedLowPower) { + stateBuilder.setIsSlowChange(false); + mAppliedLowPower = false; + } + } + + @Override + public void dump(PrintWriter pw) { + pw.println("BrightnessLowPowerModeModifier:"); + pw.println(" mAppliedLowPower=" + mAppliedLowPower); + } +} diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java new file mode 100644 index 000000000000..3a33df657ff8 --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.brightness.clamper; + +import android.hardware.display.DisplayManagerInternal; + +import com.android.server.display.DisplayBrightnessState; + +import java.io.PrintWriter; + +/** + * Modifies current brightness based on request + */ +interface BrightnessModifier { + + void apply(DisplayManagerInternal.DisplayPowerRequest request, + DisplayBrightnessState.Builder builder); + + void dump(PrintWriter pw); +} diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java index 497ed0346d97..fee54f5ed337 100644 --- a/services/core/java/com/android/server/logcat/LogcatManagerService.java +++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; +import android.os.DeadObjectException; import android.os.Handler; import android.os.ILogd; import android.os.Looper; @@ -518,7 +519,15 @@ public final class LogcatManagerService extends SystemService { Slog.d(TAG, "Approving log access: " + request); } try { - getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd); + try { + getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd); + } catch (DeadObjectException e) { + // This can happen if logd restarts, so force getting a new connection + // to logd and try once more. + Slog.w(TAG, "Logd connection no longer valid while approving, trying once more."); + mLogdService = null; + getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd); + } Integer activeCount = mActiveLogAccessCount.getOrDefault(client, 0); mActiveLogAccessCount.put(client, activeCount + 1); } catch (RemoteException e) { @@ -531,7 +540,15 @@ public final class LogcatManagerService extends SystemService { Slog.d(TAG, "Declining log access: " + request); } try { - getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd); + try { + getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd); + } catch (DeadObjectException e) { + // This can happen if logd restarts, so force getting a new connection + // to logd and try once more. + Slog.w(TAG, "Logd connection no longer valid while declining, trying once more."); + mLogdService = null; + getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd); + } } catch (RemoteException e) { Slog.e(TAG, "Fails to call remote functions", e); } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index fb3f0b3b3496..802a7f2954af 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -225,6 +225,7 @@ public final class MediaProjectionManagerService extends SystemService mMediaRouter.rebindAsUser(to.getUserIdentifier()); synchronized (mLock) { if (mProjectionGrant != null) { + Slog.d(TAG, "Content Recording: Stopped MediaProjection due to user switching"); mProjectionGrant.stop(); } } @@ -260,6 +261,8 @@ public final class MediaProjectionManagerService extends SystemService } synchronized (mLock) { + Slog.d(TAG, + "Content Recording: Stopped MediaProjection due to foreground service change"); if (mProjectionGrant != null) { mProjectionGrant.stop(); } @@ -268,6 +271,8 @@ public final class MediaProjectionManagerService extends SystemService private void startProjectionLocked(final MediaProjection projection) { if (mProjectionGrant != null) { + Slog.d(TAG, "Content Recording: Stopped MediaProjection to start new " + + "incoming projection"); mProjectionGrant.stop(); } if (mMediaRouteInfo != null) { @@ -279,6 +284,8 @@ public final class MediaProjectionManagerService extends SystemService } private void stopProjectionLocked(final MediaProjection projection) { + Slog.d(TAG, "Content Recording: Stopped active MediaProjection and " + + "dispatching stop to callbacks"); mProjectionToken = null; mProjectionGrant = null; dispatchStop(projection); @@ -351,6 +358,13 @@ public final class MediaProjectionManagerService extends SystemService if (!setSessionSucceeded) { // Unable to start mirroring, so tear down this projection. if (mProjectionGrant != null) { + String projectionType = incomingSession != null + ? ContentRecordingSession.recordContentToString( + incomingSession.getContentToRecord()) : "none"; + Slog.w(TAG, "Content Recording: Stopped MediaProjection due to failing to set " + + "ContentRecordingSession - id= " + + mProjectionGrant.getVirtualDisplayId() + "type=" + projectionType); + mProjectionGrant.stop(); } return false; @@ -464,6 +478,9 @@ public final class MediaProjectionManagerService extends SystemService // The grant may now be null if setting the session failed. if (mProjectionGrant != null) { // Always stop the projection. + Slog.w(TAG, "Content Recording: Stopped MediaProjection due to user " + + "consent result of CANCEL - " + + "id= " + mProjectionGrant.getVirtualDisplayId()); mProjectionGrant.stop(); } break; @@ -672,6 +689,7 @@ public final class MediaProjectionManagerService extends SystemService try { synchronized (mLock) { if (mProjectionGrant != null) { + Slog.d(TAG, "Content Recording: Stopping active projection"); mProjectionGrant.stop(); } } @@ -882,6 +900,10 @@ public final class MediaProjectionManagerService extends SystemService MEDIA_PROJECTION_TOKEN_EVENT_CREATED); } + int getVirtualDisplayId() { + return mVirtualDisplayId; + } + @Override // Binder call public boolean canProjectVideo() { return mType == MediaProjectionManager.TYPE_MIRRORING || @@ -958,12 +980,11 @@ public final class MediaProjectionManagerService extends SystemService registerCallback(mCallback); try { mToken = callback.asBinder(); - mDeathEater = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - mCallbackDelegate.remove(callback); - stop(); - } + mDeathEater = () -> { + Slog.d(TAG, "Content Recording: MediaProjection stopped by Binder death - " + + "id= " + mVirtualDisplayId); + mCallbackDelegate.remove(callback); + stop(); }; mToken.linkToDeath(mDeathEater, 0); } catch (RemoteException e) { @@ -1033,6 +1054,9 @@ public final class MediaProjectionManagerService extends SystemService Binder.restoreCallingIdentity(token); } } + Slog.d(TAG, "Content Recording: handling stopping this projection token" + + " createTime= " + mCreateTimeMs + + " countStarts= " + mCountStarts); stopProjectionLocked(this); mToken.unlinkToDeath(mDeathEater, 0); mToken = null; @@ -1158,6 +1182,8 @@ public final class MediaProjectionManagerService extends SystemService if ((type & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) { mMediaRouteInfo = info; if (mProjectionGrant != null) { + Slog.d(TAG, "Content Recording: Stopped MediaProjection due to " + + "route type of REMOTE_DISPLAY not selected"); mProjectionGrant.stop(); } } @@ -1329,7 +1355,7 @@ public final class MediaProjectionManagerService extends SystemService try { mCallback.onStart(mInfo); } catch (RemoteException e) { - Slog.w(TAG, "Failed to notify media projection has stopped", e); + Slog.w(TAG, "Failed to notify media projection has started", e); } } } @@ -1403,7 +1429,8 @@ public final class MediaProjectionManagerService extends SystemService return "TYPE_MIRRORING"; case MediaProjectionManager.TYPE_PRESENTATION: return "TYPE_PRESENTATION"; + default: + return Integer.toString(type); } - return Integer.toString(type); } } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 1bb10929135c..3f799dc08a3f 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -2608,7 +2608,12 @@ public class PreferencesHelper implements RankingConfig { for (NotificationChannel channel : r.channels.values()) { if (!channel.isSoundRestored()) { Uri uri = channel.getSound(); - Uri restoredUri = channel.restoreSoundUri(mContext, uri, true); + Uri restoredUri = + channel.restoreSoundUri( + mContext, + uri, + true, + channel.getAudioAttributes().getUsage()); if (Settings.System.DEFAULT_NOTIFICATION_URI.equals( restoredUri)) { Log.w(TAG, diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index a622d0743bbb..bb6e11dcf8d4 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -90,7 +90,7 @@ ] } ], - "presubmit-large": [ + "postsubmit": [ { "name": "CtsContentTestCases", "options": [ @@ -104,9 +104,7 @@ "include-filter": "android.content.pm.cts" } ] - } - ], - "postsubmit": [ + }, { "name": "CtsPermissionTestCases", "options": [ diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 62273b5bc445..5a6851ea196c 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -108,7 +108,6 @@ import static android.os.Build.VERSION_CODES.O; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.Process.SYSTEM_UID; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.view.Display.COLOR_MODE_DEFAULT; import static android.view.Display.INVALID_DISPLAY; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -2785,6 +2784,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else if (isEmbedded()) { associateStartingWindowWithTaskIfNeeded(); } + if (mTransitionController.isCollecting()) { + mStartingData.mTransitionId = mTransitionController.getCollectingTransitionId(); + } } } @@ -4691,26 +4693,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } /** - * @return Whether we are allowed to show non-starting windows at the moment. We disallow - * showing windows while the transition animation is playing in case we have windows - * that have wide-color-gamut color mode set to avoid jank in the middle of the - * animation. + * @return Whether we are allowed to show non-starting windows at the moment. */ boolean canShowWindows() { - final boolean drawn = mTransitionController.isShellTransitionsEnabled() + return mTransitionController.isShellTransitionsEnabled() ? mSyncState != SYNC_STATE_WAITING_FOR_DRAW : allDrawn; - final boolean animating = mTransitionController.isShellTransitionsEnabled() - ? mTransitionController.inPlayingTransition(this) - : isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION); - return drawn && !(animating && hasNonDefaultColorWindow()); - } - - /** - * @return true if we have a window that has a non-default color mode set; false otherwise. - */ - private boolean hasNonDefaultColorWindow() { - return forAllWindows(ws -> ws.mAttrs.getColorMode() != COLOR_MODE_DEFAULT, - true /* topToBottom */); } @Override diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index 105b2bb09cf8..148bf9bfdcd9 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -16,16 +16,14 @@ package com.android.server.wm; -import static com.android.server.wm.SnapshotController.ACTIVITY_CLOSE; -import static com.android.server.wm.SnapshotController.ACTIVITY_OPEN; -import static com.android.server.wm.SnapshotController.TASK_CLOSE; -import static com.android.server.wm.SnapshotController.TASK_OPEN; +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.os.Environment; import android.os.SystemProperties; +import android.os.Trace; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; @@ -35,7 +33,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; -import com.android.server.wm.SnapshotController.TransitionState; import java.io.File; import java.util.ArrayList; @@ -62,12 +59,6 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord static final String SNAPSHOTS_DIRNAME = "activity_snapshots"; /** - * The pending activities which should capture snapshot when process transition finish. - */ - @VisibleForTesting - final ArraySet<ActivityRecord> mPendingCaptureActivity = new ArraySet<>(); - - /** * The pending activities which should remove snapshot from memory when process transition * finish. */ @@ -86,6 +77,10 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord @VisibleForTesting final ArraySet<ActivityRecord> mPendingLoadActivity = new ArraySet<>(); + private final ArraySet<ActivityRecord> mOnBackPressedActivities = new ArraySet<>(); + + private final ArrayList<ActivityRecord> mTmpBelowActivities = new ArrayList<>(); + private final ArrayList<WindowContainer> mTmpTransitionParticipants = new ArrayList<>(); private final SnapshotPersistQueue mSnapshotPersistQueue; private final PersistInfoProvider mPersistInfoProvider; private final AppSnapshotLoader mSnapshotLoader; @@ -117,20 +112,6 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord setSnapshotEnabled(snapshotEnabled); } - void systemReady() { - if (shouldDisableSnapshots()) { - return; - } - mService.mSnapshotController.registerTransitionStateConsumer( - ACTIVITY_OPEN, this::handleOpenActivityTransition); - mService.mSnapshotController.registerTransitionStateConsumer( - ACTIVITY_CLOSE, this::handleCloseActivityTransition); - mService.mSnapshotController.registerTransitionStateConsumer( - TASK_OPEN, this::handleOpenTaskTransition); - mService.mSnapshotController.registerTransitionStateConsumer( - TASK_CLOSE, this::handleCloseTaskTransition); - } - @Override protected float initSnapshotScale() { final float config = mService.mContext.getResources().getFloat( @@ -173,6 +154,7 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord @Override void write() { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "cleanUpUserFiles"); final File file = mPersistInfoProvider.getDirectory(userId); if (file.exists()) { final File[] contents = file.listFiles(); @@ -182,15 +164,30 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord } } } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } }); } } + void addOnBackPressedActivity(ActivityRecord ar) { + if (shouldDisableSnapshots()) { + return; + } + mOnBackPressedActivities.add(ar); + } + + void clearOnBackPressedActivities() { + if (shouldDisableSnapshots()) { + return; + } + mOnBackPressedActivities.clear(); + } + /** - * Prepare to handle on transition start. Clear all temporary fields. + * Prepare to collect any change for snapshots processing. Clear all temporary fields. */ - void preTransitionStart() { + void beginSnapshotProcess() { if (shouldDisableSnapshots()) { return; } @@ -198,18 +195,22 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord } /** - * on transition start has notified, start process data. + * End collect any change for snapshots processing, start process data. */ - void postTransitionStart() { + void endSnapshotProcess() { if (shouldDisableSnapshots()) { return; } - onCommitTransition(); + for (int i = mOnBackPressedActivities.size() - 1; i >= 0; --i) { + handleActivityTransition(mOnBackPressedActivities.valueAt(i)); + } + mOnBackPressedActivities.clear(); + mTmpTransitionParticipants.clear(); + postProcess(); } @VisibleForTesting void resetTmpFields() { - mPendingCaptureActivity.clear(); mPendingRemoveActivity.clear(); mPendingDeleteActivity.clear(); mPendingLoadActivity.clear(); @@ -218,31 +219,13 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord /** * Start process all pending activities for a transition. */ - private void onCommitTransition() { + private void postProcess() { if (DEBUG) { - Slog.d(TAG, "ActivitySnapshotController#onCommitTransition result:" - + " capture " + mPendingCaptureActivity + Slog.d(TAG, "ActivitySnapshotController#postProcess result:" + " remove " + mPendingRemoveActivity + " delete " + mPendingDeleteActivity + " load " + mPendingLoadActivity); } - // task snapshots - for (int i = mPendingCaptureActivity.size() - 1; i >= 0; i--) { - recordSnapshot(mPendingCaptureActivity.valueAt(i)); - } - // clear mTmpRemoveActivity from cache - for (int i = mPendingRemoveActivity.size() - 1; i >= 0; i--) { - final ActivityRecord ar = mPendingRemoveActivity.valueAt(i); - final int code = getSystemHashCode(ar); - mCache.onIdRemoved(code); - } - // clear snapshot on cache and delete files - for (int i = mPendingDeleteActivity.size() - 1; i >= 0; i--) { - final ActivityRecord ar = mPendingDeleteActivity.valueAt(i); - final int code = getSystemHashCode(ar); - mCache.onIdRemoved(code); - removeIfUserSavedFileExist(code, ar.mUserId); - } // load snapshot to cache for (int i = mPendingLoadActivity.size() - 1; i >= 0; i--) { final ActivityRecord ar = mPendingLoadActivity.valueAt(i); @@ -258,6 +241,8 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) { @Override void write() { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, + "load_activity_snapshot"); final TaskSnapshot snapshot = mSnapshotLoader.loadTask(code, userId, false /* loadLowResolutionBitmap */); synchronized (mService.getWindowManagerLock()) { @@ -265,16 +250,36 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord mCache.putSnapshot(ar, snapshot); } } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } }); } } } + // clear mTmpRemoveActivity from cache + for (int i = mPendingRemoveActivity.size() - 1; i >= 0; i--) { + final ActivityRecord ar = mPendingRemoveActivity.valueAt(i); + final int code = getSystemHashCode(ar); + mCache.onIdRemoved(code); + } + // clear snapshot on cache and delete files + for (int i = mPendingDeleteActivity.size() - 1; i >= 0; i--) { + final ActivityRecord ar = mPendingDeleteActivity.valueAt(i); + final int code = getSystemHashCode(ar); + mCache.onIdRemoved(code); + removeIfUserSavedFileExist(code, ar.mUserId); + } // don't keep any reference resetTmpFields(); } - private void recordSnapshot(ActivityRecord activity) { + void recordSnapshot(ActivityRecord activity) { + if (shouldDisableSnapshots()) { + return; + } + if (DEBUG) { + Slog.d(TAG, "ActivitySnapshotController#recordSnapshot " + activity); + } final TaskSnapshot snapshot = recordSnapshotInner(activity, false /* allowSnapshotHome */); if (snapshot != null) { final int code = getSystemHashCode(activity); @@ -285,15 +290,20 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord /** * Called when the visibility of an app changes outside the regular app transition flow. */ - void notifyAppVisibilityChanged(ActivityRecord appWindowToken, boolean visible) { + void notifyAppVisibilityChanged(ActivityRecord ar, boolean visible) { if (shouldDisableSnapshots()) { return; } + final Task task = ar.getTask(); + if (task == null) { + return; + } + // Doesn't need to capture activity snapshot when it converts from translucent. if (!visible) { resetTmpFields(); - addBelowTopActivityIfExist(appWindowToken.getTask(), mPendingRemoveActivity, + addBelowActivityIfExist(ar, mPendingRemoveActivity, false, "remove-snapshot"); - onCommitTransition(); + postProcess(); } } @@ -301,65 +311,146 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord return System.identityHashCode(activity); } - void handleOpenActivityTransition(TransitionState<ActivityRecord> transitionState) { - ArraySet<ActivityRecord> participant = transitionState.getParticipant(false /* open */); - for (ActivityRecord ar : participant) { - mPendingCaptureActivity.add(ar); - // remove the snapshot for the one below close - final ActivityRecord below = ar.getTask().getActivityBelow(ar); - if (below != null) { - mPendingRemoveActivity.add(below); + @VisibleForTesting + void handleTransitionFinish(@NonNull ArrayList<WindowContainer> windows) { + mTmpTransitionParticipants.clear(); + mTmpTransitionParticipants.addAll(windows); + for (int i = mTmpTransitionParticipants.size() - 1; i >= 0; --i) { + final WindowContainer next = mTmpTransitionParticipants.get(i); + if (next.asTask() != null) { + handleTaskTransition(next.asTask()); + } else if (next.asTaskFragment() != null) { + final TaskFragment tf = next.asTaskFragment(); + final ActivityRecord ar = tf.getTopMostActivity(); + if (ar != null) { + handleActivityTransition(ar); + } + } else if (next.asActivityRecord() != null) { + handleActivityTransition(next.asActivityRecord()); } } } - void handleCloseActivityTransition(TransitionState<ActivityRecord> transitionState) { - ArraySet<ActivityRecord> participant = transitionState.getParticipant(true /* open */); - for (ActivityRecord ar : participant) { + private void handleActivityTransition(@NonNull ActivityRecord ar) { + if (shouldDisableSnapshots()) { + return; + } + if (ar.isVisibleRequested()) { mPendingDeleteActivity.add(ar); // load next one if exists. - final ActivityRecord below = ar.getTask().getActivityBelow(ar); - if (below != null) { - mPendingLoadActivity.add(below); - } + addBelowActivityIfExist(ar, mPendingLoadActivity, true, "load-snapshot"); + } else { + // remove the snapshot for the one below close + addBelowActivityIfExist(ar, mPendingRemoveActivity, true, "remove-snapshot"); } } - void handleCloseTaskTransition(TransitionState<Task> closeTaskTransitionRecord) { - ArraySet<Task> participant = closeTaskTransitionRecord.getParticipant(false /* open */); - for (Task close : participant) { + private void handleTaskTransition(Task task) { + if (shouldDisableSnapshots()) { + return; + } + final ActivityRecord topActivity = task.getTopMostActivity(); + if (topActivity == null) { + return; + } + if (task.isVisibleRequested()) { + // this is open task transition + // load the N - 1 to cache + addBelowActivityIfExist(topActivity, mPendingLoadActivity, true, "load-snapshot"); + // Move the activities to top of mSavedFilesInOrder, so when purge happen, there + // will trim the persisted files from the most non-accessed. + adjustSavedFileOrder(task); + } else { // this is close task transition // remove the N - 1 from cache - addBelowTopActivityIfExist(close, mPendingRemoveActivity, "remove-snapshot"); + addBelowActivityIfExist(topActivity, mPendingRemoveActivity, true, "remove-snapshot"); } } - void handleOpenTaskTransition(TransitionState<Task> openTaskTransitionRecord) { - ArraySet<Task> participant = openTaskTransitionRecord.getParticipant(true /* open */); - for (Task open : participant) { - // this is close task transition - // remove the N - 1 from cache - addBelowTopActivityIfExist(open, mPendingLoadActivity, "load-snapshot"); - // Move the activities to top of mSavedFilesInOrder, so when purge happen, there - // will trim the persisted files from the most non-accessed. - adjustSavedFileOrder(open); + /** + * Add the top -1 activity to a set if it exists. + * @param inTransition true if the activity must participant in transition. + */ + private void addBelowActivityIfExist(ActivityRecord currentActivity, + ArraySet<ActivityRecord> set, boolean inTransition, String debugMessage) { + getActivityBelow(currentActivity, inTransition, mTmpBelowActivities); + for (int i = mTmpBelowActivities.size() - 1; i >= 0; --i) { + set.add(mTmpBelowActivities.get(i)); + if (DEBUG) { + Slog.d(TAG, "ActivitySnapshotController#addBelowTopActivityIfExist " + + mTmpBelowActivities.get(i) + " from " + debugMessage); + } } + mTmpBelowActivities.clear(); } - // Add the top -1 activity to a set if it exists. - private void addBelowTopActivityIfExist(Task task, ArraySet<ActivityRecord> set, - String debugMessage) { - final ActivityRecord topActivity = task.getTopMostActivity(); - if (topActivity != null) { - final ActivityRecord below = task.getActivityBelow(topActivity); - if (below != null) { - set.add(below); - if (DEBUG) { - Slog.d(TAG, "ActivitySnapshotController#addBelowTopActivityIfExist " - + below + " from " + debugMessage); + private void getActivityBelow(ActivityRecord currentActivity, boolean inTransition, + ArrayList<ActivityRecord> result) { + final Task currentTask = currentActivity.getTask(); + if (currentTask == null) { + return; + } + final ActivityRecord initPrev = currentTask.getActivityBelow(currentActivity); + if (initPrev == null) { + return; + } + final TaskFragment currTF = currentActivity.getTaskFragment(); + final TaskFragment prevTF = initPrev.getTaskFragment(); + final TaskFragment prevAdjacentTF = prevTF != null + ? prevTF.getAdjacentTaskFragment() : null; + if (currTF == prevTF && currTF != null || prevAdjacentTF == null) { + // Current activity and previous one is in the same task fragment, or + // previous activity is not in a task fragment, or + // previous activity's task fragment doesn't adjacent to any others. + if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { + result.add(initPrev); + } + return; + } + + if (prevAdjacentTF == currTF) { + // previous activity A is adjacent to current activity B. + // Try to find anyone below previous activityA, which are C and D if exists. + // A | B + // C (| D) + getActivityBelow(initPrev, inTransition, result); + } else { + // previous activity C isn't adjacent to current activity A. + // A + // B | C + final Task prevAdjacentTask = prevAdjacentTF.getTask(); + if (prevAdjacentTask == currentTask) { + final int currentIndex = currTF != null + ? currentTask.mChildren.indexOf(currTF) + : currentTask.mChildren.indexOf(currentActivity); + final int prevAdjacentIndex = + prevAdjacentTask.mChildren.indexOf(prevAdjacentTF); + // prevAdjacentTF already above currentActivity + if (prevAdjacentIndex > currentIndex) { + return; } } + if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { + result.add(initPrev); + } + // prevAdjacentTF is adjacent to another one + final ActivityRecord prevAdjacentActivity = prevAdjacentTF.getTopMostActivity(); + if (prevAdjacentActivity != null && (!inTransition + || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) { + result.add(prevAdjacentActivity); + } + } + } + + static boolean isInParticipant(ActivityRecord ar, + ArrayList<WindowContainer> transitionParticipants) { + for (int i = transitionParticipants.size() - 1; i >= 0; --i) { + final WindowContainer wc = transitionParticipants.get(i); + if (ar == wc || ar.isDescendantOf(wc)) { + return true; + } } + return false; } private void adjustSavedFileOrder(Task nextTopTask) { @@ -376,6 +467,9 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord @Override void onAppRemoved(ActivityRecord activity) { + if (shouldDisableSnapshots()) { + return; + } super.onAppRemoved(activity); final int code = getSystemHashCode(activity); removeIfUserSavedFileExist(code, activity.mUserId); @@ -386,6 +480,9 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord @Override void onAppDied(ActivityRecord activity) { + if (shouldDisableSnapshots()) { + return; + } super.onAppDied(activity); final int code = getSystemHashCode(activity); removeIfUserSavedFileExist(code, activity.mUserId); @@ -440,7 +537,7 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord private void removeIfUserSavedFileExist(int code, int userId) { final UserSavedFile usf = getUserFiles(userId).get(code); if (usf != null) { - mUserSavedFiles.remove(code); + mUserSavedFiles.get(userId).remove(code); mSavedFilesInOrder.remove(usf); mPersister.removeSnap(code, userId); } @@ -490,11 +587,13 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) { @Override void write() { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "activity_remove_files"); for (int i = files.size() - 1; i >= 0; --i) { final UserSavedFile usf = files.get(i); mSnapshotPersistQueue.deleteSnapshot( usf.mFileId, usf.mUserId, mPersistInfoProvider); } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } }); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 78da5def43d4..fcf65872e0af 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3019,9 +3019,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { // Set to activity manager directly to make sure the state can be seen by the subsequent // update of scheduling group. proc.setRunningAnimationUnsafe(); - mH.removeMessages(H.UPDATE_PROCESS_ANIMATING_STATE, proc); - mH.sendMessageDelayed(mH.obtainMessage(H.UPDATE_PROCESS_ANIMATING_STATE, proc), + mH.sendMessage(mH.obtainMessage(H.ADD_WAKEFULNESS_ANIMATING_REASON, proc)); + mH.removeMessages(H.REMOVE_WAKEFULNESS_ANIMATING_REASON, proc); + mH.sendMessageDelayed(mH.obtainMessage(H.REMOVE_WAKEFULNESS_ANIMATING_REASON, proc), DOZE_ANIMATING_STATE_RETAIN_TIME_MS); + Trace.instant(TRACE_TAG_WINDOW_MANAGER, "requestWakefulnessAnimating"); } @Override @@ -5646,9 +5648,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final class H extends Handler { static final int REPORT_TIME_TRACKER_MSG = 1; - static final int UPDATE_PROCESS_ANIMATING_STATE = 2; static final int END_POWER_MODE_UNKNOWN_VISIBILITY_MSG = 3; static final int RESUME_FG_APP_SWITCH_MSG = 4; + static final int ADD_WAKEFULNESS_ANIMATING_REASON = 5; + static final int REMOVE_WAKEFULNESS_ANIMATING_REASON = 6; static final int FIRST_ACTIVITY_TASK_MSG = 100; static final int FIRST_SUPERVISOR_TASK_MSG = 200; @@ -5665,13 +5668,23 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { tracker.deliverResult(mContext); } break; - case UPDATE_PROCESS_ANIMATING_STATE: { + case ADD_WAKEFULNESS_ANIMATING_REASON: { final WindowProcessController proc = (WindowProcessController) msg.obj; synchronized (mGlobalLock) { - proc.updateRunningRemoteOrRecentsAnimation(); + proc.addAnimatingReason( + WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE); } } break; + case REMOVE_WAKEFULNESS_ANIMATING_REASON: { + final WindowProcessController proc = (WindowProcessController) msg.obj; + synchronized (mGlobalLock) { + proc.removeAnimatingReason( + WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE); + } + Trace.instant(TRACE_TAG_WINDOW_MANAGER, "finishWakefulnessAnimating"); + } + break; case END_POWER_MODE_UNKNOWN_VISIBILITY_MSG: { synchronized (mGlobalLock) { mRetainPowerModeAndTopProcessState = false; diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 4ce21bdadf49..2eceeccd9d8f 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -33,6 +33,7 @@ import android.view.animation.AnimationUtils; import com.android.internal.R; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.function.Consumer; @@ -224,7 +225,15 @@ class AsyncRotationController extends FadeAnimationController implements Consume * operation directly to avoid waiting until timeout. */ void updateTargetWindows() { - if (mTransitionOp == OP_LEGACY || !mIsStartTransactionCommitted) return; + if (mTransitionOp == OP_LEGACY) return; + if (!mIsStartTransactionCommitted) { + if (mTimeoutRunnable == null && !mDisplayContent.hasTopFixedRotationLaunchingApp() + && !mDisplayContent.isRotationChanging() && !mDisplayContent.inTransition()) { + Slog.d(TAG, "Cancel for no change"); + mDisplayContent.finishAsyncRotationIfPossible(); + } + return; + } for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { final Operation op = mTargetWindowTokens.valueAt(i); if (op.mIsCompletionPending || op.mAction == Operation.ACTION_SEAMLESS) { @@ -608,6 +617,16 @@ class AsyncRotationController extends FadeAnimationController implements Consume return op.mAction != Operation.ACTION_SEAMLESS; } + void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "AsyncRotationController"); + prefix += " "; + pw.println(prefix + "mTransitionOp=" + mTransitionOp); + pw.println(prefix + "mIsStartTransactionCommitted=" + mIsStartTransactionCommitted); + pw.println(prefix + "mIsSyncDrawRequested=" + mIsSyncDrawRequested); + pw.println(prefix + "mOriginalRotation=" + mOriginalRotation); + pw.println(prefix + "mTargetWindowTokens=" + mTargetWindowTokens); + } + /** The operation to control the rotation appearance associated with window token. */ private static class Operation { @Retention(RetentionPolicy.SOURCE) @@ -635,5 +654,10 @@ class AsyncRotationController extends FadeAnimationController implements Consume Operation(@Action int action) { mAction = action; } + + @Override + public String toString() { + return "Operation{a=" + mAction + " pending=" + mIsCompletionPending + '}'; + } } } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 976641b52a16..993c01672b44 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -1177,6 +1177,8 @@ class BackNavigationController { if (!composeAnimations(mCloseTarget, mOpenTarget, openActivity)) { return null; } + mCloseTarget.mTransitionController.mSnapshotController + .mActivitySnapshotController.clearOnBackPressedActivities(); applyPreviewStrategy(mOpenAdaptor, openActivity); final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback(); @@ -1222,6 +1224,8 @@ class BackNavigationController { // Call it again to make sure the activity could be visible while handling the pending // animation. activity.commitVisibility(true, true); + activity.mTransitionController.mSnapshotController + .mActivitySnapshotController.addOnBackPressedActivity(activity); } activity.mLaunchTaskBehind = true; @@ -1248,6 +1252,9 @@ class BackNavigationController { // Restore the launch-behind state. activity.mTaskSupervisor.scheduleLaunchTaskBehindComplete(activity.token); activity.mLaunchTaskBehind = false; + // Ignore all change + activity.mTransitionController.mSnapshotController + .mActivitySnapshotController.clearOnBackPressedActivities(); ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Setting Activity.mLauncherTaskBehind to false. Activity=%s", activity); diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index 2ecbf8a25ed9..5aa7c97ecb08 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -87,7 +87,7 @@ final class ContentRecorder implements WindowContainerListener { private int mLastOrientation = ORIENTATION_UNDEFINED; ContentRecorder(@NonNull DisplayContent displayContent) { - this(displayContent, new RemoteMediaProjectionManagerWrapper()); + this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId)); } @VisibleForTesting @@ -556,8 +556,14 @@ final class ContentRecorder implements WindowContainerListener { private static final class RemoteMediaProjectionManagerWrapper implements MediaProjectionManagerWrapper { + + private final int mDisplayId; @Nullable private IMediaProjectionManager mIMediaProjectionManager = null; + RemoteMediaProjectionManagerWrapper(int displayId) { + mDisplayId = displayId; + } + @Override public void stopActiveProjection() { fetchMediaProjectionManager(); @@ -565,12 +571,15 @@ final class ContentRecorder implements WindowContainerListener { return; } try { + ProtoLog.e(WM_DEBUG_CONTENT_RECORDING, + "Content Recording: stopping active projection for display %d", + mDisplayId); mIMediaProjectionManager.stopActiveProjection(); } catch (RemoteException e) { ProtoLog.e(WM_DEBUG_CONTENT_RECORDING, "Content Recording: Unable to tell MediaProjectionManagerService to stop " - + "the active projection: %s", - e); + + "the active projection for display %d: %s", + mDisplayId, e); } } diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java index f24ba5a45885..b5890856fa6f 100644 --- a/services/core/java/com/android/server/wm/ContentRecordingController.java +++ b/services/core/java/com/android/server/wm/ContentRecordingController.java @@ -117,10 +117,11 @@ final class ContentRecordingController { } incomingDisplayContent.setContentRecordingSession(incomingSession); // Updating scenario: Explicitly ask ContentRecorder to update, since no config or - // display change will trigger an update from the DisplayContent. - if (hasSessionUpdatedWithConsent) { - incomingDisplayContent.updateRecording(); - } + // display change will trigger an update from the DisplayContent. There exists a + // scenario where a DisplayContent is created, but it's ContentRecordingSession hasn't + // been set yet due to a race condition. On creation, updateRecording fails to start + // recording, so now this call guarantees recording will be started from somewhere. + incomingDisplayContent.updateRecording(); } // Takeover and stopping scenario: stop recording on the pre-existing session. if (mSession != null && !hasSessionUpdatedWithConsent) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 64c2c5d9c228..ee3014cadb0d 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3454,6 +3454,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mFixedRotationLaunchingApp != null) { setSeamlessTransitionForFixedRotation(controller.getCollectingTransition()); } + } else if (mAsyncRotationController != null && !isRotationChanging()) { + Slog.i(TAG, "Finish AsyncRotation for previous intermediate change"); + finishAsyncRotationIfPossible(); } return; } @@ -3627,6 +3630,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mFixedRotationLaunchingApp != null) { pw.println(" mFixedRotationLaunchingApp=" + mFixedRotationLaunchingApp); } + if (mAsyncRotationController != null) { + mAsyncRotationController.dump(pw, prefix); + } pw.println(); pw.print(prefix + "mHoldScreenWindow="); pw.print(mHoldScreenWindow); diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java index 20595eaa1e3c..73fdfe0d1181 100644 --- a/services/core/java/com/android/server/wm/InputManagerCallback.java +++ b/services/core/java/com/android/server/wm/InputManagerCallback.java @@ -25,6 +25,7 @@ import static com.android.server.wm.WindowManagerService.H.ON_POINTER_DOWN_OUTSI import android.annotation.NonNull; import android.annotation.Nullable; +import android.gui.StalledTransactionInfo; import android.os.Debug; import android.os.IBinder; import android.util.Slog; @@ -96,7 +97,7 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal @Override public void notifyNoFocusedWindowAnr(@NonNull InputApplicationHandle applicationHandle) { TimeoutRecord timeoutRecord = TimeoutRecord.forInputDispatchNoFocusedWindow( - timeoutMessage("Application does not have a focused window")); + timeoutMessage(OptionalInt.empty(), "Application does not have a focused window")); mService.mAnrController.notifyAppUnresponsive(applicationHandle, timeoutRecord); } @@ -104,7 +105,7 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal public void notifyWindowUnresponsive(@NonNull IBinder token, @NonNull OptionalInt pid, String reason) { TimeoutRecord timeoutRecord = TimeoutRecord.forInputDispatchWindowUnresponsive( - timeoutMessage(reason)); + timeoutMessage(pid, reason)); mService.mAnrController.notifyWindowUnresponsive(token, pid, timeoutRecord); } @@ -354,11 +355,21 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal mService.mInputManager.setInputDispatchMode(mInputDispatchEnabled, mInputDispatchFrozen); } - private String timeoutMessage(String reason) { - if (reason == null) { - return "Input dispatching timed out"; + private String timeoutMessage(OptionalInt pid, String reason) { + String message = (reason == null) ? "Input dispatching timed out." + : String.format("Input dispatching timed out (%s).", reason); + if (pid.isEmpty()) { + return message; } - return "Input dispatching timed out (" + reason + ")"; + StalledTransactionInfo stalledTransactionInfo = + SurfaceControl.getStalledTransactionInfo(pid.getAsInt()); + if (stalledTransactionInfo == null) { + return message; + } + return String.format("%s Buffer processing for the associated surface is stuck due to an " + + "unsignaled fence (window=%s, bufferId=0x%016X, frameNumber=%s). This " + + "potentially indicates a GPU hang.", message, stalledTransactionInfo.layerName, + stalledTransactionInfo.bufferId, stalledTransactionInfo.frameNumber); } void dump(PrintWriter pw, String prefix) { diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java index 35513707ebc5..f9fa9e6d4a7b 100644 --- a/services/core/java/com/android/server/wm/Letterbox.java +++ b/services/core/java/com/android/server/wm/Letterbox.java @@ -20,7 +20,6 @@ import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.view.SurfaceControl.HIDDEN; import static android.window.TaskConstants.TASK_CHILD_LAYER_LETTERBOX_BACKGROUND; -import android.content.Context; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; @@ -256,11 +255,11 @@ public class Letterbox { private final GestureDetector mDoubleTapDetector; private final DoubleTapListener mDoubleTapListener; - TapEventReceiver(InputChannel inputChannel, Context context) { + TapEventReceiver(InputChannel inputChannel, WindowManagerService wmService) { super(inputChannel, UiThread.getHandler().getLooper()); - mDoubleTapListener = new DoubleTapListener(); + mDoubleTapListener = new DoubleTapListener(wmService); mDoubleTapDetector = new GestureDetector( - context, mDoubleTapListener, UiThread.getHandler()); + wmService.mContext, mDoubleTapListener, UiThread.getHandler()); } @Override @@ -271,14 +270,24 @@ public class Letterbox { } private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { + private final WindowManagerService mWmService; + + private DoubleTapListener(WindowManagerService wmService) { + mWmService = wmService; + } + @Override public boolean onDoubleTapEvent(MotionEvent e) { - if (e.getAction() == MotionEvent.ACTION_UP) { - mDoubleTapCallbackX.accept((int) e.getRawX()); - mDoubleTapCallbackY.accept((int) e.getRawY()); - return true; + synchronized (mWmService.mGlobalLock) { + // This check prevents late events to be handled in case the Letterbox has been + // already destroyed and so mOuter.isEmpty() is true. + if (!mOuter.isEmpty() && e.getAction() == MotionEvent.ACTION_UP) { + mDoubleTapCallbackX.accept((int) e.getRawX()); + mDoubleTapCallbackY.accept((int) e.getRawY()); + return true; + } + return false; } - return false; } } @@ -294,7 +303,7 @@ public class Letterbox { mWmService = win.mWmService; final String name = namePrefix + (win.mActivityRecord != null ? win.mActivityRecord : win); mClientChannel = mWmService.mInputManager.createInputChannel(name); - mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService.mContext); + mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService); mToken = mClientChannel.getToken(); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 7d3c87a7556b..01786becda61 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -63,6 +63,8 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTAT import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; @@ -215,6 +217,11 @@ final class LetterboxUiController { @Nullable private final Boolean mBooleanPropertyAllowForceResizeOverride; + @Nullable + private final Boolean mBooleanPropertyAllowUserAspectRatioOverride; + @Nullable + private final Boolean mBooleanPropertyAllowUserAspectRatioFullscreenOverride; + /* * WindowContainerListener responsible to make translucent activities inherit * constraints from the first opaque activity beneath them. It's null for not @@ -335,6 +342,15 @@ final class LetterboxUiController { /* gatingCondition */ null, PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES); + mBooleanPropertyAllowUserAspectRatioOverride = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled(), + PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE); + mBooleanPropertyAllowUserAspectRatioFullscreenOverride = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled(), + PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE); + mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION); mIsOverrideToPortraitOrientationEnabled = isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT); @@ -953,8 +969,10 @@ final class LetterboxUiController { final Rect innerFrame = hasInheritedLetterboxBehavior() ? mActivityRecord.getBounds() : w.getFrame(); mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint); - // We need to notify Shell that letterbox position has changed. - mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); + if (mDoubleTapEvent) { + // We need to notify Shell that letterbox position has changed. + mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); + } } else if (mLetterbox != null) { mLetterbox.hide(); } @@ -1109,7 +1127,8 @@ final class LetterboxUiController { } boolean shouldApplyUserMinAspectRatioOverride() { - if (!mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled() + if (FALSE.equals(mBooleanPropertyAllowUserAspectRatioOverride) + || !mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled() || mActivityRecord.mDisplayContent == null || !mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()) { return false; @@ -1122,7 +1141,9 @@ final class LetterboxUiController { } boolean shouldApplyUserFullscreenOverride() { - if (!mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled() + if (FALSE.equals(mBooleanPropertyAllowUserAspectRatioOverride) + || FALSE.equals(mBooleanPropertyAllowUserAspectRatioFullscreenOverride) + || !mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled() || mActivityRecord.mDisplayContent == null || !mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()) { return false; @@ -1151,7 +1172,8 @@ final class LetterboxUiController { } } - private int getUserMinAspectRatioOverrideCode() { + @VisibleForTesting + int getUserMinAspectRatioOverrideCode() { try { return mActivityRecord.mAtmService.getPackageManager() .getUserMinAspectRatio(mActivityRecord.packageName, mActivityRecord.mUserId); @@ -1222,6 +1244,7 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } else if (mLetterbox.getInnerFrame().right < x) { // Moving to the next stop on the right side of the app window: left > center > right. mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop( @@ -1232,8 +1255,8 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } - mDoubleTapEvent = true; // TODO(197549949): Add animation for transition. mActivityRecord.recomputeConfiguration(); } @@ -1261,6 +1284,7 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } else if (mLetterbox.getInnerFrame().bottom < y) { // Moving to the next stop on the bottom side of the app window: top > center > bottom. mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop( @@ -1271,8 +1295,8 @@ final class LetterboxUiController { ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER; logLetterboxPositionChange(changeToLog); + mDoubleTapEvent = true; } - mDoubleTapEvent = true; // TODO(197549949): Add animation for transition. mActivityRecord.recomputeConfiguration(); } diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java index badcfa9d6964..37f9730d5443 100644 --- a/services/core/java/com/android/server/wm/SnapshotController.java +++ b/services/core/java/com/android/server/wm/SnapshotController.java @@ -16,185 +16,36 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import android.annotation.IntDef; -import android.util.ArraySet; -import android.util.Slog; -import android.util.SparseArray; +import android.os.Trace; import android.view.WindowManager; -import com.android.internal.annotations.VisibleForTesting; - import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.function.Consumer; /** * Integrates common functionality from TaskSnapshotController and ActivitySnapshotController. */ class SnapshotController { - private static final boolean DEBUG = false; - private static final String TAG = AbsAppSnapshotController.TAG; - - static final int ACTIVITY_OPEN = 1; - static final int ACTIVITY_CLOSE = 2; - static final int TASK_OPEN = 4; - static final int TASK_CLOSE = 8; - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef( - value = {ACTIVITY_OPEN, - ACTIVITY_CLOSE, - TASK_OPEN, - TASK_CLOSE}) - @interface TransitionStateType {} - private final SnapshotPersistQueue mSnapshotPersistQueue; final TaskSnapshotController mTaskSnapshotController; final ActivitySnapshotController mActivitySnapshotController; - private final ArraySet<Task> mTmpCloseTasks = new ArraySet<>(); - private final ArraySet<Task> mTmpOpenTasks = new ArraySet<>(); - - private final SparseArray<TransitionState> mTmpOpenCloseRecord = new SparseArray<>(); - private final ArraySet<Integer> mTmpAnalysisRecord = new ArraySet<>(); - private final SparseArray<ArrayList<Consumer<TransitionState>>> mTransitionStateConsumer = - new SparseArray<>(); - private int mActivatedType; - - private final ActivityOrderCheck mActivityOrderCheck = new ActivityOrderCheck(); - private final ActivityOrderCheck.AnalysisResult mResultHandler = (type, close, open) -> { - addTransitionRecord(type, true/* open */, open); - addTransitionRecord(type, false/* open */, close); - }; - - private static class ActivityOrderCheck { - private ActivityRecord mOpenActivity; - private ActivityRecord mCloseActivity; - private int mOpenIndex = -1; - private int mCloseIndex = -1; - - private void reset() { - mOpenActivity = null; - mCloseActivity = null; - mOpenIndex = -1; - mCloseIndex = -1; - } - - private void setTarget(boolean open, ActivityRecord ar, int index) { - if (open) { - mOpenActivity = ar; - mOpenIndex = index; - } else { - mCloseActivity = ar; - mCloseIndex = index; - } - } - - void analysisOrder(ArraySet<ActivityRecord> closeApps, - ArraySet<ActivityRecord> openApps, Task task, AnalysisResult result) { - for (int j = closeApps.size() - 1; j >= 0; j--) { - final ActivityRecord ar = closeApps.valueAt(j); - if (ar.getTask() == task) { - setTarget(false, ar, task.mChildren.indexOf(ar)); - break; - } - } - for (int j = openApps.size() - 1; j >= 0; j--) { - final ActivityRecord ar = openApps.valueAt(j); - if (ar.getTask() == task) { - setTarget(true, ar, task.mChildren.indexOf(ar)); - break; - } - } - if (mOpenIndex > mCloseIndex && mCloseIndex != -1) { - result.onCheckResult(ACTIVITY_OPEN, mCloseActivity, mOpenActivity); - } else if (mOpenIndex < mCloseIndex && mOpenIndex != -1) { - result.onCheckResult(ACTIVITY_CLOSE, mCloseActivity, mOpenActivity); - } - reset(); - } - private interface AnalysisResult { - void onCheckResult(@TransitionStateType int type, - ActivityRecord close, ActivityRecord open); - } - } - - private void addTransitionRecord(int type, boolean open, WindowContainer target) { - TransitionState record = mTmpOpenCloseRecord.get(type); - if (record == null) { - record = new TransitionState(); - mTmpOpenCloseRecord.set(type, record); - } - record.addParticipant(target, open); - mTmpAnalysisRecord.add(type); - } - - private void clearRecord() { - mTmpOpenCloseRecord.clear(); - mTmpAnalysisRecord.clear(); - } - - static class TransitionState<TYPE extends WindowContainer> { - private final ArraySet<TYPE> mOpenParticipant = new ArraySet<>(); - private final ArraySet<TYPE> mCloseParticipant = new ArraySet<>(); - - void addParticipant(TYPE target, boolean open) { - final ArraySet<TYPE> participant = open - ? mOpenParticipant : mCloseParticipant; - participant.add(target); - } - - ArraySet<TYPE> getParticipant(boolean open) { - return open ? mOpenParticipant : mCloseParticipant; - } - } - SnapshotController(WindowManagerService wms) { mSnapshotPersistQueue = new SnapshotPersistQueue(); mTaskSnapshotController = new TaskSnapshotController(wms, mSnapshotPersistQueue); mActivitySnapshotController = new ActivitySnapshotController(wms, mSnapshotPersistQueue); } - void registerTransitionStateConsumer(@TransitionStateType int type, - Consumer<TransitionState> consumer) { - ArrayList<Consumer<TransitionState>> consumers = mTransitionStateConsumer.get(type); - if (consumers == null) { - consumers = new ArrayList<>(); - mTransitionStateConsumer.set(type, consumers); - } - if (!consumers.contains(consumer)) { - consumers.add(consumer); - } - mActivatedType |= type; - } - - void unregisterTransitionStateConsumer(int type, Consumer<TransitionState> consumer) { - final ArrayList<Consumer<TransitionState>> consumers = mTransitionStateConsumer.get(type); - if (consumers == null) { - return; - } - consumers.remove(consumer); - if (consumers.size() == 0) { - mActivatedType &= ~type; - } - } - - private boolean hasTransitionStateConsumer(@TransitionStateType int type) { - return (mActivatedType & type) != 0; - } - void systemReady() { mSnapshotPersistQueue.systemReady(); - mTaskSnapshotController.systemReady(); - mActivitySnapshotController.systemReady(); } void setPause(boolean paused) { @@ -212,126 +63,76 @@ class SnapshotController { } void notifyAppVisibilityChanged(ActivityRecord appWindowToken, boolean visible) { - if (!visible && hasTransitionStateConsumer(TASK_CLOSE)) { - final Task task = appWindowToken.getTask(); - if (task == null || task.isVisibleRequested()) { - return; - } - // close task transition - addTransitionRecord(TASK_CLOSE, false /*open*/, task); - mActivitySnapshotController.preTransitionStart(); - notifyTransition(TASK_CLOSE); - mActivitySnapshotController.postTransitionStart(); - clearRecord(); - } + mActivitySnapshotController.notifyAppVisibilityChanged(appWindowToken, visible); } - // For legacy transition + // For legacy transition, which won't support activity snapshot void onTransitionStarting(DisplayContent displayContent) { - handleAppTransition(displayContent.mClosingApps, displayContent.mOpeningApps); + mTaskSnapshotController.handleClosingApps(displayContent.mClosingApps); } - // For shell transition, adapt to legacy transition. - void onTransitionReady(@WindowManager.TransitionType int type, - ArraySet<WindowContainer> participants) { + // For shell transition, record snapshots before transaction start. + void onTransactionReady(@WindowManager.TransitionType int type, + ArrayList<Transition.ChangeInfo> changeInfos) { final boolean isTransitionOpen = isTransitionOpen(type); final boolean isTransitionClose = isTransitionClose(type); - if (!isTransitionOpen && !isTransitionClose && type < TRANSIT_FIRST_CUSTOM - || (mActivatedType == 0)) { + if (!isTransitionOpen && !isTransitionClose && type < TRANSIT_FIRST_CUSTOM) { return; } - final ArraySet<ActivityRecord> openingApps = new ArraySet<>(); - final ArraySet<ActivityRecord> closingApps = new ArraySet<>(); - - for (int i = participants.size() - 1; i >= 0; --i) { - final ActivityRecord ar = participants.valueAt(i).asActivityRecord(); - if (ar == null || ar.getTask() == null) continue; - if (ar.isVisibleRequested()) { - openingApps.add(ar); - } else { - closingApps.add(ar); + for (int i = changeInfos.size() - 1; i >= 0; --i) { + Transition.ChangeInfo info = changeInfos.get(i); + // Intentionally skip record snapshot for changes originated from PiP. + if (info.mWindowingMode == WINDOWING_MODE_PINNED) continue; + if (info.mContainer.asTask() != null && !info.mContainer.isVisibleRequested()) { + mTaskSnapshotController.recordSnapshot(info.mContainer.asTask(), + false /* allowSnapshotHome */); + } + // Won't need to capture activity snapshot in close transition. + if (isTransitionClose) { + continue; + } + if (info.mContainer.asActivityRecord() != null + || info.mContainer.asTaskFragment() != null) { + final TaskFragment tf = info.mContainer.asTaskFragment(); + final ActivityRecord ar = tf != null ? tf.getTopMostActivity() + : info.mContainer.asActivityRecord(); + final boolean taskVis = ar != null && ar.getTask().isVisibleRequested(); + if (ar != null && !ar.isVisibleRequested() && taskVis) { + mActivitySnapshotController.recordSnapshot(ar); + } } } - handleAppTransition(closingApps, openingApps); } - private static boolean isTransitionOpen(int type) { - return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT; - } - private static boolean isTransitionClose(int type) { - return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK; - } - - @VisibleForTesting - void handleAppTransition(ArraySet<ActivityRecord> closingApps, - ArraySet<ActivityRecord> openApps) { - if (mActivatedType == 0) { + void onTransitionFinish(@WindowManager.TransitionType int type, + ArrayList<Transition.ChangeInfo> changeInfos) { + final boolean isTransitionOpen = isTransitionOpen(type); + final boolean isTransitionClose = isTransitionClose(type); + if (!isTransitionOpen && !isTransitionClose && type < TRANSIT_FIRST_CUSTOM + || (changeInfos.isEmpty())) { return; } - analysisTransition(closingApps, openApps); - mActivitySnapshotController.preTransitionStart(); - for (Integer transitionType : mTmpAnalysisRecord) { - notifyTransition(transitionType); - } - mActivitySnapshotController.postTransitionStart(); - clearRecord(); - } - - private void notifyTransition(int transitionType) { - final TransitionState record = mTmpOpenCloseRecord.get(transitionType); - final ArrayList<Consumer<TransitionState>> consumers = - mTransitionStateConsumer.get(transitionType); - for (Consumer<TransitionState> consumer : consumers) { - consumer.accept(record); - } - } - - private void analysisTransition(ArraySet<ActivityRecord> closingApps, - ArraySet<ActivityRecord> openingApps) { - getParticipantTasks(closingApps, mTmpCloseTasks, false /* isOpen */); - getParticipantTasks(openingApps, mTmpOpenTasks, true /* isOpen */); - if (DEBUG) { - Slog.d(TAG, "AppSnapshotController#analysisTransition participants" - + " mTmpCloseTasks " + mTmpCloseTasks - + " mTmpOpenTasks " + mTmpOpenTasks); - } - for (int i = mTmpCloseTasks.size() - 1; i >= 0; i--) { - final Task closeTask = mTmpCloseTasks.valueAt(i); - if (mTmpOpenTasks.contains(closeTask)) { - if (hasTransitionStateConsumer(ACTIVITY_OPEN) - || hasTransitionStateConsumer(ACTIVITY_CLOSE)) { - mActivityOrderCheck.analysisOrder(closingApps, openingApps, closeTask, - mResultHandler); - } - } else if (hasTransitionStateConsumer(TASK_CLOSE)) { - // close task transition - addTransitionRecord(TASK_CLOSE, false /*open*/, closeTask); + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "SnapshotController_analysis"); + mActivitySnapshotController.beginSnapshotProcess(); + final ArrayList<WindowContainer> windows = new ArrayList<>(); + for (int i = changeInfos.size() - 1; i >= 0; --i) { + final WindowContainer wc = changeInfos.get(i).mContainer; + if (wc.asTask() == null && wc.asTaskFragment() == null + && wc.asActivityRecord() == null) { + continue; } + windows.add(wc); } - if (hasTransitionStateConsumer(TASK_OPEN)) { - for (int i = mTmpOpenTasks.size() - 1; i >= 0; i--) { - final Task openTask = mTmpOpenTasks.valueAt(i); - if (!mTmpCloseTasks.contains(openTask)) { - // this is open task transition - addTransitionRecord(TASK_OPEN, true /*open*/, openTask); - } - } - } - mTmpCloseTasks.clear(); - mTmpOpenTasks.clear(); + mActivitySnapshotController.handleTransitionFinish(windows); + mActivitySnapshotController.endSnapshotProcess(); + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } - private void getParticipantTasks(ArraySet<ActivityRecord> activityRecords, ArraySet<Task> tasks, - boolean isOpen) { - for (int i = activityRecords.size() - 1; i >= 0; i--) { - final ActivityRecord activity = activityRecords.valueAt(i); - final Task task = activity.getTask(); - if (task == null) continue; - - if (isOpen == activity.isVisibleRequested()) { - tasks.add(task); - } - } + private static boolean isTransitionOpen(int type) { + return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT; + } + private static boolean isTransitionClose(int type) { + return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK; } void dump(PrintWriter pw, String prefix) { diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index 58e1c544202d..f4f641f35395 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -17,6 +17,7 @@ package com.android.server.wm; import static android.graphics.Bitmap.CompressFormat.JPEG; +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -26,6 +27,7 @@ import android.annotation.TestApi; import android.graphics.Bitmap; import android.os.Process; import android.os.SystemClock; +import android.os.Trace; import android.util.AtomicFile; import android.util.Slog; import android.window.TaskSnapshot; @@ -249,6 +251,7 @@ class SnapshotPersistQueue { @Override void write() { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "StoreWriteQueueItem"); if (!mPersistInfoProvider.createDirectory(mUserId)) { Slog.e(TAG, "Unable to create snapshot directory for user dir=" + mPersistInfoProvider.getDirectory(mUserId)); @@ -263,6 +266,7 @@ class SnapshotPersistQueue { if (failed) { deleteSnapshot(mId, mUserId, mPersistInfoProvider); } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } boolean writeProto() { @@ -373,7 +377,9 @@ class SnapshotPersistQueue { @Override void write() { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "DeleteWriteQueueItem"); deleteSnapshot(mId, mUserId, mPersistInfoProvider); + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } } } diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java index 2b22d75693fe..34806bd023a0 100644 --- a/services/core/java/com/android/server/wm/StartingData.java +++ b/services/core/java/com/android/server/wm/StartingData.java @@ -65,6 +65,9 @@ public abstract class StartingData { /** Whether to prepare the removal animation. */ boolean mPrepareRemoveAnimation; + /** Non-zero if this starting window is added in a collecting transition. */ + int mTransitionId; + protected StartingData(WindowManagerService service, int typeParams) { mService = service; mTypeParams = typeParams; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 6caccddcab2b..0f0189e614b6 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2878,8 +2878,8 @@ class Task extends TaskFragment { // No need to check if allowed if it's leaving dragResize if (dragResizing && !(getRootTask().getWindowingMode() == WINDOWING_MODE_FREEFORM)) { - throw new IllegalArgumentException("Drag resize not allow for root task id=" - + getRootTaskId()); + Slog.e(TAG, "Drag resize isn't allowed for root task id=" + getRootTaskId()); + return; } mDragResizing = dragResizing; resetDragResizingChangeReported(); @@ -3459,6 +3459,8 @@ class Task extends TaskFragment { info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET; + info.isUserFullscreenOverrideEnabled = top != null + && top.mLetterboxUiController.shouldApplyUserFullscreenOverride(); info.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap(); if (info.isLetterboxDoubleTapEnabled) { info.topActivityLetterboxWidth = top.getBounds().width(); diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index c747c09a6872..4eb42908c6d4 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -16,7 +16,6 @@ package com.android.server.wm; -import static com.android.server.wm.SnapshotController.TASK_CLOSE; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -77,13 +76,6 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot setSnapshotEnabled(snapshotEnabled); } - void systemReady() { - if (!shouldDisableSnapshots()) { - mService.mSnapshotController.registerTransitionStateConsumer(TASK_CLOSE, - this::handleTaskClose); - } - } - static PersistInfoProvider createPersistInfoProvider(WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver) { final float highResTaskSnapshotScale = service.mContext.getResources().getFloat( @@ -116,20 +108,23 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot enableLowResSnapshots, lowResScaleFactor, use16BitFormat); } - void handleTaskClose(SnapshotController.TransitionState<Task> closeTaskTransitionRecord) { + // Still needed for legacy transition.(AppTransitionControllerTest) + void handleClosingApps(ArraySet<ActivityRecord> closingApps) { if (shouldDisableSnapshots()) { return; } + // We need to take a snapshot of the task if and only if all activities of the task are + // either closing or hidden. mTmpTasks.clear(); - final ArraySet<Task> tasks = closeTaskTransitionRecord.getParticipant(false /* open */); - if (mService.mAtmService.getTransitionController().isShellTransitionsEnabled()) { - mTmpTasks.addAll(tasks); - } else { - for (Task task : tasks) { - getClosingTasksInner(task, mTmpTasks); - } + for (int i = closingApps.size() - 1; i >= 0; i--) { + final ActivityRecord activity = closingApps.valueAt(i); + final Task task = activity.getTask(); + if (task == null) continue; + + getClosingTasksInner(task, mTmpTasks); } snapshotTasks(mTmpTasks); + mTmpTasks.clear(); mSkipClosingAppSnapshotTasks.clear(); } diff --git a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java index cd1511914d34..3e8c0177b3b3 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java @@ -16,6 +16,9 @@ package com.android.server.wm; +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; + +import android.os.Trace; import android.util.ArraySet; import android.window.TaskSnapshot; @@ -102,6 +105,7 @@ class TaskSnapshotPersister extends BaseAppSnapshotPersister { @Override void write() { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "RemoveObsoleteFilesQueueItem"); final ArraySet<Integer> newPersistedTaskIds; synchronized (mLock) { newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete); @@ -120,6 +124,7 @@ class TaskSnapshotPersister extends BaseAppSnapshotPersister { } } } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index fd25edf7cb0f..f1fb17bbf316 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1191,8 +1191,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { " Skipping post-transition snapshot for task %d", task.mTaskId); } - snapController.mActivitySnapshotController - .notifyAppVisibilityChanged(ar, false /* visible */); } ar.commitVisibility(false /* visible */, false /* performLayout */, true /* fromTransition */); @@ -1389,6 +1387,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Handle back animation if it's already started. mController.mAtm.mBackNavigationController.onTransitionFinish(mTargets, this); mController.mFinishingTransition = null; + mController.mSnapshotController.onTransitionFinish(mType, mTargets); } void abort() { @@ -1593,16 +1592,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // transferred. If transition is transient, IME won't be moved during the transition and // the tasks are still live, so we take the snapshot at the end of the transition instead. if (mTransientLaunches == null) { - for (int i = mParticipants.size() - 1; i >= 0; --i) { - final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); - if (ar == null || ar.getTask() == null - || ar.getTask().isVisibleRequested()) continue; - final ChangeInfo change = mChanges.get(ar); - // Intentionally skip record snapshot for changes originated from PiP. - if (change != null && change.mWindowingMode == WINDOWING_MODE_PINNED) continue; - mController.mSnapshotController.mTaskSnapshotController.recordSnapshot( - ar.getTask(), false /* allowSnapshotHome */); - } + mController.mSnapshotController.onTransactionReady(mType, mTargets); } // This is non-null only if display has changes. It handles the visible windows that don't diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java index af8fb0252675..c59d2d392e93 100644 --- a/services/core/java/com/android/server/wm/TransitionTracer.java +++ b/services/core/java/com/android/server/wm/TransitionTracer.java @@ -145,6 +145,27 @@ public class TransitionTracer { } } + void logRemovingStartingWindow(@NonNull StartingData startingData) { + if (startingData.mTransitionId == 0) { + return; + } + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, + startingData.mTransitionId); + outputStream.write( + com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } + } + private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream, Transition transition, ArrayList<ChangeInfo> targets) { Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto"); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 0388496712fb..9375b297be2b 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1657,9 +1657,18 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) { - final TaskFragment root1 = WindowContainer.fromBinder(hop.getContainer()).asTaskFragment(); - final TaskFragment root2 = - WindowContainer.fromBinder(hop.getAdjacentRoot()).asTaskFragment(); + final WindowContainer wc1 = WindowContainer.fromBinder(hop.getContainer()); + if (wc1 == null || !wc1.isAttached()) { + Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc1); + return TRANSACT_EFFECTS_NONE; + } + final TaskFragment root1 = wc1.asTaskFragment(); + final WindowContainer wc2 = WindowContainer.fromBinder(hop.getAdjacentRoot()); + if (wc2 == null || !wc2.isAttached()) { + Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc2); + return TRANSACT_EFFECTS_NONE; + } + final TaskFragment root2 = wc2.asTaskFragment(); if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) { throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by" + " organizer root1=" + root1 + " root2=" + root2); @@ -1672,7 +1681,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int clearAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) { - final TaskFragment root = WindowContainer.fromBinder(hop.getContainer()).asTaskFragment(); + final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer()); + if (wc == null || !wc.isAttached()) { + Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc); + return TRANSACT_EFFECTS_NONE; + } + final TaskFragment root = wc.asTaskFragment(); if (!root.mCreatedByOrganizer) { throw new IllegalArgumentException("clearAdjacentRootsHierarchyOp: Not created by" + " organizer root=" + root); diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index d7d2b4e9dde2..caec45c555ab 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -48,6 +48,7 @@ import static com.android.server.wm.WindowManagerService.MY_PID; import static java.util.Objects.requireNonNull; import android.Manifest; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -75,7 +76,6 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.proto.ProtoOutputStream; -import android.view.IRemoteAnimationRunner; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -87,6 +87,8 @@ import com.android.server.wm.ActivityTaskManagerService.HotPath; import java.io.IOException; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -249,11 +251,30 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio @Nullable private ArrayMap<ActivityRecord, int[]> mRemoteActivities; - /** Whether our process is currently running a {@link RecentsAnimation} */ - private boolean mRunningRecentsAnimation; + /** + * It can be set for a running transition player ({@link android.window.ITransitionPlayer}) or + * remote animators (running {@link android.window.IRemoteTransition}). + */ + static final int ANIMATING_REASON_REMOTE_ANIMATION = 1; + /** It is set for wakefulness transition. */ + static final int ANIMATING_REASON_WAKEFULNESS_CHANGE = 1 << 1; + /** Whether the legacy {@link RecentsAnimation} is running. */ + static final int ANIMATING_REASON_LEGACY_RECENT_ANIMATION = 1 << 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ANIMATING_REASON_REMOTE_ANIMATION, + ANIMATING_REASON_WAKEFULNESS_CHANGE, + ANIMATING_REASON_LEGACY_RECENT_ANIMATION, + }) + @interface AnimatingReason {} - /** Whether our process is currently running a {@link IRemoteAnimationRunner} */ - private boolean mRunningRemoteAnimation; + /** + * Non-zero if this process is currently running an important animation. This should be never + * set for system server. + */ + @AnimatingReason + private int mAnimatingReasons; // The bits used for mActivityStateFlags. private static final int ACTIVITY_STATE_FLAG_IS_VISIBLE = 1 << 16; @@ -1847,30 +1868,45 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } void setRunningRecentsAnimation(boolean running) { - if (mRunningRecentsAnimation == running) { - return; + if (running) { + addAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION); + } else { + removeAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION); } - mRunningRecentsAnimation = running; - updateRunningRemoteOrRecentsAnimation(); } void setRunningRemoteAnimation(boolean running) { - if (mRunningRemoteAnimation == running) { - return; + if (running) { + addAnimatingReason(ANIMATING_REASON_REMOTE_ANIMATION); + } else { + removeAnimatingReason(ANIMATING_REASON_REMOTE_ANIMATION); + } + } + + void addAnimatingReason(@AnimatingReason int reason) { + final int prevReasons = mAnimatingReasons; + mAnimatingReasons |= reason; + if (prevReasons == 0) { + setAnimating(true); } - mRunningRemoteAnimation = running; - updateRunningRemoteOrRecentsAnimation(); } - void updateRunningRemoteOrRecentsAnimation() { + void removeAnimatingReason(@AnimatingReason int reason) { + final int prevReasons = mAnimatingReasons; + mAnimatingReasons &= ~reason; + if (prevReasons != 0 && mAnimatingReasons == 0) { + setAnimating(false); + } + } + + /** Applies the animating state to activity manager for updating process priority. */ + private void setAnimating(boolean animating) { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.sendMessage(PooledLambda.obtainMessage( - WindowProcessListener::setRunningRemoteAnimation, mListener, - isRunningRemoteTransition())); + mAtm.mH.post(() -> mListener.setRunningRemoteAnimation(animating)); } boolean isRunningRemoteTransition() { - return mRunningRecentsAnimation || mRunningRemoteAnimation; + return (mAnimatingReasons & ANIMATING_REASON_REMOTE_ANIMATION) != 0; } /** Adjusts scheduling group for animation. This method MUST NOT be called inside WM lock. */ @@ -1924,6 +1960,21 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio pw.println(prefix + " mLastReportedConfiguration=" + (mHasCachedConfiguration ? ("(cached) " + mLastReportedConfiguration) : mLastReportedConfiguration)); + final int animatingReasons = mAnimatingReasons; + if (animatingReasons != 0) { + pw.print(prefix + " mAnimatingReasons="); + if ((animatingReasons & ANIMATING_REASON_REMOTE_ANIMATION) != 0) { + pw.print("remote-animation|"); + } + if ((animatingReasons & ANIMATING_REASON_WAKEFULNESS_CHANGE) != 0) { + pw.print("wakefulness|"); + } + if ((animatingReasons & ANIMATING_REASON_LEGACY_RECENT_ANIMATION) != 0) { + pw.print("legacy-recents"); + } + pw.println(); + } + final int stateFlags = mActivityStateFlags; if (stateFlags != ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER) { pw.print(prefix + " mActivityStateFlags="); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 8faeebc8de7b..c09e6a3fda18 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2412,7 +2412,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP ProtoLog.v(WM_DEBUG_ADD_REMOVE, "removeIfPossible: %s callers=%s", this, Debug.getCallers(5)); - final boolean startingWindow = mAttrs.type == TYPE_APPLICATION_STARTING; + final boolean startingWindow = mStartingData != null; if (startingWindow) { ProtoLog.d(WM_DEBUG_STARTING_WINDOW, "Starting window removed %s", this); // Cancel the remove starting window animation on shell. The main window might changed @@ -2426,6 +2426,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return false; }, true); } + mTransitionController.mTransitionTracer.logRemovingStartingWindow(mStartingData); } else if (mAttrs.type == TYPE_BASE_APPLICATION && isSelfAnimating(0, ANIMATION_TYPE_STARTING_REVEAL)) { // Cancel the remove starting window animation in case the binder dead before remove diff --git a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java index b6204073d1cc..f5360eb9a56a 100644 --- a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java +++ b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java @@ -30,6 +30,7 @@ import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; +import android.content.ComponentName; import android.content.Context; import android.content.IntentFilter; import android.content.pm.ShortcutInfo; @@ -39,6 +40,7 @@ import android.provider.DeviceConfig; import android.util.Log; import android.util.Slog; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ChooserActivity; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; @@ -63,6 +65,7 @@ class ShareTargetPredictor extends AppTargetPredictor { private static final String REMOTE_APP_PREDICTOR_KEY = "remote_app_predictor"; private final IntentFilter mIntentFilter; private final AppPredictor mRemoteAppPredictor; + @Nullable private final String mChooserActivity; ShareTargetPredictor(@NonNull AppPredictionContext predictionContext, @NonNull Consumer<List<AppTarget>> updatePredictionsMethod, @@ -81,6 +84,9 @@ class ShareTargetPredictor extends AppTargetPredictor { } else { mRemoteAppPredictor = null; } + ComponentName component = ComponentName.unflattenFromString( + context.getResources().getString(R.string.config_chooserActivity)); + mChooserActivity = (component == null) ? null : component.getShortClassName(); } /** Reports chosen history of direct/app share targets. */ @@ -138,7 +144,7 @@ class ShareTargetPredictor extends AppTargetPredictor { SharesheetModelScorer.computeScoreForAppShare(shareTargets, getShareEventType(mIntentFilter), getPredictionContext().getPredictedTargetCount(), System.currentTimeMillis(), getDataManager(), - mCallingUserId); + mCallingUserId, mChooserActivity); Collections.sort(shareTargets, (t1, t2) -> -Float.compare(t1.getScore(), t2.getScore())); List<AppTarget> appTargetList = new ArrayList<>(); for (ShareTarget shareTarget : shareTargets) { diff --git a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java index c77843cfb044..b2f1e21a1395 100644 --- a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java +++ b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java @@ -26,7 +26,6 @@ import android.util.Range; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.app.ChooserActivity; import com.android.server.people.data.AppUsageStatsData; import com.android.server.people.data.DataManager; import com.android.server.people.data.Event; @@ -55,8 +54,6 @@ class SharesheetModelScorer { private static final float FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY = 0.3F; @VisibleForTesting static final float FOREGROUND_APP_WEIGHT = 0F; - @VisibleForTesting - static final String CHOOSER_ACTIVITY = ChooserActivity.class.getSimpleName(); // Keep constructor private to avoid class being instantiated. private SharesheetModelScorer() { @@ -169,13 +166,14 @@ class SharesheetModelScorer { */ static void computeScoreForAppShare(List<ShareTargetPredictor.ShareTarget> shareTargets, int shareEventType, int targetsLimit, long now, @NonNull DataManager dataManager, - @UserIdInt int callingUserId) { + @UserIdInt int callingUserId, @Nullable String chooserActivity) { computeScore(shareTargets, shareEventType, now); - postProcess(shareTargets, targetsLimit, dataManager, callingUserId); + postProcess(shareTargets, targetsLimit, dataManager, callingUserId, chooserActivity); } private static void postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets, - int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId, + @Nullable String chooserActivity) { // Populates a map which key is package name and value is list of shareTargets descended // on total score. Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap = new ArrayMap<>(); @@ -192,7 +190,7 @@ class SharesheetModelScorer { } targetsList.add(index, shareTarget); } - promoteForegroundApp(shareTargetMap, dataManager, callingUserId); + promoteForegroundApp(shareTargetMap, dataManager, callingUserId, chooserActivity); promoteMostChosenAndFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager, callingUserId); } @@ -272,9 +270,10 @@ class SharesheetModelScorer { */ private static void promoteForegroundApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, - @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + @NonNull DataManager dataManager, @UserIdInt int callingUserId, + @Nullable String chooserActivity) { String sharingForegroundApp = findSharingForegroundApp(shareTargetMap, dataManager, - callingUserId); + callingUserId, chooserActivity); if (sharingForegroundApp != null) { ShareTargetPredictor.ShareTarget target = shareTargetMap.get(sharingForegroundApp).get( 0); @@ -297,7 +296,8 @@ class SharesheetModelScorer { @Nullable private static String findSharingForegroundApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, - @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + @NonNull DataManager dataManager, @UserIdInt int callingUserId, + @Nullable String chooserActivity) { String sharingForegroundApp = null; long now = System.currentTimeMillis(); List<UsageEvents.Event> events = dataManager.queryAppMovingToForegroundEvents( @@ -306,8 +306,8 @@ class SharesheetModelScorer { for (int i = events.size() - 1; i >= 0; i--) { String className = events.get(i).getClassName(); String packageName = events.get(i).getPackageName(); - if (packageName == null || (className != null && className.contains(CHOOSER_ACTIVITY)) - || packageName.contains(CHOOSER_ACTIVITY)) { + if (packageName == null || (className != null && chooserActivity != null + && className.contains(chooserActivity))) { continue; } if (sourceApp == null) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowPowerModeModifierTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowPowerModeModifierTest.java new file mode 100644 index 000000000000..266f5c198f03 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowPowerModeModifierTest.java @@ -0,0 +1,126 @@ +/* + * 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.brightness.clamper; + +import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; + +import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.brightness.BrightnessReason; + +import org.junit.Before; +import org.junit.Test; + +@SmallTest +public class BrightnessLowPowerModeModifierTest { + private static final float FLOAT_TOLERANCE = 0.001f; + private static final float DEFAULT_BRIGHTNESS = 0.5f; + private static final float LOW_POWER_BRIGHTNESS_FACTOR = 0.8f; + private static final float EXPECTED_LOW_POWER_BRIGHTNESS = + DEFAULT_BRIGHTNESS * LOW_POWER_BRIGHTNESS_FACTOR; + private final DisplayPowerRequest mRequest = new DisplayPowerRequest(); + private final DisplayBrightnessState.Builder mBuilder = prepareBuilder(); + private BrightnessLowPowerModeModifier mClamper; + + @Before + public void setUp() { + mClamper = new BrightnessLowPowerModeModifier(); + mRequest.screenLowPowerBrightnessFactor = LOW_POWER_BRIGHTNESS_FACTOR; + mRequest.lowPowerMode = true; + } + + @Test + public void testApply_lowPowerModeOff() { + mRequest.lowPowerMode = false; + + mClamper.apply(mRequest, mBuilder); + + assertEquals(DEFAULT_BRIGHTNESS, mBuilder.getBrightness(), FLOAT_TOLERANCE); + assertEquals(0, mBuilder.getBrightnessReason().getModifier()); + assertTrue(mBuilder.isSlowChange()); + } + + @Test + public void testApply_lowPowerModeOn() { + mClamper.apply(mRequest, mBuilder); + + assertEquals(EXPECTED_LOW_POWER_BRIGHTNESS, mBuilder.getBrightness(), FLOAT_TOLERANCE); + assertEquals(BrightnessReason.MODIFIER_LOW_POWER, + mBuilder.getBrightnessReason().getModifier()); + assertFalse(mBuilder.isSlowChange()); + } + + @Test + public void testApply_lowPowerModeOnAndLowPowerBrightnessFactorHigh() { + mRequest.screenLowPowerBrightnessFactor = 1.1f; + + mClamper.apply(mRequest, mBuilder); + + assertEquals(DEFAULT_BRIGHTNESS, mBuilder.getBrightness(), FLOAT_TOLERANCE); + assertEquals(BrightnessReason.MODIFIER_LOW_POWER, + mBuilder.getBrightnessReason().getModifier()); + assertFalse(mBuilder.isSlowChange()); + } + + @Test + public void testApply_lowPowerModeOnAndMinBrightness() { + mBuilder.setBrightness(0.0f); + mClamper.apply(mRequest, mBuilder); + + assertEquals(0.0f, mBuilder.getBrightness(), FLOAT_TOLERANCE); + assertEquals(0, mBuilder.getBrightnessReason().getModifier()); + assertFalse(mBuilder.isSlowChange()); + } + + @Test + public void testApply_lowPowerModeOnAndLowPowerAlreadyApplied() { + mClamper.apply(mRequest, mBuilder); + DisplayBrightnessState.Builder builder = prepareBuilder(); + + mClamper.apply(mRequest, builder); + + assertEquals(EXPECTED_LOW_POWER_BRIGHTNESS, builder.getBrightness(), FLOAT_TOLERANCE); + assertEquals(BrightnessReason.MODIFIER_LOW_POWER, + builder.getBrightnessReason().getModifier()); + assertTrue(builder.isSlowChange()); + } + + @Test + public void testApply_lowPowerModeOffAfterLowPowerOn() { + mClamper.apply(mRequest, mBuilder); + mRequest.lowPowerMode = false; + DisplayBrightnessState.Builder builder = prepareBuilder(); + + mClamper.apply(mRequest, builder); + + assertEquals(DEFAULT_BRIGHTNESS, builder.getBrightness(), FLOAT_TOLERANCE); + assertEquals(0, builder.getBrightnessReason().getModifier()); + assertFalse(builder.isSlowChange()); + } + + private DisplayBrightnessState.Builder prepareBuilder() { + DisplayBrightnessState.Builder builder = DisplayBrightnessState.builder(); + builder.setBrightness(DEFAULT_BRIGHTNESS); + builder.setIsSlowChange(true); + return builder; + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 0cfddd30d721..769be177ce03 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -351,6 +351,8 @@ public class AuthSessionTest { assertEquals(startFingerprintNow ? BiometricSensor.STATE_AUTHENTICATING : BiometricSensor.STATE_COOKIE_RETURNED, session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState()); + verify(mBiometricContext).updateContext((OperationContextExt) anyObject(), + eq(session.isCrypto())); // start fingerprint sensor if it was delayed if (!startFingerprintNow) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java new file mode 100644 index 000000000000..99d66c5bda19 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java @@ -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.server.biometrics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; + +import com.android.internal.R; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class AuthenticationStatsCollectorTest { + + private AuthenticationStatsCollector mAuthenticationStatsCollector; + private static final float FRR_THRESHOLD = 0.2f; + private static final int USER_ID_1 = 1; + + @Mock + private Context mContext; + @Mock + private Resources mResources; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1)) + .thenReturn(FRR_THRESHOLD); + + mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext, + 0 /* modality */); + } + + + @Test + public void authenticate_authenticationSucceeded_mapShouldBeUpdated() { + // Assert that the user doesn't exist in the map initially. + assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, true /* authenticated*/); + + AuthenticationStats authenticationStats = + mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1); + assertEquals(USER_ID_1, authenticationStats.getUserId()); + assertEquals(1, authenticationStats.getTotalAttempts()); + assertEquals(0, authenticationStats.getRejectedAttempts()); + assertEquals(0, authenticationStats.getEnrollmentNotifications()); + } + + @Test + public void authenticate_authenticationFailed_mapShouldBeUpdated() { + // Assert that the user doesn't exist in the map initially. + assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)); + + mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated*/); + + AuthenticationStats authenticationStats = + mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1); + assertEquals(USER_ID_1, authenticationStats.getUserId()); + assertEquals(1, authenticationStats.getTotalAttempts()); + assertEquals(1, authenticationStats.getRejectedAttempts()); + assertEquals(0, authenticationStats.getEnrollmentNotifications()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java new file mode 100644 index 000000000000..e8e72cb81838 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java @@ -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.server.biometrics; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class AuthenticationStatsTest { + + @Test + public void authenticate_statsShouldBeUpdated() { + AuthenticationStats authenticationStats = + new AuthenticationStats(1 /* userId */ , 0 /* totalAttempts */, + 0 /* rejectedAttempts */, 0 /* enrollmentNotifications */, + 0 /* modality */); + + authenticationStats.authenticate(true /* authenticated */); + + assertEquals(authenticationStats.getTotalAttempts(), 1); + assertEquals(authenticationStats.getRejectedAttempts(), 0); + + authenticationStats.authenticate(false /* authenticated */); + + assertEquals(authenticationStats.getTotalAttempts(), 2); + assertEquals(authenticationStats.getRejectedAttempts(), 1); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java index 612f717226a2..a50871899575 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java @@ -39,6 +39,7 @@ import android.testing.TestableContext; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.sensors.BaseClientMonitor; import org.junit.Before; @@ -65,6 +66,8 @@ public class BiometricLoggerTest { @Mock private BiometricFrameworkStatsLogger mSink; @Mock + private AuthenticationStatsCollector mAuthenticationStatsCollector; + @Mock private SensorManager mSensorManager; @Mock private BaseClientMonitor mClient; @@ -87,7 +90,8 @@ public class BiometricLoggerTest { } private BiometricLogger createLogger(int statsModality, int statsAction, int statsClient) { - return new BiometricLogger(statsModality, statsAction, statsClient, mSink, mSensorManager); + return new BiometricLogger(statsModality, statsAction, statsClient, mSink, + mAuthenticationStatsCollector, mSensorManager); } @Test 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 d1d6e9d41b1f..f43120d1a755 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 @@ -27,6 +27,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.res.Resources; import android.hardware.biometrics.common.CommonProps; import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.ISession; @@ -39,6 +40,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; +import com.android.internal.R; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; @@ -59,11 +61,15 @@ public class FaceProviderTest { private static final String TAG = "FaceProviderTest"; + private static final float FRR_THRESHOLD = 0.2f; + @Mock private Context mContext; @Mock private UserManager mUserManager; @Mock + private Resources mResources; + @Mock private IFace mDaemon; @Mock private BiometricContext mBiometricContext; @@ -86,6 +92,10 @@ public class FaceProviderTest { when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>()); when(mDaemon.createSession(anyInt(), anyInt(), any())).thenReturn(mock(ISession.class)); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1)) + .thenReturn(FRR_THRESHOLD); + final SensorProps sensor1 = new SensorProps(); sensor1.commonProps = new CommonProps(); sensor1.commonProps.sensorId = 0; diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java index d174533d909a..e558c4d64180 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.res.Resources; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceSensorProperties; @@ -41,6 +42,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; +import com.android.internal.R; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; @@ -65,12 +67,15 @@ public class Face10Test { private static final String TAG = "Face10Test"; private static final int SENSOR_ID = 1; private static final int USER_ID = 20; + private static final float FRR_THRESHOLD = 0.2f; @Mock private Context mContext; @Mock private UserManager mUserManager; @Mock + private Resources mResources; + @Mock private BiometricScheduler mScheduler; @Mock private BiometricContext mBiometricContext; @@ -93,6 +98,10 @@ public class Face10Test { when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>()); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1)) + .thenReturn(FRR_THRESHOLD); + mLockoutResetDispatcher = new LockoutResetDispatcher(mContext); final int maxEnrollmentsPerUser = 1; diff --git a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java index 45fff48ade55..2cd9198aa0e0 100644 --- a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java @@ -57,6 +57,7 @@ public final class SharesheetModelScorerTest { private static final String PACKAGE_3 = "pkg3"; private static final String CLASS_1 = "cls1"; private static final String CLASS_2 = "cls2"; + private static final String CHOOSER_ACTIVITY = "ChooserActivity"; private static final double DELTA = 1e-6; private static final long NOW = System.currentTimeMillis(); private static final Range<Long> WITHIN_ONE_DAY = new Range( @@ -246,7 +247,7 @@ public final class SharesheetModelScorerTest { SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), - Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID, CHOOSER_ACTIVITY); // Verification assertEquals(0.514f, mShareTarget1.getScore(), DELTA); @@ -278,7 +279,7 @@ public final class SharesheetModelScorerTest { SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), - Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID, CHOOSER_ACTIVITY); verify(mDataManager, times(1)).queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet()); @@ -311,7 +312,7 @@ public final class SharesheetModelScorerTest { SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), - Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID, CHOOSER_ACTIVITY); verify(mDataManager, times(1)).queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet()); @@ -349,7 +350,7 @@ public final class SharesheetModelScorerTest { SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), - Event.TYPE_SHARE_TEXT, 4, NOW, mDataManager, USER_ID); + Event.TYPE_SHARE_TEXT, 4, NOW, mDataManager, USER_ID, CHOOSER_ACTIVITY); verify(mDataManager, never()).queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet()); @@ -377,7 +378,7 @@ public final class SharesheetModelScorerTest { anyLong())).thenReturn( List.of(createUsageEvent(PACKAGE_2), createUsageEvent(PACKAGE_3), - createUsageEvent(SharesheetModelScorer.CHOOSER_ACTIVITY), + createUsageEvent(CHOOSER_ACTIVITY), createUsageEvent(PACKAGE_3), createUsageEvent(PACKAGE_3)) ); @@ -385,7 +386,7 @@ public final class SharesheetModelScorerTest { SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), - Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID, CHOOSER_ACTIVITY); verify(mDataManager, times(1)).queryAppMovingToForegroundEvents(anyInt(), anyLong(), anyLong()); @@ -413,7 +414,7 @@ public final class SharesheetModelScorerTest { anyLong())).thenReturn( List.of(createUsageEvent(PACKAGE_3), createUsageEvent(PACKAGE_3), - createUsageEvent(SharesheetModelScorer.CHOOSER_ACTIVITY), + createUsageEvent(CHOOSER_ACTIVITY), createUsageEvent(PACKAGE_3), createUsageEvent(PACKAGE_3)) ); @@ -421,7 +422,7 @@ public final class SharesheetModelScorerTest { SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), - Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID, CHOOSER_ACTIVITY); verify(mDataManager, times(1)).queryAppMovingToForegroundEvents(anyInt(), anyLong(), anyLong()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index c242554e950b..81d939f1f05f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -600,7 +600,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel2.setDescription("descriptions for all"); - channel2.setSound(SOUND_URI, mAudioAttributes); + channel2.setSound(CANONICAL_SOUND_URI, mAudioAttributes); channel2.enableLights(true); channel2.setBypassDnd(true); channel2.setLockscreenVisibility(VISIBILITY_SECRET); @@ -1374,6 +1374,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { .when(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI)); doReturn(localUri) .when(mTestIContentProvider).uncanonicalize(any(), eq(canonicalBasedOnLocal)); + doReturn(canonicalBasedOnLocal) + .when(mTestIContentProvider).canonicalize(any(), eq(localUri)); NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); @@ -1387,7 +1389,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel actualChannel = mHelper.getNotificationChannel( PKG_N_MR1, UID_N_MR1, channel.getId(), false); - assertEquals(localUri, actualChannel.getSound()); + assertEquals(canonicalBasedOnLocal, actualChannel.getSound()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java index 0eca8c988575..98f18433e53d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java @@ -18,6 +18,9 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.junit.Assert.assertEquals; import android.platform.test.annotations.Presubmit; @@ -28,6 +31,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; + /** * Test class for {@link ActivitySnapshotController}. * @@ -42,13 +47,13 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { private ActivitySnapshotController mActivitySnapshotController; @Before public void setUp() throws Exception { + spyOn(mWm.mSnapshotController.mActivitySnapshotController); mActivitySnapshotController = mWm.mSnapshotController.mActivitySnapshotController; + doReturn(false).when(mActivitySnapshotController).shouldDisableSnapshots(); mActivitySnapshotController.resetTmpFields(); } @Test public void testOpenActivityTransition() { - final SnapshotController.TransitionState transitionState = - new SnapshotController.TransitionState(); final Task task = createTask(mDisplayContent); // note for createAppWindow: the new child is added at index 0 final WindowState openingWindow = createAppWindow(task, @@ -59,14 +64,12 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { "closingWindow"); closingWindow.mActivityRecord.commitVisibility( false /* visible */, true /* performLayout */); - transitionState.addParticipant(closingWindow.mActivityRecord, false); - transitionState.addParticipant(openingWindow.mActivityRecord, true); - mActivitySnapshotController.handleOpenActivityTransition(transitionState); + final ArrayList<WindowContainer> windows = new ArrayList<>(); + windows.add(openingWindow.mActivityRecord); + windows.add(closingWindow.mActivityRecord); + mActivitySnapshotController.handleTransitionFinish(windows); - assertEquals(1, mActivitySnapshotController.mPendingCaptureActivity.size()); assertEquals(0, mActivitySnapshotController.mPendingRemoveActivity.size()); - assertEquals(closingWindow.mActivityRecord, - mActivitySnapshotController.mPendingCaptureActivity.valueAt(0)); mActivitySnapshotController.resetTmpFields(); // simulate three activity @@ -74,19 +77,15 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { "belowClose"); belowClose.mActivityRecord.commitVisibility( false /* visible */, true /* performLayout */); - mActivitySnapshotController.handleOpenActivityTransition(transitionState); - assertEquals(1, mActivitySnapshotController.mPendingCaptureActivity.size()); + windows.add(belowClose.mActivityRecord); + mActivitySnapshotController.handleTransitionFinish(windows); assertEquals(1, mActivitySnapshotController.mPendingRemoveActivity.size()); - assertEquals(closingWindow.mActivityRecord, - mActivitySnapshotController.mPendingCaptureActivity.valueAt(0)); assertEquals(belowClose.mActivityRecord, mActivitySnapshotController.mPendingRemoveActivity.valueAt(0)); } @Test public void testCloseActivityTransition() { - final SnapshotController.TransitionState transitionState = - new SnapshotController.TransitionState(); final Task task = createTask(mDisplayContent); // note for createAppWindow: the new child is added at index 0 final WindowState closingWindow = createAppWindow(task, ACTIVITY_TYPE_STANDARD, @@ -97,10 +96,10 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { ACTIVITY_TYPE_STANDARD, "openingWindow"); openingWindow.mActivityRecord.commitVisibility( true /* visible */, true /* performLayout */); - transitionState.addParticipant(closingWindow.mActivityRecord, false); - transitionState.addParticipant(openingWindow.mActivityRecord, true); - mActivitySnapshotController.handleCloseActivityTransition(transitionState); - assertEquals(0, mActivitySnapshotController.mPendingCaptureActivity.size()); + final ArrayList<WindowContainer> windows = new ArrayList<>(); + windows.add(openingWindow.mActivityRecord); + windows.add(closingWindow.mActivityRecord); + mActivitySnapshotController.handleTransitionFinish(windows); assertEquals(1, mActivitySnapshotController.mPendingDeleteActivity.size()); assertEquals(openingWindow.mActivityRecord, mActivitySnapshotController.mPendingDeleteActivity.valueAt(0)); @@ -111,8 +110,8 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { "belowOpen"); belowOpen.mActivityRecord.commitVisibility( false /* visible */, true /* performLayout */); - mActivitySnapshotController.handleCloseActivityTransition(transitionState); - assertEquals(0, mActivitySnapshotController.mPendingCaptureActivity.size()); + windows.add(belowOpen.mActivityRecord); + mActivitySnapshotController.handleTransitionFinish(windows); assertEquals(1, mActivitySnapshotController.mPendingDeleteActivity.size()); assertEquals(1, mActivitySnapshotController.mPendingLoadActivity.size()); assertEquals(openingWindow.mActivityRecord, @@ -123,10 +122,6 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { @Test public void testTaskTransition() { - final SnapshotController.TransitionState taskCloseTransition = - new SnapshotController.TransitionState(); - final SnapshotController.TransitionState taskOpenTransition = - new SnapshotController.TransitionState(); final Task closeTask = createTask(mDisplayContent); // note for createAppWindow: the new child is added at index 0 final WindowState closingWindow = createAppWindow(closeTask, ACTIVITY_TYPE_STANDARD, @@ -147,10 +142,10 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { "openingWindowBelow"); openingWindowBelow.mActivityRecord.commitVisibility( false /* visible */, true /* performLayout */); - taskCloseTransition.addParticipant(closeTask, false); - taskOpenTransition.addParticipant(openTask, true); - mActivitySnapshotController.handleCloseTaskTransition(taskCloseTransition); - mActivitySnapshotController.handleOpenTaskTransition(taskOpenTransition); + final ArrayList<WindowContainer> windows = new ArrayList<>(); + windows.add(closeTask); + windows.add(openTask); + mActivitySnapshotController.handleTransitionFinish(windows); assertEquals(1, mActivitySnapshotController.mPendingRemoveActivity.size()); assertEquals(closingWindowBelow.mActivityRecord, diff --git a/services/tests/wmtests/src/com/android/server/wm/AppSnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/AppSnapshotControllerTests.java deleted file mode 100644 index 83af181481d9..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/AppSnapshotControllerTests.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; - -import static com.android.server.wm.SnapshotController.ACTIVITY_CLOSE; -import static com.android.server.wm.SnapshotController.ACTIVITY_OPEN; -import static com.android.server.wm.SnapshotController.TASK_CLOSE; -import static com.android.server.wm.SnapshotController.TASK_OPEN; - -import static org.junit.Assert.assertTrue; - -import android.platform.test.annotations.Presubmit; -import android.util.ArraySet; - -import androidx.test.filters.SmallTest; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - - -/** - * Test class for {@link SnapshotController}. - * - * Build/Install/Run: - * * atest WmTests:AppSnapshotControllerTests - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class AppSnapshotControllerTests extends WindowTestsBase { - final ArraySet<ActivityRecord> mClosingApps = new ArraySet<>(); - final ArraySet<ActivityRecord> mOpeningApps = new ArraySet<>(); - - final TransitionMonitor mOpenActivityMonitor = new TransitionMonitor(); - final TransitionMonitor mCloseActivityMonitor = new TransitionMonitor(); - final TransitionMonitor mOpenTaskMonitor = new TransitionMonitor(); - final TransitionMonitor mCloseTaskMonitor = new TransitionMonitor(); - - @Before - public void setUp() throws Exception { - resetStatus(); - mWm.mSnapshotController.registerTransitionStateConsumer( - ACTIVITY_CLOSE, mCloseActivityMonitor::handleTransition); - mWm.mSnapshotController.registerTransitionStateConsumer( - ACTIVITY_OPEN, mOpenActivityMonitor::handleTransition); - mWm.mSnapshotController.registerTransitionStateConsumer( - TASK_CLOSE, mCloseTaskMonitor::handleTransition); - mWm.mSnapshotController.registerTransitionStateConsumer( - TASK_OPEN, mOpenTaskMonitor::handleTransition); - } - - @After - public void tearDown() throws Exception { - mWm.mSnapshotController.unregisterTransitionStateConsumer( - ACTIVITY_CLOSE, mCloseActivityMonitor::handleTransition); - mWm.mSnapshotController.unregisterTransitionStateConsumer( - ACTIVITY_OPEN, mOpenActivityMonitor::handleTransition); - mWm.mSnapshotController.unregisterTransitionStateConsumer( - TASK_CLOSE, mCloseTaskMonitor::handleTransition); - mWm.mSnapshotController.unregisterTransitionStateConsumer( - TASK_OPEN, mOpenTaskMonitor::handleTransition); - } - - private static class TransitionMonitor { - private final ArraySet<WindowContainer> mOpenParticipant = new ArraySet<>(); - private final ArraySet<WindowContainer> mCloseParticipant = new ArraySet<>(); - void handleTransition(SnapshotController.TransitionState<ActivityRecord> state) { - mOpenParticipant.addAll(state.getParticipant(true /* open */)); - mCloseParticipant.addAll(state.getParticipant(false /* close */)); - } - void reset() { - mOpenParticipant.clear(); - mCloseParticipant.clear(); - } - } - - private void resetStatus() { - mClosingApps.clear(); - mOpeningApps.clear(); - mOpenActivityMonitor.reset(); - mCloseActivityMonitor.reset(); - mOpenTaskMonitor.reset(); - mCloseTaskMonitor.reset(); - } - - @Test - public void testHandleAppTransition_openActivityTransition() { - final Task task = createTask(mDisplayContent); - // note for createAppWindow: the new child is added at index 0 - final WindowState openingWindow = createAppWindow(task, - ACTIVITY_TYPE_STANDARD, "openingWindow"); - openingWindow.mActivityRecord.commitVisibility( - true /* visible */, true /* performLayout */); - final WindowState closingWindow = createAppWindow(task, ACTIVITY_TYPE_STANDARD, - "closingWindow"); - closingWindow.mActivityRecord.commitVisibility( - false /* visible */, true /* performLayout */); - mClosingApps.add(closingWindow.mActivityRecord); - mOpeningApps.add(openingWindow.mActivityRecord); - mWm.mSnapshotController.handleAppTransition(mClosingApps, mOpeningApps); - assertTrue(mOpenActivityMonitor.mCloseParticipant.contains(closingWindow.mActivityRecord)); - assertTrue(mOpenActivityMonitor.mOpenParticipant.contains(openingWindow.mActivityRecord)); - } - - @Test - public void testHandleAppTransition_closeActivityTransition() { - final Task task = createTask(mDisplayContent); - // note for createAppWindow: the new child is added at index 0 - final WindowState closingWindow = createAppWindow(task, ACTIVITY_TYPE_STANDARD, - "closingWindow"); - closingWindow.mActivityRecord.commitVisibility( - false /* visible */, true /* performLayout */); - final WindowState openingWindow = createAppWindow(task, - ACTIVITY_TYPE_STANDARD, "openingWindow"); - openingWindow.mActivityRecord.commitVisibility( - true /* visible */, true /* performLayout */); - mClosingApps.add(closingWindow.mActivityRecord); - mOpeningApps.add(openingWindow.mActivityRecord); - mWm.mSnapshotController.handleAppTransition(mClosingApps, mOpeningApps); - assertTrue(mCloseActivityMonitor.mCloseParticipant.contains(closingWindow.mActivityRecord)); - assertTrue(mCloseActivityMonitor.mOpenParticipant.contains(openingWindow.mActivityRecord)); - } - - @Test - public void testHandleAppTransition_TaskTransition() { - final Task closeTask = createTask(mDisplayContent); - // note for createAppWindow: the new child is added at index 0 - final WindowState closingWindow = createAppWindow(closeTask, ACTIVITY_TYPE_STANDARD, - "closingWindow"); - closingWindow.mActivityRecord.commitVisibility( - false /* visible */, true /* performLayout */); - final WindowState closingWindowBelow = createAppWindow(closeTask, ACTIVITY_TYPE_STANDARD, - "closingWindowBelow"); - closingWindowBelow.mActivityRecord.commitVisibility( - false /* visible */, true /* performLayout */); - - final Task openTask = createTask(mDisplayContent); - final WindowState openingWindow = createAppWindow(openTask, ACTIVITY_TYPE_STANDARD, - "openingWindow"); - openingWindow.mActivityRecord.commitVisibility( - true /* visible */, true /* performLayout */); - final WindowState openingWindowBelow = createAppWindow(openTask, ACTIVITY_TYPE_STANDARD, - "openingWindowBelow"); - openingWindowBelow.mActivityRecord.commitVisibility( - false /* visible */, true /* performLayout */); - - mClosingApps.add(closingWindow.mActivityRecord); - mOpeningApps.add(openingWindow.mActivityRecord); - mWm.mSnapshotController.handleAppTransition(mClosingApps, mOpeningApps); - assertTrue(mCloseTaskMonitor.mCloseParticipant.contains(closeTask)); - assertTrue(mOpenTaskMonitor.mOpenParticipant.contains(openTask)); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java index 52226c2be298..4473a31f0513 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java @@ -123,6 +123,7 @@ public class ContentRecordingControllerTests extends WindowTestsBase { controller.setContentRecordingSessionLocked(mWaitingDisplaySession, mWm); verify(mVirtualDisplayContent, atLeastOnce()).setContentRecordingSession( mWaitingDisplaySession); + verify(mVirtualDisplayContent).updateRecording(); // WHEN updating the session on the same display, so no longer waiting to record. ContentRecordingSession sessionUpdate = ContentRecordingSession.createTaskSession( @@ -135,7 +136,7 @@ public class ContentRecordingControllerTests extends WindowTestsBase { // THEN the session was accepted. assertThat(resultingSession).isEqualTo(sessionUpdate); verify(mVirtualDisplayContent, atLeastOnce()).setContentRecordingSession(sessionUpdate); - verify(mVirtualDisplayContent).updateRecording(); + verify(mVirtualDisplayContent, atLeastOnce()).updateRecording(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index bdd178b0b317..9cc4117db0bb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -2054,6 +2054,17 @@ public class DisplayContentTests extends WindowTestsBase { assertNotEquals(testPlayer.mLastReady.getChange(dcToken).getEndRotation(), testPlayer.mLastReady.getChange(dcToken).getStartRotation()); testPlayer.finish(); + + // The AsyncRotationController should only exist if there is an ongoing rotation change. + dc.finishAsyncRotationIfPossible(); + dc.setLastHasContent(); + doReturn(dr.getRotation() + 1).when(dr).rotationForOrientation(anyInt(), anyInt()); + dr.updateRotationUnchecked(true /* forceUpdate */); + assertNotNull(dc.getAsyncRotationController()); + doReturn(dr.getRotation() - 1).when(dr).rotationForOrientation(anyInt(), anyInt()); + dr.updateRotationUnchecked(true /* forceUpdate */); + assertNull("Cancel AsyncRotationController for the intermediate rotation changes 0->1->0", + dc.getAsyncRotationController()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 72ab18dca02f..2ad9fa0e5b13 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -37,6 +37,8 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER; +import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2; +import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER; @@ -48,6 +50,8 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTAT import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; @@ -807,6 +811,108 @@ public class LetterboxUiControllerTest extends WindowTestsBase { /* candidate */ SCREEN_ORIENTATION_PORTRAIT), SCREEN_ORIENTATION_PORTRAIT); } + // shouldApplyUser...Override + @Test + public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, + /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + doReturn(false).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled(); + + assertFalse(mController.shouldApplyUserFullscreenOverride()); + } + + @Test + public void testShouldApplyUserFullscreenOverride_falseFullscreenProperty_returnsFalse() + throws Exception { + prepareActivityThatShouldApplyUserFullscreenOverride(); + mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, + /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldApplyUserFullscreenOverride()); + } + + @Test + public void testShouldApplyUserFullscreenOverride_falseSettingsProperty_returnsFalse() + throws Exception { + prepareActivityThatShouldApplyUserFullscreenOverride(); + mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldApplyUserFullscreenOverride()); + } + + @Test + public void testShouldApplyUserFullscreenOverride_disabledIgnoreOrientationRequest() { + prepareActivityThatShouldApplyUserFullscreenOverride(); + mDisplayContent.setIgnoreOrientationRequest(false); + + assertFalse(mController.shouldApplyUserFullscreenOverride()); + } + + @Test + public void testShouldApplyUserFullscreenOverride_returnsTrue() { + prepareActivityThatShouldApplyUserFullscreenOverride(); + + assertTrue(mController.shouldApplyUserFullscreenOverride()); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_falseProperty_returnsFalse() + throws Exception { + prepareActivityThatShouldApplyUserMinAspectRatioOverride(); + mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldApplyUserMinAspectRatioOverride()); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_trueProperty_returnsFalse() + throws Exception { + doReturn(false).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled(); + mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldApplyUserMinAspectRatioOverride()); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_disabledIgnoreOrientationRequest() { + prepareActivityThatShouldApplyUserMinAspectRatioOverride(); + mDisplayContent.setIgnoreOrientationRequest(false); + + assertFalse(mController.shouldApplyUserMinAspectRatioOverride()); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_returnsTrue() { + prepareActivityThatShouldApplyUserMinAspectRatioOverride(); + + assertTrue(mController.shouldApplyUserMinAspectRatioOverride()); + } + + private void prepareActivityThatShouldApplyUserMinAspectRatioOverride() { + spyOn(mController); + doReturn(true).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled(); + mDisplayContent.setIgnoreOrientationRequest(true); + doReturn(USER_MIN_ASPECT_RATIO_3_2).when(mController).getUserMinAspectRatioOverrideCode(); + } + + private void prepareActivityThatShouldApplyUserFullscreenOverride() { + spyOn(mController); + doReturn(true).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled(); + mDisplayContent.setIgnoreOrientationRequest(true); + doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN).when(mController) + .getUserMinAspectRatioOverrideCode(); + } + // shouldUseDisplayLandscapeNaturalOrientation @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index ca5d8fe33dba..b4f1176f71e1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -46,7 +46,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealM import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.server.wm.SnapshotController.TASK_CLOSE; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static org.junit.Assert.assertEquals; @@ -1383,8 +1382,6 @@ public class TransitionTests extends WindowTestsBase { @Test public void testTransientLaunch() { spyOn(mWm.mSnapshotController.mTaskSnapshotController); - mWm.mSnapshotController.registerTransitionStateConsumer(TASK_CLOSE, - mWm.mSnapshotController.mTaskSnapshotController::handleTaskClose); final ArrayList<ActivityRecord> enteringAnimReports = new ArrayList<>(); final TransitionController controller = new TestTransitionController(mAtm) { @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java index cf839812f6aa..ebe40b05162d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java @@ -170,9 +170,13 @@ public class WindowProcessControllerTests extends WindowTestsBase { } @Test - public void testSetRunningRecentsAnimation() { - mWpc.setRunningRecentsAnimation(true); - mWpc.setRunningRecentsAnimation(false); + public void testSetAnimatingReason() { + mWpc.addAnimatingReason(WindowProcessController.ANIMATING_REASON_REMOTE_ANIMATION); + assertTrue(mWpc.isRunningRemoteTransition()); + mWpc.addAnimatingReason(WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE); + mWpc.removeAnimatingReason(WindowProcessController.ANIMATING_REASON_REMOTE_ANIMATION); + assertFalse(mWpc.isRunningRemoteTransition()); + mWpc.removeAnimatingReason(WindowProcessController.ANIMATING_REASON_WAKEFULNESS_CHANGE); waitHandlerIdle(mAtm.mH); InOrder orderVerifier = Mockito.inOrder(mMockListener); @@ -201,7 +205,7 @@ public class WindowProcessControllerTests extends WindowTestsBase { waitHandlerIdle(mAtm.mH); InOrder orderVerifier = Mockito.inOrder(mMockListener); - orderVerifier.verify(mMockListener, times(3)).setRunningRemoteAnimation(eq(true)); + orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(true)); orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(false)); orderVerifier.verifyNoMoreInteractions(); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index ccc4ac28876a..58da4b43aa05 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -368,29 +368,29 @@ final class HotwordDetectionConnection { /** * This method is only used by VisualQueryDetector. */ - void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) { + boolean startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) { if (DEBUG) { Slog.d(TAG, "startPerceivingLocked"); } final VisualQueryDetectorSession session = getVisualQueryDetectorSessionLocked(); if (session == null) { - return; + return false; } - session.startPerceivingLocked(callback); + return session.startPerceivingLocked(callback); } /** * This method is only used by VisaulQueryDetector. */ - void stopPerceivingLocked() { + boolean stopPerceivingLocked() { if (DEBUG) { Slog.d(TAG, "stopPerceivingLocked"); } final VisualQueryDetectorSession session = getVisualQueryDetectorSessionLocked(); if (session == null) { - return; + return false; } - session.stopPerceivingLocked(); + return session.stopPerceivingLocked(); } public void startListeningFromExternalSourceLocked( diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java index 2e05e2073e80..4720d279a676 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java @@ -95,7 +95,7 @@ final class VisualQueryDetectorSession extends DetectorSession { } @SuppressWarnings("GuardedBy") - void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) { + boolean startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) { if (DEBUG) { Slog.d(TAG, "startPerceivingLocked"); } @@ -198,15 +198,16 @@ final class VisualQueryDetectorSession extends DetectorSession { mQueryStreaming = false; } }; - mRemoteDetectionService.run(service -> service.detectWithVisualSignals(internalCallback)); + return mRemoteDetectionService.run( + service -> service.detectWithVisualSignals(internalCallback)); } @SuppressWarnings("GuardedBy") - void stopPerceivingLocked() { + boolean stopPerceivingLocked() { if (DEBUG) { Slog.d(TAG, "stopPerceivingLocked"); } - mRemoteDetectionService.run(ISandboxedDetectionService::stopDetection); + return mRemoteDetectionService.run(ISandboxedDetectionService::stopDetection); } @Override diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 3574ef8e91fb..0de9255e5822 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; -import android.app.ActivityThread; import android.app.AppGlobals; import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; @@ -51,7 +50,6 @@ import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; import android.media.AudioFormat; import android.media.permission.Identity; -import android.media.permission.IdentityContext; import android.media.permission.PermissionUtil; import android.media.permission.SafeCloseable; import android.os.Binder; @@ -61,7 +59,6 @@ import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; -import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -88,6 +85,7 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.internal.app.IVisualQueryRecognitionStatusListener; import com.android.internal.app.IVoiceActionCheckCallback; import com.android.internal.app.IVoiceInteractionManagerService; import com.android.internal.app.IVoiceInteractionSessionListener; @@ -139,6 +137,7 @@ public class VoiceInteractionManagerService extends SystemService { private final RemoteCallbackList<IVoiceInteractionSessionListener> mVoiceInteractionSessionListeners = new RemoteCallbackList<>(); + private IVisualQueryRecognitionStatusListener mVisualQueryRecognitionStatusListener; public VoiceInteractionManagerService(Context context) { super(context); @@ -1346,6 +1345,17 @@ public class VoiceInteractionManagerService extends SystemService { @android.annotation.EnforcePermission( android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE) @Override + public void subscribeVisualQueryRecognitionStatus(IVisualQueryRecognitionStatusListener + listener) { + super.subscribeVisualQueryRecognitionStatus_enforcePermission(); + synchronized (this) { + mVisualQueryRecognitionStatusListener = listener; + } + } + + @android.annotation.EnforcePermission( + android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE) + @Override public void enableVisualQueryDetection( IVisualQueryDetectionAttentionListener listener) { super.enableVisualQueryDetection_enforcePermission(); @@ -1391,7 +1401,10 @@ public class VoiceInteractionManagerService extends SystemService { } final long caller = Binder.clearCallingIdentity(); try { - mImpl.startPerceivingLocked(callback); + boolean success = mImpl.startPerceivingLocked(callback); + if (success && mVisualQueryRecognitionStatusListener != null) { + mVisualQueryRecognitionStatusListener.onStartPerceiving(); + } } finally { Binder.restoreCallingIdentity(caller); } @@ -1409,7 +1422,10 @@ public class VoiceInteractionManagerService extends SystemService { } final long caller = Binder.clearCallingIdentity(); try { - mImpl.stopPerceivingLocked(); + boolean success = mImpl.stopPerceivingLocked(); + if (success && mVisualQueryRecognitionStatusListener != null) { + mVisualQueryRecognitionStatusListener.onStopPerceiving(); + } } finally { Binder.restoreCallingIdentity(caller); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index 5d88a65ce29e..471acc118572 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -784,30 +784,30 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.setVisualQueryDetectionAttentionListenerLocked(listener); } - public void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) { + public boolean startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) { if (DEBUG) { Slog.d(TAG, "startPerceivingLocked"); } if (mHotwordDetectionConnection == null) { // TODO: callback.onError(); - return; + return false; } - mHotwordDetectionConnection.startPerceivingLocked(callback); + return mHotwordDetectionConnection.startPerceivingLocked(callback); } - public void stopPerceivingLocked() { + public boolean stopPerceivingLocked() { if (DEBUG) { Slog.d(TAG, "stopPerceivingLocked"); } if (mHotwordDetectionConnection == null) { Slog.w(TAG, "stopPerceivingLocked() called but connection isn't established"); - return; + return false; } - mHotwordDetectionConnection.stopPerceivingLocked(); + return mHotwordDetectionConnection.stopPerceivingLocked(); } public void startListeningFromMicLocked( diff --git a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java index f6568816b4f7..c0d7cb402636 100644 --- a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java +++ b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java @@ -63,6 +63,25 @@ public class LocaleStoreTest { } @Test + public void testTransformImeLanguageTagToLocaleInfo_duplicateTagFilter() { + List<InputMethodSubtype> list = List.of( + new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(), + new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(), + new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(), + new InputMethodSubtypeBuilder().setLanguageTag("zh-TW").build(), + new InputMethodSubtypeBuilder().setLanguageTag("ja-JP").build()); + + Set<LocaleInfo> localeSet = LocaleStore.transformImeLanguageTagToLocaleInfo(list); + + Set<String> expectedLanguageTag = Set.of("en-US", "zh-TW", "ja-JP"); + assertEquals(localeSet.size(), expectedLanguageTag.size()); + for (LocaleInfo info : localeSet) { + assertEquals(info.mSuggestionFlags, LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE); + assertTrue(expectedLanguageTag.contains(info.getId())); + } + } + + @Test public void convertExplicitLocales_noExplicitLcoales_returnEmptyHashMap() { Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales(); |