diff options
293 files changed, 7773 insertions, 2214 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 44068dd2aed5..ced35549769a 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2892,11 +2892,6 @@ public class Notification implements Parcelable } } - final Person person = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class); - if (person != null) { - person.visitUris(visitor); - } - final RemoteInputHistoryItem[] history = extras.getParcelableArray( Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, RemoteInputHistoryItem.class); @@ -2908,9 +2903,14 @@ public class Notification implements Parcelable } } } - } - if (isStyle(MessagingStyle.class) && extras != null) { + // Extras for MessagingStyle. We visit them even if not isStyle(MessagingStyle), since + // Notification Listeners might use directly (without the isStyle check). + final Person person = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class); + if (person != null) { + person.visitUris(visitor); + } + final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, Parcelable.class); if (!ArrayUtils.isEmpty(messages)) { @@ -2930,9 +2930,8 @@ public class Notification implements Parcelable } visitIconUri(visitor, extras.getParcelable(EXTRA_CONVERSATION_ICON, Icon.class)); - } - if (isStyle(CallStyle.class) & extras != null) { + // Extras for CallStyle (same reason for visiting without checking isStyle). Person callPerson = extras.getParcelable(EXTRA_CALL_PERSON, Person.class); if (callPerson != null) { callPerson.visitUris(visitor); @@ -12783,7 +12782,6 @@ public class Notification implements Parcelable } else { mBackgroundColor = rawColor; } - mProtectionColor = COLOR_INVALID; // filled in at the end mPrimaryTextColor = ContrastColorUtil.findAlphaToMeetContrast( ContrastColorUtil.resolvePrimaryColor(ctx, mBackgroundColor, nightMode), mBackgroundColor, 4.5); @@ -12800,7 +12798,6 @@ public class Notification implements Parcelable } else { int[] attrs = { R.attr.colorSurface, - R.attr.colorBackgroundFloating, R.attr.textColorPrimary, R.attr.textColorSecondary, R.attr.colorAccent, @@ -12812,15 +12809,14 @@ public class Notification implements Parcelable }; try (TypedArray ta = obtainDayNightAttributes(ctx, attrs)) { mBackgroundColor = getColor(ta, 0, nightMode ? Color.BLACK : Color.WHITE); - mProtectionColor = getColor(ta, 1, COLOR_INVALID); - mPrimaryTextColor = getColor(ta, 2, COLOR_INVALID); - mSecondaryTextColor = getColor(ta, 3, COLOR_INVALID); - mPrimaryAccentColor = getColor(ta, 4, COLOR_INVALID); - mSecondaryAccentColor = getColor(ta, 5, COLOR_INVALID); - mTertiaryAccentColor = getColor(ta, 6, COLOR_INVALID); - mOnAccentTextColor = getColor(ta, 7, COLOR_INVALID); - mErrorColor = getColor(ta, 8, COLOR_INVALID); - mRippleAlpha = Color.alpha(getColor(ta, 9, 0x33ffffff)); + mPrimaryTextColor = getColor(ta, 1, COLOR_INVALID); + mSecondaryTextColor = getColor(ta, 2, COLOR_INVALID); + mPrimaryAccentColor = getColor(ta, 3, COLOR_INVALID); + mSecondaryAccentColor = getColor(ta, 4, COLOR_INVALID); + mTertiaryAccentColor = getColor(ta, 5, COLOR_INVALID); + mOnAccentTextColor = getColor(ta, 6, COLOR_INVALID); + mErrorColor = getColor(ta, 7, COLOR_INVALID); + mRippleAlpha = Color.alpha(getColor(ta, 8, 0x33ffffff)); } mContrastColor = calculateContrastColor(ctx, rawColor, mPrimaryAccentColor, mBackgroundColor, nightMode); @@ -12853,9 +12849,7 @@ public class Notification implements Parcelable } } // make sure every color has a valid value - if (mProtectionColor == COLOR_INVALID) { - mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.8f); - } + mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.9f); } /** calculates the contrast color for the non-colorized notifications */ diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java index c3df17d4b53e..529363f828bb 100644 --- a/core/java/android/content/pm/CrossProfileApps.java +++ b/core/java/android/content/pm/CrossProfileApps.java @@ -344,15 +344,22 @@ public class CrossProfileApps { // If there is a label for the launcher intent, then use that as it is typically shorter. // Otherwise, just use the top-level application name. Intent launchIntent = pm.getLaunchIntentForPackage(mContext.getPackageName()); + if (launchIntent == null) { + return getDefaultCallingApplicationLabel(); + } List<ResolveInfo> infos = pm.queryIntentActivities( launchIntent, PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY)); if (infos.size() > 0) { return infos.get(0).loadLabel(pm); } + return getDefaultCallingApplicationLabel(); + } + + private CharSequence getDefaultCallingApplicationLabel() { return mContext.getApplicationInfo() .loadSafeLabel( - pm, + mContext.getPackageManager(), /* ellipsizeDip= */ 0, TextUtils.SAFE_STRING_FLAG_SINGLE_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index c2a0062b43e8..eedb25b1aa8f 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -123,7 +123,7 @@ public final class CredentialManager { * credential, display a picker when multiple credentials exist, etc. * Callers (e.g. browsers) may optionally set origin in {@link GetCredentialRequest} for an * app different from their own, to be able to get credentials on behalf of that app. They would - * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN} + * need additional permission {@code CREDENTIAL_MANAGER_SET_ORIGIN} * to use this functionality * * @param context the context used to launch any UI needed; use an activity context to make sure @@ -209,9 +209,9 @@ public final class CredentialManager { * * <p>This API doesn't invoke any UI. It only performs the preparation work so that you can * later launch the remaining get-credential operation (involves UIs) through the {@link - * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Context, + * #getCredential(Context, PrepareGetCredentialResponse.PendingGetCredentialHandle, * CancellationSignal, Executor, OutcomeReceiver)} API which incurs less latency compared to - * the {@link #getCredential(GetCredentialRequest, Context, CancellationSignal, Executor, + * the {@link #getCredential(Context, GetCredentialRequest, CancellationSignal, Executor, * OutcomeReceiver)} API that executes the whole operation in one call. * * @param request the request specifying type(s) of credentials to get from the user @@ -261,7 +261,7 @@ public final class CredentialManager { * storing the new credential, etc. * Callers (e.g. browsers) may optionally set origin in {@link CreateCredentialRequest} for an * app different from their own, to be able to get credentials on behalf of that app. They would - * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN} + * need additional permission {@code CREDENTIAL_MANAGER_SET_ORIGIN} * to use this functionality * * @param context the context used to launch any UI needed; use an activity context to make sure diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index 6baf91d720c3..ea951a55bfca 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -236,9 +236,10 @@ public final class CameraExtensionCharacteristics { private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER = new CameraExtensionManagerGlobal(); private final Object mLock = new Object(); - private final int PROXY_SERVICE_DELAY_MS = 1000; + private final int PROXY_SERVICE_DELAY_MS = 2000; private InitializerFuture mInitFuture = null; private ServiceConnection mConnection = null; + private int mConnectionCount = 0; private ICameraExtensionsProxyService mProxy = null; private boolean mSupportsAdvancedExtensions = false; @@ -249,6 +250,15 @@ public final class CameraExtensionCharacteristics { return GLOBAL_CAMERA_MANAGER; } + private void releaseProxyConnectionLocked(Context ctx) { + if (mConnection != null ) { + ctx.unbindService(mConnection); + mConnection = null; + mProxy = null; + mConnectionCount = 0; + } + } + private void connectToProxyLocked(Context ctx) { if (mConnection == null) { Intent intent = new Intent(); @@ -270,7 +280,6 @@ public final class CameraExtensionCharacteristics { mConnection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName component) { - mInitFuture.setStatus(false); mConnection = null; mProxy = null; } @@ -348,23 +357,32 @@ public final class CameraExtensionCharacteristics { public boolean registerClient(Context ctx, IBinder token) { synchronized (mLock) { + boolean ret = false; connectToProxyLocked(ctx); if (mProxy == null) { return false; } + mConnectionCount++; try { - return mProxy.registerClient(token); + ret = mProxy.registerClient(token); } catch (RemoteException e) { Log.e(TAG, "Failed to initialize extension! Extension service does " + " not respond!"); } + if (!ret) { + mConnectionCount--; + } - return false; + if (mConnectionCount <= 0) { + releaseProxyConnectionLocked(ctx); + } + + return ret; } } - public void unregisterClient(IBinder token) { + public void unregisterClient(Context ctx, IBinder token) { synchronized (mLock) { if (mProxy != null) { try { @@ -372,6 +390,11 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { Log.e(TAG, "Failed to de-initialize extension! Extension service does" + " not respond!"); + } finally { + mConnectionCount--; + if (mConnectionCount <= 0) { + releaseProxyConnectionLocked(ctx); + } } } } @@ -446,8 +469,8 @@ public final class CameraExtensionCharacteristics { /** * @hide */ - public static void unregisterClient(IBinder token) { - CameraExtensionManagerGlobal.get().unregisterClient(token); + public static void unregisterClient(Context ctx, IBinder token) { + CameraExtensionManagerGlobal.get().unregisterClient(ctx, token); } /** @@ -578,7 +601,7 @@ public final class CameraExtensionCharacteristics { } } } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return Collections.unmodifiableList(ret); @@ -626,7 +649,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension for postview availability! Extension " + "service does not respond!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return false; @@ -722,7 +745,7 @@ public final class CameraExtensionCharacteristics { + "service does not respond!"); return Collections.emptyList(); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } } @@ -791,7 +814,7 @@ public final class CameraExtensionCharacteristics { + " not respond!"); return new ArrayList<>(); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } } @@ -872,7 +895,7 @@ public final class CameraExtensionCharacteristics { } } } finally { - unregisterClient(token); + unregisterClient(mContext, token); } } catch (RemoteException e) { Log.e(TAG, "Failed to query the extension supported sizes! Extension service does" @@ -957,7 +980,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension capture latency! Extension service does" + " not respond!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return null; @@ -998,7 +1021,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does" + " not respond!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return false; @@ -1075,7 +1098,7 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { throw new IllegalStateException("Failed to query the available capture request keys!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return Collections.unmodifiableSet(ret); @@ -1155,7 +1178,7 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { throw new IllegalStateException("Failed to query the available capture result keys!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return Collections.unmodifiableSet(ret); diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index ae700a0a3c41..f0fb2f94332d 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -90,7 +90,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes private final HashMap<Integer, ImageReader> mReaderMap = new HashMap<>(); private RequestProcessor mRequestProcessor = new RequestProcessor(); private final int mSessionId; - private final IBinder mToken; + private IBinder mToken = null; private Surface mClientRepeatingRequestSurface; private Surface mClientCaptureSurface; @@ -103,6 +103,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes private boolean mInitialized; private boolean mSessionClosed; + private final Context mContext; + // Lock to synchronize cross-thread access to device public interface final Object mInterfaceLock; @@ -113,14 +115,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes public static CameraAdvancedExtensionSessionImpl createCameraAdvancedExtensionSession( @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice, @NonNull Map<String, CameraCharacteristics> characteristicsMap, - @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId) + @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId, + @NonNull IBinder token) throws CameraAccessException, RemoteException { - final IBinder token = new Binder(TAG + " : " + sessionId); - boolean success = CameraExtensionCharacteristics.registerClient(ctx, token); - if (!success) { - throw new UnsupportedOperationException("Unsupported extension!"); - } - String cameraId = cameraDevice.getId(); CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx, cameraId, characteristicsMap); @@ -204,8 +201,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes IAdvancedExtenderImpl extender = CameraExtensionCharacteristics.initializeAdvancedExtension( config.getExtension()); extender.init(cameraId, characteristicsMapNative); - CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(extender, - cameraDevice, characteristicsMapNative, repeatingRequestSurface, + + CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx, + extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface, burstCaptureSurface, postviewSurface, config.getStateCallback(), config.getExecutor(), sessionId, token); @@ -217,13 +215,16 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes return ret; } - private CameraAdvancedExtensionSessionImpl(@NonNull IAdvancedExtenderImpl extender, + private CameraAdvancedExtensionSessionImpl(Context ctx, + @NonNull IAdvancedExtenderImpl extender, @NonNull CameraDeviceImpl cameraDevice, Map<String, CameraMetadataNative> characteristicsMap, @Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface, @Nullable Surface postviewSurface, @NonNull StateCallback callback, @NonNull Executor executor, - int sessionId, @NonNull IBinder token) { + int sessionId, + @NonNull IBinder token) { + mContext = ctx; mAdvancedExtender = extender; mCameraDevice = cameraDevice; mCharacteristicsMap = characteristicsMap; @@ -578,12 +579,16 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes mSessionProcessor = null; } - CameraExtensionCharacteristics.unregisterClient(mToken); - if (mInitialized || (mCaptureSession != null)) { - notifyClose = true; - CameraExtensionCharacteristics.releaseSession(); + + if (mToken != null) { + if (mInitialized || (mCaptureSession != null)) { + notifyClose = true; + CameraExtensionCharacteristics.releaseSession(); + } + CameraExtensionCharacteristics.unregisterClient(mContext, mToken); } mInitialized = false; + mToken = null; for (ImageReader reader : mReaderMap.values()) { reader.close(); diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index d3bde4b4b8a8..181ab2cf3421 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -2550,19 +2550,32 @@ public class CameraDeviceImpl extends CameraDevice HashMap<String, CameraCharacteristics> characteristicsMap = new HashMap<>( mPhysicalIdsToChars); characteristicsMap.put(mCameraId, mCharacteristics); + boolean initializationFailed = true; + IBinder token = new Binder(TAG + " : " + mNextSessionId++); try { + boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token); + if (!ret) { + token = null; + throw new UnsupportedOperationException("Unsupported extension!"); + } + if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) { mCurrentAdvancedExtensionSession = CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession( this, characteristicsMap, mContext, extensionConfiguration, - mNextSessionId++); + mNextSessionId, token); } else { mCurrentExtensionSession = CameraExtensionSessionImpl.createCameraExtensionSession( this, characteristicsMap, mContext, extensionConfiguration, - mNextSessionId++); + mNextSessionId, token); } + initializationFailed = false; } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_ERROR); + } finally { + if (initializationFailed && (token != null)) { + CameraExtensionCharacteristics.unregisterClient(mContext, token); + } } } } diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index 1db4808b6430..7b7d7de031e4 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -91,7 +91,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { private final Set<CaptureRequest.Key> mSupportedRequestKeys; private final Set<CaptureResult.Key> mSupportedResultKeys; private final ExtensionSessionStatsAggregator mStatsAggregator; - private final IBinder mToken; + private IBinder mToken = null; private boolean mCaptureResultsSupported; private CameraCaptureSession mCaptureSession = null; @@ -119,6 +119,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { // will do so internally. private boolean mInternalRepeatingRequestEnabled = true; + private final Context mContext; + // Lock to synchronize cross-thread access to device public interface final Object mInterfaceLock; @@ -135,14 +137,9 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { @NonNull Map<String, CameraCharacteristics> characteristicsMap, @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, - int sessionId) + int sessionId, + @NonNull IBinder token) throws CameraAccessException, RemoteException { - final IBinder token = new Binder(TAG + " : " + sessionId); - boolean success = CameraExtensionCharacteristics.registerClient(ctx, token); - if (!success) { - throw new UnsupportedOperationException("Unsupported extension!"); - } - String cameraId = cameraDevice.getId(); CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx, cameraId, characteristicsMap); @@ -234,6 +231,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { characteristicsMap.get(cameraId).getNativeMetadata()); CameraExtensionSessionImpl session = new CameraExtensionSessionImpl( + ctx, extenders.second, extenders.first, supportedPreviewSizes, @@ -256,7 +254,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { return session; } - public CameraExtensionSessionImpl(@NonNull IImageCaptureExtenderImpl imageExtender, + public CameraExtensionSessionImpl(Context ctx, @NonNull IImageCaptureExtenderImpl imageExtender, @NonNull IPreviewExtenderImpl previewExtender, @NonNull List<Size> previewSizes, @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice, @@ -269,6 +267,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { @NonNull IBinder token, @NonNull Set<CaptureRequest.Key> requestKeys, @Nullable Set<CaptureResult.Key> resultKeys) { + mContext = ctx; mImageExtender = imageExtender; mPreviewExtender = previewExtender; mCameraDevice = cameraDevice; @@ -878,12 +877,15 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { + " respond!"); } - CameraExtensionCharacteristics.unregisterClient(mToken); - if (mInitialized || (mCaptureSession != null)) { - notifyClose = true; - CameraExtensionCharacteristics.releaseSession(); + if (mToken != null) { + if (mInitialized || (mCaptureSession != null)) { + notifyClose = true; + CameraExtensionCharacteristics.releaseSession(); + } + CameraExtensionCharacteristics.unregisterClient(mContext, mToken); } mInitialized = false; + mToken = null; if (mRepeatingRequestImageCallback != null) { mRepeatingRequestImageCallback.close(); diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 94bff893b5a8..4700720736b5 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -370,8 +370,9 @@ public abstract class DisplayManagerInternal { /** * Returns the default size of the surface associated with the display, or null if the surface - * is not provided for layer mirroring by SurfaceFlinger. - * Only used for mirroring started from MediaProjection. + * is not provided for layer mirroring by SurfaceFlinger. Size is rotated to reflect the current + * display device orientation. + * Used for mirroring from MediaProjection, or a physical display based on display flags. */ public abstract Point getDisplaySurfaceDefaultSize(int displayId); diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 795eb4a737ef..8f653b3808c1 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -576,6 +576,12 @@ public class InputMethodService extends AbstractInputMethodService { @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final long DISALLOW_INPUT_METHOD_INTERFACE_OVERRIDE = 148086656L; + /** + * Enable the logic to allow hiding the IME caption bar ("fake" IME navigation bar). + * @hide + */ + public static final boolean ENABLE_HIDE_IME_CAPTION_BAR = true; + LayoutInflater mInflater; TypedArray mThemeAttrs; @UnsupportedAppUsage diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index 78388efe98c7..c01664e55744 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -16,6 +16,8 @@ package android.inputmethodservice; +import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; +import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import android.animation.ValueAnimator; @@ -230,6 +232,16 @@ final class NavigationBarController { setIconTintInternal(calculateTargetDarkIntensity(mAppearance, mDrawLegacyNavigationBarBackground)); + + if (ENABLE_HIDE_IME_CAPTION_BAR) { + mNavigationBarFrame.setOnApplyWindowInsetsListener((view, insets) -> { + if (mNavigationBarFrame != null) { + boolean visible = insets.isVisible(captionBar()); + mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + return view.onApplyWindowInsets(insets); + }); + } } private void uninstallNavigationBarFrameIfNecessary() { @@ -240,6 +252,9 @@ final class NavigationBarController { if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(mNavigationBarFrame); } + if (ENABLE_HIDE_IME_CAPTION_BAR) { + mNavigationBarFrame.setOnApplyWindowInsetsListener(null); + } mNavigationBarFrame = null; } @@ -414,7 +429,9 @@ final class NavigationBarController { decor.bringChildToFront(mNavigationBarFrame); } } - mNavigationBarFrame.setVisibility(View.VISIBLE); + if (!ENABLE_HIDE_IME_CAPTION_BAR) { + mNavigationBarFrame.setVisibility(View.VISIBLE); + } } } @@ -435,6 +452,11 @@ final class NavigationBarController { mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; + if (ENABLE_HIDE_IME_CAPTION_BAR) { + mService.mWindow.getWindow().getDecorView().getWindowInsetsController() + .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight()); + } + if (imeDrawsImeNavBar) { installNavigationBarFrameIfNecessary(); if (mNavigationBarFrame == null) { @@ -528,6 +550,16 @@ final class NavigationBarController { return drawLegacyNavigationBarBackground; } + /** + * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead. + */ + private int getImeCaptionBarHeight() { + return mImeDrawsImeNavBar + ? mService.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_frame_height) + : 0; + } + @Override public String toDebugString() { return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index af09a0662795..5b24dcacbf53 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -1294,32 +1294,30 @@ public final class FileUtils { * Round the given size of a storage device to a nice round power-of-two * value, such as 256MB or 32GB. This avoids showing weird values like * "29.5GB" in UI. - * - * Some storage devices are still using GiB (powers of 1024) over - * GB (powers of 1000) measurements and this method takes it into account. - * * Round ranges: * ... - * [256 GiB + 1; 512 GiB] -> 512 GB - * [512 GiB + 1; 1 TiB] -> 1 TB - * [1 TiB + 1; 2 TiB] -> 2 TB + * (128 GB; 256 GB] -> 256 GB + * (256 GB; 512 GB] -> 512 GB + * (512 GB; 1000 GB] -> 1000 GB + * (1000 GB; 2000 GB] -> 2000 GB + * ... * etc * * @hide */ public static long roundStorageSize(long size) { long val = 1; - long kiloPow = 1; - long kibiPow = 1; - while ((val * kibiPow) < size) { + long pow = 1; + while ((val * pow) < size) { val <<= 1; if (val > 512) { val = 1; - kibiPow *= 1024; - kiloPow *= 1000; + pow *= 1000; } } - return val * kiloPow; + + Log.d(TAG, String.format("Rounded bytes from %d to %d", size, val * pow)); + return val * pow; } private static long toBytes(long value, String unit) { diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl index bc52744078ea..369a1932e437 100644 --- a/core/java/android/os/storage/IStorageManager.aidl +++ b/core/java/android/os/storage/IStorageManager.aidl @@ -174,4 +174,5 @@ interface IStorageManager { boolean isAppIoBlocked(in String volumeUuid, int uid, int tid, int reason) = 95; void setCloudMediaProvider(in String authority) = 96; String getCloudMediaProvider() = 97; + long getInternalStorageBlockDeviceSize() = 98; }
\ No newline at end of file diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 80dd48825ba7..ee387e7c284f 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1359,6 +1359,15 @@ public class StorageManager { } /** {@hide} */ + public long getInternalStorageBlockDeviceSize() { + try { + return mStorageManager.getInternalStorageBlockDeviceSize(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ public void mkdirs(File file) { BlockGuard.getVmPolicy().onPathAccess(file.getAbsolutePath()); try { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1235b787c8e2..1cf41cfaee4c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9975,6 +9975,13 @@ public final class Settings { public static final String AUDIO_DEVICE_INVENTORY = "audio_device_inventory"; /** + * Stores a boolean that defines whether the CSD as a feature is enabled or not. + * @hide + */ + public static final String AUDIO_SAFE_CSD_AS_A_FEATURE_ENABLED = + "audio_safe_csd_as_a_feature_enabled"; + + /** * Indicates whether notification display on the lock screen is enabled. * <p> * Type: int (0 for false, 1 for true) @@ -10491,6 +10498,14 @@ public final class Settings { "assist_long_press_home_enabled"; /** + * Whether press and hold on nav handle can trigger search. + * + * @hide + */ + public static final String SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED = + "search_press_hold_nav_handle_enabled"; + + /** * Control whether Trust Agents are in active unlock or extend unlock mode. * @hide */ diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index 75640bd47ab9..f3b4c6da4a01 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -92,6 +92,7 @@ public class NotificationRankingUpdate implements Parcelable { mapParcel.recycle(); if (buffer != null) { mRankingMapFd.unmap(buffer); + mRankingMapFd.close(); } } } else { diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 708ebdf3627b..a4989af3f8ae 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -67,6 +67,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVoiceInteractionManagerService; import com.android.internal.app.IVoiceInteractionSoundTriggerSession; +import com.android.internal.infra.AndroidFuture; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -1702,6 +1703,11 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { Slog.i(TAG, "onProcessRestarted"); mHandler.sendEmptyMessage(MSG_PROCESS_RESTARTED); } + + @Override + public void onOpenFile(String filename, AndroidFuture future) throws RemoteException { + throw new UnsupportedOperationException("Hotword cannot access files from the disk."); + } } void onDetectorRemoteException() { diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java index d9ee859dc66b..ccf8b67826c8 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -227,6 +227,12 @@ public abstract class HotwordDetectionService extends Service public void stopDetection() { HotwordDetectionService.this.onStopDetection(); } + + @Override + public void registerRemoteStorageService(IDetectorSessionStorageService + detectorSessionStorageService) { + throw new UnsupportedOperationException("Hotword cannot access files from the disk."); + } }; @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt b/core/java/android/service/voice/IDetectorSessionStorageService.aidl index 9c1bcec2f396..592373e0ef80 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt +++ b/core/java/android/service/voice/IDetectorSessionStorageService.aidl @@ -12,23 +12,19 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ -package com.android.systemui.biometrics.ui.binder - -import com.android.systemui.biometrics.AuthBiometricFingerprintView -import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel +package android.service.voice; -object AuthBiometricFingerprintViewBinder { +import com.android.internal.infra.AndroidFuture; +/** + * @hide + */ +oneway interface IDetectorSessionStorageService { /** - * Binds a [AuthBiometricFingerprintView.mIconView] to a [AuthBiometricFingerprintViewModel]. + * Called when a file open request is sent. Only files with the given names under the internal + * app storage, i.e., {@link Context#getFilesDir()} can be opened. */ - @JvmStatic - fun bind(view: AuthBiometricFingerprintView, viewModel: AuthBiometricFingerprintViewModel) { - if (view.isSfps) { - AuthBiometricFingerprintIconViewBinder.bind(view.getIconView(), viewModel) - } - } + void openFile(in String filename, in AndroidFuture future); } diff --git a/core/java/android/service/voice/ISandboxedDetectionService.aidl b/core/java/android/service/voice/ISandboxedDetectionService.aidl index 098536dfeb64..c76ac28eb36c 100644 --- a/core/java/android/service/voice/ISandboxedDetectionService.aidl +++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl @@ -24,6 +24,7 @@ import android.os.IRemoteCallback; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.SharedMemory; +import android.service.voice.IDetectorSessionStorageService; import android.service.voice.IDetectorSessionVisualQueryDetectionCallback; import android.service.voice.IDspHotwordDetectionCallback; import android.view.contentcapture.IContentCaptureManager; @@ -71,4 +72,10 @@ oneway interface ISandboxedDetectionService { void ping(in IRemoteCallback callback); void stopDetection(); + + /** + * Registers the interface stub to talk to the voice interaction service for initialization/ + * detection unrelated functionalities. + */ + void registerRemoteStorageService(in IDetectorSessionStorageService detectorSessionStorageService); } diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java index 7ab4fafcf312..88c211120f2e 100644 --- a/core/java/android/service/voice/SoftwareHotwordDetector.java +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -36,6 +36,7 @@ import android.util.Slog; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVoiceInteractionManagerService; +import com.android.internal.infra.AndroidFuture; import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -299,6 +300,11 @@ class SoftwareHotwordDetector extends AbstractDetector { Binder.withCleanCallingIdentity(() -> mExecutor.execute( () -> mCallback.onHotwordDetectionServiceRestarted())); } + + @Override + public void onOpenFile(String filename, AndroidFuture future) throws RemoteException { + throw new UnsupportedOperationException("Hotword cannot access files from the disk."); + } } /** @hide */ diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java index cbe7666ddf43..d184b1eb96ab 100644 --- a/core/java/android/service/voice/VisualQueryDetectionService.java +++ b/core/java/android/service/voice/VisualQueryDetectionService.java @@ -40,7 +40,12 @@ import android.util.Log; import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.IContentCaptureManager; +import com.android.internal.infra.AndroidFuture; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.util.Objects; +import java.util.concurrent.ExecutionException; import java.util.function.IntConsumer; /** @@ -86,6 +91,8 @@ public abstract class VisualQueryDetectionService extends Service private ContentCaptureManager mContentCaptureManager; @Nullable private IRecognitionServiceManager mIRecognitionServiceManager; + @Nullable + private IDetectorSessionStorageService mDetectorSessionStorageService; private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() { @@ -154,6 +161,12 @@ public abstract class VisualQueryDetectionService extends Service public void updateRecognitionServiceManager(IRecognitionServiceManager manager) { mIRecognitionServiceManager = manager; } + + @Override + public void registerRemoteStorageService(IDetectorSessionStorageService + detectorSessionStorageService) { + mDetectorSessionStorageService = detectorSessionStorageService; + } }; @Override @@ -323,4 +336,23 @@ public abstract class VisualQueryDetectionService extends Service } } + /** + * Overrides {@link Context#openFileInput} to read files with the given file names under the + * internal app storage of the {@link VoiceInteractionService}, i.e., only files stored in + * {@link Context#getFilesDir()} can be opened. + */ + @Override + public @Nullable FileInputStream openFileInput(@NonNull String filename) throws + FileNotFoundException { + try { + AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>(); + mDetectorSessionStorageService.openFile(filename, future); + ParcelFileDescriptor pfd = future.get(); + return new FileInputStream(pfd.getFileDescriptor()); + } catch (RemoteException | ExecutionException | InterruptedException e) { + Log.w(TAG, "Cannot open file due to remote service failure"); + throw new FileNotFoundException(e.getMessage()); + } + } + } diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index 93b7964705ba..5d7f47878866 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.content.Context; import android.hardware.soundtrigger.SoundTrigger; import android.media.AudioFormat; import android.os.Binder; @@ -37,7 +38,10 @@ import android.util.Slog; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVoiceInteractionManagerService; +import com.android.internal.infra.AndroidFuture; +import java.io.File; +import java.io.FileNotFoundException; import java.io.PrintWriter; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -58,17 +62,18 @@ public class VisualQueryDetector { private final Callback mCallback; private final Executor mExecutor; + private final Context mContext; private final IVoiceInteractionManagerService mManagerService; private final VisualQueryDetectorInitializationDelegate mInitializationDelegate; VisualQueryDetector( IVoiceInteractionManagerService managerService, - @NonNull @CallbackExecutor Executor executor, - Callback callback) { + @NonNull @CallbackExecutor Executor executor, Callback callback, Context context) { mManagerService = managerService; mCallback = callback; mExecutor = executor; mInitializationDelegate = new VisualQueryDetectorInitializationDelegate(); + mContext = context; } /** @@ -245,7 +250,7 @@ public class VisualQueryDetector { @Override void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) { initAndVerifyDetector(options, sharedMemory, - new InitializationStateListener(mExecutor, mCallback), + new InitializationStateListener(mExecutor, mCallback, mContext), DETECTOR_TYPE_VISUAL_QUERY_DETECTOR); } @@ -330,9 +335,12 @@ public class VisualQueryDetector { private final Executor mExecutor; private final Callback mCallback; - InitializationStateListener(Executor executor, Callback callback) { + private final Context mContext; + + InitializationStateListener(Executor executor, Callback callback, Context context) { this.mExecutor = executor; this.mCallback = callback; + this.mContext = context; } @Override @@ -426,5 +434,22 @@ public class VisualQueryDetector { !TextUtils.isEmpty(errorMessage) ? errorMessage : "Error data is null"); })); } + @Override + public void onOpenFile(String filename, AndroidFuture future) throws RemoteException { + Slog.v(TAG, "BinderCallback#onOpenFile " + filename); + Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { + Slog.v(TAG, "onOpenFile: " + filename); + File f = new File(mContext.getFilesDir(), filename); + ParcelFileDescriptor pfd = null; + try { + Slog.d(TAG, "opened a file with ParcelFileDescriptor."); + pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); + } catch (FileNotFoundException e) { + Slog.e(TAG, "Cannot open file. No ParcelFileDescriptor returned."); + } finally { + future.complete(pfd); + } + })); + } } } diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index ab9ae0acc01e..de2a99bfac44 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -965,7 +965,7 @@ public class VoiceInteractionService extends Service { } VisualQueryDetector visualQueryDetector = - new VisualQueryDetector(mSystemService, executor, callback); + new VisualQueryDetector(mSystemService, executor, callback, this); HotwordDetector visualQueryDetectorInitializationDelegate = visualQueryDetector.getInitializationDelegate(); mActiveDetectors.add(visualQueryDetectorInitializationDelegate); diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java index 9656f36d2c4d..7f313c177053 100644 --- a/core/java/android/speech/RecognitionService.java +++ b/core/java/android/speech/RecognitionService.java @@ -38,6 +38,7 @@ import android.os.Message; import android.os.RemoteException; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.pooled.PooledLambda; import java.lang.ref.WeakReference; @@ -232,39 +233,68 @@ public abstract class RecognitionService extends Service { intent, attributionSource, new ModelDownloadListener() { + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private boolean mIsTerminated = false; + @Override public void onProgress(int completedPercent) { - try { - listener.onProgress(completedPercent); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + synchronized (mLock) { + if (mIsTerminated) { + return; + } + try { + listener.onProgress(completedPercent); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @Override public void onSuccess() { - try { - listener.onSuccess(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + synchronized (mLock) { + if (mIsTerminated) { + return; + } + mIsTerminated = true; + try { + listener.onSuccess(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @Override public void onScheduled() { - try { - listener.onScheduled(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + synchronized (mLock) { + if (mIsTerminated) { + return; + } + mIsTerminated = true; + try { + listener.onScheduled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @Override public void onError(int error) { - try { - listener.onError(error); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + synchronized (mLock) { + if (mIsTerminated) { + return; + } + mIsTerminated = true; + try { + listener.onError(error); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } }); diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 4b96d74b5687..0b2b6ce33666 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -375,6 +375,14 @@ public final class Display { public static final int FLAG_REAR = 1 << 13; /** + * Display flag: Indicates that the orientation of this display is not fixed and is coupled to + * the orientation of its content. + * + * @hide + */ + public static final int FLAG_ROTATES_WITH_CONTENT = 1 << 14; + + /** * Display flag: Indicates that the contents of the display should not be scaled * to fit the physical screen dimensions. Used for development only to emulate * devices with smaller physicals screens while preserving density. diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 4cfec99dff01..9b340079313f 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -19,7 +19,10 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; @@ -78,11 +81,17 @@ public class HandwritingInitiator { private int mConnectionCount = 0; private final InputMethodManager mImm; + private final RectF mTempRectF = new RectF(); + + private final Region mTempRegion = new Region(); + + private final Matrix mTempMatrix = new Matrix(); + /** * The handwrite-able View that is currently the target of a hovering stylus pointer. This is * used to help determine whether the handwriting PointerIcon should be shown in * {@link #onResolvePointerIcon(Context, MotionEvent)} so that we can reduce the number of calls - * to {@link #findBestCandidateView(float, float)}. + * to {@link #findBestCandidateView(float, float, boolean)}. */ @Nullable private WeakReference<View> mCachedHoverTarget = null; @@ -189,8 +198,8 @@ public class HandwritingInitiator { final float y = motionEvent.getY(pointerIndex); if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { mState.mExceedHandwritingSlop = true; - View candidateView = - findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); + View candidateView = findBestCandidateView(mState.mStylusDownX, + mState.mStylusDownY, /* isHover */ false); if (candidateView != null) { if (candidateView == getConnectedView()) { if (!candidateView.hasFocus()) { @@ -398,13 +407,14 @@ public class HandwritingInitiator { final View cachedHoverTarget = getCachedHoverTarget(); if (cachedHoverTarget != null) { final Rect handwritingArea = getViewHandwritingArea(cachedHoverTarget); - if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget) + if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget, + /* isHover */ true) && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) { return cachedHoverTarget; } } - final View candidateView = findBestCandidateView(hoverX, hoverY); + final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true); if (candidateView != null) { mCachedHoverTarget = new WeakReference<>(candidateView); @@ -434,14 +444,14 @@ public class HandwritingInitiator { * @param y the y coordinates of the stylus event, in the coordinates of the window. */ @Nullable - private View findBestCandidateView(float x, float y) { + private View findBestCandidateView(float x, float y, boolean isHover) { // If the connectedView is not null and do not set any handwriting area, it will check // whether the connectedView's boundary contains the initial stylus position. If true, // directly return the connectedView. final View connectedView = getConnectedView(); if (connectedView != null) { Rect handwritingArea = getViewHandwritingArea(connectedView); - if (isInHandwritingArea(handwritingArea, x, y, connectedView) + if (isInHandwritingArea(handwritingArea, x, y, connectedView, isHover) && shouldTriggerStylusHandwritingForView(connectedView)) { return connectedView; } @@ -455,7 +465,7 @@ public class HandwritingInitiator { for (HandwritableViewInfo viewInfo : handwritableViewInfos) { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); - if (!isInHandwritingArea(handwritingArea, x, y, view) + if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) || !shouldTriggerStylusHandwritingForView(view)) { continue; } @@ -551,15 +561,48 @@ public class HandwritingInitiator { * Return true if the (x, y) is inside by the given {@link Rect} with the View's * handwriting bounds with offsets applied. */ - private static boolean isInHandwritingArea(@Nullable Rect handwritingArea, - float x, float y, View view) { + private boolean isInHandwritingArea(@Nullable Rect handwritingArea, + float x, float y, View view, boolean isHover) { if (handwritingArea == null) return false; - return contains(handwritingArea, x, y, + if (!contains(handwritingArea, x, y, view.getHandwritingBoundsOffsetLeft(), view.getHandwritingBoundsOffsetTop(), view.getHandwritingBoundsOffsetRight(), - view.getHandwritingBoundsOffsetBottom()); + view.getHandwritingBoundsOffsetBottom())) { + return false; + } + + // The returned handwritingArea computed by ViewParent#getChildVisibleRect didn't consider + // the case where a view is stacking on top of the editor. (e.g. DrawerLayout, popup) + // We must check the hit region of the editor again, and avoid the case where another + // view on top of the editor is handling MotionEvents. + ViewParent parent = view.getParent(); + if (parent == null) { + return true; + } + + Region region = mTempRegion; + mTempRegion.set(0, 0, view.getWidth(), view.getHeight()); + Matrix matrix = mTempMatrix; + matrix.reset(); + if (!parent.getChildLocalHitRegion(view, region, matrix, isHover)) { + return false; + } + + // It's not easy to extend the region by the given handwritingBoundsOffset. Instead, we + // create a rectangle surrounding the motion event location and check if this rectangle + // overlaps with the hit region of the editor. + float left = x - view.getHandwritingBoundsOffsetRight(); + float top = y - view.getHandwritingBoundsOffsetBottom(); + float right = Math.max(x + view.getHandwritingBoundsOffsetLeft(), left + 1); + float bottom = Math.max(y + view.getHandwritingBoundsOffsetTop(), top + 1); + RectF rectF = mTempRectF; + rectF.set(left, top, right, bottom); + matrix.mapRect(rectF); + + return region.op(Math.round(rectF.left), Math.round(rectF.top), + Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT); } /** diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 4ecfc4044b1d..c6d8bd18bc28 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -16,10 +16,12 @@ package android.view; +import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; import static android.os.Trace.TRACE_TAG_VIEW; import static android.view.InsetsControllerProto.CONTROL; import static android.view.InsetsControllerProto.STATE; import static android.view.InsetsSource.ID_IME; +import static android.view.InsetsSource.ID_IME_CAPTION_BAR; import static android.view.ViewRootImpl.CAPTION_ON_SHELL; import static android.view.WindowInsets.Type.FIRST; import static android.view.WindowInsets.Type.LAST; @@ -40,6 +42,7 @@ import android.app.ActivityThread; import android.content.Context; import android.content.res.CompatibilityInfo; import android.graphics.Insets; +import android.graphics.Point; import android.graphics.Rect; import android.os.CancellationSignal; import android.os.Handler; @@ -652,6 +655,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private int mLastWindowingMode; private boolean mStartingAnimation; private int mCaptionInsetsHeight = 0; + private int mImeCaptionBarInsetsHeight = 0; private boolean mAnimationsDisabled; private boolean mCompatSysUiVisibilityStaled; @@ -693,6 +697,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (!CAPTION_ON_SHELL && source1.getType() == captionBar()) { return; } + if (source1.getId() == ID_IME_CAPTION_BAR) { + return; + } // Don't change the indexes of the sources while traversing. Remove it later. mPendingRemoveIndexes.add(index1); @@ -823,6 +830,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (mFrame.equals(frame)) { return; } + if (mImeCaptionBarInsetsHeight != 0) { + setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight); + } mHost.notifyInsetsChanged(); mFrame.set(frame); } @@ -1007,6 +1017,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // Ensure to update all existing source consumers for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); + if (consumer.getId() == ID_IME_CAPTION_BAR) { + // The inset control for the IME caption bar will never be dispatched + // by the server. + continue; + } + final InsetsSourceControl control = mTmpControlArray.get(consumer.getId()); if (control != null) { controllableTypes |= control.getType(); @@ -1499,7 +1515,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation continue; } final InsetsSourceControl control = consumer.getControl(); - if (control != null && control.getLeash() != null) { + if (control != null + && (control.getLeash() != null || control.getId() == ID_IME_CAPTION_BAR)) { controls.put(control.getId(), new InsetsSourceControl(control)); typesReady |= consumer.getType(); } @@ -1885,6 +1902,35 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } @Override + public void setImeCaptionBarInsetsHeight(int height) { + if (!ENABLE_HIDE_IME_CAPTION_BAR) { + return; + } + Rect newFrame = new Rect(mFrame.left, mFrame.bottom - height, mFrame.right, mFrame.bottom); + InsetsSource source = mState.peekSource(ID_IME_CAPTION_BAR); + if (mImeCaptionBarInsetsHeight != height + || (source != null && !newFrame.equals(source.getFrame()))) { + mImeCaptionBarInsetsHeight = height; + if (mImeCaptionBarInsetsHeight != 0) { + mState.getOrCreateSource(ID_IME_CAPTION_BAR, captionBar()) + .setFrame(newFrame); + getSourceConsumer(ID_IME_CAPTION_BAR, captionBar()).setControl( + new InsetsSourceControl(ID_IME_CAPTION_BAR, captionBar(), + null /* leash */, false /* initialVisible */, + new Point(), Insets.NONE), + new int[1], new int[1]); + } else { + mState.removeSource(ID_IME_CAPTION_BAR); + InsetsSourceConsumer sourceConsumer = mSourceConsumers.get(ID_IME_CAPTION_BAR); + if (sourceConsumer != null) { + sourceConsumer.setControl(null, new int[1], new int[1]); + } + } + mHost.notifyInsetsChanged(); + } + } + + @Override public void setSystemBarsBehavior(@Behavior int behavior) { mHost.setSystemBarsBehavior(behavior); } diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 64411866f020..ff009ed09329 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -20,6 +20,7 @@ import static android.view.InsetsSourceProto.FRAME; import static android.view.InsetsSourceProto.TYPE; import static android.view.InsetsSourceProto.VISIBLE; import static android.view.InsetsSourceProto.VISIBLE_FRAME; +import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.ime; import android.annotation.IntDef; @@ -47,6 +48,9 @@ public class InsetsSource implements Parcelable { /** The insets source ID of IME */ public static final int ID_IME = createId(null, 0, ime()); + /** The insets source ID of the IME caption bar ("fake" IME navigation bar). */ + static final int ID_IME_CAPTION_BAR = + InsetsSource.createId(null /* owner */, 1 /* index */, captionBar()); /** * Controls whether this source suppresses the scrim. If the scrim is ignored, the system won't @@ -215,8 +219,12 @@ public class InsetsSource implements Parcelable { // During drag-move and drag-resizing, the caption insets position may not get updated // before the app frame get updated. To layout the app content correctly during drag events, // we always return the insets with the corresponding height covering the top. + // However, with the "fake" IME navigation bar treated as a caption bar, we return the + // insets with the corresponding height the bottom. if (getType() == WindowInsets.Type.captionBar()) { - return Insets.of(0, frame.height(), 0, 0); + return getId() == ID_IME_CAPTION_BAR + ? Insets.of(0, 0, 0, frame.height()) + : Insets.of(0, frame.height(), 0, 0); } // Checks for whether there is shared edge with insets for 0-width/height window. final boolean hasIntersection = relativeFrame.isEmpty() diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java index e8f62fc0963f..a4cbc52416b3 100644 --- a/core/java/android/view/PendingInsetsController.java +++ b/core/java/android/view/PendingInsetsController.java @@ -44,6 +44,7 @@ public class PendingInsetsController implements WindowInsetsController { private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners = new ArrayList<>(); private int mCaptionInsetsHeight = 0; + private int mImeCaptionBarInsetsHeight = 0; private WindowInsetsAnimationControlListener mLoggingListener; private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); @@ -91,6 +92,11 @@ public class PendingInsetsController implements WindowInsetsController { } @Override + public void setImeCaptionBarInsetsHeight(int height) { + mImeCaptionBarInsetsHeight = height; + } + + @Override public void setSystemBarsBehavior(int behavior) { if (mReplayedInsetsController != null) { mReplayedInsetsController.setSystemBarsBehavior(behavior); @@ -168,6 +174,9 @@ public class PendingInsetsController implements WindowInsetsController { if (mCaptionInsetsHeight != 0) { controller.setCaptionInsetsHeight(mCaptionInsetsHeight); } + if (mImeCaptionBarInsetsHeight != 0) { + controller.setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight); + } if (mAnimationsDisabled) { controller.setAnimationsDisabled(true); } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 1b1098d9d57a..7bdff8c5b858 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -7361,6 +7361,90 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + /** + * @hide + */ + @Override + public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, + @NonNull Matrix matrix, boolean isHover) { + if (!child.hasIdentityMatrix()) { + matrix.preConcat(child.getInverseMatrix()); + } + + final int dx = child.mLeft - mScrollX; + final int dy = child.mTop - mScrollY; + matrix.preTranslate(-dx, -dy); + + final int width = mRight - mLeft; + final int height = mBottom - mTop; + + // Map the bounds of this view into the region's coordinates and clip the region. + final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF(); + rect.set(0, 0, width, height); + matrix.mapRect(rect); + + boolean notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom), Region.Op.INTERSECT); + + if (isHover) { + HoverTarget target = mFirstHoverTarget; + boolean childIsHit = false; + while (target != null) { + final HoverTarget next = target.next; + if (target.child == child) { + childIsHit = true; + break; + } + target = next; + } + if (!childIsHit) { + target = mFirstHoverTarget; + while (notEmpty && target != null) { + final HoverTarget next = target.next; + final View hoveredView = target.child; + + rect.set(hoveredView.mLeft, hoveredView.mTop, hoveredView.mRight, + hoveredView.mBottom); + matrix.mapRect(rect); + notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); + target = next; + } + } + } else { + TouchTarget target = mFirstTouchTarget; + boolean childIsHit = false; + while (target != null) { + final TouchTarget next = target.next; + if (target.child == child) { + childIsHit = true; + break; + } + target = next; + } + if (!childIsHit) { + target = mFirstTouchTarget; + while (notEmpty && target != null) { + final TouchTarget next = target.next; + final View touchedView = target.child; + + rect.set(touchedView.mLeft, touchedView.mTop, touchedView.mRight, + touchedView.mBottom); + matrix.mapRect(rect); + notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); + target = next; + } + } + } + + if (notEmpty && mParent != null) { + notEmpty = mParent.getChildLocalHitRegion(this, region, matrix, isHover); + } + return notEmpty; + } + + private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) { final int[] locationInWindow = new int[2]; view.getLocationInWindow(locationInWindow); diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 1020d2ef02be..54bc3484d295 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; @@ -686,6 +687,36 @@ public interface ViewParent { } /** + * Compute the region where the child can receive the {@link MotionEvent}s from the root view. + * + * <p> Given region where the child will accept {@link MotionEvent}s. + * Modify the region to the unblocked region where the child can receive the + * {@link MotionEvent}s from the view root. + * </p> + * + * <p> The given region is always clipped by the bounds of the parent views. When there are + * on-going {@link MotionEvent}s, this method also makes use of the event dispatching results to + * determine whether a sibling view will also block the child's hit region. + * </p> + * + * @param child a child View, whose hit region we want to compute. + * @param region the initial hit region where the child view will handle {@link MotionEvent}s, + * defined in the child coordinates. Will be overwritten to the result hit region. + * @param matrix the matrix that maps the given child view's coordinates to the region + * coordinates. It will be modified to a matrix that maps window coordinates to + * the result region's coordinates. + * @param isHover if true it will return the hover events' hit region, otherwise it will + * return the touch events' hit region. + * @return true if the returned region is not empty. + * @hide + */ + default boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, + @NonNull Matrix matrix, boolean isHover) { + region.setEmpty(); + return false; + } + + /** * Unbuffered dispatch has been requested by a child of this view parent. * This method is called by the View hierarchy to signal ancestors that a View needs to * request unbuffered dispatch. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 6edf0e24d5ab..c1ce5e0693b7 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -127,6 +127,7 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.RecordingCanvas; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Region; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; @@ -2393,6 +2394,22 @@ public final class ViewRootImpl implements ViewParent, } @Override + public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, + @NonNull Matrix matrix, boolean isHover) { + if (child != mView) { + throw new IllegalArgumentException("child " + child + " is not the root view " + + mView + " managed by this ViewRootImpl"); + } + + RectF rectF = new RectF(0, 0, mWidth, mHeight); + matrix.mapRect(rectF); + // Note: don't apply scroll offset, because we want to know its + // visibility in the virtual canvas being given to the view hierarchy. + return region.op(Math.round(rectF.left), Math.round(rectF.top), + Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT); + } + + @Override public void bringChildToFront(View child) { } diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index bc0bab7b5e95..cc2cd7982841 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -250,6 +250,16 @@ public interface WindowInsetsController { void setCaptionInsetsHeight(int height); /** + * Sets the insets height for the IME caption bar, which corresponds to the + * "fake" IME navigation bar. + * + * @param height the insets height of the IME caption bar. + * @hide + */ + default void setImeCaptionBarInsetsHeight(int height) { + } + + /** * Controls the behavior of system bars. * * @param behavior Determines how the bars behave when being hidden by the application. diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index b74c8795ffb1..142df2102999 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -14994,7 +14994,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } boolean canShare() { - if (!getContext().canStartActivityForResult() || !isDeviceProvisioned()) { + if (!getContext().canStartActivityForResult() || !isDeviceProvisioned() + || !getContext().getResources().getBoolean( + com.android.internal.R.bool.config_textShareSupported)) { return false; } return canCopy(); diff --git a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl index 380118846dc7..ba87caa0697c 100644 --- a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl +++ b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl @@ -22,6 +22,7 @@ import android.service.voice.HotwordDetectionServiceFailure; import android.service.voice.HotwordRejectedResult; import android.service.voice.SoundTriggerFailure; import android.service.voice.VisualQueryDetectionServiceFailure; +import com.android.internal.infra.AndroidFuture; /** * @hide @@ -113,4 +114,9 @@ oneway interface IHotwordRecognitionStatusCallback { /** Called when the hotword detection process is restarted */ void onProcessRestarted(); + + /** + * Called when a file open request is sent. + */ + void onOpenFile(in String filename, in AndroidFuture future); } diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index a5d287c3e4b2..481a2fbd546f 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -137,6 +137,7 @@ message SecureSettingsProto { optional SettingProto gesture_setup_complete = 9 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto touch_gesture_enabled = 10 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Assist assist = 7; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 10cf353bf5e9..7d1253ccb687 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7770,8 +7770,9 @@ android:process=":ui"> </activity> <activity android:name="com.android.internal.app.PlatLogoActivity" - android:theme="@style/Theme.Wallpaper.NoTitleBar.Fullscreen" + android:theme="@style/Theme.NoTitleBar.Fullscreen" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" + android:enableOnBackInvokedCallback="true" android:icon="@drawable/platlogo" android:process=":ui"> </activity> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d86d53fd8059..09d6795a1f1d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3013,14 +3013,15 @@ on the headphone/microphone jack. When false use the older uevent framework. --> <bool name="config_useDevInputEventForAudioJack">false</bool> - <!-- Whether safe headphone volume is enabled or not (country specific). --> + <!-- Whether safe headphone hearing is enforced by any regulation (e.g. + EN50332-3, EN50332-2) or not (country specific). --> <bool name="config_safe_media_volume_enabled">true</bool> - <!-- Whether safe headphone sound dosage warning is enabled or not - (country specific). This value should only be overlaid to true - when a vendor supports offload and has the HAL sound dose - interfaces implemented. Otherwise, this can lead to a compliance - issue with the safe hearing standards EN50332-3 and IEC62368-1. + <!-- Whether safe headphone sound dosage warning is enabled or not. + This value should only be overlaid to true when a vendor supports + offload and has the HAL sound dose interfaces implemented. + Otherwise, this can lead to a compliance issue with the safe + hearing standards EN50332-3 and IEC62368-1. --> <bool name="config_safe_sound_dosage_enabled">false</bool> @@ -6084,6 +6085,9 @@ <!-- Default value for Settings.ASSIST_TOUCH_GESTURE_ENABLED --> <bool name="config_assistTouchGestureEnabledDefault">true</bool> + <!-- Default value for Settings.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED --> + <bool name="config_searchPressHoldNavHandleEnabledDefault">true</bool> + <!-- The maximum byte size of the information contained in the bundle of HotwordDetectedResult. --> <integer translatable="false" name="config_hotwordDetectedResultMaxBundleSize">0</integer> @@ -6549,4 +6553,8 @@ environment to protect the user's privacy when the device is being repaired. Off by default, since OEMs may have had a similar feature on their devices. --> <bool name="config_repairModeSupported">false</bool> + + <!-- Enables or disables the "Share" action item shown in the context menu that appears upon + long-pressing on selected text. Enabled by default. --> + <bool name="config_textShareSupported">true</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b94ede94b001..655892da9b9a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3046,6 +3046,7 @@ <java-symbol type="id" name="addToDictionaryButton" /> <java-symbol type="id" name="deleteButton" /> <!-- TextView --> + <java-symbol type="bool" name="config_textShareSupported" /> <java-symbol type="string" name="failed_to_copy_to_clipboard" /> <java-symbol type="id" name="notification_material_reply_container" /> @@ -4864,6 +4865,8 @@ <java-symbol type="bool" name="config_assistLongPressHomeEnabledDefault" /> <java-symbol type="bool" name="config_assistTouchGestureEnabledDefault" /> + <java-symbol type="bool" name="config_searchPressHoldNavHandleEnabledDefault" /> + <java-symbol type="integer" name="config_hotwordDetectedResultMaxBundleSize" /> <java-symbol type="dimen" name="config_wallpaperDimAmount" /> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index 31755efb88ed..a358c4f6f7e9 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1749,6 +1749,15 @@ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> + + <activity android:name="android.view.ViewGroupTestActivity" + android:label="ViewGroup Test" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> + </intent-filter> + </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/core/tests/coretests/res/layout/viewgroup_test.xml b/core/tests/coretests/res/layout/viewgroup_test.xml new file mode 100644 index 000000000000..04f4f5228b06 --- /dev/null +++ b/core/tests/coretests/res/layout/viewgroup_test.xml @@ -0,0 +1,77 @@ +<?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. + --> + +<!-- Demonstrates adding/removing views from ViewGroup. See corresponding Java code. --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/linear_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <EditText + android:id="@+id/view" + android:layout_width="20dp" + android:layout_height="10dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <EditText + android:id="@+id/view_scale" + android:layout_width="20dp" + android:layout_height="10dp" + android:scaleX="0.5" + android:scaleY="2" + android:transformPivotX="0dp" + android:transformPivotY="0dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <EditText + android:id="@+id/view_translate" + android:layout_width="20dp" + android:layout_height="10dp" + android:translationX="10dp" + android:translationY="20dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <FrameLayout + android:layout_width="20dp" + android:layout_height="10dp"> + <EditText + android:id="@+id/view_overlap_bottom" + android:layout_width="20dp" + android:layout_height="10dp" + android:text="Hello World!"/> + <Button + android:id="@+id/view_overlap_top" + android:layout_width="10dp" + android:layout_height="10dp"/> + </FrameLayout> + + <FrameLayout + android:layout_width="20dp" + android:layout_height="10dp"> + <EditText + android:id="@+id/view_cover_bottom" + android:layout_width="10dp" + android:layout_height="10dp" + android:text="Hello World!"/> + <Button + android:id="@+id/view_cover_top" + android:layout_width="10dp" + android:layout_height="10dp"/> + </FrameLayout> + +</LinearLayout> diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java index 394ff0ae9a2e..a0d8183b8da7 100644 --- a/core/tests/coretests/src/android/os/FileUtilsTest.java +++ b/core/tests/coretests/src/android/os/FileUtilsTest.java @@ -505,45 +505,32 @@ public class FileUtilsTest { @Test public void testRoundStorageSize() throws Exception { - final long GB1 = DataUnit.GIGABYTES.toBytes(1); - final long GiB1 = DataUnit.GIBIBYTES.toBytes(1); - final long GB2 = DataUnit.GIGABYTES.toBytes(2); - final long GiB2 = DataUnit.GIBIBYTES.toBytes(2); - final long GiB128 = DataUnit.GIBIBYTES.toBytes(128); - final long GB256 = DataUnit.GIGABYTES.toBytes(256); - final long GiB256 = DataUnit.GIBIBYTES.toBytes(256); - final long GB512 = DataUnit.GIGABYTES.toBytes(512); - final long GiB512 = DataUnit.GIBIBYTES.toBytes(512); - final long TB1 = DataUnit.TERABYTES.toBytes(1); - final long TiB1 = DataUnit.TEBIBYTES.toBytes(1); - final long TB2 = DataUnit.TERABYTES.toBytes(2); - final long TiB2 = DataUnit.TEBIBYTES.toBytes(2); - final long TB4 = DataUnit.TERABYTES.toBytes(4); - final long TiB4 = DataUnit.TEBIBYTES.toBytes(4); - final long TB8 = DataUnit.TERABYTES.toBytes(8); - final long TiB8 = DataUnit.TEBIBYTES.toBytes(8); - - assertEquals(GB1, roundStorageSize(GB1 - 1)); - assertEquals(GB1, roundStorageSize(GB1)); - assertEquals(GB1, roundStorageSize(GB1 + 1)); - assertEquals(GB1, roundStorageSize(GiB1 - 1)); - assertEquals(GB1, roundStorageSize(GiB1)); - assertEquals(GB2, roundStorageSize(GiB1 + 1)); - assertEquals(GB2, roundStorageSize(GiB2)); - - assertEquals(GB256, roundStorageSize(GiB128 + 1)); - assertEquals(GB256, roundStorageSize(GiB256)); - assertEquals(GB512, roundStorageSize(GiB256 + 1)); - assertEquals(GB512, roundStorageSize(GiB512)); - assertEquals(TB1, roundStorageSize(GiB512 + 1)); - assertEquals(TB1, roundStorageSize(TiB1)); - assertEquals(TB2, roundStorageSize(TiB1 + 1)); - assertEquals(TB2, roundStorageSize(TiB2)); - assertEquals(TB4, roundStorageSize(TiB2 + 1)); - assertEquals(TB4, roundStorageSize(TiB4)); - assertEquals(TB8, roundStorageSize(TiB4 + 1)); - assertEquals(TB8, roundStorageSize(TiB8)); - assertEquals(TB1, roundStorageSize(1013077688320L)); // b/268571529 + final long M256 = DataUnit.MEGABYTES.toBytes(256); + final long M512 = DataUnit.MEGABYTES.toBytes(512); + final long G1 = DataUnit.GIGABYTES.toBytes(1); + final long G2 = DataUnit.GIGABYTES.toBytes(2); + final long G32 = DataUnit.GIGABYTES.toBytes(32); + final long G64 = DataUnit.GIGABYTES.toBytes(64); + final long G512 = DataUnit.GIGABYTES.toBytes(512); + final long G1000 = DataUnit.TERABYTES.toBytes(1); + final long G2000 = DataUnit.TERABYTES.toBytes(2); + + assertEquals(M256, roundStorageSize(M256 - 1)); + assertEquals(M256, roundStorageSize(M256)); + assertEquals(M512, roundStorageSize(M256 + 1)); + assertEquals(M512, roundStorageSize(M512 - 1)); + assertEquals(M512, roundStorageSize(M512)); + assertEquals(G1, roundStorageSize(M512 + 1)); + assertEquals(G1, roundStorageSize(G1)); + assertEquals(G2, roundStorageSize(G1 + 1)); + + assertEquals(G32, roundStorageSize(G32 - 1)); + assertEquals(G32, roundStorageSize(G32)); + assertEquals(G64, roundStorageSize(G32 + 1)); + + assertEquals(G512, roundStorageSize(G512 - 1)); + assertEquals(G1000, roundStorageSize(G512 + 1)); + assertEquals(G2000, roundStorageSize(G1000 + 1)); } @Test diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java index a84ac55f0d5a..55ded9c8813d 100644 --- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java +++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java @@ -136,7 +136,11 @@ public class NotificationRankingUpdateTest { NotificationListenerService.RankingMap retrievedRankings = retrievedRankingUpdate.getRankingMap(); assertNotNull(retrievedRankings); - assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed()); + // The rankingUpdate file descriptor is only non-null in the new path. + if (SystemUiSystemPropertiesFlags.getResolver().isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) { + assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed()); + } NotificationListenerService.Ranking retrievedRanking = new NotificationListenerService.Ranking(); assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking)); diff --git a/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java new file mode 100644 index 000000000000..60a0a2adbbbe --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java @@ -0,0 +1,216 @@ +/* + * 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.view; + +import static com.google.common.truth.Truth.assertThat; + + +import android.graphics.Matrix; +import android.graphics.Region; +import android.platform.test.annotations.Presubmit; +import android.widget.LinearLayout; + +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.filters.SmallTest; + +import com.android.frameworks.coretests.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** + * Test basic functions of ViewGroup. + * + * Build/Install/Run: + * atest FrameworksCoreTests:ViewGroupTest + */ +@Presubmit +@SmallTest +public class ViewGroupGetChildLocalHitRegionTest { + @Rule + public ActivityScenarioRule<ViewGroupTestActivity> mScenarioRule = + new ActivityScenarioRule<>(ViewGroupTestActivity.class); + + private LinearLayout mRoot; + private final int[] mRootLocation = new int[2]; + + @Before + public void setup() { + mScenarioRule.getScenario().onActivity(activity -> { + mRoot = activity.findViewById(R.id.linear_layout); + mRoot.getLocationInWindow(mRootLocation); + }); + } + + @Test + public void testGetChildLocalHitRegion() { + assertGetChildLocalHitRegion(R.id.view); + } + + @Test + public void testGetChildLocalHitRegion_withScale() { + assertGetChildLocalHitRegion(R.id.view_scale); + } + + @Test + public void testGetChildLocalHitRegion_withTranslate() { + assertGetChildLocalHitRegion(R.id.view_translate); + } + + @Test + public void testGetChildLocalHitRegion_overlap_noMotionEvent() { + assertGetChildLocalHitRegion(R.id.view_overlap_bottom); + } + @Test + public void testGetChildLocalHitRegion_overlap_withMotionEvent() { + // In this case, view_cover_bottom is partially covered by the view_cover_top. + // The returned region is the bounds of the bottom view subtract the bounds of the top view. + assertGetChildLocalHitRegion(R.id.view_overlap_top, R.id.view_overlap_bottom); + } + + @Test + public void testGetChildLocalHitRegion_cover_withMotionEvent() { + // In this case, view_cover_bottom is completely covered by the view_cover_top. + // The returned region is expected to be empty. + assertGetChildLocalHitRegionEmpty(R.id.view_cover_top, R.id.view_cover_bottom); + } + + private void injectMotionEvent(View view, boolean isHover) { + int[] location = new int[2]; + view.getLocationInWindow(location); + + float x = location[0] + view.getWidth() / 2f; + float y = location[1] + view.getHeight() / 2f; + + int action = isHover ? MotionEvent.ACTION_HOVER_ENTER : MotionEvent.ACTION_DOWN; + MotionEvent motionEvent = MotionEvent.obtain(/* downtime= */ 0, /* eventTime= */ 0, action, + x, y, /* pressure= */ 0, /* size= */ 0, /* metaState= */ 0, + /* xPrecision= */ 1, /* yPrecision= */ 1, /* deviceId= */0, /* edgeFlags= */0); + + View rootView = view.getRootView(); + rootView.dispatchPointerEvent(motionEvent); + } + + private void assertGetChildLocalHitRegion(int viewId) { + assertGetChildLocalHitRegion(viewId, /* isHover= */ true); + assertGetChildLocalHitRegion(viewId, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion for a single view. + * @param viewId the viewId of the tested view. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegion(int viewId, boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View view = activity.findViewById(viewId); + + Matrix actualMatrix = new Matrix(); + Region actualRegion = new Region(0, 0, view.getWidth(), view.getHeight()); + boolean actualNotEmpty = view.getParent() + .getChildLocalHitRegion(view, actualRegion, actualMatrix, isHover); + + int[] windowLocation = new int[2]; + view.getLocationInWindow(windowLocation); + Matrix expectMatrix = new Matrix(); + expectMatrix.preScale(1 / view.getScaleX(), 1 / view.getScaleY()); + expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); + + Region expectRegion = new Region(0, 0, view.getWidth(), view.getHeight()); + + assertThat(actualNotEmpty).isTrue(); + assertThat(actualMatrix).isEqualTo(expectMatrix); + assertThat(actualRegion).isEqualTo(expectRegion); + }); + } + + private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom) { + assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ true); + assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion of a view that is covered by another view. It will + * inject {@link MotionEvent}s to the view on top first and then get the hit region of the + * bottom view. + * + * @param viewIdTop the view id of the test view on top. + * @param viewIdBottom the view id of the test view at the bottom. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom, boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View viewTop = activity.findViewById(viewIdTop); + View viewBottom = activity.findViewById(viewIdBottom); + + injectMotionEvent(viewTop, isHover); + + Matrix actualMatrix = new Matrix(); + Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + boolean actualNotEmpty = viewBottom.getParent() + .getChildLocalHitRegion(viewBottom, actualRegion, actualMatrix, isHover); + + int[] windowLocation = new int[2]; + viewBottom.getLocationInWindow(windowLocation); + Matrix expectMatrix = new Matrix(); + expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); + + Region expectRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + expectRegion.op(0, 0, viewTop.getWidth(), viewTop.getHeight(), Region.Op.DIFFERENCE); + + assertThat(actualNotEmpty).isTrue(); + assertThat(actualMatrix).isEqualTo(expectMatrix); + assertThat(actualRegion).isEqualTo(expectRegion); + }); + } + + private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom) { + assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ true); + assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion returns an empty region for a view that is + * completely covered by another view. It will inject {@link MotionEvent}s to the view on top + * first and then get the hit region of the + * bottom view. + * + * @param viewIdTop the view id of the test view on top. + * @param viewIdBottom the view id of the test view at the bottom. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom, + boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View viewTop = activity.findViewById(viewIdTop); + View viewBottom = activity.findViewById(viewIdBottom); + + injectMotionEvent(viewTop, isHover); + + Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + boolean actualNotEmpty = viewBottom.getParent() + .getChildLocalHitRegion(viewBottom, actualRegion, new Matrix(), isHover); + + assertThat(actualNotEmpty).isFalse(); + assertThat(actualRegion.isEmpty()).isTrue(); + }); + } +} diff --git a/core/tests/coretests/src/android/view/ViewGroupTestActivity.java b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java new file mode 100644 index 000000000000..b94bda5efba4 --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java @@ -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 android.view; + +import android.annotation.Nullable; +import android.app.Activity; +import android.os.Bundle; + +import com.android.frameworks.coretests.R; + +public class ViewGroupTestActivity extends Activity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.viewgroup_test); + } +} diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java index 388a996d8b1c..b4c72ca3226b 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java @@ -21,7 +21,9 @@ import static org.mockito.Mockito.when; import android.app.Instrumentation; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.Region; import android.view.View; import android.view.ViewGroup; @@ -45,7 +47,7 @@ public class HandwritingTestUtil { float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) { final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); final Context context = instrumentation.getTargetContext(); - // mock a parent so that HandwritingInitiator can get + // mock a parent so that HandwritingInitiator can get visible rect and hit region. final ViewGroup parent = new ViewGroup(context) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { @@ -56,6 +58,14 @@ public class HandwritingTestUtil { r.set(handwritingArea); return true; } + + @Override + public boolean getChildLocalHitRegion(View child, Region region, Matrix matrix, + boolean isHover) { + matrix.reset(); + region.set(handwritingArea); + return true; + } }; View view = spy(new View(context)); 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 bcbf72857544..3ad30457697e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -261,10 +261,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Updates the Split final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); final WindowContainerTransaction wct = transactionRecord.getTransaction(); - - mPresenter.setTaskFragmentIsolatedNavigation(wct, - splitPinContainer.getSecondaryContainer().getTaskFragmentToken(), - true /* isolatedNav */); mPresenter.updateSplitContainer(splitPinContainer, wct); transactionRecord.apply(false /* shouldApplyIndependently */); updateCallbackIfNecessary(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 5de6acfcc9db..896fe61b5611 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -382,6 +382,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(), secondaryContainer.getTaskFragmentToken(), splitRule, isStacked); + + // Setting isolated navigation and clear non-sticky pinned container if needed. + final SplitPinRule splitPinRule = + splitRule instanceof SplitPinRule ? (SplitPinRule) splitRule : null; + if (splitPinRule == null) { + return; + } + + setTaskFragmentIsolatedNavigation(wct, secondaryContainer.getTaskFragmentToken(), + !isStacked /* isolatedNav */); + if (isStacked && !splitPinRule.isSticky()) { + secondaryContainer.getTaskContainer().removeSplitPinContainer(); + } } /** diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml new file mode 100644 index 000000000000..a0a06f1b3721 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml @@ -0,0 +1,56 @@ +<?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.common.bubbles.BubblePopupView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginHorizontal="@dimen/bubble_popup_margin_horizontal" + android:layout_marginTop="@dimen/bubble_popup_margin_top" + android:elevation="@dimen/bubble_manage_menu_elevation" + android:gravity="center_horizontal" + android:orientation="vertical"> + + <ImageView + android:layout_width="32dp" + android:layout_height="32dp" + android:tint="?android:attr/colorAccent" + android:contentDescription="@null" + android:src="@drawable/pip_ic_settings"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:maxWidth="@dimen/bubble_popup_content_max_width" + android:maxLines="1" + android:ellipsize="end" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline" + android:textColor="?android:attr/textColorPrimary" + android:text="@string/bubble_bar_education_manage_title"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:maxWidth="@dimen/bubble_popup_content_max_width" + android:textAppearance="@android:style/TextAppearance.DeviceDefault" + android:textColor="?android:attr/textColorSecondary" + android:textAlignment="center" + android:text="@string/bubble_bar_education_manage_text"/> + +</com.android.wm.shell.common.bubbles.BubblePopupView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 0502a99d2a45..63a972342ba0 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -226,6 +226,20 @@ <dimen name="bubble_user_education_padding_end">58dp</dimen> <!-- Padding between the bubble and the user education text. --> <dimen name="bubble_user_education_stack_padding">16dp</dimen> + <!-- Max width for the bubble popup view. --> + <dimen name="bubble_popup_content_max_width">300dp</dimen> + <!-- Horizontal margin for the bubble popup view. --> + <dimen name="bubble_popup_margin_horizontal">32dp</dimen> + <!-- Top margin for the bubble popup view. --> + <dimen name="bubble_popup_margin_top">16dp</dimen> + <!-- Width for the bubble popup view arrow. --> + <dimen name="bubble_popup_arrow_width">12dp</dimen> + <!-- Height for the bubble popup view arrow. --> + <dimen name="bubble_popup_arrow_height">10dp</dimen> + <!-- Corner radius for the bubble popup view arrow. --> + <dimen name="bubble_popup_arrow_corner_radius">2dp</dimen> + <!-- Padding for the bubble popup view contents. --> + <dimen name="bubble_popup_padding">24dp</dimen> <!-- The size of the caption bar inset at the top of bubble bar expanded view. --> <dimen name="bubble_bar_expanded_view_caption_height">32dp</dimen> <!-- The height of the dots shown for the caption menu in the bubble bar expanded view.. --> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 8cbc3d016b01..00c63d70d3a0 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -163,6 +163,11 @@ <!-- [CHAR LIMIT=NONE] Empty overflow subtitle --> <string name="bubble_overflow_empty_subtitle">Recent bubbles and dismissed bubbles will appear here</string> + <!-- Title text for the bubble bar "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=60]--> + <string name="bubble_bar_education_manage_title">Control bubbles anytime</string> + <!-- Descriptive text for the bubble bar "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=80]--> + <string name="bubble_bar_education_manage_text">Tap here to manage which apps and conversations can bubble</string> + <!-- [CHAR LIMIT=100] Notification Importance title --> <string name="notification_bubble_title">Bubble</string> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 7e09c989e1b3..9a2b81243861 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -60,7 +60,6 @@ import java.util.concurrent.Executor; /** * Encapsulates the data and UI elements of a bubble. */ -@VisibleForTesting public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; @@ -852,7 +851,10 @@ public class Bubble implements BubbleViewProvider { return mAppIntent; } - boolean isAppBubble() { + /** + * Returns whether this bubble is from an app versus a notification. + */ + public boolean isAppBubble() { return mIsAppBubble; } 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 250e010f4d69..76662c47238f 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 @@ -52,6 +52,11 @@ public class BubbleDebugConfig { private static final boolean FORCE_SHOW_USER_EDUCATION = false; private static final String FORCE_SHOW_USER_EDUCATION_SETTING = "force_show_bubbles_user_education"; + /** + * When set to true, bubbles user education flow never shows up. + */ + private static final String FORCE_HIDE_USER_EDUCATION_SETTING = + "force_hide_bubbles_user_education"; /** * @return whether we should force show user education for bubbles. Used for debugging & demos. @@ -62,6 +67,14 @@ public class BubbleDebugConfig { return FORCE_SHOW_USER_EDUCATION || forceShow; } + /** + * @return whether we should never show user education for bubbles. Used in tests. + */ + static boolean neverShowUserEducation(Context context) { + return Settings.Secure.getInt(context.getContentResolver(), + FORCE_HIDE_USER_EDUCATION_SETTING, 0) != 0; + } + static String formatBubblesString(List<Bubble> bubbles, BubbleViewProvider selected) { StringBuilder sb = new StringBuilder(); for (Bubble bubble : bubbles) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt new file mode 100644 index 000000000000..e57f02c71e44 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt @@ -0,0 +1,74 @@ +/* + * 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.bubbles + +import android.content.Context +import android.util.Log +import androidx.core.content.edit +import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION +import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES +import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME + +/** Manages bubble education flags. Provides convenience methods to check the education state */ +class BubbleEducationController(private val context: Context) { + private val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + + /** Whether the user has seen the stack education */ + @get:JvmName(name = "hasSeenStackEducation") + var hasSeenStackEducation: Boolean + get() = prefs.getBoolean(PREF_STACK_EDUCATION, false) + set(value) = prefs.edit { putBoolean(PREF_STACK_EDUCATION, value) } + + /** Whether the user has seen the expanded view "manage" menu education */ + @get:JvmName(name = "hasSeenManageEducation") + var hasSeenManageEducation: Boolean + get() = prefs.getBoolean(PREF_MANAGED_EDUCATION, false) + set(value) = prefs.edit { putBoolean(PREF_MANAGED_EDUCATION, value) } + + /** Whether education view should show for the collapsed stack. */ + fun shouldShowStackEducation(bubble: BubbleViewProvider?): Boolean { + val shouldShow = bubble != null && + bubble.isConversationBubble && // show education for conversation bubbles only + (!hasSeenStackEducation || BubbleDebugConfig.forceShowUserEducation(context)) + logDebug("Show stack edu: $shouldShow") + return shouldShow + } + + /** Whether the educational view should show for the expanded view "manage" menu. */ + fun shouldShowManageEducation(bubble: BubbleViewProvider?): Boolean { + val shouldShow = bubble != null && + bubble.isConversationBubble && // show education for conversation bubbles only + (!hasSeenManageEducation || BubbleDebugConfig.forceShowUserEducation(context)) + logDebug("Show manage edu: $shouldShow") + return shouldShow + } + + private fun logDebug(message: String) { + if (DEBUG_USER_EDUCATION) { + Log.d(TAG, message) + } + } + + companion object { + private val TAG = if (TAG_WITH_CLASS_NAME) "BubbleEducationController" else TAG_BUBBLES + const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding" + const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding" + } +} + +/** Convenience extension method to check if the bubble is a conversation bubble */ +private val BubbleViewProvider.isConversationBubble: Boolean + get() = if (this is Bubble) isConversation else false diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt new file mode 100644 index 000000000000..bdb09e11d5ad --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles + +import android.graphics.Color +import com.android.wm.shell.R +import com.android.wm.shell.common.bubbles.BubblePopupDrawable +import com.android.wm.shell.common.bubbles.BubblePopupView + +/** + * A convenience method to setup the [BubblePopupView] with the correct config using local resources + */ +fun BubblePopupView.setup() { + val attrs = + context.obtainStyledAttributes( + intArrayOf( + com.android.internal.R.attr.materialColorSurface, + android.R.attr.dialogCornerRadius + ) + ) + + val res = context.resources + val config = + BubblePopupDrawable.Config( + color = attrs.getColor(0, Color.WHITE), + cornerRadius = attrs.getDimension(1, 0f), + contentPadding = res.getDimensionPixelSize(R.dimen.bubble_popup_padding), + arrowWidth = res.getDimension(R.dimen.bubble_popup_arrow_width), + arrowHeight = res.getDimension(R.dimen.bubble_popup_arrow_height), + arrowRadius = res.getDimension(R.dimen.bubble_popup_arrow_corner_radius) + ) + attrs.recycle() + setupBackground(config) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 2c100653dae0..ea7053d8ee49 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -653,14 +653,38 @@ public class BubblePositioner { } /** - * @return the stack position to use if we don't have a saved location or if user education - * is being shown. + * Returns whether the {@link #getRestingPosition()} is equal to the default start position + * initialized for bubbles, if {@code true} this means the user hasn't moved the bubble + * from the initial start position (or they haven't received a bubble yet). + */ + public boolean hasUserModifiedDefaultPosition() { + PointF defaultStart = getDefaultStartPosition(); + return mRestingStackPosition != null + && !mRestingStackPosition.equals(defaultStart); + } + + /** + * Returns the stack position to use if we don't have a saved location or if user education + * is being shown, for a normal bubble. */ public PointF getDefaultStartPosition() { - // Start on the left if we're in LTR, right otherwise. - final boolean startOnLeft = - mContext.getResources().getConfiguration().getLayoutDirection() - != LAYOUT_DIRECTION_RTL; + return getDefaultStartPosition(false /* isAppBubble */); + } + + /** + * The stack position to use if we don't have a saved location or if user education + * is being shown. + * + * @param isAppBubble whether this start position is for an app bubble or not. + */ + public PointF getDefaultStartPosition(boolean isAppBubble) { + final int layoutDirection = mContext.getResources().getConfiguration().getLayoutDirection(); + // Normal bubbles start on the left if we're in LTR, right otherwise. + // TODO (b/294284894): update language around "app bubble" here + // App bubbles start on the right in RTL, left otherwise. + final boolean startOnLeft = isAppBubble + ? layoutDirection == LAYOUT_DIRECTION_RTL + : layoutDirection != LAYOUT_DIRECTION_RTL; final RectF allowableStackPositionRegion = getAllowableStackPositionRegion( 1 /* default starts with 1 bubble */); if (isLargeScreen()) { 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 da5974fe3dc2..52c9bf8462ec 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 @@ -46,7 +46,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.os.SystemProperties; import android.provider.Settings; import android.util.Log; import android.view.Choreographer; @@ -108,12 +107,6 @@ import java.util.stream.Collectors; */ public class BubbleStackView extends FrameLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { - - // LINT.IfChange - public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE = - SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true); - // LINT.ThenChange(com/android/launcher3/taskbar/bubbles/BubbleDismissController.java) - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ @@ -138,7 +131,7 @@ public class BubbleStackView extends FrameLayout private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; - private static final float SCRIM_ALPHA = 0.6f; + private static final float SCRIM_ALPHA = 0.32f; /** Minimum alpha value for scrim when alpha is being changed via drag */ private static final float MIN_SCRIM_ALPHA_FOR_DRAG = 0.2f; @@ -1291,6 +1284,12 @@ public class BubbleStackView extends FrameLayout if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { Log.d(TAG, "Show manage edu: " + shouldShow); } + if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) { + if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { + Log.d(TAG, "Want to show manage edu, but it is forced hidden"); + } + return false; + } return shouldShow; } @@ -1323,6 +1322,12 @@ public class BubbleStackView extends FrameLayout if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { Log.d(TAG, "Show stack edu: " + shouldShow); } + if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) { + if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { + Log.d(TAG, "Want to show stack edu, but it is forced hidden"); + } + return false; + } return shouldShow; } @@ -1770,13 +1775,26 @@ public class BubbleStackView extends FrameLayout return; } + if (firstBubble && bubble.isAppBubble() && !mPositioner.hasUserModifiedDefaultPosition()) { + // TODO (b/294284894): update language around "app bubble" here + // If it's an app bubble and we don't have a previous resting position, update the + // controllers to use the default position for the app bubble (it'd be different from + // the position initialized with the controllers originally). + PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */); + mStackOnLeftOrWillBe = mPositioner.isStackOnLeft(startPosition); + mStackAnimationController.setStackPosition(startPosition); + mExpandedAnimationController.setCollapsePoint(startPosition); + // Set the translation x so that this bubble will animate in from the same side they + // expand / collapse on. + bubble.getIconView().setTranslationX(startPosition.x); + } else if (firstBubble) { + mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); + } + mBubbleContainer.addView(bubble.getIconView(), 0, new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), mPositioner.getBubbleSize())); - if (firstBubble) { - mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); - } // Set the dot position to the opposite of the side the stack is resting on, since the stack // resting slightly off-screen would result in the dot also being off-screen. bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java index 33629f9f4622..4d7042bbb3d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -19,7 +19,6 @@ package com.android.wm.shell.bubbles.animation; import static android.view.View.LAYOUT_DIRECTION_RTL; import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; -import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE; import android.content.res.Resources; import android.graphics.Path; @@ -132,6 +131,16 @@ public class ExpandedAnimationController private BubbleStackView mBubbleStackView; + /** + * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause + * the rest of the bubbles to animate to fill the gap. + */ + private boolean mBubbleDraggedOutEnough = false; + + /** End action to run when the lead bubble's expansion animation completes. */ + @Nullable + private Runnable mLeadBubbleEndAction; + public ExpandedAnimationController(BubblePositioner positioner, Runnable onBubbleAnimatedOutAction, BubbleStackView stackView) { mPositioner = positioner; @@ -142,14 +151,12 @@ public class ExpandedAnimationController } /** - * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause - * the rest of the bubbles to animate to fill the gap. + * Overrides the collapse location without actually collapsing the stack. + * @param point the new collapse location. */ - private boolean mBubbleDraggedOutEnough = false; - - /** End action to run when the lead bubble's expansion animation completes. */ - @Nullable - private Runnable mLeadBubbleEndAction; + public void setCollapsePoint(PointF point) { + mCollapsePoint = point; + } /** * Animates expanding the bubbles into a row along the top of the screen, optionally running an @@ -355,7 +362,6 @@ public class ExpandedAnimationController mMagnetizedBubbleDraggingOut.setMagnetListener(listener); mMagnetizedBubbleDraggingOut.setHapticsEnabled(true); mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); - mMagnetizedBubbleDraggingOut.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE); } private void springBubbleTo(View bubble, float x, float y) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 5533842f2d89..aad268394305 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -17,7 +17,6 @@ package com.android.wm.shell.bubbles.animation; import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; -import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE; import android.content.ContentResolver; import android.content.res.Resources; @@ -298,9 +297,6 @@ public class StackAnimationController extends /** Whether the stack is on the left side of the screen. */ public boolean isStackOnLeftSide() { - if (mLayout == null || !isStackPositionSet()) { - return true; // Default to left, which is where it starts by default. - } return mPositioner.isStackOnLeft(mStackPosition); } @@ -1026,7 +1022,6 @@ public class StackAnimationController extends }; mMagnetizedStack.setHapticsEnabled(true); mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); - mMagnetizedStack.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE); } final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 6b6d6baa3d39..79f188ab2611 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -39,7 +39,6 @@ import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.taskview.TaskView; -import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -48,6 +47,18 @@ import java.util.function.Supplier; * {@link BubbleController#isShowingAsBubbleBar()} */ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener { + /** + * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal + * actions and events + */ + public interface Listener { + /** Called when the task view task is first created. */ + void onTaskCreated(); + /** Called when expanded view needs to un-bubble the given conversation */ + void onUnBubbleConversation(String bubbleKey); + /** Called when expanded view task view back button pressed */ + void onBackPressed(); + } private static final String TAG = BubbleBarExpandedView.class.getSimpleName(); private static final int INVALID_TASK_ID = -1; @@ -57,7 +68,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private BubbleTaskViewHelper mBubbleTaskViewHelper; private BubbleBarMenuViewController mMenuViewController; private @Nullable Supplier<Rect> mLayerBoundsSupplier; - private @Nullable Consumer<String> mUnBubbleConversationCallback; + private @Nullable Listener mListener; private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext()); private @Nullable TaskView mTaskView; @@ -145,15 +156,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { @Override public void onMenuVisibilityChanged(boolean visible) { - if (mTaskView == null || mLayerBoundsSupplier == null) return; - // Updates the obscured touchable region for the task surface. - mTaskView.setObscuredTouchRect(visible ? mLayerBoundsSupplier.get() : null); + setObscured(visible); } @Override public void onUnBubbleConversation(Bubble bubble) { - if (mUnBubbleConversationCallback != null) { - mUnBubbleConversationCallback.accept(bubble.getKey()); + if (mListener != null) { + mListener.onUnBubbleConversation(bubble.getKey()); } } @@ -231,6 +240,9 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView public void onTaskCreated() { setContentVisibility(true); updateHandleColor(false /* animated */); + if (mListener != null) { + mListener.onTaskCreated(); + } } @Override @@ -240,7 +252,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @Override public void onBackPressed() { - mController.collapseStack(); + if (mListener == null) return; + mListener.onBackPressed(); } /** Cleans up task view, should be called when the bubble is no longer active. */ @@ -254,6 +267,18 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mMenuViewController.hideMenu(false /* animated */); } + /** + * Hides the current modal menu view or collapses the bubble stack. + * Called from {@link BubbleBarLayerView} + */ + public void hideMenuOrCollapse() { + if (mMenuViewController.isMenuVisible()) { + mMenuViewController.hideMenu(/* animated = */ true); + } else { + mController.collapseStack(); + } + } + /** Updates the bubble shown in the expanded view. */ public void update(Bubble bubble) { mBubbleTaskViewHelper.update(bubble); @@ -270,10 +295,16 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mLayerBoundsSupplier = supplier; } - /** Sets the function to call to un-bubble the given conversation. */ - public void setUnBubbleConversationCallback( - @Nullable Consumer<String> unBubbleConversationCallback) { - mUnBubbleConversationCallback = unBubbleConversationCallback; + /** Sets expanded view listener */ + void setListener(@Nullable Listener listener) { + mListener = listener; + } + + /** Sets whether the view is obscured by some modal view */ + void setObscured(boolean obscured) { + if (mTaskView == null || mLayerBoundsSupplier == null) return; + // Updates the obscured touchable region for the task surface. + mTaskView.setObscuredTouchRect(obscured ? mLayerBoundsSupplier.get() : null); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index bc04bfc8c18b..8f11253290ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -52,6 +52,7 @@ public class BubbleBarLayerView extends FrameLayout private final BubbleController mBubbleController; private final BubblePositioner mPositioner; private final BubbleBarAnimationHelper mAnimationHelper; + private final BubbleEducationViewController mEducationViewController; private final View mScrimView; @Nullable @@ -80,6 +81,10 @@ public class BubbleBarLayerView extends FrameLayout mAnimationHelper = new BubbleBarAnimationHelper(context, this, mPositioner); + mEducationViewController = new BubbleEducationViewController(context, (boolean visible) -> { + if (mExpandedView == null) return; + mExpandedView.setObscured(visible); + }); mScrimView = new View(getContext()); mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); @@ -90,9 +95,7 @@ public class BubbleBarLayerView extends FrameLayout mScrimView.setBackgroundDrawable(new ColorDrawable( getResources().getColor(android.R.color.system_neutral1_1000))); - setOnClickListener(view -> { - mBubbleController.collapseStack(); - }); + setOnClickListener(view -> hideMenuOrCollapse()); } @Override @@ -108,6 +111,7 @@ public class BubbleBarLayerView extends FrameLayout getViewTreeObserver().removeOnComputeInternalInsetsListener(this); if (mExpandedView != null) { + mEducationViewController.hideManageEducation(/* animated = */ false); removeView(mExpandedView); mExpandedView = null; } @@ -162,14 +166,27 @@ public class BubbleBarLayerView extends FrameLayout final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded); final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); mExpandedView.setVisibility(GONE); - mExpandedView.setUnBubbleConversationCallback(mUnBubbleConversationCallback); + mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height); mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight())); - mExpandedView.setUnBubbleConversationCallback(bubbleKey -> { - if (mUnBubbleConversationCallback != null) { - mUnBubbleConversationCallback.accept(bubbleKey); + mExpandedView.setListener(new BubbleBarExpandedView.Listener() { + @Override + public void onTaskCreated() { + mEducationViewController.maybeShowManageEducation(b, mExpandedView); + } + + @Override + public void onUnBubbleConversation(String bubbleKey) { + if (mUnBubbleConversationCallback != null) { + mUnBubbleConversationCallback.accept(bubbleKey); + } + } + + @Override + public void onBackPressed() { + hideMenuOrCollapse(); } }); - mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height); + addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); } @@ -193,6 +210,7 @@ public class BubbleBarLayerView extends FrameLayout public void collapse() { mIsExpanded = false; final BubbleBarExpandedView viewToRemove = mExpandedView; + mEducationViewController.hideManageEducation(/* animated = */ true); mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); mBubbleController.getSysuiProxy().onStackExpandChanged(false); mExpandedView = null; @@ -206,6 +224,17 @@ public class BubbleBarLayerView extends FrameLayout mUnBubbleConversationCallback = unBubbleConversationCallback; } + /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */ + private void hideMenuOrCollapse() { + if (mEducationViewController.isManageEducationVisible()) { + mEducationViewController.hideManageEducation(/* animated = */ true); + } else if (isExpanded() && mExpandedView != null) { + mExpandedView.hideMenuOrCollapse(); + } else { + mBubbleController.collapseStack(); + } + } + /** Updates the expanded view size and position. */ private void updateExpandedView() { if (mExpandedView == null) return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 8be140c16435..81e7582e0dba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -56,6 +56,11 @@ class BubbleBarMenuViewController { SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); } + /** Tells if the menu is visible or being animated */ + boolean isMenuVisible() { + return mMenuView != null && mMenuView.getVisibility() == View.VISIBLE; + } + /** Sets menu actions listener */ void setListener(@Nullable Listener listener) { mListener = listener; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt new file mode 100644 index 000000000000..7b39c6fd4059 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt @@ -0,0 +1,148 @@ +/* + * 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.bubbles.bar + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.doOnLayout +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.wm.shell.R +import com.android.wm.shell.animation.PhysicsAnimator +import com.android.wm.shell.bubbles.BubbleEducationController +import com.android.wm.shell.bubbles.BubbleViewProvider +import com.android.wm.shell.bubbles.setup +import com.android.wm.shell.common.bubbles.BubblePopupView + +/** Manages bubble education presentation and animation */ +class BubbleEducationViewController(private val context: Context, private val listener: Listener) { + interface Listener { + fun onManageEducationVisibilityChanged(isVisible: Boolean) + } + + private var rootView: ViewGroup? = null + private var educationView: BubblePopupView? = null + private var animator: PhysicsAnimator<BubblePopupView>? = null + + private val springConfig by lazy { + PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, + SpringForce.DAMPING_RATIO_LOW_BOUNCY + ) + } + + private val controller by lazy { BubbleEducationController(context) } + + /** Whether the education view is visible or being animated */ + val isManageEducationVisible: Boolean + get() = educationView != null && rootView != null + + /** + * Show manage bubble education if hasn't been shown before + * + * @param bubble the bubble used for the manage education check + * @param root the view to show manage education in + */ + fun maybeShowManageEducation(bubble: BubbleViewProvider, root: ViewGroup) { + if (!controller.shouldShowManageEducation(bubble)) return + showManageEducation(root) + } + + /** + * Hide the manage education view if visible + * + * @param animated whether should hide with animation + */ + fun hideManageEducation(animated: Boolean) { + rootView?.let { + fun cleanUp() { + it.removeView(educationView) + rootView = null + listener.onManageEducationVisibilityChanged(isVisible = false) + } + + if (animated) { + animateTransition(show = false, ::cleanUp) + } else { + cleanUp() + } + } + } + + /** + * Show manage education with animation + * + * @param root the view to show manage education in + */ + private fun showManageEducation(root: ViewGroup) { + hideManageEducation(animated = false) + if (educationView == null) { + val eduView = createEducationView(root) + educationView = eduView + animator = createAnimation(eduView) + } + root.addView(educationView) + rootView = root + animateTransition(show = true) { + controller.hasSeenManageEducation = true + listener.onManageEducationVisibilityChanged(isVisible = true) + } + } + + /** + * Animate show/hide transition for the education view + * + * @param show whether to show or hide the view + * @param endActions a closure to be called when the animation completes + */ + private fun animateTransition(show: Boolean, endActions: () -> Unit) { + animator?.let { animator -> + animator + .spring(DynamicAnimation.ALPHA, if (show) 1f else 0f) + .spring(DynamicAnimation.SCALE_X, if (show) 1f else EDU_SCALE_HIDDEN) + .spring(DynamicAnimation.SCALE_Y, if (show) 1f else EDU_SCALE_HIDDEN) + .withEndActions(endActions) + .start() + } ?: endActions() + } + + private fun createEducationView(root: ViewGroup): BubblePopupView { + val view = + LayoutInflater.from(context).inflate(R.layout.bubble_bar_manage_education, root, false) + as BubblePopupView + + return view.apply { + setup() + alpha = 0f + pivotY = 0f + scaleX = EDU_SCALE_HIDDEN + scaleY = EDU_SCALE_HIDDEN + doOnLayout { it.pivotX = it.width / 2f } + setOnClickListener { hideManageEducation(animated = true) } + } + } + + private fun createAnimation(view: BubblePopupView): PhysicsAnimator<BubblePopupView> { + val animator = PhysicsAnimator.getInstance(view) + animator.setDefaultSpringConfig(springConfig) + return animator + } + + companion object { + private const val EDU_SCALE_HIDDEN = 0.5f + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt new file mode 100644 index 000000000000..8b5283d83683 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.common.bubbles + +import android.annotation.ColorInt +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Matrix +import android.graphics.Outline +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.Drawable +import kotlin.math.atan +import kotlin.math.cos +import kotlin.math.sin +import kotlin.properties.Delegates + +/** A drawable for the [BubblePopupView] that draws a popup background with a directional arrow */ +class BubblePopupDrawable(private val config: Config) : Drawable() { + /** The direction of the arrow in the popup drawable */ + enum class ArrowDirection { + UP, + DOWN + } + + /** The arrow position on the side of the popup bubble */ + sealed class ArrowPosition { + object Start : ArrowPosition() + object Center : ArrowPosition() + object End : ArrowPosition() + class Custom(val value: Float) : ArrowPosition() + } + + /** The configuration for drawable features */ + data class Config( + @ColorInt val color: Int, + val cornerRadius: Float, + val contentPadding: Int, + val arrowWidth: Float, + val arrowHeight: Float, + val arrowRadius: Float + ) + + /** + * The direction of the arrow in the popup drawable. It affects the content padding and requires + * it to be updated in the view. + */ + var arrowDirection: ArrowDirection by + Delegates.observable(ArrowDirection.UP) { _, _, _ -> requestPathUpdate() } + + /** + * Arrow position along the X axis and its direction. The position is adjusted to the content + * corner radius when applied so it doesn't go into rounded corner area + */ + var arrowPosition: ArrowPosition by + Delegates.observable(ArrowPosition.Center) { _, _, _ -> requestPathUpdate() } + + private val path = Path() + private val paint = Paint() + private var shouldUpdatePath = true + + init { + paint.color = config.color + paint.style = Paint.Style.FILL + paint.isAntiAlias = true + } + + override fun draw(canvas: Canvas) { + updatePathIfNeeded() + canvas.drawPath(path, paint) + } + + override fun onBoundsChange(bounds: Rect?) { + requestPathUpdate() + } + + /** Should be applied to the view padding if arrow direction changes */ + override fun getPadding(padding: Rect): Boolean { + padding.set( + config.contentPadding, + config.contentPadding, + config.contentPadding, + config.contentPadding + ) + when (arrowDirection) { + ArrowDirection.UP -> padding.top += config.arrowHeight.toInt() + ArrowDirection.DOWN -> padding.bottom += config.arrowHeight.toInt() + } + return true + } + + override fun getOutline(outline: Outline) { + updatePathIfNeeded() + outline.setPath(path) + } + + override fun getOpacity(): Int { + return paint.alpha + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + /** Schedules path update for the next redraw */ + private fun requestPathUpdate() { + shouldUpdatePath = true + } + + /** Updates the path if required, when bounds or arrow direction/position changes */ + private fun updatePathIfNeeded() { + if (shouldUpdatePath) { + updatePath() + shouldUpdatePath = false + } + } + + /** Updates the path value using the current bounds, config, arrow direction and position */ + private fun updatePath() { + if (bounds.isEmpty) return + // Reset the path state + path.reset() + // The content rect where the filled rounded rect will be drawn + val contentRect = RectF(bounds) + when (arrowDirection) { + ArrowDirection.UP -> { + // Add rounded arrow pointing up to the path + addRoundedArrowPositioned(path, arrowPosition) + // Inset content rect by the arrow size from the top + contentRect.top += config.arrowHeight + } + ArrowDirection.DOWN -> { + val matrix = Matrix() + // Flip the path with the matrix to draw arrow pointing down + matrix.setScale(1f, -1f, bounds.width() / 2f, bounds.height() / 2f) + path.transform(matrix) + // Add rounded arrow with the flipped matrix applied, will point down + addRoundedArrowPositioned(path, arrowPosition) + // Restore the path matrix to the original state with inverted matrix + matrix.invert(matrix) + path.transform(matrix) + // Inset content rect by the arrow size from the bottom + contentRect.bottom -= config.arrowHeight + } + } + // Add the content area rounded rect + path.addRoundRect(contentRect, config.cornerRadius, config.cornerRadius, Path.Direction.CW) + } + + /** Add a rounded arrow pointing up in the horizontal position on the canvas */ + private fun addRoundedArrowPositioned(path: Path, position: ArrowPosition) { + val matrix = Matrix() + var translationX = positionValue(position) - config.arrowWidth / 2 + // Offset to position between rounded corners of the content view + translationX = translationX.coerceIn(config.cornerRadius, + bounds.width() - config.cornerRadius - config.arrowWidth) + // Translate to add the arrow in the center horizontally + matrix.setTranslate(-translationX, 0f) + path.transform(matrix) + // Add rounded arrow + addRoundedArrow(path) + // Restore the path matrix to the original state with inverted matrix + matrix.invert(matrix) + path.transform(matrix) + } + + /** Adds a rounded arrow pointing up to the path, can be flipped if needed */ + private fun addRoundedArrow(path: Path) { + // Theta is half of the angle inside the triangle tip + val thetaTan = config.arrowWidth / (config.arrowHeight * 2f) + val theta = atan(thetaTan) + val thetaDeg = Math.toDegrees(theta.toDouble()).toFloat() + // The center Y value of the circle for the triangle tip + val tipCircleCenterY = config.arrowRadius / sin(theta) + // The length from triangle tip to intersection point with the circle + val tipIntersectionSideLength = config.arrowRadius / thetaTan + // The offset from the top to the point of intersection + val intersectionTopOffset = tipIntersectionSideLength * cos(theta) + // The offset from the center to the point of intersection + val intersectionCenterOffset = tipIntersectionSideLength * sin(theta) + // The center X of the triangle + val arrowCenterX = config.arrowWidth / 2f + + // Set initial position in bottom left of the arrow + path.moveTo(0f, config.arrowHeight) + // Add the left side of the triangle + path.lineTo(arrowCenterX - intersectionCenterOffset, intersectionTopOffset) + // Add the arc from the left to the right side of the triangle + path.arcTo( + /* left = */ arrowCenterX - config.arrowRadius, + /* top = */ tipCircleCenterY - config.arrowRadius, + /* right = */ arrowCenterX + config.arrowRadius, + /* bottom = */ tipCircleCenterY + config.arrowRadius, + /* startAngle = */ 180 + thetaDeg, + /* sweepAngle = */ 180 - (2 * thetaDeg), + /* forceMoveTo = */ false + ) + // Add the right side of the triangle + path.lineTo(config.arrowWidth, config.arrowHeight) + // Close the path + path.close() + } + + /** The value of the arrow position provided the position and current bounds */ + private fun positionValue(position: ArrowPosition): Float { + return when (position) { + is ArrowPosition.Start -> 0f + is ArrowPosition.Center -> bounds.width().toFloat() / 2f + is ArrowPosition.End -> bounds.width().toFloat() + is ArrowPosition.Custom -> position.value + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt new file mode 100644 index 000000000000..f8a4946bb5c5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.common.bubbles + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.widget.LinearLayout + +/** A popup container view that uses [BubblePopupDrawable] as a background */ +open class BubblePopupView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { + private var popupDrawable: BubblePopupDrawable? = null + + /** + * Sets up the popup drawable with the config provided. Required to remove dependency on local + * resources + */ + fun setupBackground(config: BubblePopupDrawable.Config) { + popupDrawable = BubblePopupDrawable(config) + background = popupDrawable + forceLayout() + } + + /** + * Sets the arrow direction for the background drawable and updates the padding to fit the + * content inside of the popup drawable + */ + fun setArrowDirection(direction: BubblePopupDrawable.ArrowDirection) { + popupDrawable?.let { + it.arrowDirection = direction + val padding = Rect() + if (it.getPadding(padding)) { + setPadding(padding.left, padding.top, padding.right, padding.bottom) + } + } + } + + /** Sets the arrow position for the background drawable and triggers redraw */ + fun setArrowPosition(position: BubblePopupDrawable.ArrowPosition) { + popupDrawable?.let { + it.arrowPosition = position + invalidate() + } + } +} 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 index a141ff951684..4abb35c2a428 100644 --- 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 @@ -19,7 +19,6 @@ 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, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMediaController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMediaController.kt new file mode 100644 index 000000000000..427a555eee92 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMediaController.kt @@ -0,0 +1,364 @@ +/* + * 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.annotation.DrawableRes +import android.annotation.StringRes +import android.app.PendingIntent +import android.app.RemoteAction +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.drawable.Icon +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import android.os.Handler +import android.os.HandlerExecutor +import android.os.UserHandle +import com.android.wm.shell.R +import java.util.function.Consumer + +/** + * Interfaces with the [MediaSessionManager] to compose the right set of actions to show (only + * if there are no actions from the PiP activity itself). The active media controller is only set + * when there is a media session from the top PiP activity. + */ +class PipMediaController(private val mContext: Context, private val mMainHandler: Handler) { + /** + * A listener interface to receive notification on changes to the media actions. + */ + interface ActionListener { + /** + * Called when the media actions changed. + */ + fun onMediaActionsChanged(actions: List<RemoteAction?>?) + } + + /** + * A listener interface to receive notification on changes to the media metadata. + */ + interface MetadataListener { + /** + * Called when the media metadata changed. + */ + fun onMediaMetadataChanged(metadata: MediaMetadata?) + } + + /** + * A listener interface to receive notification on changes to the media session token. + */ + interface TokenListener { + /** + * Called when the media session token changed. + */ + fun onMediaSessionTokenChanged(token: MediaSession.Token?) + } + + private val mHandlerExecutor: HandlerExecutor = HandlerExecutor(mMainHandler) + private val mMediaSessionManager: MediaSessionManager? + private var mMediaController: MediaController? = null + private val mPauseAction: RemoteAction + private val mPlayAction: RemoteAction + private val mNextAction: RemoteAction + private val mPrevAction: RemoteAction + private val mMediaActionReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (mMediaController == null) { + // no active media session, bail early. + return + } + when (intent.action) { + ACTION_PLAY -> mMediaController!!.transportControls.play() + ACTION_PAUSE -> mMediaController!!.transportControls.pause() + ACTION_NEXT -> mMediaController!!.transportControls.skipToNext() + ACTION_PREV -> mMediaController!!.transportControls.skipToPrevious() + } + } + } + private val mPlaybackChangedListener: MediaController.Callback = + object : MediaController.Callback() { + override fun onPlaybackStateChanged(state: PlaybackState?) { + notifyActionsChanged() + } + + override fun onMetadataChanged(metadata: MediaMetadata?) { + notifyMetadataChanged(metadata) + } + } + private val mSessionsChangedListener = + MediaSessionManager.OnActiveSessionsChangedListener { controllers: List<MediaController>? -> + resolveActiveMediaController(controllers) + } + private val mActionListeners = ArrayList<ActionListener>() + private val mMetadataListeners = ArrayList<MetadataListener>() + private val mTokenListeners = ArrayList<TokenListener>() + + init { + val mediaControlFilter = IntentFilter() + mediaControlFilter.addAction(ACTION_PLAY) + mediaControlFilter.addAction(ACTION_PAUSE) + mediaControlFilter.addAction(ACTION_NEXT) + mediaControlFilter.addAction(ACTION_PREV) + mContext.registerReceiverForAllUsers( + mMediaActionReceiver, mediaControlFilter, + SYSTEMUI_PERMISSION, mMainHandler, Context.RECEIVER_EXPORTED + ) + + // Creates the standard media buttons that we may show. + mPauseAction = getDefaultRemoteAction( + R.string.pip_pause, + R.drawable.pip_ic_pause_white, ACTION_PAUSE + ) + mPlayAction = getDefaultRemoteAction( + R.string.pip_play, + R.drawable.pip_ic_play_arrow_white, ACTION_PLAY + ) + mNextAction = getDefaultRemoteAction( + R.string.pip_skip_to_next, + R.drawable.pip_ic_skip_next_white, ACTION_NEXT + ) + mPrevAction = getDefaultRemoteAction( + R.string.pip_skip_to_prev, + R.drawable.pip_ic_skip_previous_white, ACTION_PREV + ) + mMediaSessionManager = mContext.getSystemService( + MediaSessionManager::class.java + ) + } + + /** + * Handles when an activity is pinned. + */ + fun onActivityPinned() { + // Once we enter PiP, try to find the active media controller for the top most activity + resolveActiveMediaController( + mMediaSessionManager!!.getActiveSessionsForUser( + null, + UserHandle.CURRENT + ) + ) + } + + /** + * Adds a new media action listener. + */ + fun addActionListener(listener: ActionListener) { + if (!mActionListeners.contains(listener)) { + mActionListeners.add(listener) + listener.onMediaActionsChanged(mediaActions) + } + } + + /** + * Removes a media action listener. + */ + fun removeActionListener(listener: ActionListener) { + listener.onMediaActionsChanged(emptyList<RemoteAction>()) + mActionListeners.remove(listener) + } + + /** + * Adds a new media metadata listener. + */ + fun addMetadataListener(listener: MetadataListener) { + if (!mMetadataListeners.contains(listener)) { + mMetadataListeners.add(listener) + listener.onMediaMetadataChanged(mediaMetadata) + } + } + + /** + * Removes a media metadata listener. + */ + fun removeMetadataListener(listener: MetadataListener) { + listener.onMediaMetadataChanged(null) + mMetadataListeners.remove(listener) + } + + /** + * Adds a new token listener. + */ + fun addTokenListener(listener: TokenListener) { + if (!mTokenListeners.contains(listener)) { + mTokenListeners.add(listener) + listener.onMediaSessionTokenChanged(token) + } + } + + /** + * Removes a token listener. + */ + fun removeTokenListener(listener: TokenListener) { + listener.onMediaSessionTokenChanged(null) + mTokenListeners.remove(listener) + } + + private val token: MediaSession.Token? + get() = if (mMediaController == null) { + null + } else mMediaController!!.sessionToken + private val mediaMetadata: MediaMetadata? + get() = if (mMediaController != null) mMediaController!!.metadata else null + + private val mediaActions: List<RemoteAction?> + /** + * Gets the set of media actions currently available. + */ + get() { + if (mMediaController == null) { + return emptyList<RemoteAction>() + } + // Cache the PlaybackState since it's a Binder call. + // Safe because mMediaController is guaranteed non-null here. + val playbackState: PlaybackState = mMediaController!!.playbackState + ?: return emptyList<RemoteAction>() + val mediaActions = ArrayList<RemoteAction?>() + val isPlaying = playbackState.isActive + val actions = playbackState.actions + + // Prev action + mPrevAction.isEnabled = + actions and PlaybackState.ACTION_SKIP_TO_PREVIOUS != 0L + mediaActions.add(mPrevAction) + + // Play/pause action + if (!isPlaying && actions and PlaybackState.ACTION_PLAY != 0L) { + mediaActions.add(mPlayAction) + } else if (isPlaying && actions and PlaybackState.ACTION_PAUSE != 0L) { + mediaActions.add(mPauseAction) + } + + // Next action + mNextAction.isEnabled = + actions and PlaybackState.ACTION_SKIP_TO_NEXT != 0L + mediaActions.add(mNextAction) + return mediaActions + } + + /** @return Default [RemoteAction] sends broadcast back to SysUI. + */ + private fun getDefaultRemoteAction( + @StringRes titleAndDescription: Int, + @DrawableRes icon: Int, + action: String + ): RemoteAction { + val titleAndDescriptionStr = mContext.getString(titleAndDescription) + val intent = Intent(action) + intent.setPackage(mContext.packageName) + return RemoteAction( + Icon.createWithResource(mContext, icon), + titleAndDescriptionStr, titleAndDescriptionStr, + PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + } + + /** + * Re-registers the session listener for the current user. + */ + fun registerSessionListenerForCurrentUser() { + mMediaSessionManager!!.removeOnActiveSessionsChangedListener(mSessionsChangedListener) + mMediaSessionManager.addOnActiveSessionsChangedListener( + null, UserHandle.CURRENT, + mHandlerExecutor, mSessionsChangedListener + ) + } + + /** + * Tries to find and set the active media controller for the top PiP activity. + */ + private fun resolveActiveMediaController(controllers: List<MediaController>?) { + if (controllers != null) { + val topActivity = PipUtils.getTopPipActivity(mContext).first + if (topActivity != null) { + for (i in controllers.indices) { + val controller = controllers[i] + if (controller.packageName == topActivity.packageName) { + setActiveMediaController(controller) + return + } + } + } + } + setActiveMediaController(null) + } + + /** + * Sets the active media controller for the top PiP activity. + */ + private fun setActiveMediaController(controller: MediaController?) { + if (controller != mMediaController) { + if (mMediaController != null) { + mMediaController!!.unregisterCallback(mPlaybackChangedListener) + } + mMediaController = controller + controller?.registerCallback(mPlaybackChangedListener, mMainHandler) + notifyActionsChanged() + notifyMetadataChanged(mediaMetadata) + notifyTokenChanged(token) + + // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV) + } + } + + /** + * Notifies all listeners that the actions have changed. + */ + private fun notifyActionsChanged() { + if (mActionListeners.isNotEmpty()) { + val actions = mediaActions + mActionListeners.forEach( + Consumer { l: ActionListener -> l.onMediaActionsChanged(actions) }) + } + } + + /** + * Notifies all listeners that the metadata have changed. + */ + private fun notifyMetadataChanged(metadata: MediaMetadata?) { + if (mMetadataListeners.isNotEmpty()) { + mMetadataListeners.forEach(Consumer { l: MetadataListener -> + l.onMediaMetadataChanged( + metadata + ) + }) + } + } + + private fun notifyTokenChanged(token: MediaSession.Token?) { + if (mTokenListeners.isNotEmpty()) { + mTokenListeners.forEach(Consumer { l: TokenListener -> + l.onMediaSessionTokenChanged( + token + ) + }) + } + } + + companion object { + private const val SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF" + private const val ACTION_PLAY = "com.android.wm.shell.pip.PLAY" + private const val ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE" + private const val ACTION_NEXT = "com.android.wm.shell.pip.NEXT" + private const val ACTION_PREV = "com.android.wm.shell.pip.PREV" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUiEventLogger.kt index 3e5a19b69a59..642dacc425d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUiEventLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 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. @@ -13,68 +13,59 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.wm.shell.common.pip -package com.android.wm.shell.pip; - -import android.app.TaskInfo; -import android.content.pm.PackageManager; - -import com.android.internal.logging.UiEvent; -import com.android.internal.logging.UiEventLogger; +import android.app.TaskInfo +import android.content.pm.PackageManager +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger /** * Helper class that ends PiP log to UiEvent, see also go/uievent */ -public class PipUiEventLogger { - - private static final int INVALID_PACKAGE_UID = -1; - - private final UiEventLogger mUiEventLogger; - private final PackageManager mPackageManager; - - private String mPackageName; - private int mPackageUid = INVALID_PACKAGE_UID; - - public PipUiEventLogger(UiEventLogger uiEventLogger, PackageManager packageManager) { - mUiEventLogger = uiEventLogger; - mPackageManager = packageManager; - } - - public void setTaskInfo(TaskInfo taskInfo) { - if (taskInfo != null && taskInfo.topActivity != null) { - mPackageName = taskInfo.topActivity.getPackageName(); - mPackageUid = getUid(mPackageName, taskInfo.userId); +class PipUiEventLogger( + private val mUiEventLogger: UiEventLogger, + private val mPackageManager: PackageManager +) { + private var mPackageName: String? = null + private var mPackageUid = INVALID_PACKAGE_UID + fun setTaskInfo(taskInfo: TaskInfo?) { + if (taskInfo?.topActivity != null) { + // safe because topActivity is guaranteed non-null here + mPackageName = taskInfo.topActivity!!.packageName + mPackageUid = getUid(mPackageName!!, taskInfo.userId) } else { - mPackageName = null; - mPackageUid = INVALID_PACKAGE_UID; + mPackageName = null + mPackageUid = INVALID_PACKAGE_UID } } /** * Sends log via UiEvent, reference go/uievent for how to debug locally */ - public void log(PipUiEventEnum event) { + fun log(event: PipUiEventEnum?) { if (mPackageName == null || mPackageUid == INVALID_PACKAGE_UID) { - return; + return } - mUiEventLogger.log(event, mPackageUid, mPackageName); + mUiEventLogger.log(event!!, mPackageUid, mPackageName) } - private int getUid(String packageName, int userId) { - int uid = INVALID_PACKAGE_UID; + private fun getUid(packageName: String, userId: Int): Int { + var uid = INVALID_PACKAGE_UID try { uid = mPackageManager.getApplicationInfoAsUser( - packageName, 0 /* ApplicationInfoFlags */, userId).uid; - } catch (PackageManager.NameNotFoundException e) { + packageName, 0 /* ApplicationInfoFlags */, userId + ).uid + } catch (e: PackageManager.NameNotFoundException) { // do nothing. } - return uid; + return uid } /** * Enums for logging the PiP events to UiEvent */ - public enum PipUiEventEnum implements UiEventLogger.UiEventEnum { + enum class PipUiEventEnum(private val mId: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "Activity enters picture-in-picture mode") PICTURE_IN_PICTURE_ENTER(603), @@ -99,8 +90,10 @@ public class PipUiEventLogger { @UiEvent(doc = "Hides picture-in-picture menu") PICTURE_IN_PICTURE_HIDE_MENU(608), - @UiEvent(doc = "Changes the aspect ratio of picture-in-picture window. This is inherited" - + " from previous Tron-based logging and currently not in use.") + @UiEvent( + doc = "Changes the aspect ratio of picture-in-picture window. This is inherited" + + " from previous Tron-based logging and currently not in use." + ) PICTURE_IN_PICTURE_CHANGE_ASPECT_RATIO(609), @UiEvent(doc = "User resize of the picture-in-picture window") @@ -121,15 +114,12 @@ public class PipUiEventLogger { @UiEvent(doc = "Closes PiP with app-provided close action") PICTURE_IN_PICTURE_CUSTOM_CLOSE(1058); - private final int mId; - - PipUiEventEnum(int id) { - mId = id; + override fun getId(): Int { + return mId } + } - @Override - public int getId() { - return mId; - } + companion object { + private const val INVALID_PACKAGE_UID = -1 } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt new file mode 100644 index 000000000000..84feb03e6a40 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt @@ -0,0 +1,144 @@ +/* + * 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.ActivityTaskManager +import android.app.RemoteAction +import android.app.WindowConfiguration +import android.content.ComponentName +import android.content.Context +import android.os.RemoteException +import android.os.SystemProperties +import android.util.DisplayMetrics +import android.util.Log +import android.util.Pair +import android.util.TypedValue +import android.window.TaskSnapshot +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.protolog.ShellProtoLogGroup +import kotlin.math.abs + +/** A class that includes convenience methods. */ +object PipUtils { + private const val TAG = "PipUtils" + + // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal. + private const val EPSILON = 1e-7 + private const val ENABLE_PIP2_IMPLEMENTATION = "persist.wm.debug.enable_pip2_implementation" + + /** + * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack. + * The component name may be null if no such activity exists. + */ + @JvmStatic + fun getTopPipActivity(context: Context): Pair<ComponentName?, Int> { + try { + val sysUiPackageName = context.packageName + val pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo( + WindowConfiguration.WINDOWING_MODE_PINNED, + WindowConfiguration.ACTIVITY_TYPE_UNDEFINED + ) + if (pinnedTaskInfo?.childTaskIds != null && pinnedTaskInfo.childTaskIds.isNotEmpty()) { + for (i in pinnedTaskInfo.childTaskNames.indices.reversed()) { + val cn = ComponentName.unflattenFromString( + pinnedTaskInfo.childTaskNames[i] + ) + if (cn != null && cn.packageName != sysUiPackageName) { + return Pair(cn, pinnedTaskInfo.childTaskUserIds[i]) + } + } + } + } catch (e: RemoteException) { + ProtoLog.w( + ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unable to get pinned stack.", TAG + ) + } + return Pair(null, 0) + } + + /** + * @return the pixels for a given dp value. + */ + @JvmStatic + fun dpToPx(dpValue: Float, dm: DisplayMetrics?): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, dm).toInt() + } + + /** + * @return true if the aspect ratios differ + */ + @JvmStatic + fun aspectRatioChanged(aspectRatio1: Float, aspectRatio2: Float): Boolean { + return abs(aspectRatio1 - aspectRatio2) > EPSILON + } + + /** + * Checks whether title, description and intent match. + * Comparing icons would be good, but using equals causes false negatives + */ + @JvmStatic + fun remoteActionsMatch(action1: RemoteAction?, action2: RemoteAction?): Boolean { + if (action1 === action2) return true + if (action1 == null || action2 == null) return false + return action1.isEnabled == action2.isEnabled && + action1.shouldShowIcon() == action2.shouldShowIcon() && + action1.title == action2.title && + action1.contentDescription == action2.contentDescription && + action1.actionIntent == action2.actionIntent + } + + /** + * Returns true if the actions in the lists match each other according to + * [ ][PipUtils.remoteActionsMatch], including their position. + */ + @JvmStatic + fun remoteActionsChanged(list1: List<RemoteAction?>?, list2: List<RemoteAction?>?): Boolean { + if (list1 == null && list2 == null) { + return false + } + if (list1 == null || list2 == null) { + return true + } + if (list1.size != list2.size) { + return true + } + for (i in list1.indices) { + if (!remoteActionsMatch(list1[i], list2[i])) { + return true + } + } + return false + } + + /** @return [TaskSnapshot] for a given task id. + */ + @JvmStatic + fun getTaskSnapshot(taskId: Int, isLowResolution: Boolean): TaskSnapshot? { + return if (taskId <= 0) null else try { + ActivityTaskManager.getService().getTaskSnapshot( + taskId, isLowResolution, false /* takeSnapshotIfNeeded */ + ) + } catch (e: RemoteException) { + Log.e(TAG, "Failed to get task snapshot, taskId=$taskId", e) + null + } + } + + @JvmStatic + val isPip2ExperimentEnabled: Boolean + get() = SystemProperties.getBoolean(ENABLE_PIP2_IMPLEMENTATION, false) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java index ef93a336305f..be1b9b1227de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -16,6 +16,7 @@ package com.android.wm.shell.common.split; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -60,7 +61,8 @@ public class SplitScreenConstants { public static final int[] CONTROLLED_WINDOWING_MODES = {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = - {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW, + WINDOWING_MODE_FREEFORM}; /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; 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 54f89846ac85..7bf0893c60c7 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 @@ -230,8 +230,10 @@ public class CompatUIController implements OnDisplaysChangedListener, // 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; + if (mUserAspectRatioSettingsLayout != null) { + mUserAspectRatioSettingsLayout.release(); + mUserAspectRatioSettingsLayout = null; + } return; } if (!taskInfo.isFromLetterboxDoubleTap) { 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 1a84f4b41b57..aafd9fdddb7f 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 @@ -20,6 +20,7 @@ import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HAN import android.app.ActivityTaskManager; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Handler; import android.os.SystemProperties; import android.view.IWindowManager; @@ -56,6 +57,8 @@ import com.android.wm.shell.common.annotations.ShellAnimationThread; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.annotations.ShellSplashscreenThread; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.compatui.CompatUIConfiguration; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.compatui.CompatUIShellCommandHandler; @@ -319,6 +322,24 @@ public abstract class WMShellBaseModule { return Optional.empty(); } + // + // PiP (optional feature) + // + + @WMSingleton + @Provides + static PipUiEventLogger providePipUiEventLogger(UiEventLogger uiEventLogger, + PackageManager packageManager) { + return new PipUiEventLogger(uiEventLogger, packageManager); + } + + @WMSingleton + @Provides + static PipMediaController providePipMediaController(Context context, + @ShellMainThread Handler mainHandler) { + return new PipMediaController(context, mainHandler); + } + // // Bubbles (optional feature) 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 9bf973f523bf..4e92ca113114 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 @@ -32,6 +32,9 @@ 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.PipMediaController; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; @@ -41,7 +44,6 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipDisplayLayoutState; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; @@ -49,8 +51,6 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransition; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; -import com.android.wm.shell.pip.PipUiEventLogger; -import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.pip.phone.PhonePipKeepClearAlgorithm; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.pip.phone.PipController; 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 e8fae2490bc5..c4ca5013afb6 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 @@ -17,15 +17,9 @@ package com.android.wm.shell.dagger.pip; import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Handler; -import com.android.internal.logging.UiEventLogger; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.dagger.WMSingleton; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; -import com.android.wm.shell.pip.PipUiEventLogger; import dagger.Module; import dagger.Provides; @@ -36,24 +30,9 @@ import dagger.Provides; */ @Module public abstract class Pip1SharedModule { - // Needs handler for registering broadcast receivers - @WMSingleton - @Provides - static PipMediaController providePipMediaController(Context context, - @ShellMainThread Handler mainHandler) { - return new PipMediaController(context, mainHandler); - } - @WMSingleton @Provides static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) { return new PipSurfaceTransactionHelper(context); } - - @WMSingleton - @Provides - static PipUiEventLogger providePipUiEventLogger(UiEventLogger uiEventLogger, - PackageManager packageManager) { - return new PipUiEventLogger(uiEventLogger, packageManager); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index c7c6e8a14278..8dec4ea542a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -18,6 +18,7 @@ package com.android.wm.shell.dagger.pip; import android.annotation.Nullable; +import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.pip2.PipTransition; @@ -26,9 +27,9 @@ import dagger.Provides; /** * Provides dependencies from {@link com.android.wm.shell.pip2}, this implementation is meant to be - * the successor of its sibling {@link Pip1SharedModule}. + * the successor of its sibling {@link Pip1Module}. */ -@Module +@Module(includes = WMShellBaseModule.class) public abstract class Pip2Module { @WMSingleton @Provides diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java index 04032bb17fec..9c9364e17e0e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java @@ -18,9 +18,9 @@ package com.android.wm.shell.dagger.pip; import android.annotation.Nullable; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.pip.PipTransitionController; -import com.android.wm.shell.pip.PipUtils; import dagger.Module; import dagger.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 80ffbb0968f3..a6ff9ecf7f4f 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 @@ -30,20 +30,20 @@ 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.PipMediaController; +import com.android.wm.shell.common.pip.PipUiEventLogger; 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.PipDisplayLayoutState; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; -import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipBoundsController; import com.android.wm.shell.pip.tv.TvPipBoundsState; 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 1d46e755ec9d..633f627e8e71 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.R import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -301,6 +302,24 @@ class DesktopTasksController( } } + /** Move a desktop app to split screen. */ + fun moveToSplit(task: RunningTaskInfo) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToSplit taskId=%d", + task.taskId + ) + val wct = WindowContainerTransaction() + wct.setWindowingMode(task.token, WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW) + wct.setBounds(task.token, null) + wct.setDensityDpi(task.token, getDefaultDensityDpi()) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + /** * The second part of the animated move to desktop transition, called after * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java index ac711eafe3ef..4fef672b2cd8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -28,6 +28,7 @@ import android.util.Size; import android.view.Gravity; import com.android.wm.shell.R; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; import java.io.PrintWriter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java index 456f85b52da4..4aa260b44646 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java @@ -16,7 +16,7 @@ package com.android.wm.shell.pip; -import static com.android.wm.shell.pip.PipUtils.dpToPx; +import static com.android.wm.shell.common.pip.PipUtils.dpToPx; import android.content.Context; import android.content.res.Resources; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java deleted file mode 100644 index ddffb5bdacde..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java +++ /dev/null @@ -1,369 +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.PendingIntent.FLAG_IMMUTABLE; -import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; - -import android.annotation.DrawableRes; -import android.annotation.StringRes; -import android.annotation.SuppressLint; -import android.app.PendingIntent; -import android.app.RemoteAction; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Icon; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.media.session.MediaSessionManager; -import android.media.session.PlaybackState; -import android.os.Handler; -import android.os.HandlerExecutor; -import android.os.UserHandle; - -import androidx.annotation.Nullable; - -import com.android.wm.shell.R; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only - * if there are no actions from the PiP activity itself). The active media controller is only set - * when there is a media session from the top PiP activity. - */ -public class PipMediaController { - private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; - - private static final String ACTION_PLAY = "com.android.wm.shell.pip.PLAY"; - private static final String ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE"; - private static final String ACTION_NEXT = "com.android.wm.shell.pip.NEXT"; - private static final String ACTION_PREV = "com.android.wm.shell.pip.PREV"; - - /** - * A listener interface to receive notification on changes to the media actions. - */ - public interface ActionListener { - /** - * Called when the media actions changed. - */ - void onMediaActionsChanged(List<RemoteAction> actions); - } - - /** - * A listener interface to receive notification on changes to the media metadata. - */ - public interface MetadataListener { - /** - * Called when the media metadata changed. - */ - void onMediaMetadataChanged(MediaMetadata metadata); - } - - /** - * A listener interface to receive notification on changes to the media session token. - */ - public interface TokenListener { - /** - * Called when the media session token changed. - */ - void onMediaSessionTokenChanged(MediaSession.Token token); - } - - private final Context mContext; - private final Handler mMainHandler; - private final HandlerExecutor mHandlerExecutor; - - private final MediaSessionManager mMediaSessionManager; - private MediaController mMediaController; - - private RemoteAction mPauseAction; - private RemoteAction mPlayAction; - private RemoteAction mNextAction; - private RemoteAction mPrevAction; - - private final BroadcastReceiver mMediaActionReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (mMediaController == null || mMediaController.getTransportControls() == null) { - // no active media session, bail early. - return; - } - switch (intent.getAction()) { - case ACTION_PLAY: - mMediaController.getTransportControls().play(); - break; - case ACTION_PAUSE: - mMediaController.getTransportControls().pause(); - break; - case ACTION_NEXT: - mMediaController.getTransportControls().skipToNext(); - break; - case ACTION_PREV: - mMediaController.getTransportControls().skipToPrevious(); - break; - } - } - }; - - private final MediaController.Callback mPlaybackChangedListener = - new MediaController.Callback() { - @Override - public void onPlaybackStateChanged(PlaybackState state) { - notifyActionsChanged(); - } - - @Override - public void onMetadataChanged(@Nullable MediaMetadata metadata) { - notifyMetadataChanged(metadata); - } - }; - - private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener = - this::resolveActiveMediaController; - - private final ArrayList<ActionListener> mActionListeners = new ArrayList<>(); - private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>(); - private final ArrayList<TokenListener> mTokenListeners = new ArrayList<>(); - - public PipMediaController(Context context, Handler mainHandler) { - mContext = context; - mMainHandler = mainHandler; - mHandlerExecutor = new HandlerExecutor(mMainHandler); - if (!PipUtils.isPip2ExperimentEnabled()) { - IntentFilter mediaControlFilter = new IntentFilter(); - mediaControlFilter.addAction(ACTION_PLAY); - mediaControlFilter.addAction(ACTION_PAUSE); - mediaControlFilter.addAction(ACTION_NEXT); - mediaControlFilter.addAction(ACTION_PREV); - mContext.registerReceiverForAllUsers(mMediaActionReceiver, mediaControlFilter, - SYSTEMUI_PERMISSION, mainHandler, Context.RECEIVER_EXPORTED); - } - - // Creates the standard media buttons that we may show. - mPauseAction = getDefaultRemoteAction(R.string.pip_pause, - R.drawable.pip_ic_pause_white, ACTION_PAUSE); - mPlayAction = getDefaultRemoteAction(R.string.pip_play, - R.drawable.pip_ic_play_arrow_white, ACTION_PLAY); - mNextAction = getDefaultRemoteAction(R.string.pip_skip_to_next, - R.drawable.pip_ic_skip_next_white, ACTION_NEXT); - mPrevAction = getDefaultRemoteAction(R.string.pip_skip_to_prev, - R.drawable.pip_ic_skip_previous_white, ACTION_PREV); - - mMediaSessionManager = context.getSystemService(MediaSessionManager.class); - } - - /** - * Handles when an activity is pinned. - */ - public void onActivityPinned() { - // Once we enter PiP, try to find the active media controller for the top most activity - resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null, - UserHandle.CURRENT)); - } - - /** - * Adds a new media action listener. - */ - public void addActionListener(ActionListener listener) { - if (!mActionListeners.contains(listener)) { - mActionListeners.add(listener); - listener.onMediaActionsChanged(getMediaActions()); - } - } - - /** - * Removes a media action listener. - */ - public void removeActionListener(ActionListener listener) { - listener.onMediaActionsChanged(Collections.emptyList()); - mActionListeners.remove(listener); - } - - /** - * Adds a new media metadata listener. - */ - public void addMetadataListener(MetadataListener listener) { - if (!mMetadataListeners.contains(listener)) { - mMetadataListeners.add(listener); - listener.onMediaMetadataChanged(getMediaMetadata()); - } - } - - /** - * Removes a media metadata listener. - */ - public void removeMetadataListener(MetadataListener listener) { - listener.onMediaMetadataChanged(null); - mMetadataListeners.remove(listener); - } - - /** - * Adds a new token listener. - */ - public void addTokenListener(TokenListener listener) { - if (!mTokenListeners.contains(listener)) { - mTokenListeners.add(listener); - listener.onMediaSessionTokenChanged(getToken()); - } - } - - /** - * Removes a token listener. - */ - public void removeTokenListener(TokenListener listener) { - listener.onMediaSessionTokenChanged(null); - mTokenListeners.remove(listener); - } - - private MediaSession.Token getToken() { - if (mMediaController == null) { - return null; - } - return mMediaController.getSessionToken(); - } - - private MediaMetadata getMediaMetadata() { - return mMediaController != null ? mMediaController.getMetadata() : null; - } - - /** - * Gets the set of media actions currently available. - */ - // This is due to using PlaybackState#isActive, which is added in API 31. - // It can be removed when min_sdk of the app is set to 31 or greater. - @SuppressLint("NewApi") - private List<RemoteAction> getMediaActions() { - // Cache the PlaybackState since it's a Binder call. - final PlaybackState playbackState; - if (mMediaController == null - || (playbackState = mMediaController.getPlaybackState()) == null) { - return Collections.emptyList(); - } - - ArrayList<RemoteAction> mediaActions = new ArrayList<>(); - boolean isPlaying = playbackState.isActive(); - long actions = playbackState.getActions(); - - // Prev action - mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0); - mediaActions.add(mPrevAction); - - // Play/pause action - if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) { - mediaActions.add(mPlayAction); - } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) { - mediaActions.add(mPauseAction); - } - - // Next action - mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0); - mediaActions.add(mNextAction); - return mediaActions; - } - - /** @return Default {@link RemoteAction} sends broadcast back to SysUI. */ - private RemoteAction getDefaultRemoteAction(@StringRes int titleAndDescription, - @DrawableRes int icon, String action) { - final String titleAndDescriptionStr = mContext.getString(titleAndDescription); - final Intent intent = new Intent(action); - intent.setPackage(mContext.getPackageName()); - return new RemoteAction(Icon.createWithResource(mContext, icon), - titleAndDescriptionStr, titleAndDescriptionStr, - PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent, - FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); - } - - /** - * Re-registers the session listener for the current user. - */ - public void registerSessionListenerForCurrentUser() { - mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener); - mMediaSessionManager.addOnActiveSessionsChangedListener(null, UserHandle.CURRENT, - mHandlerExecutor, mSessionsChangedListener); - } - - /** - * Tries to find and set the active media controller for the top PiP activity. - */ - private void resolveActiveMediaController(List<MediaController> controllers) { - if (controllers != null) { - final ComponentName topActivity = PipUtils.getTopPipActivity(mContext).first; - if (topActivity != null) { - for (int i = 0; i < controllers.size(); i++) { - final MediaController controller = controllers.get(i); - if (controller.getPackageName().equals(topActivity.getPackageName())) { - setActiveMediaController(controller); - return; - } - } - } - } - setActiveMediaController(null); - } - - /** - * Sets the active media controller for the top PiP activity. - */ - private void setActiveMediaController(MediaController controller) { - if (controller != mMediaController) { - if (mMediaController != null) { - mMediaController.unregisterCallback(mPlaybackChangedListener); - } - mMediaController = controller; - if (controller != null) { - controller.registerCallback(mPlaybackChangedListener, mMainHandler); - } - notifyActionsChanged(); - notifyMetadataChanged(getMediaMetadata()); - notifyTokenChanged(getToken()); - - // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV) - } - } - - /** - * Notifies all listeners that the actions have changed. - */ - private void notifyActionsChanged() { - if (!mActionListeners.isEmpty()) { - List<RemoteAction> actions = getMediaActions(); - mActionListeners.forEach(l -> l.onMediaActionsChanged(actions)); - } - } - - /** - * Notifies all listeners that the metadata have changed. - */ - private void notifyMetadataChanged(MediaMetadata metadata) { - if (!mMetadataListeners.isEmpty()) { - mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata)); - } - } - - private void notifyTokenChanged(MediaSession.Token token) { - if (!mTokenListeners.isEmpty()) { - mTokenListeners.forEach(l -> l.onMediaSessionTokenChanged(token)); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 19c60c2a9117..ed9ff1c169c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -82,6 +82,8 @@ import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index db7e2c0c529f..83e03dc850a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -64,6 +64,7 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 0f74f9e323a5..64bba672a5b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -38,6 +38,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java deleted file mode 100644 index 3cd9848d5686..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java +++ /dev/null @@ -1,145 +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.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.util.TypedValue.COMPLEX_UNIT_DIP; - -import android.annotation.Nullable; -import android.app.ActivityTaskManager; -import android.app.ActivityTaskManager.RootTaskInfo; -import android.app.RemoteAction; -import android.content.ComponentName; -import android.content.Context; -import android.os.RemoteException; -import android.os.SystemProperties; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.Pair; -import android.util.TypedValue; -import android.window.TaskSnapshot; - -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.protolog.ShellProtoLogGroup; - -import java.util.List; -import java.util.Objects; - -/** A class that includes convenience methods. */ -public class PipUtils { - private static final String TAG = "PipUtils"; - - // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal. - private static final double EPSILON = 1e-7; - - private static final String ENABLE_PIP2_IMPLEMENTATION = - "persist.wm.debug.enable_pip2_implementation"; - - /** - * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack. - * The component name may be null if no such activity exists. - */ - public static Pair<ComponentName, Integer> getTopPipActivity(Context context) { - try { - final String sysUiPackageName = context.getPackageName(); - final RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo( - WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); - if (pinnedTaskInfo != null && pinnedTaskInfo.childTaskIds != null - && pinnedTaskInfo.childTaskIds.length > 0) { - for (int i = pinnedTaskInfo.childTaskNames.length - 1; i >= 0; i--) { - ComponentName cn = ComponentName.unflattenFromString( - pinnedTaskInfo.childTaskNames[i]); - if (cn != null && !cn.getPackageName().equals(sysUiPackageName)) { - return new Pair<>(cn, pinnedTaskInfo.childTaskUserIds[i]); - } - } - } - } catch (RemoteException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Unable to get pinned stack.", TAG); - } - return new Pair<>(null, 0); - } - - /** - * @return the pixels for a given dp value. - */ - public static int dpToPx(float dpValue, DisplayMetrics dm) { - return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); - } - - /** - * @return true if the aspect ratios differ - */ - public static boolean aspectRatioChanged(float aspectRatio1, float aspectRatio2) { - return Math.abs(aspectRatio1 - aspectRatio2) > EPSILON; - } - - /** - * Checks whether title, description and intent match. - * Comparing icons would be good, but using equals causes false negatives - */ - public static boolean remoteActionsMatch(RemoteAction action1, RemoteAction action2) { - if (action1 == action2) return true; - if (action1 == null || action2 == null) return false; - return action1.isEnabled() == action2.isEnabled() - && action1.shouldShowIcon() == action2.shouldShowIcon() - && Objects.equals(action1.getTitle(), action2.getTitle()) - && Objects.equals(action1.getContentDescription(), action2.getContentDescription()) - && Objects.equals(action1.getActionIntent(), action2.getActionIntent()); - } - - /** - * Returns true if the actions in the lists match each other according to {@link - * PipUtils#remoteActionsMatch(RemoteAction, RemoteAction)}, including their position. - */ - public static boolean remoteActionsChanged(List<RemoteAction> list1, List<RemoteAction> list2) { - if (list1 == null && list2 == null) { - return false; - } - if (list1 == null || list2 == null) { - return true; - } - if (list1.size() != list2.size()) { - return true; - } - for (int i = 0; i < list1.size(); i++) { - if (!remoteActionsMatch(list1.get(i), list2.get(i))) { - return true; - } - } - return false; - } - - /** @return {@link TaskSnapshot} for a given task id. */ - @Nullable - public static TaskSnapshot getTaskSnapshot(int taskId, boolean isLowResolution) { - if (taskId <= 0) return null; - try { - return ActivityTaskManager.getService().getTaskSnapshot( - taskId, isLowResolution, false /* takeSnapshotIfNeeded */); - } catch (RemoteException e) { - Log.e(TAG, "Failed to get task snapshot, taskId=" + taskId, e); - return null; - } - } - - public static boolean isPip2ExperimentEnabled() { - return SystemProperties.getBoolean(ENABLE_PIP2_IMPLEMENTATION, false); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 5e1b6becfa45..cc182ba89985 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -38,12 +38,12 @@ import android.view.WindowManagerGlobal; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipMediaController.ActionListener; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.pip.PipBoundsState; -import com.android.wm.shell.pip.PipMediaController; -import com.android.wm.shell.pip.PipMediaController.ActionListener; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; -import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; 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 b872267947dc..ddea574c3c89 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 @@ -76,6 +76,8 @@ 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.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.IPip; @@ -87,13 +89,11 @@ import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; -import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.KeyguardChangeListener; @@ -123,14 +123,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb private static final long PIP_KEEP_CLEAR_AREAS_DELAY = SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200); - private boolean mEnablePipKeepClearAlgorithm = - SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true); - - @VisibleForTesting - void setEnablePipKeepClearAlgorithm(boolean value) { - mEnablePipKeepClearAlgorithm = value; - } - private Context mContext; protected ShellExecutor mMainExecutor; private DisplayController mDisplayController; @@ -166,10 +158,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb // early bail out if the change was caused by keyguard showing up return; } - if (!mEnablePipKeepClearAlgorithm) { - // early bail out if the keep clear areas feature is disabled - return; - } if (mPipBoundsState.isStashed()) { // don't move when stashed return; @@ -187,10 +175,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void updatePipPositionForKeepClearAreas() { - if (!mEnablePipKeepClearAlgorithm) { - // early bail out if the keep clear areas feature is disabled - return; - } if (mIsKeyguardShowingOrAnimating) { // early bail out if the change was caused by keyguard showing up return; @@ -343,19 +327,17 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { if (mPipDisplayLayoutState.getDisplayId() == displayId) { - if (mEnablePipKeepClearAlgorithm) { - mPipBoundsState.setKeepClearAreas(restricted, unrestricted); - - mMainExecutor.removeCallbacks( - mMovePipInResponseToKeepClearAreasChangeCallback); - mMainExecutor.executeDelayed( - mMovePipInResponseToKeepClearAreasChangeCallback, - PIP_KEEP_CLEAR_AREAS_DELAY); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "onKeepClearAreasChanged: restricted=%s, unrestricted=%s", - restricted, unrestricted); - } + mPipBoundsState.setKeepClearAreas(restricted, unrestricted); + + mMainExecutor.removeCallbacks( + mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.executeDelayed( + mMovePipInResponseToKeepClearAreasChangeCallback, + PIP_KEEP_CLEAR_AREAS_DELAY); + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "onKeepClearAreasChanged: restricted=%s, unrestricted=%s", + restricted, unrestricted); } } }; @@ -660,25 +642,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb // there's a keyguard present return; } - int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; onDisplayChangedUncheck(mDisplayController .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()), false /* saveRestoreSnapFraction */); - int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; - if (!mEnablePipKeepClearAlgorithm) { - // offset PiP to adjust for bottom inset change - int pipTop = mPipBoundsState.getBounds().top; - int diff = newMaxMovementBound - oldMaxMovementBound; - if (diff < 0 && pipTop > newMaxMovementBound) { - // bottom inset has increased, move PiP up if it is too low - mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), - newMaxMovementBound - pipTop); - } - if (diff > 0 && oldMaxMovementBound == pipTop) { - // bottom inset has decreased, move PiP down if it was by the edge - mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), diff); - } - } } }); @@ -947,14 +913,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Sets both shelf visibility and its height. */ private void setShelfHeight(boolean visible, int height) { - if (mEnablePipKeepClearAlgorithm) { - // turn this into Launcher keep clear area registration instead - setLauncherKeepClearAreaHeight(visible, height); - return; - } - if (!mIsKeyguardShowingOrAnimating) { - setShelfHeightLocked(visible, height); - } + // turn this into Launcher keep clear area registration instead + setLauncherKeepClearAreaHeight(visible, height); } private void setLauncherKeepClearAreaHeight(boolean visible, int height) { @@ -1015,16 +975,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb private Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams, int launcherRotation, Rect hotseatKeepClearArea) { - - if (mEnablePipKeepClearAlgorithm) { - // preemptively add the keep clear area for Hotseat, so that it is taken into account - // when calculating the entry destination bounds of PiP window - mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, - hotseatKeepClearArea); - } else { - int shelfHeight = hotseatKeepClearArea.height(); - setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight); - } + // preemptively add the keep clear area for Hotseat, so that it is taken into account + // when calculating the entry destination bounds of PiP window + mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, + hotseatKeepClearArea); onDisplayRotationChangedNotInPip(mContext, launcherRotation); final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo, pictureInPictureParams); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java index da455f85d908..4e75847b6bc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -38,7 +38,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.bubbles.DismissCircleView; import com.android.wm.shell.common.bubbles.DismissView; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; -import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUiEventLogger; import kotlin.Unit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index 779c539a2097..fc34772f2fce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -66,8 +66,8 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.pip.PipUiEventLogger; -import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; 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 b251f6f55794..8f0a8e11103c 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 @@ -33,7 +33,6 @@ import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.os.Debug; -import android.os.SystemProperties; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; @@ -48,19 +47,16 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.function.Consumer; - import kotlin.Unit; import kotlin.jvm.functions.Function0; +import java.util.function.Consumer; + /** * A helper to animate and manipulate the PiP. */ public class PipMotionHelper implements PipAppOpsListener.Callback, FloatingContentCoordinator.FloatingContent { - - public static final boolean ENABLE_FLING_TO_DISMISS_PIP = - SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", false); private static final String TAG = "PipMotionHelper"; private static final boolean DEBUG = false; @@ -707,7 +703,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, loc[1] = animatedPipBounds.top; } }; - mMagnetizedPip.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_PIP); + mMagnetizedPip.setFlingToTargetEnabled(false); } return mMagnetizedPip; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index abe2db094a5c..4e687dda4e77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -46,11 +46,11 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.policy.TaskResizingAlgorithm; import com.android.wm.shell.R; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipTaskOrganizer; -import com.android.wm.shell.pip.PipUiEventLogger; import java.io.PrintWriter; import java.util.function.Consumer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index ab65c9ebe7e5..cb4e6c8b6631 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -34,7 +34,6 @@ import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; -import android.os.SystemProperties; import android.provider.DeviceConfig; import android.util.Size; import android.view.DisplayCutout; @@ -51,14 +50,14 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; -import com.android.wm.shell.pip.PipUiEventLogger; -import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellInit; @@ -73,14 +72,6 @@ public class PipTouchHandler { private static final String TAG = "PipTouchHandler"; private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; - private boolean mEnablePipKeepClearAlgorithm = - SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true); - - @VisibleForTesting - void setEnablePipKeepClearAlgorithm(boolean value) { - mEnablePipKeepClearAlgorithm = value; - } - // Allow PIP to resize to a slightly bigger state upon touch private boolean mEnableResize; private final Context mContext; @@ -430,48 +421,6 @@ public class PipTouchHandler { mIsImeShowing ? mImeOffset : 0, !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0); - // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not - // occluded by the IME or shelf. - if (fromImeAdjustment || fromShelfAdjustment) { - if (mTouchState.isUserInteracting() && mTouchState.isDragging()) { - // Defer the update of the current movement bounds until after the user finishes - // touching the screen - } else if (mEnablePipKeepClearAlgorithm) { - // Ignore moving PiP if keep clear algorithm is enabled, since IME and shelf height - // now are accounted for in the keep clear algorithm calculations - } else { - final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu(); - final Rect toMovementBounds = new Rect(); - mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds, - toMovementBounds, mIsImeShowing ? mImeHeight : 0); - final int prevBottom = mPipBoundsState.getMovementBounds().bottom - - mMovementBoundsExtraOffsets; - // This is to handle landscape fullscreen IMEs, don't apply the extra offset in this - // case - final int toBottom = toMovementBounds.bottom < toMovementBounds.top - ? toMovementBounds.bottom - : toMovementBounds.bottom - extraOffset; - - if (isExpanded) { - curBounds.set(mPipBoundsState.getExpandedBounds()); - mPipBoundsAlgorithm.getSnapAlgorithm().applySnapFraction(curBounds, - toMovementBounds, mSavedSnapFraction); - } - - if (prevBottom < toBottom) { - // The movement bounds are expanding - if (curBounds.top > prevBottom - mBottomOffsetBufferPx) { - mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); - } - } else if (prevBottom > toBottom) { - // The movement bounds are shrinking - if (curBounds.top > toBottom - mBottomOffsetBufferPx) { - mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); - } - } - } - } - // Update the movement bounds after doing the calculations based on the old movement bounds // above mPipBoundsState.setNormalMovementBounds(normalMovementBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java index 3b44f10ebe62..4bba9690707a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java @@ -35,8 +35,8 @@ import android.content.Context; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; -import com.android.wm.shell.pip.PipMediaController; -import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; 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 5e583c20061f..0816dc74dfa9 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 @@ -48,11 +48,11 @@ 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.common.pip.PipMediaController; 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.PipDisplayLayoutState; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 613791ccc062..45e1cde8f9a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -55,7 +55,7 @@ import com.android.internal.widget.LinearLayoutManager; import com.android.internal.widget.RecyclerView; import com.android.wm.shell.R; import com.android.wm.shell.common.TvWindowMenuActionButton; -import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index f22ee595e6c9..1c94625ddde9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -36,9 +36,9 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ImageUtils; import com.android.wm.shell.R; -import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipParamsChangedForwarder; -import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index 4819f665d6d3..672080407658 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -25,6 +25,8 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -35,8 +37,6 @@ import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; -import com.android.wm.shell.pip.PipUiEventLogger; -import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.Objects; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS new file mode 100644 index 000000000000..ec09827fa4d1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS @@ -0,0 +1,3 @@ +# WM shell sub-module pip owner +hwwang@google.com +mateuszc@google.com 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 2be7a491fdf2..29fff03500a5 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 @@ -71,6 +71,7 @@ import com.android.wm.shell.desktopmode.DesktopModeController; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; @@ -200,6 +201,19 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Override public void setSplitScreenController(SplitScreenController splitScreenController) { mSplitScreenController = splitScreenController; + mSplitScreenController.registerSplitScreenListener(new SplitScreen.SplitScreenListener() { + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + if (visible) { + DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId); + if (decor != null && DesktopModeStatus.isActive(mContext) + && decor.mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false)); + mDesktopTasksController.ifPresent(c -> c.moveToSplit(decor.mTaskInfo)); + } + } + } + }); } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java index 139724f709c7..58d9a6486ff2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java @@ -203,6 +203,60 @@ public class BubblePositionerTest extends ShellTestCase { assertThat(restingPosition.y).isEqualTo(getDefaultYPosition()); } + /** Test that the default resting position on tablet is middle right. */ + @Test + public void testGetDefaultPosition_appBubble_onTablet() { + new WindowManagerConfig().setLargeScreen().setUpConfig(); + mPositioner.update(); + + RectF allowableStackRegion = + mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); + PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */); + + assertThat(startPosition.x).isEqualTo(allowableStackRegion.right); + assertThat(startPosition.y).isEqualTo(getDefaultYPosition()); + } + + @Test + public void testGetRestingPosition_appBubble_onTablet_RTL() { + new WindowManagerConfig().setLargeScreen().setLayoutDirection( + LAYOUT_DIRECTION_RTL).setUpConfig(); + mPositioner.update(); + + RectF allowableStackRegion = + mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); + PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */); + + assertThat(startPosition.x).isEqualTo(allowableStackRegion.left); + assertThat(startPosition.y).isEqualTo(getDefaultYPosition()); + } + + @Test + public void testHasUserModifiedDefaultPosition_false() { + new WindowManagerConfig().setLargeScreen().setLayoutDirection( + LAYOUT_DIRECTION_RTL).setUpConfig(); + mPositioner.update(); + + assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse(); + + mPositioner.setRestingPosition(mPositioner.getDefaultStartPosition()); + + assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse(); + } + + @Test + public void testHasUserModifiedDefaultPosition_true() { + new WindowManagerConfig().setLargeScreen().setLayoutDirection( + LAYOUT_DIRECTION_RTL).setUpConfig(); + mPositioner.update(); + + assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse(); + + mPositioner.setRestingPosition(new PointF(0, 100)); + + assertThat(mPositioner.hasUserModifiedDefaultPosition()).isTrue(); + } + /** * Calculates the Y position bubbles should be placed based on the config. Based on * the calculations in {@link BubblePositioner#getDefaultStartPosition()} and diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 1e3fe421140a..248d66590542 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -53,6 +53,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; 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 0ae290847644..911f5e165ef1 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 @@ -56,12 +56,12 @@ 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.common.pip.PipMediaController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipDisplayLayoutState; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; @@ -76,7 +76,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -329,21 +328,7 @@ public class PipControllerTest extends ShellTestCase { } @Test - public void onKeepClearAreasChanged_featureDisabled_pipBoundsStateDoesntChange() { - mPipController.setEnablePipKeepClearAlgorithm(false); - final int displayId = 1; - final Rect keepClearArea = new Rect(0, 0, 10, 10); - when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); - - mPipController.mDisplaysChangedListener.onKeepClearAreasChanged( - displayId, Set.of(keepClearArea), Set.of()); - - verify(mMockPipBoundsState, never()).setKeepClearAreas(Mockito.anySet(), Mockito.anySet()); - } - - @Test - public void onKeepClearAreasChanged_featureEnabled_updatesPipBoundsState() { - mPipController.setEnablePipKeepClearAlgorithm(true); + public void onKeepClearAreasChanged_updatesPipBoundsState() { final int displayId = 1; final Rect keepClearArea = new Rect(0, 0, 10, 10); when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index 689b5c5a708c..12b4f3e50f32 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -37,6 +37,7 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -45,7 +46,6 @@ import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; -import com.android.wm.shell.pip.PipUiEventLogger; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index 852183cbcbac..314f195d87ac 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -18,7 +18,6 @@ package com.android.wm.shell.pip.phone; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -34,6 +33,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -42,7 +42,6 @@ import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; -import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; @@ -174,16 +173,4 @@ public class PipTouchHandlerTest extends ShellTestCase { verify(mPipResizeGestureHandler, times(1)) .updateMaxSize(expectedMaxSize.getWidth(), expectedMaxSize.getHeight()); } - - @Test - public void updateMovementBounds_withImeAdjustment_movesPip() { - mPipTouchHandler.setEnablePipKeepClearAlgorithm(false); - mFromImeAdjustment = true; - mPipTouchHandler.onImeVisibilityChanged(true /* imeVisible */, mImeHeight); - - mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mPipBounds, mCurBounds, - mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation); - - verify(mMotionHelper, times(1)).animateToOffset(any(), anyInt()); - } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java index 02e6b8c71663..c40cd4069cab 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java @@ -37,7 +37,7 @@ import android.testing.TestableLooper; import android.util.Log; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipMediaController; import org.junit.Before; import org.junit.Test; diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index e8c9d0dbd884..c3087bc1c0d2 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -6915,7 +6915,10 @@ public class AudioManager { /** * @hide - * Returns whether CSD is enabled and supported by the HAL on this device. + * Returns whether CSD is enabled and supported by the current active audio module HAL. + * This method will return {@code false) for setups in which CSD as a feature is available + * (see {@link AudioManager#isCsdAsAFeatureAvailable()}) and not enabled (see + * {@link AudioManager#isCsdAsAFeatureEnabled()}). */ @TestApi @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) @@ -6929,6 +6932,49 @@ public class AudioManager { /** * @hide + * Returns whether CSD as a feature can be manipulated by a client. This method + * returns {@code true} in countries where there isn't a safe hearing regulation + * enforced. + */ + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public boolean isCsdAsAFeatureAvailable() { + try { + return getService().isCsdAsAFeatureAvailable(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Returns {@code true} if the client has enabled CSD. This function should only + * be called if {@link AudioManager#isCsdAsAFeatureAvailable()} returns {@code true}. + */ + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public boolean isCsdAsAFeatureEnabled() { + try { + return getService().isCsdAsAFeatureEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Enables/disables the CSD feature. This function should only be called if + * {@link AudioManager#isCsdAsAFeatureAvailable()} returns {@code true}. + */ + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void setCsdAsAFeatureEnabled(boolean csdToggleValue) { + try { + getService().setCsdAsAFeatureEnabled(csdToggleValue); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide * Describes an audio device that has not been categorized with a specific * audio type. */ diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 180c7fde6a15..9d62e37ec97b 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -323,6 +323,15 @@ interface IAudioService { boolean isCsdEnabled(); @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + boolean isCsdAsAFeatureAvailable(); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + boolean isCsdAsAFeatureEnabled(); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + oneway void setCsdAsAFeatureEnabled(boolean csdToggleValue); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") oneway void setBluetoothAudioDeviceCategory(in String address, boolean isBle, int deviceType); @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java index 9ab84d254ed6..f90a17ae8761 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java @@ -45,6 +45,7 @@ public class BatteryStatus { private static final int EXTREME_LOW_BATTERY_THRESHOLD = 3; private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000; + public static final int BATTERY_LEVEL_UNKNOWN = -1; public static final int CHARGING_UNKNOWN = -1; public static final int CHARGING_SLOWLY = 0; public static final int CHARGING_REGULAR = 1; @@ -186,12 +187,13 @@ public class BatteryStatus { /** Gets the battery level from the intent. */ public static int getBatteryLevel(Intent batteryChangedIntent) { if (batteryChangedIntent == null) { - return -1; /*invalid battery level*/ + return BATTERY_LEVEL_UNKNOWN; } - final int level = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + final int level = + batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_UNKNOWN); final int scale = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); return scale == 0 - ? -1 /*invalid battery level*/ + ? BATTERY_LEVEL_UNKNOWN : Math.round((level / (float) scale) * 100f); } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 3efb41dbfe5c..c2dbf9813178 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -240,6 +240,7 @@ public class SecureSettings { Settings.Secure.HEARING_AID_CALL_ROUTING, Settings.Secure.HEARING_AID_MEDIA_ROUTING, Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING, - Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED + Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, + Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index e5f7f889138e..a49461e3a5af 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -198,6 +198,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ASSIST_GESTURE_WAKE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"})); VALIDATORS.put(Secure.NOTIFICATION_BADGING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_DISMISS_RTL, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index a83bfda41f3d..ef4e84fcff7b 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1926,6 +1926,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, SecureSettingsProto.Assist.LONG_PRESS_HOME_ENABLED); + dumpSetting(s, p, + Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, + SecureSettingsProto.Assist.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED); p.end(assistToken); final long assistHandlesToken = p.start(SecureSettingsProto.ASSIST_HANDLES); diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 9b8e47300f9b..91c72b543cc4 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -703,7 +703,8 @@ public class SettingsBackupTest { Settings.Secure.ASSIST_SCREENSHOT_ENABLED, Settings.Secure.ASSIST_STRUCTURE_ENABLED, Settings.Secure.ATTENTIVE_TIMEOUT, - Settings.Secure.AUDIO_DEVICE_INVENTORY, // setting not controllable by user + Settings.Secure.AUDIO_DEVICE_INVENTORY, // not controllable by user + Settings.Secure.AUDIO_SAFE_CSD_AS_A_FEATURE_ENABLED, // not controllable by user Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION, Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index da48762e1960..0a100babde75 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -14,26 +14,20 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.keyguard.ui.composable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import android.view.View +import android.view.ViewGroup import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.SceneScope -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.qualifiers.KeyguardRootView import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey @@ -42,6 +36,7 @@ import com.android.systemui.scene.shared.model.UserAction import com.android.systemui.scene.ui.composable.ComposableScene import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -54,6 +49,7 @@ class LockscreenScene constructor( @Application private val applicationScope: CoroutineScope, private val viewModel: LockscreenSceneViewModel, + @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View, ) : ComposableScene { override val key = SceneKey.Lockscreen @@ -72,6 +68,7 @@ constructor( ) { LockscreenScene( viewModel = viewModel, + viewProvider = viewProvider, modifier = modifier, ) } @@ -89,25 +86,22 @@ constructor( @Composable private fun LockscreenScene( viewModel: LockscreenSceneViewModel, + viewProvider: () -> View, modifier: Modifier = Modifier, ) { - // TODO(b/280879610): implement the real UI. - - val lockButtonIcon: Icon by viewModel.lockButtonIcon.collectAsState() - - Box(modifier = modifier) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.align(Alignment.Center) - ) { - Text("Lockscreen", style = MaterialTheme.typography.headlineMedium) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button(onClick = { viewModel.onLockButtonClicked() }) { Icon(lockButtonIcon) } - - Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") } + AndroidView( + factory = { _ -> + val keyguardRootView = viewProvider() + // Remove the KeyguardRootView from any parent it might already have in legacy code just + // in case (a view can't have two parents). + (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView) + keyguardRootView + }, + update = { keyguardRootView -> + keyguardRootView.requireViewById<View>(R.id.lock_icon_view).setOnClickListener { + viewModel.onLockButtonClicked() } - } - } + }, + modifier = modifier, + ) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index d65edae1caf0..dc93400e6581 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -51,15 +51,18 @@ import kotlinx.coroutines.withContext private val KEY_TIMESTAMP = "appliedTimestamp" private val KNOWN_PLUGINS = mapOf<String, List<ClockMetadata>>( - "com.android.systemui.falcon.one" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")), - "com.android.systemui.falcon.two" to listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")), - "com.android.systemui.falcon.three" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")), - "com.android.systemui.falcon.four" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")), - "com.android.systemui.falcon.five" to listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")), - "com.android.systemui.falcon.six" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")), - "com.android.systemui.falcon.seven" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")), - "com.android.systemui.falcon.eight" to listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")), - "com.android.systemui.falcon.nine" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")), + "com.android.systemui.clocks.bignum" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")), + "com.android.systemui.clocks.calligraphy" to + listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")), + "com.android.systemui.clocks.flex" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")), + "com.android.systemui.clocks.growth" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")), + "com.android.systemui.clocks.handwritten" to + listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")), + "com.android.systemui.clocks.inflate" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")), + "com.android.systemui.clocks.metro" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")), + "com.android.systemui.clocks.numoverlap" to + listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")), + "com.android.systemui.clocks.weather" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")), ) private fun <TKey, TVal> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut( @@ -409,6 +412,18 @@ open class ClockRegistry( scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } } } + // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this + // is dependent on which clocks are connected, it may change when a clock is installed or + // removed from the device (unlike currentClockId). + // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors. + val activeClockId: String + get() { + if (!availableClocks.containsKey(currentClockId)) { + return DEFAULT_CLOCK_ID + } + return currentClockId + } + init { // Register default clock designs for (clock in defaultClockProvider.getClocks()) { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index e7a53e5baaf4..b28920c590c5 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -65,7 +65,7 @@ class DefaultClockController( protected var onSecondaryDisplay: Boolean = false override val events: DefaultClockEvents - override val config = ClockConfig() + override val config = ClockConfig(DEFAULT_CLOCK_ID) init { val parent = FrameLayout(ctx) diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index 006fc09fb400..31df2baec08c 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -292,6 +292,7 @@ -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt +-packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt -packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt -packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt -packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 527f80072222..e2f4793b8f91 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -199,6 +199,8 @@ data class ClockMetadata( /** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */ data class ClockConfig( + val id: String, + /** Transition to AOD should move smartspace like large clock instead of small clock */ val useAlternateSmartspaceAODTransition: Boolean = false, diff --git a/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml new file mode 100644 index 000000000000..952f056b3023 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml @@ -0,0 +1,42 @@ +<?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.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/carrier_combo" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:orientation="horizontal" > + + <com.android.systemui.util.AutoMarqueeTextView + android:id="@+id/mobile_carrier_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginEnd="@dimen/qs_carrier_margin_width" + android:visibility="gone" + android:textDirection="locale" + android:marqueeRepeatLimit="marquee_forever" + android:singleLine="true" + android:maxEms="7"/> + + <include layout="@layout/status_bar_mobile_signal_group_new" /> + +</com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView> + diff --git a/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml new file mode 100644 index 000000000000..a8abd793bd00 --- /dev/null +++ b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml @@ -0,0 +1,22 @@ +<?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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" android:color="?attr/onShadeActive" android:alpha="0.12" /> + <item android:state_hovered="true" android:color="?attr/onShadeActive" android:alpha="0.09" /> + <item android:color="@color/transparent" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml index a8c034986425..47a2965bcfac 100644 --- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml +++ b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml @@ -32,6 +32,12 @@ <corners android:radius="@dimen/qs_footer_action_corner_radius"/> </shape> </item> + <item> + <shape android:shape="rectangle"> + <solid android:color="@color/qs_footer_power_button_overlay_color"/> + <corners android:radius="@dimen/qs_footer_action_corner_radius"/> + </shape> + </item> </ripple> </inset>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml index 4a9d41fae1d5..b83f15a1a247 100644 --- a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml +++ b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml @@ -14,6 +14,4 @@ Copyright (C) 2015 The Android Open Source Project limitations under the License. --> <inset xmlns:android="http://schemas.android.com/apk/res/android" - android:insetLeft="3dp" - android:insetRight="3dp" android:drawable="@drawable/ic_speaker_mute" /> diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml index 78cd7184b485..39ec09b14157 100644 --- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml +++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml @@ -34,8 +34,8 @@ android:layout_height="@dimen/overlay_dismiss_button_tappable_size" android:contentDescription="@string/screenshot_dismiss_work_profile"> <ImageView - android:layout_width="16dp" - android:layout_height="16dp" + android:layout_width="24dp" + android:layout_height="24dp" android:layout_gravity="center" android:background="@drawable/circular_background" android:backgroundTint="?androidprv:attr/materialColorSurfaceContainerHigh" diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl index 4bc949116807..33e453cf4354 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl @@ -76,26 +76,11 @@ oneway interface IOverviewProxy { void onSystemBarAttributesChanged(int displayId, int behavior) = 20; /** - * Sent when screen turned on and ready to use (blocker scrim is hidden) - */ - void onScreenTurnedOn() = 21; - - /** * Sent when the desired dark intensity of the nav buttons has changed */ void onNavButtonsDarkIntensityChanged(float darkIntensity) = 22; /** - * Sent when screen started turning on. - */ - void onScreenTurningOn() = 23; - - /** - * Sent when screen started turning off. - */ - void onScreenTurningOff() = 24; - - /** * Sent when split keyboard shortcut is triggered to enter stage split. */ void enterStageSplitFromRunningApp(boolean leftOrTop) = 25; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index cab54d08b3ec..8200e5c84186 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -39,6 +39,7 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; +import android.os.SystemProperties; import android.provider.Settings; import android.util.Log; import android.view.HapticFeedbackConstants; @@ -76,6 +77,8 @@ public class RotationButtonController { private static final String TAG = "RotationButtonController"; private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100; private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000; + private static final boolean OEM_DISALLOW_ROTATION_IN_SUW = + SystemProperties.getBoolean("ro.setupwizard.rotation_locked", false); private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3; @@ -375,6 +378,12 @@ public class RotationButtonController { } public void onRotationProposal(int rotation, boolean isValid) { + boolean isUserSetupComplete = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0) != 0; + if (!isUserSetupComplete && OEM_DISALLOW_ROTATION_IN_SUW) { + return; + } + int windowRotation = mWindowRotationProvider.get(); if (!mRotationButton.acceptRotationProposal()) { @@ -497,8 +506,7 @@ public class RotationButtonController { boolean canShowRotationButton() { return mIsNavigationBarShowing || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT - || isGesturalMode(mNavBarMode) - || mTaskBarVisible; + || isGesturalMode(mNavBarMode); } @DrawableRes diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 5915b22c9a5f..5d0e8f70f744 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -39,6 +39,7 @@ import java.lang.annotation.RetentionPolicy; public class KeyguardClockSwitch extends RelativeLayout { private static final String TAG = "KeyguardClockSwitch"; + public static final String MISSING_CLOCK_ID = "CLOCK_MISSING"; private static final long CLOCK_OUT_MILLIS = 133; private static final long CLOCK_IN_MILLIS = 167; @@ -192,6 +193,14 @@ public class KeyguardClockSwitch extends RelativeLayout { return mLogBuffer; } + /** Returns the id of the currently rendering clock */ + public String getClockId() { + if (mClock == null) { + return MISSING_CLOCK_ID; + } + return mClock.getConfig().getId(); + } + void setClock(ClockController clock, int statusBarState) { mClock = clock; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 81080760822b..4515a6638c07 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -222,8 +222,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large); - mDumpManager.unregisterDumpable(getClass().toString()); // unregister previous clocks - mDumpManager.registerDumpable(getClass().toString(), this); + if (!mOnlyClock) { + mDumpManager.unregisterDumpable(getClass().toString()); // unregister previous clocks + mDumpManager.registerDumpable(getClass().toString(), this); + } if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) { mStatusArea = mView.findViewById(R.id.keyguard_status_area); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 04692c48a123..9f3908a4ab92 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -27,6 +27,7 @@ import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_PRIMARY; import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER; import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import static com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES; import android.app.ActivityManager; @@ -370,8 +371,12 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onOrientationChanged(int orientation) { - KeyguardSecurityContainerController.this + if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)) { + // TODO(b/295603468) + // Fix reinflation of views when flag is enabled. + KeyguardSecurityContainerController.this .onDensityOrFontScaleOrOrientationChanged(); + } } }; private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 58c8000a2328..802a550c4d29 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -73,9 +73,7 @@ import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteracto import com.android.systemui.biometrics.domain.model.BiometricModalities; import com.android.systemui.biometrics.ui.BiometricPromptLayout; import com.android.systemui.biometrics.ui.CredentialView; -import com.android.systemui.biometrics.ui.binder.AuthBiometricFingerprintViewBinder; import com.android.systemui.biometrics.ui.binder.BiometricViewBinder; -import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel; import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel; import com.android.systemui.dagger.qualifiers.Background; @@ -142,8 +140,6 @@ public class AuthContainerView extends LinearLayout // TODO: these should be migrated out once ready private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor; - private final Provider<AuthBiometricFingerprintViewModel> - mAuthBiometricFingerprintViewModelProvider; private final @NonNull Provider<PromptSelectorInteractor> mPromptSelectorInteractorProvider; // TODO(b/251476085): these should be migrated out of the view private final Provider<CredentialViewModel> mCredentialViewModelProvider; @@ -283,8 +279,6 @@ public class AuthContainerView extends LinearLayout @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, - @NonNull Provider<AuthBiometricFingerprintViewModel> - authBiometricFingerprintViewModelProvider, @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractor, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor, @NonNull PromptViewModel promptViewModel, @@ -293,9 +287,9 @@ public class AuthContainerView extends LinearLayout @NonNull VibratorHelper vibratorHelper) { this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps, wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, - jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor, - promptCredentialInteractor, promptViewModel, credentialViewModelProvider, - new Handler(Looper.getMainLooper()), bgExecutor, vibratorHelper); + jankMonitor, promptSelectorInteractor, promptCredentialInteractor, promptViewModel, + credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor, + vibratorHelper); } @VisibleForTesting @@ -309,8 +303,6 @@ public class AuthContainerView extends LinearLayout @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, - @NonNull Provider<AuthBiometricFingerprintViewModel> - authBiometricFingerprintViewModelProvider, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider, @NonNull Provider<PromptCredentialInteractor> credentialInteractor, @NonNull PromptViewModel promptViewModel, @@ -359,7 +351,6 @@ public class AuthContainerView extends LinearLayout mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; mPromptCredentialInteractor = credentialInteractor; - mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider; mPromptSelectorInteractorProvider = promptSelectorInteractorProvider; mCredentialViewModelProvider = credentialViewModelProvider; mPromptViewModel = promptViewModel; @@ -442,9 +433,6 @@ public class AuthContainerView extends LinearLayout fingerprintAndFaceView.updateOverrideIconLayoutParamsSize(); fingerprintAndFaceView.setFaceClass3( faceProperties.sensorStrength == STRENGTH_STRONG); - final AuthBiometricFingerprintViewModel fpAndFaceViewModel = - mAuthBiometricFingerprintViewModelProvider.get(); - AuthBiometricFingerprintViewBinder.bind(fingerprintAndFaceView, fpAndFaceViewModel); mBiometricView = fingerprintAndFaceView; } else if (fpProperties != null) { final AuthBiometricFingerprintView fpView = @@ -453,9 +441,6 @@ public class AuthContainerView extends LinearLayout fpView.setSensorProperties(fpProperties); fpView.setScaleFactorProvider(config.mScaleProvider); fpView.updateOverrideIconLayoutParamsSize(); - final AuthBiometricFingerprintViewModel fpViewModel = - mAuthBiometricFingerprintViewModelProvider.get(); - AuthBiometricFingerprintViewBinder.bind(fpView, fpViewModel); mBiometricView = fpView; } else if (faceProperties != null) { mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 7b288a8d49f1..d5289a49be51 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -73,7 +73,6 @@ import com.android.systemui.CoreStartable; import com.android.systemui.biometrics.domain.interactor.LogContextInteractor; import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor; import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor; -import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel; import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel; import com.android.systemui.dagger.SysUISingleton; @@ -134,8 +133,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, private final CoroutineScope mApplicationCoroutineScope; // TODO: these should be migrated out once ready - @NonNull private final Provider<AuthBiometricFingerprintViewModel> - mAuthBiometricFingerprintViewModelProvider; @NonNull private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor; @NonNull private final Provider<PromptSelectorInteractor> mPromptSelectorInteractor; @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider; @@ -765,8 +762,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull LockPatternUtils lockPatternUtils, @NonNull UdfpsLogger udfpsLogger, @NonNull LogContextInteractor logContextInteractor, - @NonNull Provider<AuthBiometricFingerprintViewModel> - authBiometricFingerprintViewModelProvider, @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractorProvider, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @@ -801,7 +796,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mVibratorHelper = vibratorHelper; mLogContextInteractor = logContextInteractor; - mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider; mPromptSelectorInteractor = promptSelectorInteractorProvider; mPromptCredentialInteractor = promptCredentialInteractorProvider; mPromptViewModelProvider = promptViewModelProvider; @@ -1344,9 +1338,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, config.mScaleProvider = this::getScaleFactor; return new AuthContainerView(config, mFeatureFlags, mApplicationCoroutineScope, mFpProps, mFaceProps, wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, - mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider, - mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel, - mCredentialViewModelProvider, bgExecutor, mVibratorHelper); + mInteractionJankMonitor, mPromptCredentialInteractor, mPromptSelectorInteractor, + viewModel, mCredentialViewModelProvider, bgExecutor, mVibratorHelper); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index bb87dca1cdc4..5badcaf06003 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -21,15 +21,18 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.Utils.getCredentialType import com.android.systemui.biometrics.Utils.isDeviceCredentialAllowed +import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.PromptRepository import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.domain.model.BiometricOperationInfo import com.android.systemui.biometrics.domain.model.BiometricPromptRequest import com.android.systemui.biometrics.shared.model.BiometricUserInfo +import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -65,6 +68,9 @@ interface PromptSelectorInteractor { */ val isConfirmationRequired: Flow<Boolean> + /** Fingerprint sensor type */ + val sensorType: StateFlow<FingerprintSensorType> + /** Use biometrics for authentication. */ fun useBiometricsForAuthentication( promptInfo: PromptInfo, @@ -89,6 +95,7 @@ interface PromptSelectorInteractor { class PromptSelectorInteractorImpl @Inject constructor( + private val fingerprintPropertyRepository: FingerprintPropertyRepository, private val promptRepository: PromptRepository, lockPatternUtils: LockPatternUtils, ) : PromptSelectorInteractor { @@ -140,6 +147,9 @@ constructor( } } + override val sensorType: StateFlow<FingerprintSensorType> = + fingerprintPropertyRepository.sensorType + override fun useBiometricsForAuthentication( promptInfo: PromptInfo, userId: Int, 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 d054751b760b..b1439fd7421d 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 @@ -108,6 +108,9 @@ object BiometricViewBinder { val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay) val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon) + + PromptFingerprintIconViewBinder.bind(iconView, viewModel.fingerprintIconViewModel) + val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) // Negative-side (left) buttons diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt index bd0907e588ca..188c82b52374 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt @@ -21,26 +21,29 @@ import android.view.DisplayInfo import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.biometrics.AuthBiometricFingerprintView -import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel +import com.android.systemui.biometrics.ui.viewmodel.PromptFingerprintIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached import kotlinx.coroutines.launch -/** Sub-binder for [AuthBiometricFingerprintView.mIconView]. */ -object AuthBiometricFingerprintIconViewBinder { +/** Sub-binder for [BiometricPromptLayout.iconView]. */ +object PromptFingerprintIconViewBinder { - /** - * Binds a [AuthBiometricFingerprintView.mIconView] to a [AuthBiometricFingerprintViewModel]. - */ + /** Binds [BiometricPromptLayout.iconView] to [PromptFingerprintIconViewModel]. */ @JvmStatic - fun bind(view: LottieAnimationView, viewModel: AuthBiometricFingerprintViewModel) { + fun bind(view: LottieAnimationView, viewModel: PromptFingerprintIconViewModel) { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { val displayInfo = DisplayInfo() view.context.display?.getDisplayInfo(displayInfo) viewModel.setRotation(displayInfo.rotation) viewModel.onConfigurationChanged(view.context.resources.configuration) - launch { viewModel.iconAsset.collect { iconAsset -> view.setAnimation(iconAsset) } } + launch { + viewModel.iconAsset.collect { iconAsset -> + if (iconAsset != -1) { + view.setAnimation(iconAsset) + } + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt index 617d80cee09d..9b30acb84428 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt @@ -22,23 +22,35 @@ import android.content.res.Configuration import android.view.Surface import com.android.systemui.R import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor +import com.android.systemui.biometrics.shared.model.FingerprintSensorType import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -/** Models UI of AuthBiometricFingerprintView to support rear display state changes. */ -class AuthBiometricFingerprintViewModel +/** Models UI of [BiometricPromptLayout.iconView] */ +class PromptFingerprintIconViewModel @Inject -constructor(private val interactor: DisplayStateInteractor) { +constructor( + private val displayStateInteractor: DisplayStateInteractor, + private val promptSelectorInteractor: PromptSelectorInteractor, +) { /** Current device rotation. */ private var rotation: Int = Surface.ROTATION_0 - /** Current AuthBiometricFingerprintView asset. */ + /** Current BiometricPromptLayout.iconView asset. */ val iconAsset: Flow<Int> = - combine(interactor.isFolded, interactor.isInRearDisplayMode) { - isFolded: Boolean, - isInRearDisplayMode: Boolean -> - getSideFpsAnimationAsset(isFolded, isInRearDisplayMode) + combine( + displayStateInteractor.isFolded, + displayStateInteractor.isInRearDisplayMode, + promptSelectorInteractor.sensorType, + ) { isFolded: Boolean, isInRearDisplayMode: Boolean, sensorType: FingerprintSensorType -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + getSideFpsAnimationAsset(isFolded, isInRearDisplayMode) + // Replace below when non-SFPS iconAsset logic is migrated to this ViewModel + else -> -1 + } } @RawRes @@ -75,7 +87,7 @@ constructor(private val interactor: DisplayStateInteractor) { /** Called on configuration changes */ fun onConfigurationChanged(newConfig: Configuration) { - interactor.onConfigurationChanged(newConfig) + displayStateInteractor.onConfigurationChanged(newConfig) } fun setRotation(newRotation: Int) { 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 89561a5a212b..4dc7720ef447 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 @@ -20,6 +20,7 @@ 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.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality @@ -45,16 +46,23 @@ import kotlinx.coroutines.launch class PromptViewModel @Inject constructor( - private val interactor: PromptSelectorInteractor, + private val displayStateInteractor: DisplayStateInteractor, + private val promptSelectorInteractor: PromptSelectorInteractor, private val vibrator: VibratorHelper, private val featureFlags: FeatureFlags, ) { + /** Models UI of [BiometricPromptLayout.iconView] */ + val fingerprintIconViewModel: PromptFingerprintIconViewModel = + PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor) + /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = - interactor.prompt.map { it?.modalities ?: BiometricModalities() }.distinctUntilChanged() + promptSelectorInteractor.prompt + .map { it?.modalities ?: BiometricModalities() } + .distinctUntilChanged() // TODO(b/251476085): remove after icon controllers are migrated - do not keep this state - private var _legacyState = MutableStateFlow(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN) + private var _legacyState = MutableStateFlow(AuthBiometricView.STATE_IDLE) val legacyState: StateFlow<Int> = _legacyState.asStateFlow() private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -75,17 +83,18 @@ constructor( * successful authentication. */ val isConfirmationRequired: Flow<Boolean> = - combine(_isOverlayTouched, interactor.isConfirmationRequired) { + combine(_isOverlayTouched, promptSelectorInteractor.isConfirmationRequired) { isOverlayTouched, isConfirmationRequired -> !isOverlayTouched && isConfirmationRequired } /** The kind of credential the user has. */ - val credentialKind: Flow<PromptKind> = interactor.credentialKind + val credentialKind: Flow<PromptKind> = promptSelectorInteractor.credentialKind /** The label to use for the cancel button. */ - val negativeButtonText: Flow<String> = interactor.prompt.map { it?.negativeButtonText ?: "" } + val negativeButtonText: Flow<String> = + promptSelectorInteractor.prompt.map { it?.negativeButtonText ?: "" } private val _message: MutableStateFlow<PromptMessage> = MutableStateFlow(PromptMessage.Empty) @@ -113,7 +122,7 @@ constructor( _forceLargeSize, _forceMediumSize, modalities, - interactor.isConfirmationRequired, + promptSelectorInteractor.isConfirmationRequired, fingerprintStartMode, ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode -> when { @@ -129,14 +138,16 @@ constructor( .distinctUntilChanged() /** Title for the prompt. */ - val title: Flow<String> = interactor.prompt.map { it?.title ?: "" }.distinctUntilChanged() + val title: Flow<String> = + promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() /** Subtitle for the prompt. */ - val subtitle: Flow<String> = interactor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged() + val subtitle: Flow<String> = + promptSelectorInteractor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged() /** Description for the prompt. */ val description: Flow<String> = - interactor.prompt.map { it?.description ?: "" }.distinctUntilChanged() + promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged() /** If the indicator (help, error) message should be shown. */ val isIndicatorMessageVisible: Flow<Boolean> = @@ -160,7 +171,9 @@ constructor( /** If the icon can be used as a confirmation button. */ val isIconConfirmButton: Flow<Boolean> = - combine(size, interactor.isConfirmationRequired) { size, isConfirmationRequired -> + combine(size, promptSelectorInteractor.isConfirmationRequired) { + size, + isConfirmationRequired -> size.isNotSmall && isConfirmationRequired } @@ -169,7 +182,7 @@ constructor( combine( size, isAuthenticated, - interactor.isCredentialAllowed, + promptSelectorInteractor.isCredentialAllowed, ) { size, authState, credentialAllowed -> size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed } @@ -221,7 +234,7 @@ constructor( combine( size, isAuthenticated, - interactor.isCredentialAllowed, + promptSelectorInteractor.isCredentialAllowed, ) { size, authState, credentialAllowed -> size.isNotSmall && authState.isNotAuthenticated && credentialAllowed } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 484be9ce1975..1b2a9ebc9ca4 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -49,6 +49,7 @@ import com.android.systemui.recents.Recents import com.android.systemui.settings.dagger.MultiUserUtilsModule import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.statusbar.ImmersiveModeConfirmation +import com.android.systemui.statusbar.gesture.GesturePointerEventListener import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.phone.KeyguardLiftController import com.android.systemui.statusbar.phone.LockscreenWallpaper @@ -182,6 +183,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(ScreenDecorations::class) abstract fun bindScreenDecorations(sysui: ScreenDecorations): CoreStartable + /** Inject into GesturePointerEventHandler. */ + @Binds + @IntoMap + @ClassKey(GesturePointerEventListener::class) + abstract fun bindGesturePointerEventListener(sysui: GesturePointerEventListener): CoreStartable + /** Inject into SessionTracker. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index f73a602bda63..64c4eec3b98a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -107,6 +107,7 @@ import com.android.systemui.statusbar.notification.row.dagger.NotificationRowCom import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.LetterboxModule; +import com.android.systemui.statusbar.phone.NotificationIconAreaControllerModule; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule; import com.android.systemui.statusbar.policy.HeadsUpManager; @@ -184,6 +185,7 @@ import javax.inject.Named; MediaProjectionModule.class, MediaProjectionTaskSwitcherModule.class, MotionToolModule.class, + NotificationIconAreaControllerModule.class, PeopleHubModule.class, PeopleModule.class, PluginModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 93bdac249bef..6d68eef3a470 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -398,6 +398,10 @@ object Flags { // TODO(b/294588085): Tracking Bug val WIFI_SECONDARY_NETWORKS = releasedFlag("wifi_secondary_networks") + // TODO(b/290676905): Tracking Bug + val NEW_SHADE_CARRIER_GROUP_MOBILE_ICONS = + unreleasedFlag("new_shade_carrier_group_mobile_icons") + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag("ongoing_call_status_bar_chip") @@ -502,21 +506,6 @@ object Flags { val WM_CAPTION_ON_SHELL = sysPropBooleanFlag("persist.wm.debug.caption_on_shell", default = true) - @Keep - @JvmField - val ENABLE_FLING_TO_DISMISS_BUBBLE = - sysPropBooleanFlag("persist.wm.debug.fling_to_dismiss_bubble", default = true) - - @Keep - @JvmField - val ENABLE_FLING_TO_DISMISS_PIP = - sysPropBooleanFlag("persist.wm.debug.fling_to_dismiss_pip", default = true) - - @Keep - @JvmField - val ENABLE_PIP_KEEP_CLEAR_ALGORITHM = - sysPropBooleanFlag("persist.wm.debug.enable_pip_keep_clear_algorithm", default = true) - // TODO(b/256873975): Tracking Bug @JvmField @Keep @@ -538,13 +527,6 @@ object Flags { teamfood = false ) - // TODO(b/198643358): Tracking bug - @Keep - @JvmField - val ENABLE_PIP_SIZE_LARGE_SCREEN = - sysPropBooleanFlag("persist.wm.debug.enable_pip_size_large_screen", default = true) - - // TODO(b/293252410) : Tracking Bug @JvmField val LOCKSCREEN_ENABLE_LANDSCAPE = @@ -628,6 +610,10 @@ object Flags { // TODO(b/251205791): Tracking Bug @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips") + /** TODO(b/295143676): Tracking bug. When enable, captures a screenshot for each display. */ + @JvmField + val MULTI_DISPLAY_SCREENSHOT = unreleasedFlag("multi_display_screenshot") + // 1400 - columbus // TODO(b/254512756): Tracking Bug val QUICK_TAP_IN_PCC = releasedFlag("quick_tap_in_pcc") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/qualifiers/KeyguardRootView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/qualifiers/KeyguardRootView.kt new file mode 100644 index 000000000000..c2d2725f4f06 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/qualifiers/KeyguardRootView.kt @@ -0,0 +1,24 @@ +/* + * 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. + */ + +package com.android.systemui.keyguard.qualifiers + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardRootView diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/LockscreenSceneModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/LockscreenSceneModule.kt new file mode 100644 index 000000000000..c88737e6bd70 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/LockscreenSceneModule.kt @@ -0,0 +1,41 @@ +/* + * 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.keyguard.ui.view + +import android.view.View +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.KeyguardViewConfigurator +import com.android.systemui.keyguard.qualifiers.KeyguardRootView +import dagger.Module +import dagger.Provides +import javax.inject.Provider +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@Module +object LockscreenSceneModule { + + @Provides + @SysUISingleton + @KeyguardRootView + fun viewProvider( + configurator: Provider<KeyguardViewConfigurator>, + ): () -> View { + return { configurator.get().getKeyguardRootView() } + } +} 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 6d3b7f18e974..93c4902332c5 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 @@ -16,41 +16,22 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.R import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import 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 -import kotlinx.coroutines.flow.stateIn /** Models UI state and handles user input for the lockscreen scene. */ @SysUISingleton class LockscreenSceneViewModel @Inject constructor( - @Application applicationScope: CoroutineScope, authenticationInteractor: AuthenticationInteractor, private val bouncerInteractor: BouncerInteractor, ) { - /** The icon for the "lock" button on the lockscreen. */ - val lockButtonIcon: StateFlow<Icon> = - authenticationInteractor.isUnlocked - .map { isUnlocked -> lockIcon(isUnlocked = isUnlocked) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = lockIcon(isUnlocked = authenticationInteractor.isUnlocked.value), - ) - /** The key of the scene we should switch to when swiping up. */ val upDestinationSceneKey: Flow<SceneKey> = authenticationInteractor.isUnlocked.map { isUnlocked -> @@ -65,31 +46,4 @@ constructor( fun onLockButtonClicked() { bouncerInteractor.showOrUnlockDevice() } - - /** Notifies that some content on the lock screen was clicked. */ - fun onContentClicked() { - bouncerInteractor.showOrUnlockDevice() - } - - private fun upDestinationSceneKey( - canSwipeToDismiss: Boolean, - ): SceneKey { - return if (canSwipeToDismiss) SceneKey.Gone else SceneKey.Bouncer - } - - private fun lockIcon( - isUnlocked: Boolean, - ): Icon { - return if (isUnlocked) { - Icon.Resource( - R.drawable.ic_device_lock_off, - ContentDescription.Resource(R.string.accessibility_unlock_button) - ) - } else { - Icon.Resource( - R.drawable.ic_device_lock_on, - ContentDescription.Resource(R.string.accessibility_lock_icon) - ) - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java index 72352e397806..cbd9a34e9a66 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java @@ -54,6 +54,7 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.screenrecord.MediaProjectionPermissionDialog; import com.android.systemui.screenrecord.ScreenShareOption; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -71,6 +72,7 @@ public class MediaProjectionPermissionActivity extends Activity private final FeatureFlags mFeatureFlags; private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver; + private final ActivityStarter mActivityStarter; private String mPackageName; private int mUid; @@ -86,8 +88,10 @@ public class MediaProjectionPermissionActivity extends Activity @Inject public MediaProjectionPermissionActivity(FeatureFlags featureFlags, - Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) { + Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver, + ActivityStarter activityStarter) { mFeatureFlags = featureFlags; + mActivityStarter = activityStarter; mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver; } @@ -306,8 +310,16 @@ public class MediaProjectionPermissionActivity extends Activity // Start activity from the current foreground user to avoid creating a separate // SystemUI process without access to recent tasks because it won't have // WM Shell running inside. + // It is also important to make sure the shade is dismissed, otherwise users won't + // see the app selector. mUserSelectingTask = true; - startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser())); + mActivityStarter.startActivity( + intent, + /* dismissShade= */ true, + /* animationController= */ null, + /* showOverLockscreenWhenLocked= */ false, + UserHandle.of(ActivityManager.getCurrentUser()) + ); } } catch (RemoteException e) { Log.e(TAG, "Error granting projection permission", e); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index a3d1d8c6959d..d88249830739 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -515,11 +515,13 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setOnTouchListener((v, event) -> false); updateIconAreaClickListener((v) -> { if (device.getCurrentVolume() == 0) { + mController.logInteractionUnmuteDevice(device); mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME); mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME); updateUnmutedVolumeIcon(); mIconAreaLayout.setOnTouchListener(((iconV, event) -> false)); } else { + mController.logInteractionMuteDevice(device); mSeekBar.resetVolume(); mController.adjustVolume(device, 0); updateMutedVolumeIcon(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index b431babb9b40..13cd8e3d7ee6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -843,6 +843,14 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mMetricLogger.logInteractionAdjustVolume(device); } + void logInteractionMuteDevice(MediaDevice device) { + mMetricLogger.logInteractionMute(device); + } + + void logInteractionUnmuteDevice(MediaDevice device) { + mMetricLogger.logInteractionUnmute(device); + } + String getPackageName() { return mPackageName; } 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 412d1a3a5013..ffd626abbfe8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java @@ -154,6 +154,38 @@ public class MediaOutputMetricLogger { } /** + * Do the metric logging of muting device. + */ + public void logInteractionMute(MediaDevice source) { + if (DEBUG) { + Log.d(TAG, "logInteraction - Mute"); + } + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__MUTE, + getInteractionDeviceType(source), + getLoggingPackageName(), + source.isSuggestedDevice()); + } + + /** + * Do the metric logging of unmuting device. + */ + public void logInteractionUnmute(MediaDevice source) { + if (DEBUG) { + Log.d(TAG, "logInteraction - Unmute"); + } + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__UNMUTE, + getInteractionDeviceType(source), + getLoggingPackageName(), + source.isSuggestedDevice()); + } + + /** * Do the metric logging of content switching failure. * * @param deviceItemList media item list for device count updating diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index e134f7c10b9b..ae0ab8423a99 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -25,6 +25,7 @@ import static android.app.StatusBarManager.WindowType; import static android.app.StatusBarManager.WindowVisibleState; import static android.app.StatusBarManager.windowStateToString; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; import static android.view.InsetsSource.FLAG_SUPPRESS_SCRIM; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; @@ -1714,10 +1715,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) { final InsetsFrameProvider navBarProvider = - new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars()) - .setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] { - new InsetsFrameProvider.InsetsSizeOverride( - TYPE_INPUT_METHOD, null)}); + new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars()); + if (!ENABLE_HIDE_IME_CAPTION_BAR) { + navBarProvider.setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] { + new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null) + }); + } if (insetsHeight != -1 && !mEdgeBackGestureHandler.isButtonForcedVisible()) { navBarProvider.setInsetsSize(Insets.of(0, 0, 0, insetsHeight)); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 9f45f66d9971..1e82d44e413e 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -87,7 +87,6 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBar; @@ -571,7 +570,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis SysUiState sysUiState, Provider<SceneInteractor> sceneInteractor, UserTracker userTracker, - ScreenLifecycle screenLifecycle, WakefulnessLifecycle wakefulnessLifecycle, UiEventLogger uiEventLogger, DisplayTracker displayTracker, @@ -651,7 +649,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis // Listen for user setup mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); - screenLifecycle.addObserver(mScreenLifecycleObserver); wakefulnessLifecycle.addObserver(mWakefulnessLifecycleObserver); // Connect to the service updateEnabledAndBinding(); @@ -923,60 +920,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } } - private final ScreenLifecycle.Observer mScreenLifecycleObserver = - new ScreenLifecycle.Observer() { - /** - * Notifies the Launcher that screen turned on and ready to use - */ - @Override - public void onScreenTurnedOn() { - try { - if (mOverviewProxy != null) { - mOverviewProxy.onScreenTurnedOn(); - } else { - Log.e(TAG_OPS, - "Failed to get overview proxy for screen turned on event."); - } - } catch (RemoteException e) { - Log.e(TAG_OPS, "Failed to call onScreenTurnedOn()", e); - } - } - - /** - * Notifies the Launcher that screen is starting to turn on. - */ - @Override - public void onScreenTurningOff() { - try { - if (mOverviewProxy != null) { - mOverviewProxy.onScreenTurningOff(); - } else { - Log.e(TAG_OPS, - "Failed to get overview proxy for screen turning off event."); - } - } catch (RemoteException e) { - Log.e(TAG_OPS, "Failed to call onScreenTurningOff()", e); - } - } - - /** - * Notifies the Launcher that screen is starting to turn on. - */ - @Override - public void onScreenTurningOn() { - try { - if (mOverviewProxy != null) { - mOverviewProxy.onScreenTurningOn(); - } else { - Log.e(TAG_OPS, - "Failed to get overview proxy for screen turning on event."); - } - } catch (RemoteException e) { - Log.e(TAG_OPS, "Failed to call onScreenTurningOn()", e); - } - } - }; - private final WakefulnessLifecycle.Observer mWakefulnessLifecycleObserver = new WakefulnessLifecycle.Observer() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 398e64b1981b..714795109454 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene +import com.android.systemui.keyguard.ui.view.LockscreenSceneModule import com.android.systemui.scene.domain.startable.SceneContainerStartableModule import com.android.systemui.scene.shared.model.SceneContainerConfigModule import com.android.systemui.scene.ui.composable.SceneModule @@ -24,6 +25,7 @@ import dagger.Module @Module( includes = [ + LockscreenSceneModule::class, SceneContainerConfigModule::class, SceneContainerStartableModule::class, SceneModule::class, diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt index e8683fbea735..fb99775b6549 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt @@ -63,6 +63,7 @@ class ScreenRecordPermissionDialog( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setDialogTitle(R.string.screenrecord_permission_dialog_title) + setTitle(R.string.screenrecord_title) setStartButtonText(R.string.screenrecord_permission_dialog_continue) setStartButtonOnClickListener { v: View? -> onStartRecordingClicked?.run() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 132cd6115bc7..014093de62bd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -985,6 +985,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Make sure the clock is in the correct position after the unlock animation // so that it's not in the wrong place when we show the keyguard again. positionClockAndNotifications(true /* forceClockUpdate */); + mScrimController.onUnlockAnimationFinished(); } private void unlockAnimationStarted( @@ -1243,6 +1244,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.init(); } mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled); + mKeyguardStatusViewController.getView().addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + int oldHeight = oldBottom - oldTop; + if (v.getHeight() != oldHeight) { + mNotificationStackScrollLayoutController.animateNextTopPaddingChange(); + } + }); updateClockAppearance(); @@ -3198,6 +3206,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } + @Override + public void performHapticFeedback(int constant) { + mVibratorHelper.performHapticFeedback(mView, constant); + } + private class ShadeHeadsUpTrackerImpl implements ShadeHeadsUpTracker { @Override public void addTrackingHeadsUpListener(Consumer<ExpandableNotificationRow> listener) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 832a25bfbc41..798f2d586bab 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -21,8 +21,6 @@ import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.app.StatusBarManager; -import android.media.AudioManager; -import android.media.session.MediaSessionLegacyHelper; import android.os.PowerManager; import android.util.Log; import android.view.GestureDetector; @@ -47,6 +45,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -101,13 +100,21 @@ public class NotificationShadeWindowViewController { private final NotificationInsetsController mNotificationInsetsController; private final boolean mIsTrackpadCommonEnabled; private final FeatureFlags mFeatureFlags; + private final KeyEventInteractor mKeyEventInteractor; private GestureDetector mPulsingWakeupGestureHandler; private GestureDetector mDreamingWakeupGestureHandler; private View mBrightnessMirror; private boolean mTouchActive; private boolean mTouchCancelled; private MotionEvent mDownEvent; + // TODO rename to mLaunchAnimationRunning private boolean mExpandAnimationRunning; + /** + * When mExpandAnimationRunning is true and the touch dispatcher receives a down even after + * uptime exceeds this, the dispatcher will stop blocking touches for the launch animation, + * which has presumabely not completed due to an error. + */ + private long mLaunchAnimationTimeout; private NotificationStackScrollLayout mStackScrollLayout; private PhoneStatusBarViewController mStatusBarViewController; private final CentralSurfaces mService; @@ -164,7 +171,8 @@ public class NotificationShadeWindowViewController { FeatureFlags featureFlags, SystemClock clock, BouncerMessageInteractor bouncerMessageInteractor, - BouncerLogger bouncerLogger) { + BouncerLogger bouncerLogger, + KeyEventInteractor keyEventInteractor) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; mStatusBarStateController = statusBarStateController; @@ -190,6 +198,7 @@ public class NotificationShadeWindowViewController { mNotificationInsetsController = notificationInsetsController; mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON); mFeatureFlags = featureFlags; + mKeyEventInteractor = keyEventInteractor; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -278,7 +287,12 @@ public class NotificationShadeWindowViewController { return logDownDispatch(ev, "touch cancelled", false); } if (mExpandAnimationRunning) { - return logDownDispatch(ev, "expand animation running", false); + if (isDown && mClock.uptimeMillis() > mLaunchAnimationTimeout) { + mShadeLogger.d("NSWVC: launch animation timed out"); + setExpandAnimationRunning(false); + } else { + return logDownDispatch(ev, "expand animation running", false); + } } if (mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) { @@ -457,44 +471,17 @@ public class NotificationShadeWindowViewController { @Override public boolean interceptMediaKey(KeyEvent event) { - return mService.interceptMediaKey(event); + return mKeyEventInteractor.interceptMediaKey(event); } @Override public boolean dispatchKeyEventPreIme(KeyEvent event) { - return mService.dispatchKeyEventPreIme(event); + return mKeyEventInteractor.dispatchKeyEventPreIme(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { - boolean down = event.getAction() == KeyEvent.ACTION_DOWN; - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_BACK: - if (!down) { - mBackActionInteractor.onBackRequested(); - } - return true; - case KeyEvent.KEYCODE_MENU: - if (!down) { - return mService.onMenuPressed(); - } - break; - case KeyEvent.KEYCODE_SPACE: - if (!down) { - return mService.onSpacePressed(); - } - break; - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_UP: - if (mStatusBarStateController.isDozing()) { - MediaSessionLegacyHelper.getHelper(mView.getContext()) - .sendVolumeKeyEvent( - event, AudioManager.USE_DEFAULT_STREAM_TYPE, true); - return true; - } - break; - } - return false; + return mKeyEventInteractor.dispatchKeyEvent(event); } }); @@ -555,8 +542,12 @@ public class NotificationShadeWindowViewController { pw.println(mTouchActive); } - private void setExpandAnimationRunning(boolean running) { + @VisibleForTesting + void setExpandAnimationRunning(boolean running) { if (mExpandAnimationRunning != running) { + if (running) { + mLaunchAnimationTimeout = mClock.uptimeMillis() + 5000; + } mExpandAnimationRunning = running; mNotificationShadeWindowController.setLaunchingActivity(mExpandAnimationRunning); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index d5b5c87ec781..182a676c9841 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -248,6 +248,16 @@ interface ShadeViewController { /** Starts tracking a shade expansion gesture that originated from the status bar. */ fun startTrackingExpansionFromStatusBar() + /** + * Performs haptic feedback from a view with a haptic feedback constant. + * + * The implementation of this method should use the [android.view.View.performHapticFeedback] + * method with the provided constant. + * + * @param[constant] One of [android.view.HapticFeedbackConstants] + */ + fun performHapticFeedback(constant: Int) + // ******* End Keyguard Section ********* /** Returns the ShadeHeadsUpTracker. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt index 287ac528385f..09b74b213ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -86,6 +86,8 @@ class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeViewController { return false } override fun startTrackingExpansionFromStatusBar() {} + override fun performHapticFeedback(constant: Int) {} + override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl() override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java index 8586828af0cd..8612cdf12c6e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java @@ -34,6 +34,7 @@ import com.android.settingslib.Utils; import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView; import com.android.systemui.util.LargeScreenUtils; import java.util.Objects; @@ -44,6 +45,7 @@ public class ShadeCarrier extends LinearLayout { private TextView mCarrierText; private ImageView mMobileSignal; private ImageView mMobileRoaming; + private ModernShadeCarrierGroupMobileView mModernMobileView; private View mSpacer; @Nullable private CellSignalState mLastSignalState; @@ -77,6 +79,23 @@ public class ShadeCarrier extends LinearLayout { updateResources(); } + /** Removes a ModernStatusBarMobileView from the ViewGroup. */ + public void removeModernMobileView() { + if (mModernMobileView != null) { + removeView(mModernMobileView); + mModernMobileView = null; + } + } + + /** Adds a ModernStatusBarMobileView to the ViewGroup. */ + public void addModernMobileView(ModernShadeCarrierGroupMobileView mobileView) { + mModernMobileView = mobileView; + mMobileGroup.setVisibility(View.GONE); + mSpacer.setVisibility(View.GONE); + mCarrierText.setVisibility(View.GONE); + addView(mobileView); + } + /** * Update the state of this view * @param state the current state of the signal for this view diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java index ad49b267c6ac..98d8a53b83c8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java @@ -16,6 +16,7 @@ package com.android.systemui.shade.carrier; +import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX; import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; import android.annotation.MainThread; @@ -46,8 +47,17 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; +import com.android.systemui.statusbar.phone.StatusBarLocation; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel; import com.android.systemui.util.CarrierConfigTracker; +import java.util.List; import java.util.function.Consumer; import javax.inject.Inject; @@ -62,12 +72,16 @@ public class ShadeCarrierGroupController { private final ActivityStarter mActivityStarter; private final Handler mBgHandler; + private final Context mContext; private final NetworkController mNetworkController; private final CarrierTextManager mCarrierTextManager; private final TextView mNoSimTextView; // Non final for testing private H mMainHandler; private final Callback mCallback; + private final MobileIconsViewModel mMobileIconsViewModel; + private final MobileContextProvider mMobileContextProvider; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; private boolean mListening; private final CellSignalState[] mInfos = new CellSignalState[SIM_SLOTS]; @@ -91,7 +105,7 @@ public class ShadeCarrierGroupController { Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex); return; } - if (slotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + if (slotIndex == INVALID_SIM_SLOT_INDEX) { Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId); return; } @@ -129,15 +143,25 @@ public class ShadeCarrierGroupController { } } - private ShadeCarrierGroupController(ShadeCarrierGroup view, ActivityStarter activityStarter, - @Background Handler bgHandler, @Main Looper mainLooper, + private ShadeCarrierGroupController( + ShadeCarrierGroup view, + ActivityStarter activityStarter, + @Background Handler bgHandler, + @Main Looper mainLooper, NetworkController networkController, - CarrierTextManager.Builder carrierTextManagerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver) { - + CarrierTextManager.Builder carrierTextManagerBuilder, + Context context, + CarrierConfigTracker carrierConfigTracker, + SlotIndexResolver slotIndexResolver, + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider, + StatusBarPipelineFlags statusBarPipelineFlags + ) { + mContext = context; mActivityStarter = activityStarter; mBgHandler = bgHandler; mNetworkController = networkController; + mStatusBarPipelineFlags = statusBarPipelineFlags; mCarrierTextManager = carrierTextManagerBuilder .setShowAirplaneMode(false) .setShowMissingSim(false) @@ -162,6 +186,14 @@ public class ShadeCarrierGroupController { mCarrierGroups[1] = view.getCarrier2View(); mCarrierGroups[2] = view.getCarrier3View(); + mMobileContextProvider = mobileContextProvider; + mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); + + if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + mobileUiAdapter.setShadeCarrierGroupController(this); + MobileIconsBinder.bind(view, mMobileIconsViewModel); + } + mCarrierDividers[0] = view.getCarrierDivider1(); mCarrierDividers[1] = view.getCarrierDivider2(); @@ -193,6 +225,50 @@ public class ShadeCarrierGroupController { }); } + /** Updates the number of visible mobile icons using the new pipeline. */ + public void updateModernMobileIcons(List<Integer> subIds) { + if (!mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + Log.d(TAG, "ignoring new pipeline callback because new mobile icon is disabled"); + return; + } + + for (ShadeCarrier carrier : mCarrierGroups) { + carrier.removeModernMobileView(); + } + + List<IconData> iconDataList = processSubIdList(subIds); + + for (IconData iconData : iconDataList) { + ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex]; + + Context mobileContext = + mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext); + ModernShadeCarrierGroupMobileView modernMobileView = ModernShadeCarrierGroupMobileView + .constructAndBind( + mobileContext, + mMobileIconsViewModel.getLogger(), + "mobile_carrier_shade_group", + (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel + .viewModelForSub(iconData.subId, + StatusBarLocation.SHADE_CARRIER_GROUP) + ); + carrier.addModernMobileView(modernMobileView); + } + } + + @VisibleForTesting + List<IconData> processSubIdList(List<Integer> subIds) { + return subIds + .stream() + .limit(SIM_SLOTS) + .map(subId -> new IconData(subId, getSlotIndex(subId))) + .filter(iconData -> + iconData.slotIndex < SIM_SLOTS + && iconData.slotIndex != INVALID_SIM_SLOT_INDEX + ) + .toList(); + } + @VisibleForTesting protected int getSlotIndex(int subscriptionId) { return mSlotIndexResolver.getSlotIndex(subscriptionId); @@ -269,8 +345,12 @@ public class ShadeCarrierGroupController { } } - for (int i = 0; i < SIM_SLOTS; i++) { - mCarrierGroups[i].updateState(mInfos[i], singleCarrier); + if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + Log.d(TAG, "ignoring old pipeline callback because new mobile icon is enabled"); + } else { + for (int i = 0; i < SIM_SLOTS; i++) { + mCarrierGroups[i].updateState(mInfos[i], singleCarrier); + } } mCarrierDividers[0].setVisibility( @@ -306,7 +386,7 @@ public class ShadeCarrierGroupController { Log.w(TAG, "updateInfoCarrier - slot: " + slot); continue; } - if (slot == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + if (slot == INVALID_SIM_SLOT_INDEX) { Log.e(TAG, "Invalid SIM slot index for subscription: " + info.subscriptionIds[i]); @@ -385,12 +465,24 @@ public class ShadeCarrierGroupController { private final Context mContext; private final CarrierConfigTracker mCarrierConfigTracker; private final SlotIndexResolver mSlotIndexResolver; + private final MobileUiAdapter mMobileUiAdapter; + private final MobileContextProvider mMobileContextProvider; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; @Inject - public Builder(ActivityStarter activityStarter, @Background Handler handler, - @Main Looper looper, NetworkController networkController, - CarrierTextManager.Builder carrierTextControllerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver) { + public Builder( + ActivityStarter activityStarter, + @Background Handler handler, + @Main Looper looper, + NetworkController networkController, + CarrierTextManager.Builder carrierTextControllerBuilder, + Context context, + CarrierConfigTracker carrierConfigTracker, + SlotIndexResolver slotIndexResolver, + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider, + StatusBarPipelineFlags statusBarPipelineFlags + ) { mActivityStarter = activityStarter; mHandler = handler; mLooper = looper; @@ -399,6 +491,9 @@ public class ShadeCarrierGroupController { mContext = context; mCarrierConfigTracker = carrierConfigTracker; mSlotIndexResolver = slotIndexResolver; + mMobileUiAdapter = mobileUiAdapter; + mMobileContextProvider = mobileContextProvider; + mStatusBarPipelineFlags = statusBarPipelineFlags; } public Builder setShadeCarrierGroup(ShadeCarrierGroup view) { @@ -407,9 +502,20 @@ public class ShadeCarrierGroupController { } public ShadeCarrierGroupController build() { - return new ShadeCarrierGroupController(mView, mActivityStarter, mHandler, mLooper, - mNetworkController, mCarrierTextControllerBuilder, mContext, - mCarrierConfigTracker, mSlotIndexResolver); + return new ShadeCarrierGroupController( + mView, + mActivityStarter, + mHandler, + mLooper, + mNetworkController, + mCarrierTextControllerBuilder, + mContext, + mCarrierConfigTracker, + mSlotIndexResolver, + mMobileUiAdapter, + mMobileContextProvider, + mStatusBarPipelineFlags + ); } } @@ -448,4 +554,15 @@ public class ShadeCarrierGroupController { return SubscriptionManager.getSlotIndex(subscriptionId); } } + + @VisibleForTesting + static class IconData { + public final int subId; + public final int slotIndex; + + IconData(int subId, int slotIndex) { + this.subId = subId; + this.slotIndex = slotIndex; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt index d3c19b75a71d..5042f1b279e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt @@ -31,56 +31,68 @@ class ActionClickLogger @Inject constructor( ) { fun logInitialClick( entry: NotificationEntry?, + index: Integer?, pendingIntent: PendingIntent ) { buffer.log(TAG, LogLevel.DEBUG, { str1 = entry?.key str2 = entry?.ranking?.channel?.id - str3 = pendingIntent.intent.toString() + str3 = pendingIntent.toString() + int1 = index?.toInt() ?: Int.MIN_VALUE }, { - "ACTION CLICK $str1 (channel=$str2) for pending intent $str3" + "ACTION CLICK $str1 (channel=$str2) for pending intent $str3 at index $int1" }) } fun logRemoteInputWasHandled( - entry: NotificationEntry? + entry: NotificationEntry?, + index: Int? ) { buffer.log(TAG, LogLevel.DEBUG, { str1 = entry?.key + int1 = index ?: Int.MIN_VALUE }, { - " [Action click] Triggered remote input (for $str1))" + " [Action click] Triggered remote input (for $str1) at index $int1" }) } fun logStartingIntentWithDefaultHandler( entry: NotificationEntry?, - pendingIntent: PendingIntent + pendingIntent: PendingIntent, + index: Int? ) { buffer.log(TAG, LogLevel.DEBUG, { str1 = entry?.key - str2 = pendingIntent.intent.toString() + str2 = pendingIntent.toString() + int1 = index ?: Int.MIN_VALUE }, { - " [Action click] Launching intent $str2 via default handler (for $str1)" + " [Action click] Launching intent $str2 via default handler (for $str1 at index $int1)" }) } fun logWaitingToCloseKeyguard( - pendingIntent: PendingIntent + pendingIntent: PendingIntent, + index: Int? ) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = pendingIntent.intent.toString() + str1 = pendingIntent.toString() + int1 = index ?: Int.MIN_VALUE }, { - " [Action click] Intent $str1 launches an activity, dismissing keyguard first..." + " [Action click] Intent $str1 at index $int1 launches an activity, dismissing " + + "keyguard first..." }) } fun logKeyguardGone( - pendingIntent: PendingIntent + pendingIntent: PendingIntent, + index: Int? ) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = pendingIntent.intent.toString() + str1 = pendingIntent.toString() + int1 = index ?: Int.MIN_VALUE }, { - " [Action click] Keyguard dismissed, calling default handler for intent $str1" + " [Action click] Keyguard dismissed, calling default handler for intent $str1 at " + + "index $int1" }) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt index abf81c5c4cb6..692a9977c364 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt @@ -2,10 +2,12 @@ package com.android.systemui.statusbar import android.app.Notification import android.os.RemoteException +import android.util.Log import com.android.internal.statusbar.IStatusBarService import com.android.internal.statusbar.NotificationVisibility import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.util.Assert import java.util.concurrent.Executor import javax.inject.Inject @@ -21,7 +23,8 @@ import javax.inject.Inject @SysUISingleton public class NotificationClickNotifier @Inject constructor( val barService: IStatusBarService, - @Main val mainExecutor: Executor + @Main val mainExecutor: Executor, + @UiBackground val backgroundExecutor: Executor ) { val listeners = mutableListOf<NotificationInteractionListener>() @@ -48,13 +51,14 @@ public class NotificationClickNotifier @Inject constructor( visibility: NotificationVisibility, generatedByAssistant: Boolean ) { - try { - barService.onNotificationActionClick( - key, actionIndex, action, visibility, generatedByAssistant) - } catch (e: RemoteException) { - // nothing + backgroundExecutor.execute { + try { + barService.onNotificationActionClick( + key, actionIndex, action, visibility, generatedByAssistant) + } catch (e: RemoteException) { + // nothing + } } - mainExecutor.execute { notifyListenersAboutInteraction(key) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index da84afef42c5..8089fd94f7db 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -119,11 +119,14 @@ public class NotificationRemoteInputManager implements Dumpable { mPowerInteractor.wakeUpIfDozing( "NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE); + Integer actionIndex = (Integer) + view.getTag(com.android.internal.R.id.notification_action_index_tag); + final NotificationEntry entry = getNotificationForParent(view.getParent()); - mLogger.logInitialClick(entry, pendingIntent); + mLogger.logInitialClick(entry, actionIndex, pendingIntent); if (handleRemoteInput(view, pendingIntent)) { - mLogger.logRemoteInputWasHandled(entry); + mLogger.logRemoteInputWasHandled(entry, actionIndex); return true; } @@ -141,9 +144,9 @@ public class NotificationRemoteInputManager implements Dumpable { } Notification.Action action = getActionFromView(view, entry, pendingIntent); return mCallback.handleRemoteViewClick(view, pendingIntent, - action == null ? false : action.isAuthenticationRequired(), () -> { + action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> { Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view); - mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent); + mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent, actionIndex); boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options); if (started) releaseNotificationIfKeptForRemoteInputHistory(entry); return started; @@ -692,11 +695,13 @@ public class NotificationRemoteInputManager implements Dumpable { * @param view * @param pendingIntent * @param appRequestedAuth + * @param actionIndex * @param defaultHandler * @return true iff the click was handled */ boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, - boolean appRequestedAuth, ClickHandler defaultHandler); + boolean appRequestedAuth, @Nullable Integer actionIndex, + ClickHandler defaultHandler); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index d6a14604ffb0..6dd24ea4062b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -48,8 +48,10 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.internal.logging.UiEventLogger; +import com.android.keyguard.KeyguardClockSwitch; import com.android.systemui.DejankUtils; 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.plugins.statusbar.StatusBarStateController.StateListener; @@ -420,6 +422,25 @@ public class StatusBarStateControllerImpl implements } } + /** Returns the id of the currently rendering clock */ + public String getClockId() { + if (mView == null) { + return KeyguardClockSwitch.MISSING_CLOCK_ID; + } + + View clockSwitch = mView.findViewById(R.id.keyguard_clock_container); + if (clockSwitch == null) { + Log.e(TAG, "Clock container was missing"); + return KeyguardClockSwitch.MISSING_CLOCK_ID; + } + if (!(clockSwitch instanceof KeyguardClockSwitch)) { + Log.e(TAG, "Clock container was incorrect type: " + clockSwitch); + return KeyguardClockSwitch.MISSING_CLOCK_ID; + } + + return ((KeyguardClockSwitch) clockSwitch).getClockId(); + } + private void beginInteractionJankMonitor() { final boolean shouldPost = (mIsDozing && mDozeAmount == 0) || (!mIsDozing && mDozeAmount == 1); @@ -429,6 +450,7 @@ public class StatusBarStateControllerImpl implements Choreographer.CALLBACK_ANIMATION, this::beginInteractionJankMonitor, null); } else { Configuration.Builder builder = Configuration.Builder.withView(getCujType(), mView) + .setTag(getClockId()) .setDeferMonitorForAnimationStart(false); mInteractionJankMonitor.begin(builder); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt new file mode 100644 index 000000000000..b34c3ac5af41 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.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.statusbar.gesture + +import android.content.Context +import android.view.InputEvent +import android.view.MotionEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.settings.DisplayTracker +import javax.inject.Inject + +/** + * A class to detect when a motion event happens. To be notified when the event is detected, add a + * callback via [addOnGestureDetectedCallback]. + */ +@SysUISingleton +class GesturePointerEventDetector @Inject constructor( + private val context: Context, + displayTracker: DisplayTracker +) : GenericGestureDetector( + GesturePointerEventDetector::class.simpleName!!, + displayTracker.defaultDisplayId +) { + override fun onInputEvent(ev: InputEvent) { + if (ev !is MotionEvent) { + return + } + // Pass all events to [gestureDetector], which will then notify [gestureListener] when a tap + // is detected. + onGestureDetected(ev) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt new file mode 100644 index 000000000000..8505c5ff32e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt @@ -0,0 +1,504 @@ +/* + * 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.gesture + +import android.content.Context +import android.graphics.Rect +import android.graphics.Region +import android.hardware.display.DisplayManagerGlobal +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.util.Log +import android.view.DisplayCutout +import android.view.DisplayInfo +import android.view.GestureDetector +import android.view.InputDevice +import android.view.InputEvent +import android.view.MotionEvent +import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT +import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE +import android.view.ViewRootImpl.CLIENT_TRANSIENT +import android.widget.OverScroller +import com.android.internal.R +import com.android.systemui.CoreStartable +import java.io.PrintWriter +import javax.inject.Inject + +/** + * Watches for gesture events that may trigger system bar related events and notify the registered + * callbacks. Add callback to this listener by calling {@link setCallbacks}. + */ +class GesturePointerEventListener +@Inject +constructor(context: Context, gestureDetector: GesturePointerEventDetector) : CoreStartable { + private val mContext: Context + private val mHandler = Handler(Looper.getMainLooper()) + private var mGestureDetector: GesturePointerEventDetector + private var mFlingGestureDetector: GestureDetector? = null + private var mDisplayCutoutTouchableRegionSize = 0 + + // The thresholds for each edge of the display + private val mSwipeStartThreshold = Rect() + private var mSwipeDistanceThreshold = 0 + private var mCallbacks: Callbacks? = null + private val mDownPointerId = IntArray(MAX_TRACKED_POINTERS) + private val mDownX = FloatArray(MAX_TRACKED_POINTERS) + private val mDownY = FloatArray(MAX_TRACKED_POINTERS) + private val mDownTime = LongArray(MAX_TRACKED_POINTERS) + var screenHeight = 0 + var screenWidth = 0 + private var mDownPointers = 0 + private var mSwipeFireable = false + private var mDebugFireable = false + private var mMouseHoveringAtLeft = false + private var mMouseHoveringAtTop = false + private var mMouseHoveringAtRight = false + private var mMouseHoveringAtBottom = false + private var mLastFlingTime: Long = 0 + + init { + mContext = checkNull("context", context) + mGestureDetector = checkNull("gesture detector", gestureDetector) + onConfigurationChanged() + } + + override fun start() { + if (!CLIENT_TRANSIENT) { + return + } + mGestureDetector.addOnGestureDetectedCallback(TAG) { ev -> onInputEvent(ev) } + mGestureDetector.startGestureListening() + + mFlingGestureDetector = + object : GestureDetector(mContext, FlingGestureDetector(), mHandler) {} + } + + fun onDisplayInfoChanged(info: DisplayInfo) { + screenWidth = info.logicalWidth + screenHeight = info.logicalHeight + onConfigurationChanged() + } + + fun onConfigurationChanged() { + if (!CLIENT_TRANSIENT) { + return + } + val r = mContext.resources + val defaultThreshold = r.getDimensionPixelSize(R.dimen.system_gestures_start_threshold) + mSwipeStartThreshold[defaultThreshold, defaultThreshold, defaultThreshold] = + defaultThreshold + mSwipeDistanceThreshold = defaultThreshold + val display = DisplayManagerGlobal.getInstance().getRealDisplay(mContext.displayId) + val displayCutout = display.cutout + if (displayCutout != null) { + // Expand swipe start threshold such that we can catch touches that just start beyond + // the notch area + mDisplayCutoutTouchableRegionSize = + r.getDimensionPixelSize(R.dimen.display_cutout_touchable_region_size) + val bounds = displayCutout.boundingRectsAll + if (bounds[DisplayCutout.BOUNDS_POSITION_LEFT] != null) { + mSwipeStartThreshold.left = + Math.max( + mSwipeStartThreshold.left, + bounds[DisplayCutout.BOUNDS_POSITION_LEFT]!!.width() + + mDisplayCutoutTouchableRegionSize + ) + } + if (bounds[DisplayCutout.BOUNDS_POSITION_TOP] != null) { + mSwipeStartThreshold.top = + Math.max( + mSwipeStartThreshold.top, + bounds[DisplayCutout.BOUNDS_POSITION_TOP]!!.height() + + mDisplayCutoutTouchableRegionSize + ) + } + if (bounds[DisplayCutout.BOUNDS_POSITION_RIGHT] != null) { + mSwipeStartThreshold.right = + Math.max( + mSwipeStartThreshold.right, + bounds[DisplayCutout.BOUNDS_POSITION_RIGHT]!!.width() + + mDisplayCutoutTouchableRegionSize + ) + } + if (bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM] != null) { + mSwipeStartThreshold.bottom = + Math.max( + mSwipeStartThreshold.bottom, + bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM]!!.height() + + mDisplayCutoutTouchableRegionSize + ) + } + } + if (DEBUG) + Log.d( + TAG, + "mSwipeStartThreshold=$mSwipeStartThreshold" + + " mSwipeDistanceThreshold=$mSwipeDistanceThreshold" + ) + } + + fun onInputEvent(ev: InputEvent) { + if (ev !is MotionEvent) { + return + } + if (DEBUG) Log.d(TAG, "Received motion event $ev") + if (ev.isTouchEvent) { + mFlingGestureDetector?.onTouchEvent(ev) + } + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + mSwipeFireable = true + mDebugFireable = true + mDownPointers = 0 + captureDown(ev, 0) + if (mMouseHoveringAtLeft) { + mMouseHoveringAtLeft = false + mCallbacks?.onMouseLeaveFromLeft() + } + if (mMouseHoveringAtTop) { + mMouseHoveringAtTop = false + mCallbacks?.onMouseLeaveFromTop() + } + if (mMouseHoveringAtRight) { + mMouseHoveringAtRight = false + mCallbacks?.onMouseLeaveFromRight() + } + if (mMouseHoveringAtBottom) { + mMouseHoveringAtBottom = false + mCallbacks?.onMouseLeaveFromBottom() + } + mCallbacks?.onDown() + } + MotionEvent.ACTION_POINTER_DOWN -> { + captureDown(ev, ev.actionIndex) + if (mDebugFireable) { + mDebugFireable = ev.pointerCount < 5 + if (!mDebugFireable) { + if (DEBUG) Log.d(TAG, "Firing debug") + mCallbacks?.onDebug() + } + } + } + MotionEvent.ACTION_MOVE -> + if (mSwipeFireable) { + val trackpadSwipe = detectTrackpadThreeFingerSwipe(ev) + mSwipeFireable = trackpadSwipe == TRACKPAD_SWIPE_NONE + if (!mSwipeFireable) { + if (trackpadSwipe == TRACKPAD_SWIPE_FROM_TOP) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop from trackpad") + mCallbacks?.onSwipeFromTop() + } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_BOTTOM) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom from trackpad") + mCallbacks?.onSwipeFromBottom() + } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_RIGHT) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight from trackpad") + mCallbacks?.onSwipeFromRight() + } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_LEFT) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft from trackpad") + mCallbacks?.onSwipeFromLeft() + } + } else { + val swipe = detectSwipe(ev) + mSwipeFireable = swipe == SWIPE_NONE + if (swipe == SWIPE_FROM_TOP) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop") + mCallbacks?.onSwipeFromTop() + } else if (swipe == SWIPE_FROM_BOTTOM) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom") + mCallbacks?.onSwipeFromBottom() + } else if (swipe == SWIPE_FROM_RIGHT) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight") + mCallbacks?.onSwipeFromRight() + } else if (swipe == SWIPE_FROM_LEFT) { + if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft") + mCallbacks?.onSwipeFromLeft() + } + } + } + MotionEvent.ACTION_HOVER_MOVE -> + if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) { + val eventX = ev.x + val eventY = ev.y + if (!mMouseHoveringAtLeft && eventX == 0f) { + mCallbacks?.onMouseHoverAtLeft() + mMouseHoveringAtLeft = true + } else if (mMouseHoveringAtLeft && eventX > 0) { + mCallbacks?.onMouseLeaveFromLeft() + mMouseHoveringAtLeft = false + } + if (!mMouseHoveringAtTop && eventY == 0f) { + mCallbacks?.onMouseHoverAtTop() + mMouseHoveringAtTop = true + } else if (mMouseHoveringAtTop && eventY > 0) { + mCallbacks?.onMouseLeaveFromTop() + mMouseHoveringAtTop = false + } + if (!mMouseHoveringAtRight && eventX >= screenWidth - 1) { + mCallbacks?.onMouseHoverAtRight() + mMouseHoveringAtRight = true + } else if (mMouseHoveringAtRight && eventX < screenWidth - 1) { + mCallbacks?.onMouseLeaveFromRight() + mMouseHoveringAtRight = false + } + if (!mMouseHoveringAtBottom && eventY >= screenHeight - 1) { + mCallbacks?.onMouseHoverAtBottom() + mMouseHoveringAtBottom = true + } else if (mMouseHoveringAtBottom && eventY < screenHeight - 1) { + mCallbacks?.onMouseLeaveFromBottom() + mMouseHoveringAtBottom = false + } + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + mSwipeFireable = false + mDebugFireable = false + mCallbacks?.onUpOrCancel() + } + else -> if (DEBUG) Log.d(TAG, "Ignoring $ev") + } + } + + fun setCallbacks(callbacks: Callbacks) { + mCallbacks = callbacks + } + + private fun captureDown(event: MotionEvent, pointerIndex: Int) { + val pointerId = event.getPointerId(pointerIndex) + val i = findIndex(pointerId) + if (DEBUG) Log.d(TAG, "pointer $pointerId down pointerIndex=$pointerIndex trackingIndex=$i") + if (i != UNTRACKED_POINTER) { + mDownX[i] = event.getX(pointerIndex) + mDownY[i] = event.getY(pointerIndex) + mDownTime[i] = event.eventTime + if (DEBUG) + Log.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i]) + } + } + + protected fun currentGestureStartedInRegion(r: Region): Boolean { + return r.contains(mDownX[0].toInt(), mDownY[0].toInt()) + } + + private fun findIndex(pointerId: Int): Int { + for (i in 0 until mDownPointers) { + if (mDownPointerId[i] == pointerId) { + return i + } + } + if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) { + return UNTRACKED_POINTER + } + mDownPointerId[mDownPointers++] = pointerId + return mDownPointers - 1 + } + + private fun detectTrackpadThreeFingerSwipe(move: MotionEvent): Int { + if (!isTrackpadThreeFingerSwipe(move)) { + return TRACKPAD_SWIPE_NONE + } + val dx = move.x - mDownX[0] + val dy = move.y - mDownY[0] + if (Math.abs(dx) < Math.abs(dy)) { + if (Math.abs(dy) > mSwipeDistanceThreshold) { + return if (dy > 0) TRACKPAD_SWIPE_FROM_TOP else TRACKPAD_SWIPE_FROM_BOTTOM + } + } else { + if (Math.abs(dx) > mSwipeDistanceThreshold) { + return if (dx > 0) TRACKPAD_SWIPE_FROM_LEFT else TRACKPAD_SWIPE_FROM_RIGHT + } + } + return TRACKPAD_SWIPE_NONE + } + + private fun isTrackpadThreeFingerSwipe(event: MotionEvent): Boolean { + return (event.classification == CLASSIFICATION_MULTI_FINGER_SWIPE && + event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3f) + } + private fun detectSwipe(move: MotionEvent): Int { + val historySize = move.historySize + val pointerCount = move.pointerCount + for (p in 0 until pointerCount) { + val pointerId = move.getPointerId(p) + val i = findIndex(pointerId) + if (i != UNTRACKED_POINTER) { + for (h in 0 until historySize) { + val time = move.getHistoricalEventTime(h) + val x = move.getHistoricalX(p, h) + val y = move.getHistoricalY(p, h) + val swipe = detectSwipe(i, time, x, y) + if (swipe != SWIPE_NONE) { + return swipe + } + } + val swipe = detectSwipe(i, move.eventTime, move.getX(p), move.getY(p)) + if (swipe != SWIPE_NONE) { + return swipe + } + } + } + return SWIPE_NONE + } + + private fun detectSwipe(i: Int, time: Long, x: Float, y: Float): Int { + val fromX = mDownX[i] + val fromY = mDownY[i] + val elapsed = time - mDownTime[i] + if (DEBUG) + Log.d( + TAG, + "pointer " + + mDownPointerId[i] + + " moved (" + + fromX + + "->" + + x + + "," + + fromY + + "->" + + y + + ") in " + + elapsed + ) + if ( + fromY <= mSwipeStartThreshold.top && + y > fromY + mSwipeDistanceThreshold && + elapsed < SWIPE_TIMEOUT_MS + ) { + return SWIPE_FROM_TOP + } + if ( + fromY >= screenHeight - mSwipeStartThreshold.bottom && + y < fromY - mSwipeDistanceThreshold && + elapsed < SWIPE_TIMEOUT_MS + ) { + return SWIPE_FROM_BOTTOM + } + if ( + fromX >= screenWidth - mSwipeStartThreshold.right && + x < fromX - mSwipeDistanceThreshold && + elapsed < SWIPE_TIMEOUT_MS + ) { + return SWIPE_FROM_RIGHT + } + return if ( + fromX <= mSwipeStartThreshold.left && + x > fromX + mSwipeDistanceThreshold && + elapsed < SWIPE_TIMEOUT_MS + ) { + SWIPE_FROM_LEFT + } else SWIPE_NONE + } + + fun dump(pw: PrintWriter, prefix: String) { + val inner = "$prefix " + pw.println(prefix + TAG + ":") + pw.print(inner) + pw.print("mDisplayCutoutTouchableRegionSize=") + pw.println(mDisplayCutoutTouchableRegionSize) + pw.print(inner) + pw.print("mSwipeStartThreshold=") + pw.println(mSwipeStartThreshold) + pw.print(inner) + pw.print("mSwipeDistanceThreshold=") + pw.println(mSwipeDistanceThreshold) + } + + private inner class FlingGestureDetector internal constructor() : + GestureDetector.SimpleOnGestureListener() { + private val mOverscroller: OverScroller = OverScroller(mContext) + + override fun onSingleTapUp(e: MotionEvent): Boolean { + if (!mOverscroller.isFinished) { + mOverscroller.forceFinished(true) + } + return true + } + + override fun onFling( + down: MotionEvent?, + up: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + mOverscroller.computeScrollOffset() + val now = SystemClock.uptimeMillis() + if (mLastFlingTime != 0L && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) { + mOverscroller.forceFinished(true) + } + mOverscroller.fling( + 0, + 0, + velocityX.toInt(), + velocityY.toInt(), + Int.MIN_VALUE, + Int.MAX_VALUE, + Int.MIN_VALUE, + Int.MAX_VALUE + ) + var duration = mOverscroller.duration + if (duration > MAX_FLING_TIME_MILLIS) { + duration = MAX_FLING_TIME_MILLIS + } + mLastFlingTime = now + mCallbacks?.onFling(duration) + return true + } + } + + interface Callbacks { + fun onSwipeFromTop() + fun onSwipeFromBottom() + fun onSwipeFromRight() + fun onSwipeFromLeft() + fun onFling(durationMs: Int) + fun onDown() + fun onUpOrCancel() + fun onMouseHoverAtLeft() + fun onMouseHoverAtTop() + fun onMouseHoverAtRight() + fun onMouseHoverAtBottom() + fun onMouseLeaveFromLeft() + fun onMouseLeaveFromTop() + fun onMouseLeaveFromRight() + fun onMouseLeaveFromBottom() + fun onDebug() + } + + companion object { + private const val TAG = "GesturePointerEventHandler" + private const val DEBUG = false + private const val SWIPE_TIMEOUT_MS: Long = 500 + private const val MAX_TRACKED_POINTERS = 32 // max per input system + private const val UNTRACKED_POINTER = -1 + private const val MAX_FLING_TIME_MILLIS = 5000 + private const val SWIPE_NONE = 0 + private const val SWIPE_FROM_TOP = 1 + private const val SWIPE_FROM_BOTTOM = 2 + private const val SWIPE_FROM_RIGHT = 3 + private const val SWIPE_FROM_LEFT = 4 + private const val TRACKPAD_SWIPE_NONE = 0 + private const val TRACKPAD_SWIPE_FROM_TOP = 1 + private const val TRACKPAD_SWIPE_FROM_BOTTOM = 2 + private const val TRACKPAD_SWIPE_FROM_RIGHT = 3 + private const val TRACKPAD_SWIPE_FROM_LEFT = 4 + + private fun <T> checkNull(name: String, arg: T?): T { + requireNotNull(arg) { "$name must not be null" } + return arg + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 62a0d138fd05..5c2f9a8d28ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -39,7 +39,10 @@ class StackCoordinator @Inject internal constructor( override fun attach(pipeline: NotifPipeline) { pipeline.addOnAfterRenderListListener(::onAfterRenderList) - groupExpansionManagerImpl.attach(pipeline) + // TODO(b/282865576): This has an issue where it makes changes to some groups without + // notifying listeners. To be fixed in QPR, but for now let's comment it out to avoid the + // group expansion bug. + // groupExpansionManagerImpl.attach(pipeline) } fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java index 5d33804ab6a7..46af03a438f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java @@ -67,29 +67,18 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl * Cleanup entries from mExpandedGroups that no longer exist in the pipeline. */ private final OnBeforeRenderListListener mNotifTracker = (entries) -> { - if (mExpandedGroups.isEmpty()) { - return; // nothing to do - } - final Set<NotificationEntry> renderingSummaries = new HashSet<>(); for (ListEntry entry : entries) { if (entry instanceof GroupEntry) { renderingSummaries.add(entry.getRepresentativeEntry()); } } - - // Create a copy of mExpandedGroups so we can modify it in a thread-safe way. - final var currentExpandedGroups = new HashSet<>(mExpandedGroups); - for (NotificationEntry entry : currentExpandedGroups) { - setExpanded(entry, renderingSummaries.contains(entry)); - } + mExpandedGroups.removeIf(expandedGroup -> !renderingSummaries.contains(expandedGroup)); }; public void attach(NotifPipeline pipeline) { - if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) { - mDumpManager.registerDumpable(this); - pipeline.addOnBeforeRenderListListener(mNotifTracker); - } + mDumpManager.registerDumpable(this); + pipeline.addOnBeforeRenderListListener(mNotifTracker); } @Override @@ -105,24 +94,11 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public void setGroupExpanded(NotificationEntry entry, boolean expanded) { final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry); - setExpanded(groupSummary, expanded); - } - - /** - * Add or remove {@code entry} to/from {@code mExpandedGroups} and notify listeners if - * something changed. This assumes that {@code entry} is a group summary. - * <p> - * TODO(b/293434635): Currently, in spite of its docs, - * {@code mGroupMembershipManager.getGroupSummary(entry)} returns null if {@code entry} is - * already a summary. Instead of needing this helper method to bypass that, we probably want to - * move this code back to {@code setGroupExpanded} and use that everywhere. - */ - private void setExpanded(NotificationEntry entry, boolean expanded) { boolean changed; if (expanded) { - changed = mExpandedGroups.add(entry); + changed = mExpandedGroups.add(groupSummary); } else { - changed = mExpandedGroups.remove(entry); + changed = mExpandedGroups.remove(groupSummary); } // Only notify listeners if something changed. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt new file mode 100644 index 000000000000..26dfe3edf793 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt @@ -0,0 +1,684 @@ +/* + * 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.notification.icon.ui.viewbinder + +import android.content.Context +import android.graphics.Color +import android.graphics.Rect +import android.os.Bundle +import android.os.Trace +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.ColorInt +import androidx.annotation.VisibleForTesting +import androidx.collection.ArrayMap +import com.android.app.animation.Interpolators +import com.android.internal.statusbar.StatusBarIcon +import com.android.internal.util.ContrastColorUtil +import com.android.settingslib.Utils +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.demomode.DemoMode +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.flags.ViewRefactorFlag +import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.CrossFadeHelper +import com.android.systemui.statusbar.NotificationListener +import com.android.systemui.statusbar.NotificationMediaManager +import com.android.systemui.statusbar.NotificationShelfController +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator +import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel +import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl +import com.android.systemui.statusbar.phone.DozeParameters +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.phone.NotificationIconAreaController +import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.wm.shell.bubbles.Bubbles +import java.util.Optional +import java.util.function.Function +import javax.inject.Inject +import kotlinx.coroutines.DisposableHandle + +/** + * Controller class for [NotificationIconContainer]. This implementation serves as a temporary + * wrapper around [NotificationIconContainerViewBinder], so that external code can continue to + * depend on the [NotificationIconAreaController] interface. Once + * [LegacyNotificationIconAreaControllerImpl] is removed, this class can go away and the ViewBinder + * can be used directly. + */ +@SysUISingleton +class NotificationIconAreaControllerViewBinderWrapperImpl +@Inject +constructor( + private val context: Context, + private val statusBarStateController: StatusBarStateController, + private val wakeUpCoordinator: NotificationWakeUpCoordinator, + private val bypassController: KeyguardBypassController, + private val mediaManager: NotificationMediaManager, + notificationListener: NotificationListener, + private val dozeParameters: DozeParameters, + private val sectionStyleProvider: SectionStyleProvider, + private val bubblesOptional: Optional<Bubbles>, + demoModeController: DemoModeController, + darkIconDispatcher: DarkIconDispatcher, + featureFlags: FeatureFlags, + private val statusBarWindowController: StatusBarWindowController, + private val screenOffAnimationController: ScreenOffAnimationController, + private val shelfIconsViewModel: NotificationIconContainerShelfViewModel, + private val statusBarIconsViewModel: NotificationIconContainerStatusBarViewModel, + private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, +) : + NotificationIconAreaController, + DarkIconDispatcher.DarkReceiver, + StatusBarStateController.StateListener, + NotificationWakeUpCoordinator.WakeUpListener, + DemoMode { + + private val contrastColorUtil: ContrastColorUtil = ContrastColorUtil.getInstance(context) + private val updateStatusBarIcons = Runnable { updateStatusBarIcons() } + private val shelfRefactor = ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR) + private val tintAreas = ArrayList<Rect>() + + private var iconSize = 0 + private var iconHPadding = 0 + private var iconTint = Color.WHITE + private var notificationEntries = listOf<ListEntry>() + private var notificationIconArea: View? = null + private var notificationIcons: NotificationIconContainer? = null + private var shelfIcons: NotificationIconContainer? = null + private var aodIcons: NotificationIconContainer? = null + private var aodBindJob: DisposableHandle? = null + private var aodIconAppearTranslation = 0 + private var animationsEnabled = false + private var aodIconTint = 0 + private var aodIconsVisible = false + private var showLowPriority = true + + @VisibleForTesting + val settingsListener: NotificationListener.NotificationSettingsListener = + object : NotificationListener.NotificationSettingsListener { + override fun onStatusBarIconsBehaviorChanged(hideSilentStatusIcons: Boolean) { + showLowPriority = !hideSilentStatusIcons + updateStatusBarIcons() + } + } + + init { + statusBarStateController.addCallback(this) + wakeUpCoordinator.addListener(this) + demoModeController.addCallback(this) + notificationListener.addNotificationSettingsListener(settingsListener) + initializeNotificationAreaViews(context) + reloadAodColor() + darkIconDispatcher.addDarkReceiver(this) + } + + @VisibleForTesting + fun shouldShowLowPriorityIcons(): Boolean { + return showLowPriority + } + + /** Called by the Keyguard*ViewController whose view contains the aod icons. */ + override fun setupAodIcons(aodIcons: NotificationIconContainer) { + val changed = this.aodIcons != null && aodIcons !== this.aodIcons + if (changed) { + this.aodIcons!!.setAnimationsEnabled(false) + this.aodIcons!!.removeAllViews() + aodBindJob?.dispose() + } + this.aodIcons = aodIcons + this.aodIcons!!.setOnLockScreen(true) + aodBindJob = NotificationIconContainerViewBinder.bind(aodIcons, aodIconsViewModel) + updateAodIconsVisibility(animate = false, forceUpdate = changed) + updateAnimations() + if (changed) { + updateAodNotificationIcons() + } + updateIconLayoutParams(context) + } + + override fun setupShelf(notificationShelfController: NotificationShelfController) = + NotificationShelfViewBinderWrapperControllerImpl.unsupported + + override fun setShelfIcons(icons: NotificationIconContainer) { + if (shelfRefactor.expectEnabled()) { + NotificationIconContainerViewBinder.bind(icons, shelfIconsViewModel) + shelfIcons = icons + } + } + + override fun onDensityOrFontScaleChanged(context: Context) { + updateIconLayoutParams(context) + } + + /** Returns the view that represents the notification area. */ + override fun getNotificationInnerAreaView(): View? { + return notificationIconArea + } + + /** + * See [com.android.systemui.statusbar.policy.DarkIconDispatcher.setIconsDarkArea]. Sets the + * color that should be used to tint any icons in the notification area. + * + * @param tintAreas the areas in which to tint the icons, specified in screen coordinates + * @param darkIntensity + */ + override fun onDarkChanged(tintAreas: ArrayList<Rect>, darkIntensity: Float, iconTint: Int) { + this.tintAreas.clear() + this.tintAreas.addAll(tintAreas) + if (DarkIconDispatcher.isInAreas(tintAreas, notificationIconArea)) { + this.iconTint = iconTint + } + applyNotificationIconsTint() + } + + /** Updates the notifications with the given list of notifications to display. */ + override fun updateNotificationIcons(entries: List<ListEntry>) { + notificationEntries = entries + updateNotificationIcons() + } + + private fun updateStatusBarIcons() { + updateIconsForLayout( + { entry: NotificationEntry -> entry.icons.statusBarIcon }, + notificationIcons, + showAmbient = false /* showAmbient */, + showLowPriority = showLowPriority, + hideDismissed = true /* hideDismissed */, + hideRepliedMessages = true /* hideRepliedMessages */, + hideCurrentMedia = false /* hideCurrentMedia */, + hidePulsing = false /* hidePulsing */ + ) + } + + override fun updateAodNotificationIcons() { + if (aodIcons == null) { + return + } + updateIconsForLayout( + { entry: NotificationEntry -> entry.icons.aodIcon }, + aodIcons, + showAmbient = false /* showAmbient */, + showLowPriority = true /* showLowPriority */, + hideDismissed = true /* hideDismissed */, + hideRepliedMessages = true /* hideRepliedMessages */, + hideCurrentMedia = true /* hideCurrentMedia */, + hidePulsing = bypassController.bypassEnabled /* hidePulsing */ + ) + } + + override fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) { + notificationIcons!!.showIconIsolated(icon, animated) + } + + override fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean) { + notificationIcons!!.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate) + } + + override fun onDozingChanged(isDozing: Boolean) { + if (aodIcons == null) { + return + } + val animate = (dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking) + aodIcons!!.setDozing(isDozing, animate, 0) + } + + override fun setAnimationsEnabled(enabled: Boolean) { + animationsEnabled = enabled + updateAnimations() + } + + override fun onStateChanged(newState: Int) { + updateAodIconsVisibility(animate = false, forceUpdate = false) + updateAnimations() + } + + override fun onThemeChanged() { + reloadAodColor() + updateAodIconColors() + } + + override fun getHeight(): Int { + return if (aodIcons == null) 0 else aodIcons!!.height + } + + @VisibleForTesting + fun appearAodIcons() { + if (aodIcons == null) { + return + } + if (screenOffAnimationController.shouldAnimateAodIcons()) { + aodIcons!!.translationY = -aodIconAppearTranslation.toFloat() + aodIcons!!.alpha = 0f + animateInAodIconTranslation() + aodIcons!! + .animate() + .alpha(1f) + .setInterpolator(Interpolators.LINEAR) + .setDuration(AOD_ICONS_APPEAR_DURATION) + .start() + } else { + aodIcons!!.alpha = 1.0f + aodIcons!!.translationY = 0f + } + } + + override fun onFullyHiddenChanged(isFullyHidden: Boolean) { + var animate = true + if (!bypassController.bypassEnabled) { + animate = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking + // We only want the appear animations to happen when the notifications get fully hidden, + // since otherwise the unhide animation overlaps + animate = animate and isFullyHidden + } + updateAodIconsVisibility(animate, false /* force */) + updateAodNotificationIcons() + updateAodIconColors() + } + + override fun onPulseExpansionChanged(expandingChanged: Boolean) { + if (expandingChanged) { + updateAodIconsVisibility(animate = true, forceUpdate = false) + } + } + + override fun demoCommands(): List<String> { + val commands = ArrayList<String>() + commands.add(DemoMode.COMMAND_NOTIFICATIONS) + return commands + } + + override fun dispatchDemoCommand(command: String, args: Bundle) { + if (notificationIconArea != null) { + val visible = args.getString("visible") + val vis = if ("false" == visible) View.INVISIBLE else View.VISIBLE + notificationIconArea?.visibility = vis + } + } + + override fun onDemoModeFinished() { + if (notificationIconArea != null) { + notificationIconArea?.visibility = View.VISIBLE + } + } + + private fun inflateIconArea(inflater: LayoutInflater): View { + return inflater.inflate(R.layout.notification_icon_area, null) + } + + /** Initializes the views that will represent the notification area. */ + private fun initializeNotificationAreaViews(context: Context) { + reloadDimens(context) + val layoutInflater = LayoutInflater.from(context) + notificationIconArea = inflateIconArea(layoutInflater) + notificationIcons = notificationIconArea?.findViewById(R.id.notificationIcons) + NotificationIconContainerViewBinder.bind(notificationIcons!!, statusBarIconsViewModel) + } + + private fun updateIconLayoutParams(context: Context) { + reloadDimens(context) + val params = generateIconLayoutParams() + for (i in 0 until notificationIcons!!.childCount) { + val child = notificationIcons!!.getChildAt(i) + child.layoutParams = params + } + if (shelfIcons != null) { + for (i in 0 until shelfIcons!!.childCount) { + val child = shelfIcons!!.getChildAt(i) + child.layoutParams = params + } + } + if (aodIcons != null) { + for (i in 0 until aodIcons!!.childCount) { + val child = aodIcons!!.getChildAt(i) + child.layoutParams = params + } + } + } + + private fun generateIconLayoutParams(): FrameLayout.LayoutParams { + return FrameLayout.LayoutParams( + iconSize + 2 * iconHPadding, + statusBarWindowController.statusBarHeight + ) + } + + private fun reloadDimens(context: Context) { + val res = context.resources + iconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size_sp) + iconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin) + aodIconAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation) + } + + private fun shouldShowNotificationIcon( + entry: NotificationEntry, + showAmbient: Boolean, + showLowPriority: Boolean, + hideDismissed: Boolean, + hideRepliedMessages: Boolean, + hideCurrentMedia: Boolean, + hidePulsing: Boolean + ): Boolean { + if (!showAmbient && sectionStyleProvider.isMinimized(entry)) { + return false + } + if (hideCurrentMedia && entry.key == mediaManager.mediaNotificationKey) { + return false + } + if (!showLowPriority && sectionStyleProvider.isSilent(entry)) { + return false + } + if (entry.isRowDismissed && hideDismissed) { + return false + } + if (hideRepliedMessages && entry.isLastMessageFromReply) { + return false + } + // showAmbient == show in shade but not shelf + if (!showAmbient && entry.shouldSuppressStatusBar()) { + return false + } + if ( + hidePulsing && + entry.showingPulsing() && + (!wakeUpCoordinator.notificationsFullyHidden || !entry.isPulseSuppressed) + ) { + return false + } + return if (bubblesOptional.isPresent && bubblesOptional.get().isBubbleExpanded(entry.key)) { + false + } else true + } + + private fun updateNotificationIcons() { + Trace.beginSection("NotificationIconAreaController.updateNotificationIcons") + updateStatusBarIcons() + updateShelfIcons() + updateAodNotificationIcons() + applyNotificationIconsTint() + Trace.endSection() + } + + private fun updateShelfIcons() { + if (shelfIcons == null) { + return + } + updateIconsForLayout( + { entry: NotificationEntry -> entry.icons.shelfIcon }, + shelfIcons, + showAmbient = true, + showLowPriority = true, + hideDismissed = false, + hideRepliedMessages = false, + hideCurrentMedia = false, + hidePulsing = false + ) + } + + /** + * Updates the notification icons for a host layout. This will ensure that the notification host + * layout will have the same icons like the ones in here. + * + * @param function A function to look up an icon view based on an entry + * @param hostLayout which layout should be updated + * @param showAmbient should ambient notification icons be shown + * @param showLowPriority should icons from silent notifications be shown + * @param hideDismissed should dismissed icons be hidden + * @param hideRepliedMessages should messages that have been replied to be hidden + * @param hidePulsing should pulsing notifications be hidden + */ + private fun updateIconsForLayout( + function: Function<NotificationEntry, StatusBarIconView?>, + hostLayout: NotificationIconContainer?, + showAmbient: Boolean, + showLowPriority: Boolean, + hideDismissed: Boolean, + hideRepliedMessages: Boolean, + hideCurrentMedia: Boolean, + hidePulsing: Boolean, + ) { + val toShow = ArrayList<StatusBarIconView>(notificationEntries.size) + // Filter out ambient notifications and notification children. + for (i in notificationEntries.indices) { + val entry = notificationEntries[i].representativeEntry + if (entry != null && entry.row != null) { + if ( + shouldShowNotificationIcon( + entry, + showAmbient, + showLowPriority, + hideDismissed, + hideRepliedMessages, + hideCurrentMedia, + hidePulsing + ) + ) { + val iconView = function.apply(entry) + if (iconView != null) { + toShow.add(iconView) + } + } + } + } + + // In case we are changing the suppression of a group, the replacement shouldn't flicker + // and it should just be replaced instead. We therefore look for notifications that were + // just replaced by the child or vice-versa to suppress this. + val replacingIcons = ArrayMap<String, ArrayList<StatusBarIcon>>() + val toRemove = ArrayList<View>() + for (i in 0 until hostLayout!!.childCount) { + val child = hostLayout.getChildAt(i) as? StatusBarIconView ?: continue + if (!toShow.contains(child)) { + var iconWasReplaced = false + val removedGroupKey = child.notification.groupKey + for (j in toShow.indices) { + val candidate = toShow[j] + if ( + candidate.sourceIcon.sameAs(child.sourceIcon) && + candidate.notification.groupKey == removedGroupKey + ) { + if (!iconWasReplaced) { + iconWasReplaced = true + } else { + iconWasReplaced = false + break + } + } + } + if (iconWasReplaced) { + var statusBarIcons = replacingIcons[removedGroupKey] + if (statusBarIcons == null) { + statusBarIcons = ArrayList() + replacingIcons[removedGroupKey] = statusBarIcons + } + statusBarIcons.add(child.statusBarIcon) + } + toRemove.add(child) + } + } + // removing all duplicates + val duplicates = ArrayList<String?>() + for (key in replacingIcons.keys) { + val statusBarIcons = replacingIcons[key]!! + if (statusBarIcons.size != 1) { + duplicates.add(key) + } + } + replacingIcons.removeAll(duplicates) + hostLayout.setReplacingIcons(replacingIcons) + val toRemoveCount = toRemove.size + for (i in 0 until toRemoveCount) { + hostLayout.removeView(toRemove[i]) + } + val params = generateIconLayoutParams() + for (i in toShow.indices) { + val v = toShow[i] + // The view might still be transiently added if it was just removed and added again + hostLayout.removeTransientView(v) + if (v.parent == null) { + if (hideDismissed) { + v.setOnDismissListener(updateStatusBarIcons) + } + hostLayout.addView(v, i, params) + } + } + hostLayout.setChangingViewPositions(true) + // Re-sort notification icons + val childCount = hostLayout.childCount + for (i in 0 until childCount) { + val actual = hostLayout.getChildAt(i) + val expected = toShow[i] + if (actual === expected) { + continue + } + hostLayout.removeView(expected) + hostLayout.addView(expected, i) + } + hostLayout.setChangingViewPositions(false) + hostLayout.setReplacingIcons(null) + } + + /** Applies [.mIconTint] to the notification icons. */ + private fun applyNotificationIconsTint() { + for (i in 0 until notificationIcons!!.childCount) { + val iv = notificationIcons!!.getChildAt(i) as StatusBarIconView + if (iv.width != 0) { + updateTintForIcon(iv, iconTint) + } else { + iv.executeOnLayout { updateTintForIcon(iv, iconTint) } + } + } + updateAodIconColors() + } + + private fun updateTintForIcon(v: StatusBarIconView, tint: Int) { + val isPreL = java.lang.Boolean.TRUE == v.getTag(R.id.icon_is_pre_L) + var color = StatusBarIconView.NO_COLOR + val colorize = !isPreL || NotificationUtils.isGrayscale(v, contrastColorUtil) + if (colorize) { + color = DarkIconDispatcher.getTint(tintAreas, v, tint) + } + v.staticDrawableColor = color + v.setDecorColor(tint) + } + + private fun updateAnimations() { + val inShade = statusBarStateController.state == StatusBarState.SHADE + if (aodIcons != null) { + aodIcons!!.setAnimationsEnabled(animationsEnabled && !inShade) + } + notificationIcons!!.setAnimationsEnabled(animationsEnabled && inShade) + } + + private fun animateInAodIconTranslation() { + aodIcons!! + .animate() + .setInterpolator(Interpolators.DECELERATE_QUINT) + .translationY(0f) + .setDuration(AOD_ICONS_APPEAR_DURATION) + .start() + } + + private fun reloadAodColor() { + aodIconTint = + Utils.getColorAttrDefaultColor( + context, + R.attr.wallpaperTextColor, + DEFAULT_AOD_ICON_COLOR + ) + } + + private fun updateAodIconColors() { + if (aodIcons != null) { + for (i in 0 until aodIcons!!.childCount) { + val iv = aodIcons!!.getChildAt(i) as StatusBarIconView + if (iv.width != 0) { + updateTintForIcon(iv, aodIconTint) + } else { + iv.executeOnLayout { updateTintForIcon(iv, aodIconTint) } + } + } + } + } + + private fun updateAodIconsVisibility(animate: Boolean, forceUpdate: Boolean) { + if (aodIcons == null) { + return + } + var visible = (bypassController.bypassEnabled || wakeUpCoordinator.notificationsFullyHidden) + + // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off animation is + // playing, in which case we want them to be visible since we're animating in the AOD UI and + // will be switching to KEYGUARD shortly. + if ( + statusBarStateController.state != StatusBarState.KEYGUARD && + !screenOffAnimationController.shouldShowAodIconsWhenShade() + ) { + visible = false + } + if (visible && wakeUpCoordinator.isPulseExpanding() && !bypassController.bypassEnabled) { + visible = false + } + if (aodIconsVisible != visible || forceUpdate) { + aodIconsVisible = visible + aodIcons!!.animate().cancel() + if (animate) { + val wasFullyInvisible = aodIcons!!.visibility != View.VISIBLE + if (aodIconsVisible) { + if (wasFullyInvisible) { + // No fading here, let's just appear the icons instead! + aodIcons!!.visibility = View.VISIBLE + aodIcons!!.alpha = 1.0f + appearAodIcons() + } else { + // Let's make sure the icon are translated to 0, since we cancelled it above + animateInAodIconTranslation() + // We were fading out, let's fade in instead + CrossFadeHelper.fadeIn(aodIcons) + } + } else { + // Let's make sure the icon are translated to 0, since we cancelled it above + animateInAodIconTranslation() + CrossFadeHelper.fadeOut(aodIcons) + } + } else { + aodIcons!!.alpha = 1.0f + aodIcons!!.translationY = 0f + aodIcons!!.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } + } + } + + companion object { + private const val AOD_ICONS_APPEAR_DURATION: Long = 200 + + @ColorInt private val DEFAULT_AOD_ICON_COLOR = -0x1 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt new file mode 100644 index 000000000000..8293bb329a01 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt @@ -0,0 +1,33 @@ +/* + * 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.notification.icon.ui.viewbinder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel +import com.android.systemui.statusbar.phone.NotificationIconContainer +import kotlinx.coroutines.DisposableHandle + +/** Binds a [NotificationIconContainer] to its [view model][NotificationIconContainerViewModel]. */ +object NotificationIconContainerViewBinder { + fun bind( + view: NotificationIconContainer, + viewModel: NotificationIconContainerViewModel, + ): DisposableHandle { + return view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) {} } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt new file mode 100644 index 000000000000..f68b0ef79638 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt @@ -0,0 +1,22 @@ +/* + * 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.notification.icon.ui.viewmodel + +import javax.inject.Inject + +/** View-model for the row of notification icons displayed on the always-on display. */ +class NotificationIconContainerAlwaysOnDisplayViewModel @Inject constructor() : + NotificationIconContainerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt new file mode 100644 index 000000000000..933c76f19aee --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt @@ -0,0 +1,22 @@ +/* + * 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.notification.icon.ui.viewmodel + +import javax.inject.Inject + +/** View-model for the overflow row of notification icons displayed in the notification shade. */ +class NotificationIconContainerShelfViewModel @Inject constructor() : + NotificationIconContainerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt new file mode 100644 index 000000000000..2217646e6022 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt @@ -0,0 +1,22 @@ +/* + * 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.notification.icon.ui.viewmodel + +import javax.inject.Inject + +/** View-model for the row of notification icons displayed in the status bar, */ +class NotificationIconContainerStatusBarViewModel @Inject constructor() : + NotificationIconContainerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt new file mode 100644 index 000000000000..892b2be9ed6e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt @@ -0,0 +1,22 @@ +/* + * 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.notification.icon.ui.viewmodel + +/** + * View-model for the row of notification icons displayed in the NotificationShelf, StatusBar, and + * AOD. + */ +interface NotificationIconContainerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt index 22a87a7c9432..b92c51fac5f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -64,8 +64,10 @@ class NotificationShelfViewBinderWrapperControllerImpl @Inject constructor() : override fun setOnClickListener(listener: View.OnClickListener) = unsupported - private val unsupported: Nothing - get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled") + companion object { + val unsupported: Nothing + get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled") + } } /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 4668aa433533..6db8df91edcc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -28,7 +28,6 @@ import static com.android.systemui.statusbar.notification.stack.NotificationStac import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows; -import static com.android.systemui.statusbar.phone.NotificationIconAreaController.HIGH_PRIORITY; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.res.Configuration; @@ -150,6 +149,7 @@ import javax.inject.Named; public class NotificationStackScrollLayoutController { private static final String TAG = "StackScrollerController"; private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); + private static final String HIGH_PRIORITY = "high_priority"; private final boolean mAllowLongPress; private final NotificationGutsManager mNotificationGutsManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 5c28be3bc678..af09bf281c0c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserHandle; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationAdapter; import android.view.View; @@ -276,19 +275,11 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void userActivity(); - boolean interceptMediaKey(KeyEvent event); - - boolean dispatchKeyEventPreIme(KeyEvent event); - - boolean onMenuPressed(); - void endAffordanceLaunch(); /** Should the keyguard be hidden immediately in response to a back press/gesture. */ boolean shouldKeyguardHideImmediately(); - boolean onSpacePressed(); - void showBouncerWithDimissAndCancelIfKeyguard(OnDismissAction performAction, Runnable cancelAction); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 6431ef958239..2bc7b996ffb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING; @@ -33,12 +34,15 @@ import android.os.VibrationEffect; import android.os.Vibrator; import android.util.Log; import android.util.Slog; +import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; +import androidx.annotation.VisibleForTesting; + import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.LetterboxDetails; @@ -49,6 +53,7 @@ import com.android.systemui.assist.AssistManager; import com.android.systemui.camera.CameraIntents; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSHost; @@ -107,6 +112,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba private final Lazy<CameraLauncher> mCameraLauncherLazy; private final QuickSettingsController mQsController; private final QSHost mQSHost; + private final FeatureFlags mFeatureFlags; private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); @@ -144,7 +150,8 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba Lazy<CameraLauncher> cameraLauncherLazy, UserTracker userTracker, QSHost qsHost, - ActivityStarter activityStarter) { + ActivityStarter activityStarter, + FeatureFlags featureFlags) { mCentralSurfaces = centralSurfaces; mQsController = quickSettingsController; mContext = context; @@ -171,6 +178,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mCameraLauncherLazy = cameraLauncherLazy; mUserTracker = userTracker; mQSHost = qsHost; + mFeatureFlags = featureFlags; mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation); mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect( @@ -314,7 +322,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_DOWN); if (mShadeViewController.isFullyCollapsed()) { if (mVibrateOnOpening) { - mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + vibrateOnNavigationKeyDown(); } mShadeViewController.expand(true /* animate */); mNotificationStackScrollLayoutController.setWillExpand(true); @@ -587,4 +595,15 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba } return VibrationEffect.createWaveform(timings, /* repeat= */ -1); } + + @VisibleForTesting + void vibrateOnNavigationKeyDown() { + if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + mShadeViewController.performHapticFeedback( + HapticFeedbackConstants.GESTURE_START + ); + } else { + mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + } + } } 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 8ffd43a6eb89..ccb87bf44dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -89,7 +89,6 @@ import android.util.MathUtils; import android.view.Display; import android.view.IRemoteAnimationRunner; import android.view.IWindowManager; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ThreadedRenderer; import android.view.View; @@ -2636,44 +2635,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public boolean interceptMediaKey(KeyEvent event) { - return mState == StatusBarState.KEYGUARD - && mStatusBarKeyguardViewManager.interceptMediaKey(event); - } - - /** - * While IME is active and a BACK event is detected, check with - * {@link StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event - * should be handled before routing to IME, in order to prevent the user having to hit back - * twice to exit bouncer. - */ - @Override - public boolean dispatchKeyEventPreIme(KeyEvent event) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_BACK: - if (mState == StatusBarState.KEYGUARD - && mStatusBarKeyguardViewManager.dispatchBackKeyEventPreIme()) { - return mBackActionInteractor.onBackRequested(); - } - } - return false; - } - - protected boolean shouldUnlockOnMenuPressed() { - return mDeviceInteractive && mState != StatusBarState.SHADE - && mStatusBarKeyguardViewManager.shouldDismissOnMenuPressed(); - } - - @Override - public boolean onMenuPressed() { - if (shouldUnlockOnMenuPressed()) { - mShadeController.animateCollapseShadeForced(); - return true; - } - return false; - } - - @Override public void endAffordanceLaunch() { releaseGestureWakeLock(); mCameraLauncherLazy.get().setLaunchingAffordance(false); @@ -2692,15 +2653,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return (isScrimmedBouncer || isBouncerOverDream); } - @Override - public boolean onSpacePressed() { - if (mDeviceInteractive && mState != StatusBarState.SHADE) { - mShadeController.animateCollapseShadeForced(); - return true; - } - return false; - } - private void showBouncerOrLockScreenIfKeyguard() { // If the keyguard is animating away, we aren't really the keyguard anymore and should not // show the bouncer/lockscreen. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java index 0bf0f4b504b0..d22ed3802eed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java @@ -1,3 +1,18 @@ +/* + * 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.content.Context; @@ -43,6 +58,8 @@ import com.android.systemui.statusbar.notification.collection.provider.SectionSt import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.wm.shell.bubbles.Bubbles; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -55,13 +72,13 @@ import javax.inject.Inject; * normally reserved for notifications. */ @SysUISingleton -public class NotificationIconAreaController implements +public class LegacyNotificationIconAreaControllerImpl implements + NotificationIconAreaController, DarkReceiver, StatusBarStateController.StateListener, NotificationWakeUpCoordinator.WakeUpListener, DemoMode { - public static final String HIGH_PRIORITY = "high_priority"; private static final long AOD_ICONS_APPEAR_DURATION = 200; @ColorInt private static final int DEFAULT_AOD_ICON_COLOR = 0xffffffff; @@ -110,7 +127,7 @@ public class NotificationIconAreaController implements }; @Inject - public NotificationIconAreaController( + public LegacyNotificationIconAreaControllerImpl( Context context, StatusBarStateController statusBarStateController, NotificationWakeUpCoordinator wakeUpCoordinator, @@ -192,7 +209,7 @@ public class NotificationIconAreaController implements } } - public void onDensityOrFontScaleChanged(Context context) { + public void onDensityOrFontScaleChanged(@NotNull Context context) { updateIconLayoutParams(context); } @@ -493,7 +510,7 @@ public class NotificationIconAreaController implements mNotificationIcons.showIconIsolated(icon, animated); } - public void setIsolatedIconLocation(Rect iconDrawingRect, boolean requireStateUpdate) { + public void setIsolatedIconLocation(@NotNull Rect iconDrawingRect, boolean requireStateUpdate) { mNotificationIcons.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt new file mode 100644 index 000000000000..0079f7ceb539 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.phone + +import android.content.Context +import android.graphics.Rect +import android.view.View +import com.android.systemui.statusbar.NotificationShelfController +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.notification.collection.ListEntry + +/** + * A controller for the space in the status bar to the left of the system icons. This area is + * normally reserved for notifications. + */ +interface NotificationIconAreaController { + /** Called by the Keyguard*ViewController whose view contains the aod icons. */ + fun setupAodIcons(aodIcons: NotificationIconContainer) + fun setupShelf(notificationShelfController: NotificationShelfController) + fun setShelfIcons(icons: NotificationIconContainer) + fun onDensityOrFontScaleChanged(context: Context) + + /** Returns the view that represents the notification area. */ + fun getNotificationInnerAreaView(): View? + + /** Updates the notifications with the given list of notifications to display. */ + fun updateNotificationIcons(entries: List<@JvmSuppressWildcards ListEntry>) + fun updateAodNotificationIcons() + fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) + fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean) + fun setAnimationsEnabled(enabled: Boolean) + fun onThemeChanged() + fun getHeight(): Int +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt new file mode 100644 index 000000000000..d1ddd51a04c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt @@ -0,0 +1,38 @@ +/* + * 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 com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconAreaControllerViewBinderWrapperImpl +import dagger.Module +import dagger.Provides +import javax.inject.Provider + +@Module +object NotificationIconAreaControllerModule { + @Provides + fun provideNotificationIconAreaControllerImpl( + featureFlags: FeatureFlags, + legacyProvider: Provider<LegacyNotificationIconAreaControllerImpl>, + newProvider: Provider<NotificationIconAreaControllerViewBinderWrapperImpl>, + ): NotificationIconAreaController = + if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) { + newProvider.get() + } else { + legacyProvider.get() + } +} 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 fc661384146c..62a8cfde80da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -709,6 +709,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } } + public void onUnlockAnimationFinished() { + mAnimatingPanelExpansionOnUnlock = false; + applyAndDispatchState(); + } + /** * Set the amount of progress we are currently in if we're transitioning to the full shade. * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt index 5ace22695ec3..32e5c355889a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt @@ -24,4 +24,6 @@ enum class StatusBarLocation { KEYGUARD, /** Quick settings (inside the shade). */ QS, + /** ShadeCarrierGroup (above QS status bar in expanded mode). */ + SHADE_CARRIER_GROUP, } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 7fe01825890f..a6284e3f62ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -32,6 +32,8 @@ import android.os.UserHandle; import android.view.View; import android.view.ViewParent; +import androidx.annotation.Nullable; + import com.android.systemui.ActivityIntentHelper; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; @@ -254,16 +256,16 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, @Override public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, - boolean appRequestedAuth, + boolean appRequestedAuth, @Nullable Integer actionIndex, NotificationRemoteInputManager.ClickHandler defaultHandler) { final boolean isActivity = pendingIntent.isActivity(); if (isActivity || appRequestedAuth) { - mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent); + mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent, actionIndex); final boolean afterKeyguardGone = mActivityIntentHelper .wouldPendingLaunchResolverActivity(pendingIntent, mLockscreenUserManager.getCurrentUserId()); mActivityStarter.dismissKeyguardThenExecute(() -> { - mActionClickLogger.logKeyguardGone(pendingIntent); + mActionClickLogger.logKeyguardGone(pendingIntent, actionIndex); try { ActivityManager.getService().resumeAppSwitches(); 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 1bceb29dc85d..cd1afc727346 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -223,10 +223,14 @@ class UnlockedScreenOffAnimationController @Inject constructor( } .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN), true /* animate */) - interactionJankMonitor.begin( - notifShadeWindowControllerLazy.get().windowRootView, - CUJ_SCREEN_OFF_SHOW_AOD - ) + val builder = InteractionJankMonitor.Configuration.Builder + .withView( + InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD, + notifShadeWindowControllerLazy.get().windowRootView + ) + .setTag(statusBarStateControllerImpl.getClockId()) + + interactionJankMonitor.begin(builder) } override fun onStartedWakingUp() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 6e51ed0eba37..c69577308608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.pipeline import android.content.Context import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.shade.carrier.ShadeCarrierGroup import javax.inject.Inject /** All flagging methods related to the new status bar pipeline (see b/238425913). */ @@ -26,11 +29,19 @@ class StatusBarPipelineFlags @Inject constructor( context: Context, + private val featureFlags: FeatureFlags, ) { private val mobileSlot = context.getString(com.android.internal.R.string.status_bar_mobile) private val wifiSlot = context.getString(com.android.internal.R.string.status_bar_wifi) /** + * True if we should display the mobile icons in the [ShadeCarrierGroup] using the new status + * bar Data pipeline. + */ + fun useNewShadeCarrierGroupMobileIcons(): Boolean = + featureFlags.isEnabled(Flags.NEW_SHADE_CARRIER_GROUP_MOBILE_ICONS) + + /** * For convenience in the StatusBarIconController, we want to gate some actions based on slot * name and the flag together. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt index 78231e28803e..99ed2d99c749 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt @@ -60,6 +60,19 @@ sealed interface NetworkNameModel : Diffable<NetworkNameModel> { } } + /** This name has been derived from SubscriptionModel. see [SubscriptionModel] */ + data class SubscriptionDerived(override val name: String) : NetworkNameModel { + override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) { + if (prevVal !is SubscriptionDerived || prevVal.name != name) { + row.logChange(COL_NETWORK_NAME, "SubscriptionDerived($name)") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_NAME, "SubscriptionDerived($name)") + } + } + /** * This name has been derived from the sim via * [android.telephony.TelephonyManager.getSimOperatorName]. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt index 16c4027ef645..27f6df4c26e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt @@ -34,4 +34,7 @@ data class SubscriptionModel( /** Subscriptions in the same group may be filtered or treated as a single subscription */ val groupUuid: ParcelUuid? = null, + + /** Text representing the name for this connection */ + val carrierName: String, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index c1af6df12bd1..a89b1b2db6b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -115,10 +115,18 @@ interface MobileConnectionRepository { */ val cdmaRoaming: StateFlow<Boolean> - /** The service provider name for this network connection, or the default name */ + /** The service provider name for this network connection, or the default name. */ val networkName: StateFlow<NetworkNameModel> /** + * The service provider name for this network connection, or the default name. + * + * TODO(b/296600321): De-duplicate this field with [networkName] after determining the data + * provided is identical + */ + val carrierName: StateFlow<NetworkNameModel> + + /** * True if this type of connection is allowed while airplane mode is on, and false otherwise. */ val isAllowedDuringAirplaneMode: StateFlow<Boolean> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt index 17d20c297861..c576b822da15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt @@ -184,7 +184,10 @@ class DemoMobileConnectionRepository( override val cdmaRoaming = MutableStateFlow(false) - override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo network")) + override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived(DEMO_CARRIER_NAME)) + + override val carrierName = + MutableStateFlow(NetworkNameModel.SubscriptionDerived(DEMO_CARRIER_NAME)) override val isAllowedDuringAirplaneMode = MutableStateFlow(false) @@ -200,6 +203,7 @@ class DemoMobileConnectionRepository( // This is always true here, because we split out disabled states at the data-source level dataEnabled.value = true networkName.value = NetworkNameModel.IntentDerived(event.name) + carrierName.value = NetworkNameModel.SubscriptionDerived("${event.name} ${event.subId}") _carrierId.value = event.carrierId ?: INVALID_SUBSCRIPTION_ID @@ -227,6 +231,7 @@ class DemoMobileConnectionRepository( // This is always true here, because we split out disabled states at the data-source level dataEnabled.value = true networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME) + carrierName.value = NetworkNameModel.SubscriptionDerived(CARRIER_MERGED_NAME) // TODO(b/276943904): is carrierId a thing with carrier merged networks? _carrierId.value = INVALID_SUBSCRIPTION_ID numberOfLevels.value = event.numberOfLevels @@ -248,6 +253,7 @@ class DemoMobileConnectionRepository( } companion object { + private const val DEMO_CARRIER_NAME = "Demo Carrier" private const val CARRIER_MERGED_NAME = "Carrier Merged Network" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index 0e4ceebcc854..ee13d93e735d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -92,9 +92,12 @@ constructor( private fun maybeCreateSubscription(subId: Int) { if (!subscriptionInfoCache.containsKey(subId)) { - SubscriptionModel(subscriptionId = subId, isOpportunistic = false).also { - subscriptionInfoCache[subId] = it - } + SubscriptionModel( + subscriptionId = subId, + isOpportunistic = false, + carrierName = DEFAULT_CARRIER_NAME, + ) + .also { subscriptionInfoCache[subId] = it } _subscriptions.value = subscriptionInfoCache.values.toList() } @@ -327,6 +330,7 @@ constructor( private const val TAG = "DemoMobileConnectionsRepo" private const val DEFAULT_SUB_ID = 1 + private const val DEFAULT_CARRIER_NAME = "demo carrier" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt index 65f486683837..28be3be28928 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -108,6 +108,8 @@ class CarrierMergedConnectionRepository( NetworkNameModel.SimDerived(telephonyManager.simOperatorName), ) + override val carrierName: StateFlow<NetworkNameModel> = networkName + override val numberOfLevels: StateFlow<Int> = wifiRepository.wifiNetwork .map { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index 8ba7d2197c14..ee11c06ef3f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -22,6 +22,7 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -47,6 +48,7 @@ class FullMobileConnectionRepository( override val subId: Int, startingIsCarrierMerged: Boolean, override val tableLogBuffer: TableLogBuffer, + subscriptionModel: StateFlow<SubscriptionModel?>, private val defaultNetworkName: NetworkNameModel, private val networkNameSeparator: String, @Application scope: CoroutineScope, @@ -80,6 +82,7 @@ class FullMobileConnectionRepository( mobileRepoFactory.build( subId, tableLogBuffer, + subscriptionModel, defaultNetworkName, networkNameSeparator, ) @@ -287,6 +290,16 @@ class FullMobileConnectionRepository( ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value) + override val carrierName = + activeRepo + .flatMapLatest { it.carrierName } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + initialValue = activeRepo.value.carrierName.value, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.carrierName.value) + override val isAllowedDuringAirplaneMode = activeRepo .flatMapLatest { it.isAllowedDuringAirplaneMode } @@ -307,6 +320,7 @@ class FullMobileConnectionRepository( fun build( subId: Int, startingIsCarrierMerged: Boolean, + subscriptionModel: StateFlow<SubscriptionModel?>, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, ): FullMobileConnectionRepository { @@ -317,6 +331,7 @@ class FullMobileConnectionRepository( subId, startingIsCarrierMerged, mobileLogger, + subscriptionModel, defaultNetworkName, networkNameSeparator, scope, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index aadc975a10de..1f1ac92b3956 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameMode import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel @@ -80,6 +81,7 @@ import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( override val subId: Int, + subscriptionModel: StateFlow<SubscriptionModel?>, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, private val telephonyManager: TelephonyManager, @@ -281,6 +283,14 @@ class MobileConnectionRepositoryImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) + override val carrierName = + subscriptionModel + .map { + it?.let { model -> NetworkNameModel.SubscriptionDerived(model.carrierName) } + ?: defaultNetworkName + } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) + /** * There are a few cases where we will need to poll [TelephonyManager] so we can update some * internal state where callbacks aren't provided. Any of those events should be merged into @@ -350,11 +360,13 @@ class MobileConnectionRepositoryImpl( fun build( subId: Int, mobileLogger: TableLogBuffer, + subscriptionModel: StateFlow<SubscriptionModel?>, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, ): MobileConnectionRepository { return MobileConnectionRepositoryImpl( subId, + subscriptionModel, defaultNetworkName, networkNameSeparator, telephonyManager.createForSubscriptionId(subId), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 54948a4a41c8..67b04db64463 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -319,10 +319,17 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache + private fun subscriptionModelForSubId(subId: Int): StateFlow<SubscriptionModel?> { + return subscriptions + .map { list -> list.firstOrNull { model -> model.subscriptionId == subId } } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + } + private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository { return fullMobileRepoFactory.build( subId, isCarrierMerged(subId), + subscriptionModelForSubId(subId), defaultNetworkName, networkNameSeparator, ) @@ -373,6 +380,7 @@ constructor( subscriptionId = subscriptionId, isOpportunistic = isOpportunistic, groupUuid = groupUuid, + carrierName = carrierName.toString(), ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 1a138272d67c..4cfde5bd5622 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -92,6 +92,22 @@ interface MobileIconInteractor { */ val networkName: StateFlow<NetworkNameModel> + /** + * Provider name for this network connection. The name can be one of 3 values: + * 1. The default network name, if one is configured + * 2. A name provided by the [SubscriptionModel] of this network connection + * 3. Or, in the case where the repository sends us the default network name, we check for an + * override in [connectionInfo.operatorAlphaShort], a value that is derived from + * [ServiceState] + * + * TODO(b/296600321): De-duplicate this field with [networkName] after determining the data + * provided is identical + */ + val carrierName: StateFlow<String> + + /** True if there is only one active subscription. */ + val isSingleCarrier: StateFlow<Boolean> + /** True if this line of service is emergency-only */ val isEmergencyOnly: StateFlow<Boolean> @@ -126,6 +142,7 @@ class MobileIconInteractorImpl( defaultSubscriptionHasDataEnabled: StateFlow<Boolean>, override val alwaysShowDataRatIcon: StateFlow<Boolean>, override val alwaysUseCdmaLevel: StateFlow<Boolean>, + override val isSingleCarrier: StateFlow<Boolean>, override val mobileIsDefault: StateFlow<Boolean>, defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>, defaultMobileIconGroup: StateFlow<MobileIconGroup>, @@ -171,6 +188,22 @@ class MobileIconInteractorImpl( connectionRepository.networkName.value ) + override val carrierName = + combine(connectionRepository.operatorAlphaShort, connectionRepository.carrierName) { + operatorAlphaShort, + networkName -> + if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) { + operatorAlphaShort + } else { + networkName.name + } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + connectionRepository.carrierName.value.name + ) + /** What the mobile icon would be before carrierId overrides */ private val defaultNetworkType: StateFlow<MobileIconGroup> = combine( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index e90f40c74cc5..d08808b65399 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -76,6 +76,9 @@ interface MobileIconsInteractor { /** True if the CDMA level should be preferred over the primary level. */ val alwaysUseCdmaLevel: StateFlow<Boolean> + /** True if there is only one active subscription. */ + val isSingleCarrier: StateFlow<Boolean> + /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> @@ -252,6 +255,17 @@ constructor( .mapLatest { it.alwaysShowCdmaRssi } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isSingleCarrier: StateFlow<Boolean> = + mobileConnectionsRepo.subscriptions + .map { it.size == 1 } + .logDiffsForTable( + tableLogger, + columnPrefix = LOGGING_PREFIX, + columnName = "isSingleCarrier", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = mobileConnectionsRepo.defaultMobileIconGroup.stateIn( @@ -298,6 +312,7 @@ constructor( activeDataConnectionHasDataEnabled, alwaysShowDataRatIcon, alwaysUseCdmaLevel, + isSingleCarrier, mobileIsDefault, defaultMobileIconMapping, defaultMobileIconGroup, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index d7fcf4876c28..02e50a007f50 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.carrier.ShadeCarrierGroupController import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel @@ -49,6 +50,8 @@ constructor( private var isCollecting: Boolean = false private var lastValue: List<Int>? = null + private var shadeCarrierGroupController: ShadeCarrierGroupController? = null + override fun start() { // Start notifying the icon controller of subscriptions scope.launch { @@ -57,10 +60,16 @@ constructor( logger.logUiAdapterSubIdsSentToIconController(it) lastValue = it iconController.setNewMobileIconSubIds(it) + shadeCarrierGroupController?.updateModernMobileIcons(it) } } } + /** Set the [ShadeCarrierGroupController] to notify of subscription updates */ + fun setShadeCarrierGroupController(controller: ShadeCarrierGroupController) { + shadeCarrierGroupController = controller + } + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println("isCollecting=$isCollecting") pw.println("Last values sent to icon controller: $lastValue") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt index cea6654a48de..2af6795b39c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt @@ -57,7 +57,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) @@ -71,7 +71,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) @@ -85,7 +85,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 55bc8d58be23..4b2fb4373957 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -50,6 +50,7 @@ object MobileIconBinder { fun bind( view: ViewGroup, viewModel: LocationBasedMobileViewModel, + @StatusBarIconView.VisibleState initialVisibilityState: Int = STATE_HIDDEN, logger: MobileViewLogger, ): ModernStatusBarViewBinding { val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group) @@ -68,12 +69,12 @@ object MobileIconBinder { // TODO(b/238425913): We should log this visibility state. @StatusBarIconView.VisibleState - val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) + val visibilityState: MutableStateFlow<Int> = MutableStateFlow(initialVisibilityState) val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) - var isCollecting: Boolean = false + var isCollecting = false view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt new file mode 100644 index 000000000000..081e1015e26e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.binder + +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.util.AutoMarqueeTextView +import kotlinx.coroutines.launch + +object ShadeCarrierBinder { + /** Binds the view to the view-model, continuing to update the former based on the latter */ + @JvmStatic + fun bind( + carrierTextView: AutoMarqueeTextView, + viewModel: ShadeCarrierGroupMobileIconViewModel, + ) { + carrierTextView.isVisible = true + + carrierTextView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { viewModel.carrierName.collect { carrierTextView.text = it } } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt new file mode 100644 index 000000000000..f407127f3161 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt @@ -0,0 +1,77 @@ +/* + * 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.pipeline.mobile.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.util.AutoMarqueeTextView + +/** + * ViewGroup containing a mobile carrier name and icon in the Shade Header. Can be multiple + * instances as children under [ShadeCarrierGroup] + */ +class ModernShadeCarrierGroupMobileView( + context: Context, + attrs: AttributeSet?, +) : LinearLayout(context, attrs) { + + var subId: Int = -1 + + override fun toString(): String { + return "ModernShadeCarrierGroupMobileView(" + + "subId=$subId, " + + "viewString=${super.toString()}" + } + + companion object { + + /** + * Inflates a new instance of [ModernShadeCarrierGroupMobileView], binds it to [viewModel], + * and returns it. + */ + @JvmStatic + fun constructAndBind( + context: Context, + logger: MobileViewLogger, + slot: String, + viewModel: ShadeCarrierGroupMobileIconViewModel, + ): ModernShadeCarrierGroupMobileView { + return (LayoutInflater.from(context).inflate(R.layout.shade_carrier_new, null) + as ModernShadeCarrierGroupMobileView) + .also { + it.subId = viewModel.subscriptionId + + val iconView = it.requireViewById<ModernStatusBarMobileView>(R.id.mobile_combo) + iconView.initView(slot) { + MobileIconBinder.bind(iconView, viewModel, STATE_ICON, logger) + } + logger.logNewViewBinding(it, viewModel) + + val textView = it.requireViewById<AutoMarqueeTextView>(R.id.mobile_carrier_text) + ShadeCarrierBinder.bind(textView, viewModel) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index 4144293d5ccd..68d02de51dc9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -60,7 +60,9 @@ class ModernStatusBarMobileView( as ModernStatusBarMobileView) .also { it.subId = viewModel.subscriptionId - it.initView(slot) { MobileIconBinder.bind(it, viewModel, logger) } + it.initView(slot) { + MobileIconBinder.bind(view = it, viewModel = viewModel, logger = logger) + } logger.logNewViewBinding(it, viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt index a51982c41255..e7c311d7ec74 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -18,7 +18,13 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn /** * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This @@ -26,12 +32,12 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger * * @param commonImpl for convenience, this class wraps a base interface that can provides all of the * common implementations between locations. See [MobileIconViewModel] - * @property locationName the name of the location of this VM, used for logging. + * @property location the [StatusBarLocation] of this VM. * @property verboseLogger an optional logger to log extremely verbose view updates. */ abstract class LocationBasedMobileViewModel( val commonImpl: MobileIconViewModelCommon, - val locationName: String, + val location: StatusBarLocation, val verboseLogger: VerboseMobileViewLogger?, ) : MobileIconViewModelCommon by commonImpl { val defaultColor: Int = Color.WHITE @@ -39,10 +45,12 @@ abstract class LocationBasedMobileViewModel( companion object { fun viewModelForLocation( commonImpl: MobileIconViewModelCommon, + interactor: MobileIconInteractor, verboseMobileViewLogger: VerboseMobileViewLogger, - loc: StatusBarLocation, + location: StatusBarLocation, + scope: CoroutineScope, ): LocationBasedMobileViewModel = - when (loc) { + when (location) { StatusBarLocation.HOME -> HomeMobileIconViewModel( commonImpl, @@ -50,6 +58,12 @@ abstract class LocationBasedMobileViewModel( ) StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl) StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl) + StatusBarLocation.SHADE_CARRIER_GROUP -> + ShadeCarrierGroupMobileIconViewModel( + commonImpl, + interactor, + scope, + ) } } } @@ -61,7 +75,7 @@ class HomeMobileIconViewModel( MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "Home", + location = StatusBarLocation.HOME, verboseMobileViewLogger, ) @@ -71,18 +85,40 @@ class QsMobileIconViewModel( MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "QS", + location = StatusBarLocation.QS, // Only do verbose logging for the Home location. verboseLogger = null, ) +class ShadeCarrierGroupMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + interactor: MobileIconInteractor, + scope: CoroutineScope, +) : + MobileIconViewModelCommon, + LocationBasedMobileViewModel( + commonImpl, + location = StatusBarLocation.SHADE_CARRIER_GROUP, + // Only do verbose logging for the Home location. + verboseLogger = null, + ) { + private val isSingleCarrier = interactor.isSingleCarrier + val carrierName = interactor.carrierName + + override val isVisible: StateFlow<Boolean> = + combine(super.isVisible, isSingleCarrier) { isVisible, isSingleCarrier -> + if (isSingleCarrier) false else isVisible + } + .stateIn(scope, SharingStarted.WhileSubscribed(), super.isVisible.value) +} + class KeyguardMobileIconViewModel( commonImpl: MobileIconViewModelCommon, ) : MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "Keyguard", + location = StatusBarLocation.KEYGUARD, // Only do verbose logging for the Home location. verboseLogger = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 5cf887e4d41a..216afb987f91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger @@ -58,6 +59,8 @@ constructor( private val statusBarPipelineFlags: StatusBarPipelineFlags, ) { @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>() + @VisibleForTesting + val mobileIconInteractorSubIdCache = mutableMapOf<Int, MobileIconInteractor>() val subscriptionIdsFlow: StateFlow<List<Int>> = interactor.filteredSubscriptions @@ -91,15 +94,17 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), false) init { - scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } } + scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } } fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { val common = commonViewModelForSub(subId) return LocationBasedMobileViewModel.viewModelForLocation( common, + mobileIconInteractorForSub(subId), verboseLogger, location, + scope, ) } @@ -107,7 +112,7 @@ constructor( return mobileIconSubIdCache[subId] ?: MobileIconViewModel( subId, - interactor.createMobileConnectionInteractorForSubId(subId), + mobileIconInteractorForSub(subId), airplaneModeInteractor, constants, scope, @@ -115,8 +120,20 @@ constructor( .also { mobileIconSubIdCache[subId] = it } } - private fun removeInvalidModelsFromCache(subIds: List<Int>) { + @VisibleForTesting + fun mobileIconInteractorForSub(subId: Int): MobileIconInteractor { + return mobileIconInteractorSubIdCache[subId] + ?: interactor.createMobileConnectionInteractorForSubId(subId).also { + mobileIconInteractorSubIdCache[subId] = it + } + } + + private fun invalidateCaches(subIds: List<Int>) { val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) } subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) } + + mobileIconInteractorSubIdCache.keys + .filter { !subIds.contains(it) } + .forEach { subId -> mobileIconInteractorSubIdCache.remove(subId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index cd5b92cf24c7..00bd616e04ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation +import java.lang.IllegalArgumentException /** * A view model for a wifi icon in a specific location. This allows us to control parameters that @@ -43,6 +44,8 @@ abstract class LocationBasedWifiViewModel( StatusBarLocation.HOME -> HomeWifiViewModel(commonImpl) StatusBarLocation.KEYGUARD -> KeyguardWifiViewModel(commonImpl) StatusBarLocation.QS -> QsWifiViewModel(commonImpl) + StatusBarLocation.SHADE_CARRIER_GROUP -> + throw IllegalArgumentException("invalid location for WifiViewModel: $location") } } } @@ -64,3 +67,11 @@ class KeyguardWifiViewModel( class QsWifiViewModel( commonImpl: WifiViewModelCommon, ) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl) + +/** + * A view model for the wifi icon in the shade carrier group (visible when quick settings is fully + * expanded, and in large screen shade). Currently unused. + */ +class ShadeCarrierGroupWifiViewModel( + commonImpl: WifiViewModelCommon, +) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl) diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 091a54fe9801..316b54eb0c80 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -109,7 +109,6 @@ public class ImageWallpaper extends WallpaperService { private WallpaperManager mWallpaperManager; private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor; private SurfaceHolder mSurfaceHolder; - private boolean mDrawn = false; @VisibleForTesting static final int MIN_SURFACE_WIDTH = 128; @VisibleForTesting @@ -239,7 +238,6 @@ public class ImageWallpaper extends WallpaperService { private void drawFrameSynchronized() { synchronized (mLock) { - if (mDrawn) return; drawFrameInternal(); } } @@ -277,7 +275,6 @@ public class ImageWallpaper extends WallpaperService { Rect dest = mSurfaceHolder.getSurfaceFrame(); try { canvas.drawBitmap(bitmap, null, dest, null); - mDrawn = true; } finally { surface.unlockCanvasAndPost(canvas); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index a9aed2ffd277..b5317fa6c014 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -620,6 +620,51 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { configurationListenerArgumentCaptor.value.onUiModeChanged() verify(view).reloadColors() } + @Test + fun onOrientationChanged_landscapeKeyguardFlagDisabled_blockReinflate() { + featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) + + // Run onOrientationChanged + val configurationListenerArgumentCaptor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) + underTest.onViewAttached() + verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) + clearInvocations(viewFlipperController) + configurationListenerArgumentCaptor.value.onOrientationChanged( + Configuration.ORIENTATION_LANDSCAPE + ) + // Verify view is reinflated when flag is on + verify(viewFlipperController, never()).clearViews() + verify(viewFlipperController, never()) + .asynchronouslyInflateView( + eq(SecurityMode.PIN), + any(), + onViewInflatedCallbackArgumentCaptor.capture() + ) + } + + @Test + fun onOrientationChanged_landscapeKeyguardFlagEnabled_doesReinflate() { + featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, true) + + // Run onOrientationChanged + val configurationListenerArgumentCaptor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) + underTest.onViewAttached() + verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) + clearInvocations(viewFlipperController) + configurationListenerArgumentCaptor.value.onOrientationChanged( + Configuration.ORIENTATION_LANDSCAPE + ) + // Verify view is reinflated when flag is on + verify(viewFlipperController).clearViews() + verify(viewFlipperController) + .asynchronouslyInflateView( + eq(SecurityMode.PIN), + any(), + onViewInflatedCallbackArgumentCaptor.capture() + ) + } @Test fun hasDismissActions() { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index 7d23c800321a..b8b0198f94df 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -77,7 +77,7 @@ public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControll public void updatePosition_primaryClockAnimation() { ClockController mockClock = mock(ClockController.class); when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock); - when(mockClock.getConfig()).thenReturn(new ClockConfig(false, true)); + when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", false, true)); mController.updatePosition(10, 15, 20f, true); @@ -92,7 +92,7 @@ public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControll public void updatePosition_alternateClockAnimation() { ClockController mockClock = mock(ClockController.class); when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock); - when(mockClock.getConfig()).thenReturn(new ClockConfig(true, true)); + when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", true, true)); mController.updatePosition(10, 15, 20f, true); 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 4e52e64a8af1..9584d888b01f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -39,13 +39,13 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FakePromptRepository import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl -import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.flags.FakeFeatureFlags @@ -109,6 +109,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val biometricPromptRepository = FakePromptRepository() + private val fingerprintRepository = FakeFingerprintPropertyRepository() private val rearDisplayStateRepository = FakeRearDisplayStateRepository() private val credentialInteractor = FakeCredentialInteractor() private val bpCredentialInteractor = PromptCredentialInteractor( @@ -118,10 +119,12 @@ open class AuthContainerViewTest : SysuiTestCase() { ) private val promptSelectorInteractor by lazy { PromptSelectorInteractorImpl( + fingerprintRepository, biometricPromptRepository, lockPatternUtils, ) } + private val displayStateInteractor = DisplayStateInteractorImpl( testScope.backgroundScope, mContext, @@ -129,9 +132,7 @@ open class AuthContainerViewTest : SysuiTestCase() { rearDisplayStateRepository ) - private val authBiometricFingerprintViewModel = AuthBiometricFingerprintViewModel( - displayStateInteractor - ) + private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) private var authContainer: TestAuthContainerView? = null @@ -524,10 +525,14 @@ open class AuthContainerViewTest : SysuiTestCase() { userManager, lockPatternUtils, interactionJankMonitor, - { authBiometricFingerprintViewModel }, { promptSelectorInteractor }, { bpCredentialInteractor }, - PromptViewModel(promptSelectorInteractor, vibrator, featureFlags), + PromptViewModel( + displayStateInteractor, + promptSelectorInteractor, + vibrator, + featureFlags + ), { credentialViewModel }, Handler(TestableLooper.get(this).looper), fakeExecutor, 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 48e513140c3a..6d71dd5cd8ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -92,7 +92,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.domain.interactor.LogContextInteractor; import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor; import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor; -import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel; import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel; import com.android.systemui.flags.FakeFeatureFlags; @@ -177,8 +176,6 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private PromptSelectorInteractor mPromptSelectionInteractor; @Mock - private AuthBiometricFingerprintViewModel mAuthBiometricFingerprintViewModel; - @Mock private CredentialViewModel mCredentialViewModel; @Mock private PromptViewModel mPromptViewModel; @@ -1095,11 +1092,10 @@ public class AuthControllerTest extends SysuiTestCase { mFingerprintManager, mFaceManager, () -> mUdfpsController, () -> mSideFpsController, mDisplayManager, mWakefulnessLifecycle, mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger, - mLogContextInteractor, () -> mAuthBiometricFingerprintViewModel, - () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor, - () -> mCredentialViewModel, () -> mPromptViewModel, - mInteractionJankMonitor, mHandler, - mBackgroundExecutor, mUdfpsUtils, mVibratorHelper); + mLogContextInteractor, () -> mBiometricPromptCredentialInteractor, + () -> mPromptSelectionInteractor, () -> mCredentialViewModel, + () -> mPromptViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor, + mUdfpsUtils, mVibratorHelper); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index 81cbaeab2a32..4d5e1b7de60f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.Utils +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FakePromptRepository import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.faceSensorPropertiesInternal @@ -61,13 +62,15 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils private val testScope = TestScope() + private val fingerprintRepository = FakeFingerprintPropertyRepository() private val promptRepository = FakePromptRepository() private lateinit var interactor: PromptSelectorInteractor @Before fun setup() { - interactor = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils) + interactor = + PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt index 0c210e51cb90..7697c098ed3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt @@ -2,10 +2,17 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.res.Configuration import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.data.repository.FakePromptRepository import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl +import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor +import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock @@ -16,38 +23,52 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) -class AuthBiometricFingerprintViewModelTest : SysuiTestCase() { +class PromptFingerprintIconViewModelTest : SysuiTestCase() { + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + + private val fingerprintRepository = FakeFingerprintPropertyRepository() + private val promptRepository = FakePromptRepository() private val rearDisplayStateRepository = FakeRearDisplayStateRepository() + private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) - private lateinit var interactor: DisplayStateInteractor - private lateinit var viewModel: AuthBiometricFingerprintViewModel + private lateinit var promptSelectorInteractor: PromptSelectorInteractor + private lateinit var displayStateInteractor: DisplayStateInteractor + private lateinit var viewModel: PromptFingerprintIconViewModel @Before fun setup() { - interactor = + promptSelectorInteractor = + PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils) + displayStateInteractor = DisplayStateInteractorImpl( testScope.backgroundScope, mContext, fakeExecutor, rearDisplayStateRepository ) - viewModel = AuthBiometricFingerprintViewModel(interactor) + viewModel = PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor) } @Test - fun iconUpdates_onConfigurationChanged() { + fun sfpsIconUpdates_onConfigurationChanged() { testScope.runTest { runCurrent() + configureFingerprintPropertyRepository(FingerprintSensorType.POWER_BUTTON) val testConfig = Configuration() val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1 val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1 @@ -64,6 +85,10 @@ class AuthBiometricFingerprintViewModelTest : SysuiTestCase() { assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) } } + + private fun configureFingerprintPropertyRepository(sensorType: FingerprintSensorType) { + fingerprintRepository.setProperties(0, SensorStrength.STRONG, sensorType, mapOf()) + } } internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600 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 4d19543d41ff..11b0b0798ebc 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 @@ -24,7 +24,10 @@ import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthBiometricView +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl import com.android.systemui.biometrics.domain.model.BiometricModalities @@ -37,7 +40,9 @@ 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.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first @@ -69,8 +74,19 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var vibrator: VibratorHelper + private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val testScope = TestScope() + private val fingerprintRepository = FakeFingerprintPropertyRepository() private val promptRepository = FakePromptRepository() + private val rearDisplayStateRepository = FakeRearDisplayStateRepository() + + private val displayStateInteractor = + DisplayStateInteractorImpl( + testScope.backgroundScope, + mContext, + fakeExecutor, + rearDisplayStateRepository + ) private lateinit var selector: PromptSelectorInteractor private lateinit var viewModel: PromptViewModel @@ -78,10 +94,11 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Before fun setup() { - selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils) + selector = + PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils) selector.resetPrompt() - viewModel = PromptViewModel(selector, vibrator, featureFlags) + viewModel = PromptViewModel(displayStateInteractor, selector, vibrator, featureFlags) featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) } @@ -105,7 +122,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } assertThat(message).isEqualTo(PromptMessage.Empty) assertThat(size).isEqualTo(expectedSize) - assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN) + assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_IDLE) val startMessage = "here we go" viewModel.showAuthenticating(startMessage, isRetry = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt index dcaafe8dd052..6fcf54c0e54a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt @@ -17,8 +17,8 @@ package com.android.systemui.keyguard.data.repository import android.graphics.Point -import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase @@ -47,7 +47,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RoboPilotTest @OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidTestingRunner::class) +@RunWith(AndroidJUnit4::class) class LightRevealScrimRepositoryTest : SysuiTestCase() { private lateinit var fakeKeyguardRepository: FakeKeyguardRepository private lateinit var underTest: LightRevealScrimRepositoryImpl diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 23f243c8ebda..a9f288d3575f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -17,10 +17,8 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.filters.SmallTest -import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.model.AuthenticationMethodModel -import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey @@ -48,7 +46,6 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { private val underTest = LockscreenSceneViewModel( - applicationScope = testScope.backgroundScope, authenticationInteractor = authenticationInteractor, bouncerInteractor = utils.bouncerInteractor( @@ -58,32 +55,6 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { ) @Test - fun lockButtonIcon_whenLocked() = - testScope.runTest { - val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password - ) - utils.authenticationRepository.setUnlocked(false) - - assertThat((lockButtonIcon as? Icon.Resource)?.res) - .isEqualTo(R.drawable.ic_device_lock_on) - } - - @Test - fun lockButtonIcon_whenUnlocked() = - testScope.runTest { - val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password - ) - utils.authenticationRepository.setUnlocked(true) - - assertThat((lockButtonIcon as? Icon.Resource)?.res) - .isEqualTo(R.drawable.ic_device_lock_off) - } - - @Test fun upTransitionSceneKey_canSwipeToUnlock_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) @@ -120,32 +91,6 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { } @Test - fun onContentClicked_deviceUnlocked_switchesToGone() = - testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setUnlocked(true) - runCurrent() - - underTest.onContentClicked() - - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) - } - - @Test - fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = - testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setUnlocked(false) - runCurrent() - - underTest.onContentClicked() - - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) - } - - @Test fun onLockButtonClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.desiredScene) diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt index 49ece66e0cfd..ef07fab70bb0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -31,7 +31,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.KeyguardUnlockAnimationController -import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.model.SysUiState import com.android.systemui.navigationbar.NavigationBarController @@ -84,7 +83,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { private val fakeSystemClock = FakeSystemClock() private val sysUiState = SysUiState(displayTracker) private val featureFlags = FakeFeatureFlags() - private val screenLifecycle = ScreenLifecycle(dumpManager) private val wakefulnessLifecycle = WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager) @@ -142,7 +140,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { sysUiState, mock(), userTracker, - screenLifecycle, wakefulnessLifecycle, uiEventLogger, displayTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 53c04ccbdb38..46cbfacb0044 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -116,7 +116,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private val lockscreenSceneViewModel = LockscreenSceneViewModel( - applicationScope = testScope.backgroundScope, authenticationInteractor = authenticationInteractor, bouncerInteractor = bouncerInteractor, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index eb4ae1a743ef..7aeafeb2a752 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -539,6 +539,23 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + public void onKeyguardStatusViewHeightChange_animatesNextTopPaddingChangeForNSSL() { + ArgumentCaptor<View.OnLayoutChangeListener> captor = + ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); + verify(mKeyguardStatusView).addOnLayoutChangeListener(captor.capture()); + View.OnLayoutChangeListener listener = captor.getValue(); + + clearInvocations(mNotificationStackScrollLayoutController); + + when(mKeyguardStatusView.getHeight()).thenReturn(0); + listener.onLayoutChange(mKeyguardStatusView, /* left= */ 0, /* top= */ 0, /* right= */ + 0, /* bottom= */ 0, /* oldLeft= */ 0, /* oldTop= */ 0, /* oldRight= */ + 0, /* oldBottom = */ 200); + + verify(mNotificationStackScrollLayoutController).animateNextTopPaddingChange(); + } + + @Test public void testCanCollapsePanelOnTouch_trueForKeyGuard() { mStatusBarStateController.setState(KEYGUARD); 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 1edeeffe5217..3cce4232ab7a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.view.KeyEvent import android.view.MotionEvent import android.view.ViewGroup import androidx.test.filters.SmallTest @@ -38,6 +39,7 @@ import com.android.systemui.dock.DockManager import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionStep @@ -118,8 +120,10 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel + @Mock lateinit var keyEventInteractor: KeyEventInteractor private val notificationExpansionRepository = NotificationExpansionRepository() + private lateinit var fakeClock: FakeSystemClock private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler> private lateinit var interactionEventHandler: InteractionEventHandler @@ -148,6 +152,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) testScope = TestScope() + fakeClock = FakeSystemClock() underTest = NotificationShadeWindowViewController( lockscreenShadeTransitionController, @@ -180,7 +185,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { primaryBouncerToGoneTransitionViewModel, notificationExpansionRepository, featureFlags, - FakeSystemClock(), + fakeClock, BouncerMessageInteractor( FakeBouncerMessageRepository(), mock(BouncerMessageFactory::class.java), @@ -188,7 +193,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { CountDownTimerUtil(), featureFlags ), - BouncerLogger(logcatLogBuffer("BouncerLog")) + BouncerLogger(logcatLogBuffer("BouncerLog")), + keyEventInteractor, ) underTest.setupExpandedStatusBar() @@ -328,6 +334,33 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { } @Test + fun handleDispatchTouchEvent_launchAnimationRunningTimesOut() = + testScope.runTest { + // GIVEN touch dispatcher in a state that returns true + underTest.setStatusBarViewController(phoneStatusBarViewController) + whenever(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn( + true + ) + assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() + + // WHEN launch animation is running for 2 seconds + fakeClock.setUptimeMillis(10000) + underTest.setExpandAnimationRunning(true) + fakeClock.advanceTime(2000) + + // THEN touch is ignored + assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isFalse() + + // WHEN Launch animation is running for 6 seconds + fakeClock.advanceTime(4000) + + // THEN move is ignored, down is handled, and window is notified + assertThat(interactionEventHandler.handleDispatchTouchEvent(MOVE_EVENT)).isFalse() + assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() + verify(notificationShadeWindowController).setLaunchingActivity(false) + } + + @Test fun shouldInterceptTouchEvent_statusBarKeyguardViewManagerShouldIntercept() { // down event should be intercepted by keyguardViewManager whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT)) @@ -345,8 +378,30 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area) } + @Test + fun forwardsDispatchKeyEvent() { + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B) + interactionEventHandler.dispatchKeyEvent(keyEvent) + verify(keyEventInteractor).dispatchKeyEvent(keyEvent) + } + + @Test + fun forwardsDispatchKeyEventPreIme() { + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B) + interactionEventHandler.dispatchKeyEventPreIme(keyEvent) + verify(keyEventInteractor).dispatchKeyEventPreIme(keyEvent) + } + + @Test + fun forwardsInterceptMediaKey() { + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP) + interactionEventHandler.interceptMediaKey(keyEvent) + verify(keyEventInteractor).interceptMediaKey(keyEvent) + } + companion object { private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) + private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) private const val VIEW_BOTTOM = 100 } } 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 829184c4f05a..66d48d64b3a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.dock.DockManager import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel @@ -198,7 +199,8 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { CountDownTimerUtil(), featureFlags ), - BouncerLogger(logcatLogBuffer("BouncerLog")) + BouncerLogger(logcatLogBuffer("BouncerLog")), + Mockito.mock(KeyEventInteractor::class.java), ) controller.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java index 31bfa3fdf8cb..5fa6b3a15d35 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -50,6 +51,12 @@ import com.android.systemui.statusbar.connectivity.IconState; import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel; import com.android.systemui.util.CarrierConfigTracker; import com.android.systemui.utils.leaks.LeakCheckedTest; import com.android.systemui.utils.os.FakeHandler; @@ -61,6 +68,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest @@ -95,6 +106,18 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { private TestableLooper mTestableLooper; @Mock private ShadeCarrierGroupController.OnSingleCarrierChangedListener mOnSingleCarrierChangedListener; + @Mock + private MobileUiAdapter mMobileUiAdapter; + @Mock + private MobileIconsViewModel mMobileIconsViewModel; + @Mock + private ShadeCarrierGroupMobileIconViewModel mShadeCarrierGroupMobileIconViewModel; + @Mock + private MobileViewLogger mMobileViewLogger; + @Mock + private MobileContextProvider mMobileContextProvider; + @Mock + private StatusBarPipelineFlags mStatusBarPipelineFlags; private FakeSlotIndexResolver mSlotIndexResolver; private ClickListenerTextView mNoCarrierTextView; @@ -133,16 +156,35 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { mSlotIndexResolver = new FakeSlotIndexResolver(); + when(mMobileUiAdapter.getMobileIconsViewModel()).thenReturn(mMobileIconsViewModel); + mShadeCarrierGroupController = new ShadeCarrierGroupController.Builder( - mActivityStarter, handler, TestableLooper.get(this).getLooper(), - mNetworkController, mCarrierTextControllerBuilder, mContext, mCarrierConfigTracker, - mSlotIndexResolver) + mActivityStarter, + handler, + TestableLooper.get(this).getLooper(), + mNetworkController, + mCarrierTextControllerBuilder, + mContext, + mCarrierConfigTracker, + mSlotIndexResolver, + mMobileUiAdapter, + mMobileContextProvider, + mStatusBarPipelineFlags + ) .setShadeCarrierGroup(mShadeCarrierGroup) .build(); mShadeCarrierGroupController.setListening(true); } + private void setupWithNewPipeline() { + when(mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()).thenReturn(true); + when(mMobileContextProvider.getMobileContextForSub(anyInt(), any())).thenReturn(mContext); + when(mMobileIconsViewModel.getLogger()).thenReturn(mMobileViewLogger); + when(mMobileIconsViewModel.viewModelForSub(anyInt(), any())) + .thenReturn(mShadeCarrierGroupMobileIconViewModel); + } + @Test public void testInitiallyMultiCarrier() { assertFalse(mShadeCarrierGroupController.isSingleCarrier()); @@ -406,6 +448,129 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { verify(mOnSingleCarrierChangedListener, never()).onSingleCarrierChanged(anyBoolean()); } + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_addSubscription() { + setupWithNewPipeline(); + + mShadeCarrier1.setVisibility(View.GONE); + mShadeCarrier2.setVisibility(View.GONE); + mShadeCarrier3.setVisibility(View.GONE); + + List<Integer> subIds = new ArrayList<>(); + subIds.add(0); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.add(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1, times(1)).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_removeSubscription() { + setupWithNewPipeline(); + + List<Integer> subIds = new ArrayList<>(); + subIds.add(0); + subIds.add(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.remove(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1, times(1)).removeModernMobileView(); + verify(mShadeCarrier2, times(1)).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_removeSubscriptionOutOfOrder() { + setupWithNewPipeline(); + + List<Integer> subIds = new ArrayList<>(); + subIds.add(0); + subIds.add(1); + subIds.add(2); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.remove(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).removeModernMobileView(); + verify(mShadeCarrier2).removeModernMobileView(); + verify(mShadeCarrier3).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_moreSubsThanSimSlots_listLimitedToMax() { + setupWithNewPipeline(); + + List<Integer> subIds = Arrays.asList(0, 1, 2, 2); + + assertThat(mShadeCarrierGroupController.processSubIdList(subIds).size()).isEqualTo(3); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_invalidSimSlotIndexFilteredOut() { + setupWithNewPipeline(); + + List<Integer> subIds = Arrays.asList(0, 1, -1); + + List<ShadeCarrierGroupController.IconData> processedSubs = + mShadeCarrierGroupController.processSubIdList(subIds); + assertThat(processedSubs).hasSize(2); + assertThat(processedSubs.get(0).subId).isNotEqualTo(-1); + assertThat(processedSubs.get(1).subId).isNotEqualTo(-1); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_indexGreaterThanSimSlotsFilteredOut() { + setupWithNewPipeline(); + + List<Integer> subIds = Arrays.asList(0, 4); + + List<ShadeCarrierGroupController.IconData> processedSubs = + mShadeCarrierGroupController.processSubIdList(subIds); + assertThat(processedSubs).hasSize(1); + assertThat(processedSubs.get(0).subId).isNotEqualTo(4); + } + + @Test public void testOnlyInternalViewsHaveClickableListener() { ArgumentCaptor<View.OnClickListener> captor = @@ -447,6 +612,12 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { .isEqualTo(Settings.ACTION_WIRELESS_SETTINGS); } + private void resetShadeCarriers() { + reset(mShadeCarrier1); + reset(mShadeCarrier2); + reset(mShadeCarrier3); + } + private class FakeSlotIndexResolver implements ShadeCarrierGroupController.SlotIndexResolver { public boolean overrideInvalid; diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index 9495fdd50a7e..48665fe0c9b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -227,6 +227,25 @@ class ClockRegistryTest : SysuiTestCase() { } @Test + fun activeClockId_changeAfterPluginConnected() { + val plugin1 = FakeClockPlugin() + .addClock("clock_1", "clock 1") + .addClock("clock_2", "clock 2") + + val plugin2 = FakeClockPlugin() + .addClock("clock_3", "clock 3", { mockClock }) + .addClock("clock_4", "clock 4") + + registry.applySettings(ClockSettings("clock_3", null)) + + pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle) + assertEquals(DEFAULT_CLOCK_ID, registry.activeClockId) + + pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle) + assertEquals("clock_3", registry.activeClockId) + } + + @Test fun createDefaultClock_pluginDisconnected() { val plugin1 = FakeClockPlugin() .addClock("clock_1", "clock 1") @@ -321,9 +340,9 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun knownPluginAttached_clockAndListChanged_notLoaded() { val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>() - whenever(mockPluginLifecycle1.getPackage()).thenReturn("com.android.systemui.falcon.one") + whenever(mockPluginLifecycle1.getPackage()).thenReturn("com.android.systemui.clocks.metro") val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>() - whenever(mockPluginLifecycle2.getPackage()).thenReturn("com.android.systemui.falcon.two") + whenever(mockPluginLifecycle2.getPackage()).thenReturn("com.android.systemui.clocks.bignum") var changeCallCount = 0 var listChangeCallCount = 0 diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt index 9393a4f4eead..ee3d87089b6d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt @@ -60,18 +60,4 @@ class RotationButtonControllerTest : SysuiTestCase() { assertThat(mController.canShowRotationButton()).isTrue() } - - @Test - fun ifTaskbarVisible_showRotationSuggestion() { - mController.onNavigationBarWindowVisibilityChange( /* showing = */ false) - mController.onBehaviorChanged(Display.DEFAULT_DISPLAY, - WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) - mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON) - mController.onTaskbarStateChange( /* visible = */ false, /* stashed = */ false) - assertThat(mController.canShowRotationButton()).isFalse() - - mController.onTaskbarStateChange( /* visible = */ true, /* stashed = */ false) - - assertThat(mController.canShowRotationButton()).isTrue() - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index 38a8f414b0fb..4a94dc819a9e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -21,21 +21,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder -import com.android.systemui.statusbar.notification.collection.ListEntry -import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener -import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.withArgCaptor import org.junit.Assert import org.junit.Before import org.junit.Test -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever @SmallTest @@ -46,43 +36,13 @@ class GroupExpansionManagerTest : SysuiTestCase() { private val groupMembershipManager: GroupMembershipManager = mock() private val featureFlags = FakeFeatureFlags() - private val pipeline: NotifPipeline = mock() - private lateinit var beforeRenderListListener: OnBeforeRenderListListener - - private val summary1 = notificationEntry("foo", 1) - private val summary2 = notificationEntry("bar", 1) - private val entries = - listOf<ListEntry>( - GroupEntryBuilder() - .setSummary(summary1) - .setChildren( - listOf( - notificationEntry("foo", 2), - notificationEntry("foo", 3), - notificationEntry("foo", 4) - ) - ) - .build(), - GroupEntryBuilder() - .setSummary(summary2) - .setChildren( - listOf( - notificationEntry("bar", 2), - notificationEntry("bar", 3), - notificationEntry("bar", 4) - ) - ) - .build(), - notificationEntry("baz", 1) - ) - - private fun notificationEntry(pkg: String, id: Int) = - NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() } + private val entry1 = NotificationEntryBuilder().build() + private val entry2 = NotificationEntryBuilder().build() @Before fun setUp() { - whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1) - whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2) + whenever(groupMembershipManager.getGroupSummary(entry1)).thenReturn(entry1) + whenever(groupMembershipManager.getGroupSummary(entry2)).thenReturn(entry2) gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags) } @@ -94,15 +54,15 @@ class GroupExpansionManagerTest : SysuiTestCase() { var listenerCalledCount = 0 gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } - gem.setGroupExpanded(summary1, false) + gem.setGroupExpanded(entry1, false) Assert.assertEquals(0, listenerCalledCount) - gem.setGroupExpanded(summary1, true) + gem.setGroupExpanded(entry1, true) Assert.assertEquals(1, listenerCalledCount) - gem.setGroupExpanded(summary2, true) + gem.setGroupExpanded(entry2, true) Assert.assertEquals(2, listenerCalledCount) - gem.setGroupExpanded(summary1, true) + gem.setGroupExpanded(entry1, true) Assert.assertEquals(2, listenerCalledCount) - gem.setGroupExpanded(summary2, false) + gem.setGroupExpanded(entry2, false) Assert.assertEquals(3, listenerCalledCount) } @@ -113,39 +73,15 @@ class GroupExpansionManagerTest : SysuiTestCase() { var listenerCalledCount = 0 gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } - gem.setGroupExpanded(summary1, false) + gem.setGroupExpanded(entry1, false) Assert.assertEquals(1, listenerCalledCount) - gem.setGroupExpanded(summary1, true) + gem.setGroupExpanded(entry1, true) Assert.assertEquals(2, listenerCalledCount) - gem.setGroupExpanded(summary2, true) + gem.setGroupExpanded(entry2, true) Assert.assertEquals(3, listenerCalledCount) - gem.setGroupExpanded(summary1, true) + gem.setGroupExpanded(entry1, true) Assert.assertEquals(4, listenerCalledCount) - gem.setGroupExpanded(summary2, false) + gem.setGroupExpanded(entry2, false) Assert.assertEquals(5, listenerCalledCount) } - - @Test - fun testSyncWithPipeline() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - gem.attach(pipeline) - beforeRenderListListener = withArgCaptor { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - val listener: OnGroupExpansionChangeListener = mock() - gem.registerGroupExpansionChangeListener(listener) - - beforeRenderListListener.onBeforeRenderList(entries) - verify(listener, never()).onGroupExpansionChange(any(), any()) - - // Expand one of the groups. - gem.setGroupExpanded(summary1, true) - verify(listener).onGroupExpansionChange(summary1.row, true) - - // Empty the pipeline list and verify that the group is no longer expanded. - beforeRenderListListener.onBeforeRenderList(emptyList()) - verify(listener).onGroupExpansionChange(summary1.row, false) - verifyNoMoreInteractions(listener) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt new file mode 100644 index 000000000000..b8792a8aeacd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt @@ -0,0 +1,120 @@ +/* + * 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.notification.icon.ui.viewbinder + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.NotificationListener +import com.android.systemui.statusbar.NotificationMediaManager +import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel +import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel +import com.android.systemui.statusbar.phone.DozeParameters +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.util.mockito.whenever +import com.android.wm.shell.bubbles.Bubbles +import java.util.Optional +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +class NotificationIconAreaControllerViewBinderWrapperImplTest : SysuiTestCase() { + @Mock private lateinit var notifListener: NotificationListener + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var wakeUpCoordinator: NotificationWakeUpCoordinator + @Mock private lateinit var keyguardBypassController: KeyguardBypassController + @Mock private lateinit var notifMediaManager: NotificationMediaManager + @Mock private lateinit var dozeParams: DozeParameters + @Mock private lateinit var sectionStyleProvider: SectionStyleProvider + @Mock private lateinit var darkIconDispatcher: DarkIconDispatcher + @Mock private lateinit var statusBarWindowController: StatusBarWindowController + @Mock private lateinit var screenOffAnimController: ScreenOffAnimationController + @Mock private lateinit var bubbles: Bubbles + @Mock private lateinit var demoModeController: DemoModeController + @Mock private lateinit var aodIcons: NotificationIconContainer + @Mock private lateinit var featureFlags: FeatureFlags + + private val shelfViewModel = NotificationIconContainerShelfViewModel() + private val statusBarViewModel = NotificationIconContainerStatusBarViewModel() + private val aodViewModel = NotificationIconContainerAlwaysOnDisplayViewModel() + + private lateinit var underTest: NotificationIconAreaControllerViewBinderWrapperImpl + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = + NotificationIconAreaControllerViewBinderWrapperImpl( + mContext, + statusBarStateController, + wakeUpCoordinator, + keyguardBypassController, + notifMediaManager, + notifListener, + dozeParams, + sectionStyleProvider, + Optional.of(bubbles), + demoModeController, + darkIconDispatcher, + featureFlags, + statusBarWindowController, + screenOffAnimController, + shelfViewModel, + statusBarViewModel, + aodViewModel, + ) + } + + @Test + fun testNotificationIcons_settingHideIcons() { + underTest.settingsListener.onStatusBarIconsBehaviorChanged(true) + assertFalse(underTest.shouldShowLowPriorityIcons()) + } + + @Test + fun testNotificationIcons_settingShowIcons() { + underTest.settingsListener.onStatusBarIconsBehaviorChanged(false) + assertTrue(underTest.shouldShowLowPriorityIcons()) + } + + @Test + fun testAppearResetsTranslation() { + underTest.setupAodIcons(aodIcons) + whenever(dozeParams.shouldControlScreenOff()).thenReturn(false) + underTest.appearAodIcons() + verify(aodIcons).translationY = 0f + verify(aodIcons).alpha = 1.0f + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index 8545b894ad41..3ad3c15f158a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -29,8 +31,10 @@ import android.app.ActivityManager; import android.app.StatusBarManager; import android.os.PowerManager; import android.os.UserHandle; +import android.os.VibrationEffect; import android.os.Vibrator; import android.testing.AndroidTestingRunner; +import android.view.HapticFeedbackConstants; import android.view.WindowInsets; import androidx.test.filters.SmallTest; @@ -42,6 +46,7 @@ import com.android.internal.view.AppearanceRegion; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; import com.android.systemui.assist.AssistManager; +import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSHost; @@ -98,6 +103,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { @Mock private UserTracker mUserTracker; @Mock private QSHost mQSHost; @Mock private ActivityStarter mActivityStarter; + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); CentralSurfacesCommandQueueCallbacks mSbcqCallbacks; @@ -134,7 +140,8 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { mCameraLauncherLazy, mUserTracker, mQSHost, - mActivityStarter); + mActivityStarter, + mFeatureFlags); when(mUserTracker.getUserHandle()).thenReturn( UserHandle.of(ActivityManager.getCurrentUser())); @@ -241,4 +248,24 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { verifyZeroInteractions(mSystemBarAttributesListener); } + + @Test + public void vibrateOnNavigationKeyDown_oneWayHapticsDisabled_usesVibrate() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); + + mSbcqCallbacks.vibrateOnNavigationKeyDown(); + + verify(mVibratorHelper).vibrate(VibrationEffect.EFFECT_TICK); + } + + @Test + public void vibrateOnNavigationKeyDown_oneWayHapticsEnabled_usesPerformHapticFeedback() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); + + mSbcqCallbacks.vibrateOnNavigationKeyDown(); + + verify(mShadeViewController).performHapticFeedback( + HapticFeedbackConstants.GESTURE_START + ); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java index 8e1dcf0b1bb7..1b8cfd43e6b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java @@ -48,7 +48,7 @@ import java.util.Optional; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class NotificationIconAreaControllerTest extends SysuiTestCase { +public class LegacyNotificationIconAreaControllerImplTest extends SysuiTestCase { @Mock private NotificationListener mListener; @@ -70,7 +70,7 @@ public class NotificationIconAreaControllerTest extends SysuiTestCase { StatusBarWindowController mStatusBarWindowController; @Mock ScreenOffAnimationController mScreenOffAnimationController; - private NotificationIconAreaController mController; + private LegacyNotificationIconAreaControllerImpl mController; @Mock private Bubbles mBubbles; @Mock private DemoModeController mDemoModeController; @@ -82,7 +82,7 @@ public class NotificationIconAreaControllerTest extends SysuiTestCase { @Before public void setup() { MockitoAnnotations.initMocks(this); - mController = new NotificationIconAreaController( + mController = new LegacyNotificationIconAreaControllerImpl( mContext, mStatusBarStateController, mWakeUpCoordinator, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 0dc1d9a4b177..6b3bd22d5e62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -1802,6 +1802,15 @@ public class ScrimControllerTest extends SysuiTestCase { assertFalse(ScrimState.UNLOCKED.mAnimateChange); } + @Test + public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() { + mScrimController.transitionTo(ScrimState.KEYGUARD); + when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true); + mScrimController.transitionTo(ScrimState.UNLOCKED); + mScrimController.onUnlockAnimationFinished(); + assertAlphaAfterExpansion(mNotificationsScrim, 1f, 1f); + } + private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index 50ee6a31d389..ff2875355a6a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -52,12 +52,19 @@ class FakeMobileConnectionRepository( override val cdmaRoaming = MutableStateFlow(false) - override val networkName = - MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default")) + override val networkName: MutableStateFlow<NetworkNameModel> = + MutableStateFlow(NetworkNameModel.Default(DEFAULT_NETWORK_NAME)) + + override val carrierName: MutableStateFlow<NetworkNameModel> = + MutableStateFlow(NetworkNameModel.Default(DEFAULT_NETWORK_NAME)) override val isAllowedDuringAirplaneMode = MutableStateFlow(false) fun setDataEnabled(enabled: Boolean) { _dataEnabled.value = enabled } + + companion object { + const val DEFAULT_NETWORK_NAME = "default name" + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 3591c1740329..99e4030e1192 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -75,7 +75,11 @@ class FakeMobileConnectionsRepository( override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] - ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it } + ?: FakeMobileConnectionRepository( + subId, + tableLogBuffer, + ) + .also { subIdRepos[subId] = it } } override val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 5a887ebcee80..d005972043d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -243,13 +243,29 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 + private const val SUB_1_NAME = "Carrier $SUB_1_ID" private val SUB_1 = - mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - private val MODEL_1 = SubscriptionModel(subscriptionId = SUB_1_ID) + mock<SubscriptionInfo>().also { + whenever(it.subscriptionId).thenReturn(SUB_1_ID) + whenever(it.carrierName).thenReturn(SUB_1_NAME) + } + private val MODEL_1 = + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = SUB_1_NAME, + ) private const val SUB_2_ID = 2 + private const val SUB_2_NAME = "Carrier $SUB_2_ID" private val SUB_2 = - mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } - private val MODEL_2 = SubscriptionModel(subscriptionId = SUB_2_ID) + mock<SubscriptionInfo>().also { + whenever(it.subscriptionId).thenReturn(SUB_2_ID) + whenever(it.carrierName).thenReturn(SUB_2_NAME) + } + private val MODEL_2 = + SubscriptionModel( + subscriptionId = SUB_2_ID, + carrierName = SUB_2_NAME, + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index 7573b28c8a7f..57f97ec66a00 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt @@ -38,7 +38,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -140,6 +139,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC launch { conn.carrierNetworkChangeActive.collect {} } launch { conn.isRoaming.collect {} } launch { conn.networkName.collect {} } + launch { conn.carrierName.collect {} } launch { conn.isEmergencyOnly.collect {} } launch { conn.dataConnectionState.collect {} } } @@ -163,6 +163,8 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC assertThat(conn.isRoaming.value).isEqualTo(model.roaming) assertThat(conn.networkName.value) .isEqualTo(NetworkNameModel.IntentDerived(model.name)) + assertThat(conn.carrierName.value) + .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}")) // TODO(b/261029387): check these once we start handling them assertThat(conn.isEmergencyOnly.value).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt index efaf15235b46..2712b70a9745 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt @@ -546,6 +546,7 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { launch { conn.carrierNetworkChangeActive.collect {} } launch { conn.isRoaming.collect {} } launch { conn.networkName.collect {} } + launch { conn.carrierName.collect {} } launch { conn.isEmergencyOnly.collect {} } launch { conn.dataConnectionState.collect {} } } @@ -571,6 +572,8 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(conn.isRoaming.value).isEqualTo(model.roaming) assertThat(conn.networkName.value) .isEqualTo(NetworkNameModel.IntentDerived(model.name)) + assertThat(conn.carrierName.value) + .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}")) // TODO(b/261029387) check these once we start handling them assertThat(conn.isEmergencyOnly.value).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt index 3dd2eaff7bce..9c0cb17700a6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY @@ -43,6 +44,7 @@ import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -79,28 +81,51 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>() private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>() + private val subscriptionModel = + MutableStateFlow( + SubscriptionModel( + subscriptionId = SUB_ID, + carrierName = DEFAULT_NAME, + ) + ) + private lateinit var mobileRepo: FakeMobileConnectionRepository private lateinit var carrierMergedRepo: FakeMobileConnectionRepository @Before fun setUp() { - mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) + mobileRepo = + FakeMobileConnectionRepository( + SUB_ID, + tableLogBuffer, + ) carrierMergedRepo = - FakeMobileConnectionRepository(SUB_ID, tableLogBuffer).apply { - // Mimicks the real carrier merged repository - this.isAllowedDuringAirplaneMode.value = true - } + FakeMobileConnectionRepository( + SUB_ID, + tableLogBuffer, + ) + .apply { + // Mimicks the real carrier merged repository + this.isAllowedDuringAirplaneMode.value = true + } whenever( mobileFactory.build( eq(SUB_ID), any(), - eq(DEFAULT_NAME), + any(), + eq(DEFAULT_NAME_MODEL), eq(SEP), ) ) .thenReturn(mobileRepo) - whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(carrierMergedRepo) + whenever( + carrierMergedFactory.build( + eq(SUB_ID), + any(), + ) + ) + .thenReturn(carrierMergedRepo) } @Test @@ -120,7 +145,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { .build( SUB_ID, tableLogBuffer, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) } @@ -138,7 +164,11 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo) assertThat(underTest.operatorAlphaShort.value).isEqualTo(nonCarrierMergedName) - verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer) + verify(carrierMergedFactory, never()) + .build( + SUB_ID, + tableLogBuffer, + ) } @Test @@ -348,7 +378,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = false, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -356,7 +387,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = false, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -388,7 +420,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = false, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -397,7 +430,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = true, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -623,7 +657,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { SUB_ID, startingIsCarrierMerged, tableLogBuffer, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, testScope.backgroundScope, mobileFactory, @@ -639,8 +674,9 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { val realRepo = MobileConnectionRepositoryImpl( SUB_ID, - defaultNetworkName = NetworkNameModel.Default("default"), - networkNameSeparator = SEP, + subscriptionModel, + DEFAULT_NAME_MODEL, + SEP, telephonyManager, systemUiCarrierConfig = mock(), fakeBroadcastDispatcher, @@ -654,7 +690,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { mobileFactory.build( eq(SUB_ID), any(), - eq(DEFAULT_NAME), + any(), + eq(DEFAULT_NAME_MODEL), eq(SEP), ) ) @@ -677,7 +714,13 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { testScope.backgroundScope, wifiRepository, ) - whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(realRepo) + whenever( + carrierMergedFactory.build( + eq(SUB_ID), + any(), + ) + ) + .thenReturn(realRepo) return realRepo } @@ -690,7 +733,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { private companion object { const val SUB_ID = 42 - private val DEFAULT_NAME = NetworkNameModel.Default("default name") + private val DEFAULT_NAME = "default name" + private val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME) private const val SEP = "-" private const val BUFFER_SEPARATOR = "|" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index 1ff737bfc137..e50e5e31a786 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -62,6 +62,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetwork import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.configWithOverride import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig @@ -78,6 +79,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -109,6 +111,14 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) + private val subscriptionModel: MutableStateFlow<SubscriptionModel?> = + MutableStateFlow( + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = DEFAULT_NAME, + ) + ) + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -119,7 +129,8 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { underTest = MobileConnectionRepositoryImpl( SUB_1_ID, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, telephonyManager, systemUiCarrierConfig, @@ -179,6 +190,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { // gsmLevel updates, no change to cdmaLevel strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) assertThat(latest).isEqualTo(2) @@ -638,12 +650,51 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test + fun networkNameForSubId_updates() = + testScope.runTest { + var latest: NetworkNameModel? = null + val job = underTest.carrierName.onEach { latest = it }.launchIn(this) + + subscriptionModel.value = + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = DEFAULT_NAME, + ) + + assertThat(latest?.name).isEqualTo(DEFAULT_NAME) + + val updatedName = "Derived Carrier" + subscriptionModel.value = + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = updatedName, + ) + + assertThat(latest?.name).isEqualTo(updatedName) + + job.cancel() + } + + @Test + fun networkNameForSubId_defaultWhenSubscriptionModelNull() = + testScope.runTest { + var latest: NetworkNameModel? = null + val job = underTest.carrierName.onEach { latest = it }.launchIn(this) + + subscriptionModel.value = null + + assertThat(latest?.name).isEqualTo(DEFAULT_NAME) + + job.cancel() + } + + @Test fun networkName_default() = testScope.runTest { var latest: NetworkNameModel? = null val job = underTest.networkName.onEach { latest = it }.launchIn(this) - assertThat(latest).isEqualTo(DEFAULT_NAME) + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) job.cancel() } @@ -701,7 +752,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intentWithoutInfo) - assertThat(latest).isEqualTo(DEFAULT_NAME) + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) job.cancel() } @@ -852,8 +903,9 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { companion object { private const val SUB_1_ID = 1 - private val DEFAULT_NAME = NetworkNameModel.Default("default name") - private const val SEP = "-" + private val DEFAULT_NAME = "Fake Mobile Network" + private val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME) + private val SEP = "-" private const val SPN = "testSpn" private const val PLMN = "testPlmn" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt index 4f15aed00230..ea60aa74f12b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt @@ -36,6 +36,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository @@ -47,6 +48,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -97,6 +99,7 @@ class MobileConnectionTelephonySmokeTests : SysuiTestCase() { @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: MobileInputLogger @Mock private lateinit var tableLogger: TableLogBuffer + @Mock private lateinit var subscriptionModel: StateFlow<SubscriptionModel?> private val mobileMappings = FakeMobileMappingsProxy() private val systemUiCarrierConfig = @@ -113,11 +116,16 @@ class MobileConnectionTelephonySmokeTests : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) - connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger) + connectionsRepo = + FakeMobileConnectionsRepository( + mobileMappings, + tableLogger, + ) underTest = MobileConnectionRepositoryImpl( SUB_1_ID, + subscriptionModel, DEFAULT_NAME, SEP, telephonyManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index c8b6f13d6902..fd05cc495692 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -1190,30 +1190,36 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { companion object { // Subscription 1 private const val SUB_1_ID = 1 + private const val SUB_1_NAME = "Carrier $SUB_1_ID" private val GROUP_1 = ParcelUuid(UUID.randomUUID()) private val SUB_1 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) whenever(it.groupUuid).thenReturn(GROUP_1) + whenever(it.carrierName).thenReturn(SUB_1_NAME) } private val MODEL_1 = SubscriptionModel( subscriptionId = SUB_1_ID, groupUuid = GROUP_1, + carrierName = SUB_1_NAME, ) // Subscription 2 private const val SUB_2_ID = 2 + private const val SUB_2_NAME = "Carrier $SUB_2_ID" private val GROUP_2 = ParcelUuid(UUID.randomUUID()) private val SUB_2 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) whenever(it.groupUuid).thenReturn(GROUP_2) + whenever(it.carrierName).thenReturn(SUB_2_NAME) } private val MODEL_2 = SubscriptionModel( subscriptionId = SUB_2_ID, groupUuid = GROUP_2, + carrierName = SUB_2_NAME, ) // Subs 3 and 4 are considered to be in the same group ------------------------------------ @@ -1242,9 +1248,14 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // Carrier merged subscription private const val SUB_CM_ID = 5 + private const val SUB_CM_NAME = "Carrier $SUB_CM_ID" private val SUB_CM = - mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) } - private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID) + mock<SubscriptionInfo>().also { + whenever(it.subscriptionId).thenReturn(SUB_CM_ID) + whenever(it.carrierName).thenReturn(SUB_CM_NAME) + } + private val MODEL_CM = + SubscriptionModel(subscriptionId = SUB_CM_ID, carrierName = SUB_CM_NAME) private val WIFI_INFO_CM = mock<WifiInfo>().apply { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 8d1da69d6877..a3df785c5dae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -44,6 +44,8 @@ class FakeMobileIconInteractor( override val mobileIsDefault = MutableStateFlow(true) + override val isSingleCarrier = MutableStateFlow(true) + override val networkTypeIconGroup = MutableStateFlow<NetworkTypeIconModel>( NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) @@ -51,6 +53,8 @@ class FakeMobileIconInteractor( override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo mode")) + override val carrierName = MutableStateFlow("demo mode") + private val _isEmergencyOnly = MutableStateFlow(false) override val isEmergencyOnly = _isEmergencyOnly diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index b2bbcfd3d6ef..82b7ec41d148 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -64,6 +64,8 @@ class FakeMobileIconsInteractor( override val mobileIsDefault = MutableStateFlow(false) + override val isSingleCarrier = MutableStateFlow(true) + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 58d3804b7155..e3c59adef529 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameMode import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G @@ -40,6 +41,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -56,6 +58,15 @@ class MobileIconInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconInteractor private val mobileMappingsProxy = FakeMobileMappingsProxy() private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock()) + + private val subscriptionModel = + MutableStateFlow( + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = DEFAULT_NAME, + ) + ) + private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID, mock()) private val testDispatcher = UnconfinedTestDispatcher() @@ -432,7 +443,7 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun networkName_usesOperatorAlphaShotWhenNonNullAndRepoIsDefault() = + fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = testScope.runTest { var latest: NetworkNameModel? = null val job = underTest.networkName.onEach { latest = it }.launchIn(this) @@ -440,7 +451,7 @@ class MobileIconInteractorTest : SysuiTestCase() { val testOperatorName = "operatorAlphaShort" // Default network name, operator name is non-null, uses the operator name - connectionRepository.networkName.value = DEFAULT_NAME + connectionRepository.networkName.value = DEFAULT_NAME_MODEL connectionRepository.operatorAlphaShort.value = testOperatorName assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName)) @@ -448,10 +459,39 @@ class MobileIconInteractorTest : SysuiTestCase() { // Default network name, operator name is null, uses the default connectionRepository.operatorAlphaShort.value = null + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + + // Derived network name, operator name non-null, uses the derived name + connectionRepository.networkName.value = DERIVED_NAME_MODEL + connectionRepository.operatorAlphaShort.value = testOperatorName + + assertThat(latest).isEqualTo(DERIVED_NAME_MODEL) + + job.cancel() + } + + @Test + fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = + testScope.runTest { + var latest: String? = null + val job = underTest.carrierName.onEach { latest = it }.launchIn(this) + + val testOperatorName = "operatorAlphaShort" + + // Default network name, operator name is non-null, uses the operator name + connectionRepository.carrierName.value = DEFAULT_NAME_MODEL + connectionRepository.operatorAlphaShort.value = testOperatorName + + assertThat(latest).isEqualTo(testOperatorName) + + // Default network name, operator name is null, uses the default + connectionRepository.operatorAlphaShort.value = null + assertThat(latest).isEqualTo(DEFAULT_NAME) // Derived network name, operator name non-null, uses the derived name - connectionRepository.networkName.value = DERIVED_NAME + connectionRepository.carrierName.value = + NetworkNameModel.SubscriptionDerived(DERIVED_NAME) connectionRepository.operatorAlphaShort.value = testOperatorName assertThat(latest).isEqualTo(DERIVED_NAME) @@ -460,6 +500,21 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test + fun isSingleCarrier_matchesParent() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this) + + mobileIconsInteractor.isSingleCarrier.value = true + assertThat(latest).isTrue() + + mobileIconsInteractor.isSingleCarrier.value = false + assertThat(latest).isFalse() + + job.cancel() + } + + @Test fun isForceHidden_matchesParent() = testScope.runTest { var latest: Boolean? = null @@ -494,6 +549,7 @@ class MobileIconInteractorTest : SysuiTestCase() { mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.alwaysShowDataRatIcon, mobileIconsInteractor.alwaysUseCdmaLevel, + mobileIconsInteractor.isSingleCarrier, mobileIconsInteractor.mobileIsDefault, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, @@ -510,7 +566,9 @@ class MobileIconInteractorTest : SysuiTestCase() { private const val SUB_1_ID = 1 - private val DEFAULT_NAME = NetworkNameModel.Default("test default name") - private val DERIVED_NAME = NetworkNameModel.IntentDerived("test derived name") + private val DEFAULT_NAME = "test default name" + private val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME) + private val DERIVED_NAME = "test derived name" + private val DERIVED_NAME_MODEL = NetworkNameModel.IntentDerived(DERIVED_NAME) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 1fb76b048d47..3e6f90931b87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -527,6 +527,57 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test + fun isSingleCarrier_zeroSubscriptions_false() = + testScope.runTest { + var latest: Boolean? = true + val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this) + + connectionsRepository.setSubscriptions(emptyList()) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isSingleCarrier_oneSubscription_true() = + testScope.runTest { + var latest: Boolean? = false + val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this) + + connectionsRepository.setSubscriptions(listOf(SUB_1)) + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isSingleCarrier_twoSubscriptions_false() = + testScope.runTest { + var latest: Boolean? = true + val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this) + + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isSingleCarrier_updates() = + testScope.runTest { + var latest: Boolean? = false + val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this) + + connectionsRepository.setSubscriptions(listOf(SUB_1)) + assertThat(latest).isTrue() + + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() = testScope.runTest { var latest: Boolean? = null @@ -745,6 +796,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { subscriptionId = subscriptionIds.first, isOpportunistic = opportunistic.first, groupUuid = groupUuid, + carrierName = "Carrier ${subscriptionIds.first}" ) val sub2 = @@ -752,6 +804,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { subscriptionId = subscriptionIds.second, isOpportunistic = opportunistic.second, groupUuid = groupUuid, + carrierName = "Carrier ${opportunistic.second}" ) return Pair(sub1, sub2) @@ -760,11 +813,13 @@ class MobileIconsInteractorTest : SysuiTestCase() { companion object { private const val SUB_1_ID = 1 - private val SUB_1 = SubscriptionModel(subscriptionId = SUB_1_ID) + private val SUB_1 = + SubscriptionModel(subscriptionId = SUB_1_ID, carrierName = "Carrier $SUB_1_ID") private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, mock()) private const val SUB_2_ID = 2 - private val SUB_2 = SubscriptionModel(subscriptionId = SUB_2_ID) + private val SUB_2 = + SubscriptionModel(subscriptionId = SUB_2_ID, carrierName = "Carrier $SUB_2_ID") private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, mock()) private const val SUB_3_ID = 3 @@ -773,6 +828,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { subscriptionId = SUB_3_ID, isOpportunistic = true, groupUuid = ParcelUuid(UUID.randomUUID()), + carrierName = "Carrier $SUB_3_ID" ) private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, mock()) @@ -782,6 +838,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { subscriptionId = SUB_4_ID, isOpportunistic = true, groupUuid = ParcelUuid(UUID.randomUUID()), + carrierName = "Carrier $SUB_4_ID" ) private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, mock()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt index f0458fa83d38..065dfbabf051 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -92,15 +92,31 @@ class MobileIconsViewModelTest : SysuiTestCase() { interactor.filteredSubscriptions.value = listOf( - SubscriptionModel(subscriptionId = 1, isOpportunistic = false), + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = false, + carrierName = "Carrier 1", + ), ) assertThat(latest).isEqualTo(listOf(1)) interactor.filteredSubscriptions.value = listOf( - SubscriptionModel(subscriptionId = 2, isOpportunistic = false), - SubscriptionModel(subscriptionId = 5, isOpportunistic = true), - SubscriptionModel(subscriptionId = 7, isOpportunistic = true), + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = false, + carrierName = "Carrier 2", + ), + SubscriptionModel( + subscriptionId = 5, + isOpportunistic = true, + carrierName = "Carrier 5", + ), + SubscriptionModel( + subscriptionId = 7, + isOpportunistic = true, + carrierName = "Carrier 7", + ), ) assertThat(latest).isEqualTo(listOf(2, 5, 7)) @@ -138,6 +154,33 @@ class MobileIconsViewModelTest : SysuiTestCase() { } @Test + fun caching_mobileIconInteractorIsReusedForSameSubId() = + testScope.runTest { + val interactor1 = underTest.mobileIconInteractorForSub(1) + val interactor2 = underTest.mobileIconInteractorForSub(1) + + assertThat(interactor1).isSameInstanceAs(interactor2) + } + + @Test + fun caching_invalidInteractorssAreRemovedFromCacheWhenSubDisappears() = + testScope.runTest { + // Retrieve interactors to trigger caching + val interactor1 = underTest.mobileIconInteractorForSub(1) + val interactor2 = underTest.mobileIconInteractorForSub(2) + + // Both impls are cached + assertThat(underTest.mobileIconInteractorSubIdCache) + .containsExactly(1, interactor1, 2, interactor2) + + // SUB_1 is removed from the list... + interactor.filteredSubscriptions.value = listOf(SUB_2) + + // ... and dropped from the cache + assertThat(underTest.mobileIconInteractorSubIdCache).containsExactly(2, interactor2) + } + + @Test fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() = testScope.runTest { var latest: Boolean? = null @@ -308,8 +351,23 @@ class MobileIconsViewModelTest : SysuiTestCase() { } companion object { - private val SUB_1 = SubscriptionModel(subscriptionId = 1, isOpportunistic = false) - private val SUB_2 = SubscriptionModel(subscriptionId = 2, isOpportunistic = false) - private val SUB_3 = SubscriptionModel(subscriptionId = 3, isOpportunistic = false) + private val SUB_1 = + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = false, + carrierName = "Carrier 1", + ) + private val SUB_2 = + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = false, + carrierName = "Carrier 2", + ) + private val SUB_3 = + SubscriptionModel( + subscriptionId = 3, + isOpportunistic = false, + carrierName = "Carrier 3", + ) } } 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 21e4f5ad589a..a6a2761f66ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -32,7 +32,6 @@ 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; import static org.mockito.Mockito.times; @@ -480,123 +479,45 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void ifPortraitHalfOpen_drawVerticallyTop() { - DevicePostureController devicePostureController = mock(DevicePostureController.class); - when(devicePostureController.getDevicePosture()) - .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED); - - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - devicePostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0 , null); - - verify(devicePostureController).addCallback(any()); - dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); + mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); mTestableLooper.processAllMessages(); // let dismiss() finish setOrientation(Configuration.ORIENTATION_PORTRAIT); // Call show() to trigger layout updates before verifying position - dialog.show(SHOW_REASON_UNKNOWN); + mDialog.show(SHOW_REASON_UNKNOWN); mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect - int gravity = dialog.getWindowGravity(); + int gravity = mDialog.getWindowGravity(); assertEquals(Gravity.TOP, gravity & Gravity.VERTICAL_GRAVITY_MASK); - - cleanUp(dialog); } @Test public void ifPortraitAndOpen_drawCenterVertically() { - DevicePostureController devicePostureController = mock(DevicePostureController.class); - when(devicePostureController.getDevicePosture()) - .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED); - - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - devicePostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0, null); - - verify(devicePostureController).addCallback(any()); - dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED); + mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED); mTestableLooper.processAllMessages(); // let dismiss() finish setOrientation(Configuration.ORIENTATION_PORTRAIT); - dialog.show(SHOW_REASON_UNKNOWN); + mDialog.show(SHOW_REASON_UNKNOWN); mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect - int gravity = dialog.getWindowGravity(); + int gravity = mDialog.getWindowGravity(); assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK); - - cleanUp(dialog); } @Test public void ifLandscapeAndHalfOpen_drawCenterVertically() { - DevicePostureController devicePostureController = mock(DevicePostureController.class); - when(devicePostureController.getDevicePosture()) - .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED); - - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - devicePostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0, null); - - verify(devicePostureController).addCallback(any()); - dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); + mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); mTestableLooper.processAllMessages(); // let dismiss() finish setOrientation(Configuration.ORIENTATION_LANDSCAPE); - dialog.show(SHOW_REASON_UNKNOWN); + mDialog.show(SHOW_REASON_UNKNOWN); mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect - int gravity = dialog.getWindowGravity(); + int gravity = mDialog.getWindowGravity(); assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK); - - cleanUp(dialog); } @Test @@ -607,31 +528,9 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void dialogDestroy_removesPostureControllerCallback() { - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - mPostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0, null); - verify(mPostureController, never()).removeCallback(any()); - dialog.destroy(); - + mDialog.destroy(); verify(mPostureController).removeCallback(any()); - - cleanUp(dialog); } private void setOrientation(int orientation) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt new file mode 100644 index 000000000000..94ed608f4844 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.wmshell + +import android.content.ContentResolver +import android.content.Context +import android.content.SharedPreferences +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.model.SysUiStateTest +import com.android.wm.shell.bubbles.Bubble +import com.android.wm.shell.bubbles.BubbleEducationController +import com.android.wm.shell.bubbles.PREF_MANAGED_EDUCATION +import com.android.wm.shell.bubbles.PREF_STACK_EDUCATION +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class BubbleEducationControllerTest : SysUiStateTest() { + private val sharedPrefsEditor = Mockito.mock(SharedPreferences.Editor::class.java) + private val sharedPrefs = Mockito.mock(SharedPreferences::class.java) + private val context = Mockito.mock(Context::class.java) + private lateinit var sut: BubbleEducationController + + @Before + fun setUp() { + Mockito.`when`(context.packageName).thenReturn("packageName") + Mockito.`when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs) + Mockito.`when`(context.contentResolver) + .thenReturn(Mockito.mock(ContentResolver::class.java)) + Mockito.`when`(sharedPrefs.edit()).thenReturn(sharedPrefsEditor) + sut = BubbleEducationController(context) + } + + @Test + fun testSeenStackEducation_read() { + Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true) + assertEquals(sut.hasSeenStackEducation, true) + Mockito.verify(sharedPrefs).getBoolean(PREF_STACK_EDUCATION, false) + } + + @Test + fun testSeenStackEducation_write() { + sut.hasSeenStackEducation = true + Mockito.verify(sharedPrefsEditor).putBoolean(PREF_STACK_EDUCATION, true) + } + + @Test + fun testSeenManageEducation_read() { + Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true) + assertEquals(sut.hasSeenManageEducation, true) + Mockito.verify(sharedPrefs).getBoolean(PREF_MANAGED_EDUCATION, false) + } + + @Test + fun testSeenManageEducation_write() { + sut.hasSeenManageEducation = true + Mockito.verify(sharedPrefsEditor).putBoolean(PREF_MANAGED_EDUCATION, true) + } + + @Test + fun testShouldShowStackEducation() { + val bubble = Mockito.mock(Bubble::class.java) + // When bubble is null + assertEquals(sut.shouldShowStackEducation(null), false) + // When bubble is not conversation + Mockito.`when`(bubble.isConversation).thenReturn(false) + assertEquals(sut.shouldShowStackEducation(bubble), false) + // When bubble is conversation and has seen stack edu + Mockito.`when`(bubble.isConversation).thenReturn(true) + Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true) + assertEquals(sut.shouldShowStackEducation(bubble), false) + // When bubble is conversation and has not seen stack edu + Mockito.`when`(bubble.isConversation).thenReturn(true) + Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(false) + assertEquals(sut.shouldShowStackEducation(bubble), true) + } + + @Test + fun testShouldShowManageEducation() { + val bubble = Mockito.mock(Bubble::class.java) + // When bubble is null + assertEquals(sut.shouldShowManageEducation(null), false) + // When bubble is not conversation + Mockito.`when`(bubble.isConversation).thenReturn(false) + assertEquals(sut.shouldShowManageEducation(bubble), false) + // When bubble is conversation and has seen stack edu + Mockito.`when`(bubble.isConversation).thenReturn(true) + Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true) + assertEquals(sut.shouldShowManageEducation(bubble), false) + // When bubble is conversation and has not seen stack edu + Mockito.`when`(bubble.isConversation).thenReturn(true) + Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(false) + assertEquals(sut.shouldShowManageEducation(bubble), true) + } +} diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index d5aee9284116..4c137bce4f7b 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -132,7 +132,8 @@ import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.transport.TransportNotAvailableException; import com.android.server.backup.transport.TransportNotRegisteredException; import com.android.server.backup.utils.BackupEligibilityRules; -import com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupManagerMonitorDumpsysUtils; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.android.server.backup.utils.BackupObserverUtils; import com.android.server.backup.utils.SparseArrayUtils; @@ -141,6 +142,7 @@ import dalvik.annotation.optimization.NeverCompile; import com.google.android.collect.Sets; import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -149,6 +151,7 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.io.RandomAccessFile; @@ -1830,12 +1833,14 @@ public class UserBackupManagerService { */ public int requestBackup(String[] packages, IBackupObserver observer, IBackupManagerMonitor monitor, int flags) { + BackupManagerMonitorEventSender mBackupManagerMonitorEventSender = + getBMMEventSender(monitor); mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "requestBackup"); if (packages == null || packages.length < 1) { Slog.e(TAG, addUserIdToLogMessage(mUserId, "No packages named for backup request")); BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED); - monitor = BackupManagerMonitorUtils.monitorEvent(monitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_NO_PACKAGES, null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null); throw new IllegalArgumentException("No packages are provided for backup"); @@ -1853,7 +1858,7 @@ public class UserBackupManagerService { final int logTag = mSetupComplete ? BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED : BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED; - monitor = BackupManagerMonitorUtils.monitorEvent(monitor, logTag, null, + mBackupManagerMonitorEventSender.monitorEvent(logTag, null, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); return BackupManager.ERROR_BACKUP_NOT_ALLOWED; } @@ -1871,7 +1876,7 @@ public class UserBackupManagerService { } catch (TransportNotRegisteredException | TransportNotAvailableException | RemoteException e) { BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED); - monitor = BackupManagerMonitorUtils.monitorEvent(monitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL, null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null); return BackupManager.ERROR_TRANSPORT_ABORTED; @@ -3066,7 +3071,9 @@ public class UserBackupManagerService { /* caller */ "BMS.reportDelayedRestoreResult"); IBackupManagerMonitor monitor = transportClient.getBackupManagerMonitor(); - BackupManagerMonitorUtils.sendAgentLoggingResults(monitor, packageInfo, results, + BackupManagerMonitorEventSender mBackupManagerMonitorEventSender = + getBMMEventSender(monitor); + mBackupManagerMonitorEventSender.sendAgentLoggingResults(packageInfo, results, BackupAnnotations.OperationType.RESTORE); } catch (NameNotFoundException | TransportNotAvailableException | TransportNotRegisteredException | RemoteException e) { @@ -3190,6 +3197,11 @@ public class UserBackupManagerService { } } + @VisibleForTesting + BackupManagerMonitorEventSender getBMMEventSender(IBackupManagerMonitor monitor) { + return new BackupManagerMonitorEventSender(monitor); + } + /** User-configurable enabling/disabling of backups. */ public void setBackupEnabled(boolean enable) { setBackupEnabled(enable, /* persistToDisk */ true); @@ -4148,6 +4160,7 @@ public class UserBackupManagerService { } } dumpInternal(pw); + dumpBMMEvents(pw); } finally { Binder.restoreCallingIdentity(identityToken); } @@ -4165,6 +4178,23 @@ public class UserBackupManagerService { } } + private void dumpBMMEvents(PrintWriter pw) { + BackupManagerMonitorDumpsysUtils bm = + new BackupManagerMonitorDumpsysUtils(); + File events = bm.getBMMEventsFile(); + pw.println("START OF BACKUP MANAGER MONITOR EVENTS"); + try (BufferedReader reader = new BufferedReader(new FileReader(events))) { + String line; + while ((line = reader.readLine()) != null) { + pw.println(line); + } + } catch (IOException e) { + Slog.e(TAG, "IO Exception when reading BMM events from file: " + e); + pw.println("IO Exception when reading BMM events from file"); + } + pw.println("END OF BACKUP MANAGER MONITOR EVENTS"); + } + @NeverCompile // Avoid size overhead of debugging code. private void dumpInternal(PrintWriter pw) { // Add prefix for only non-system users so that system user dumpsys is the same as before diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java index ad29422501c6..12712063e344 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java +++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java @@ -23,13 +23,11 @@ import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME; import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE; -import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ApplicationThreadConstants; import android.app.IBackupAgent; import android.app.backup.BackupTransport; import android.app.backup.FullBackupDataOutput; -import android.app.backup.IBackupManagerMonitor; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -44,7 +42,7 @@ import com.android.server.backup.OperationStorage.OpType; import com.android.server.backup.UserBackupManagerService; import com.android.server.backup.remote.RemoteCall; import com.android.server.backup.utils.BackupEligibilityRules; -import com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.android.server.backup.utils.FullBackupUtils; import java.io.File; @@ -69,7 +67,7 @@ public class FullBackupEngine { private final int mTransportFlags; private final BackupAgentTimeoutParameters mAgentTimeoutParameters; private final BackupEligibilityRules mBackupEligibilityRules; - @Nullable private final IBackupManagerMonitor mMonitor; + private final BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; class FullBackupRunner implements Runnable { private final @UserIdInt int mUserId; @@ -198,7 +196,7 @@ public class FullBackupEngine { int opToken, int transportFlags, BackupEligibilityRules backupEligibilityRules, - IBackupManagerMonitor monitor) { + BackupManagerMonitorEventSender backupManagerMonitorEventSender) { this.backupManagerService = backupManagerService; mOutput = output; mPreflightHook = preflightHook; @@ -213,7 +211,7 @@ public class FullBackupEngine { backupManagerService.getAgentTimeoutParameters(), "Timeout parameters cannot be null"); mBackupEligibilityRules = backupEligibilityRules; - mMonitor = monitor; + mBackupManagerMonitorEventSender = backupManagerMonitorEventSender; } public int preflightCheck() throws RemoteException { @@ -270,7 +268,7 @@ public class FullBackupEngine { result = BackupTransport.TRANSPORT_OK; } - BackupManagerMonitorUtils.monitorAgentLoggingResults(mMonitor, mPkg, mAgent); + mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mPkg, mAgent); } catch (IOException e) { Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage()); result = BackupTransport.AGENT_ERROR; diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java index cba1e299ff58..dc6709141b25 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java @@ -40,6 +40,7 @@ import com.android.server.backup.KeyValueAdbBackupEngine; import com.android.server.backup.OperationStorage; import com.android.server.backup.UserBackupManagerService; import com.android.server.backup.utils.BackupEligibilityRules; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.android.server.backup.utils.PasswordUtils; import java.io.ByteArrayOutputStream; @@ -421,7 +422,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor mCurrentOpToken, /*transportFlags=*/ 0, mBackupEligibilityRules, - /* monitor= */ null); + new BackupManagerMonitorEventSender(null)); sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName); // Don't need to check preflight result as there is no preflight hook. diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java index 162046a66560..6aed9aa15860 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java @@ -54,7 +54,7 @@ import com.android.server.backup.transport.BackupTransportClient; import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.transport.TransportNotAvailableException; import com.android.server.backup.utils.BackupEligibilityRules; -import com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.android.server.backup.utils.BackupObserverUtils; import com.google.android.collect.Sets; @@ -153,7 +153,6 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba CountDownLatch mLatch; FullBackupJob mJob; // if a scheduled job needs to be finished afterwards IBackupObserver mBackupObserver; - @Nullable private IBackupManagerMonitor mMonitor; boolean mUserInitiated; SinglePackageBackupRunner mBackupRunner; private final int mBackupRunnerOpToken; @@ -167,6 +166,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba private final int mCurrentOpToken; private final BackupAgentTimeoutParameters mAgentTimeoutParameters; private final BackupEligibilityRules mBackupEligibilityRules; + private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; public PerformFullTransportBackupTask(UserBackupManagerService backupManagerService, OperationStorage operationStorage, @@ -185,11 +185,12 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba mJob = runningJob; mPackages = new ArrayList<>(whichPackages.length); mBackupObserver = backupObserver; - mMonitor = monitor; mListener = (listener != null) ? listener : OnTaskFinishedListener.NOP; mUserInitiated = userInitiated; mCurrentOpToken = backupManagerService.generateRandomIntegerToken(); mBackupRunnerOpToken = backupManagerService.generateRandomIntegerToken(); + mBackupManagerMonitorEventSender = + new BackupManagerMonitorEventSender(monitor); mAgentTimeoutParameters = Objects.requireNonNull( backupManagerService.getAgentTimeoutParameters(), "Timeout parameters cannot be null"); @@ -218,7 +219,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (MORE_DEBUG) { Slog.d(TAG, "Ignoring ineligible package " + pkg); } - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -233,7 +234,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba Slog.d(TAG, "Ignoring full-data backup of key/value participant " + pkg); } - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -248,7 +249,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (MORE_DEBUG) { Slog.d(TAG, "Ignoring stopped package " + pkg); } - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -260,7 +261,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba mPackages.add(info); } catch (NameNotFoundException e) { Slog.i(TAG, "Requested package " + pkg + " not found; ignoring"); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -356,8 +357,8 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } else { monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED; } - mMonitor = BackupManagerMonitorUtils - .monitorEvent(mMonitor, monitoringEvent, null, + mBackupManagerMonitorEventSender + .monitorEvent(monitoringEvent, null, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); mUpdateSchedule = false; @@ -369,7 +370,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (transport == null) { Slog.w(TAG, "Transport not present; full data backup not performed"); backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED; - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null); @@ -378,9 +379,10 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba // In some cases there may not be a monitor passed in when creating this task. So, if we // don't have one already we ask the transport for a monitor. - if (mMonitor == null) { + if (mBackupManagerMonitorEventSender.getMonitor() == null) { try { - mMonitor = transport.getBackupManagerMonitor(); + mBackupManagerMonitorEventSender + .setMonitor(transport.getBackupManagerMonitor()); } catch (RemoteException e) { Slog.i(TAG, "Failed to retrieve monitor from transport"); } @@ -457,11 +459,11 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba + packageName + ": " + preflightResult + ", not running backup."); } - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra(null, + mBackupManagerMonitorEventSender.putMonitoringExtra(null, BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR, preflightResult)); backupPackageStatus = (int) preflightResult; @@ -492,7 +494,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) { Slog.w(TAG, "Package hit quota limit in-flight " + packageName + ": " + totalRead + " of " + quota); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, @@ -647,11 +649,11 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } catch (Exception e) { backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED; Slog.w(TAG, "Exception trying full transport backup", e); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra(null, + mBackupManagerMonitorEventSender.putMonitoringExtra(null, BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP, Log.getStackTraceString(e))); @@ -885,7 +887,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba mCurrentOpToken, mTransportFlags, mBackupEligibilityRules, - mMonitor); + mBackupManagerMonitorEventSender); try { try { if (!mIsCancelled) { @@ -967,7 +969,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba Slog.w(TAG, "Full backup cancel of " + mTarget.packageName); } - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null); mIsCancelled = true; diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java index 4632cb0e51e2..20c8cf6c6923 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java @@ -32,7 +32,7 @@ import com.android.server.backup.BackupManagerService; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.UserBackupManagerService; import com.android.server.backup.remote.RemoteResult; -import com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.android.server.backup.utils.BackupObserverUtils; import java.io.File; @@ -65,21 +65,21 @@ public class KeyValueBackupReporter { private final UserBackupManagerService mBackupManagerService; private final IBackupObserver mObserver; - @Nullable private IBackupManagerMonitor mMonitor; + private final BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; KeyValueBackupReporter( UserBackupManagerService backupManagerService, IBackupObserver observer, - @Nullable IBackupManagerMonitor monitor) { + BackupManagerMonitorEventSender backupManagerMonitorEventSender) { mBackupManagerService = backupManagerService; mObserver = observer; - mMonitor = monitor; + mBackupManagerMonitorEventSender = backupManagerMonitorEventSender; } /** Returns the monitor or {@code null} if we lost connection to it. */ @Nullable IBackupManagerMonitor getMonitor() { - return mMonitor; + return mBackupManagerMonitorEventSender.getMonitor(); } IBackupObserver getObserver() { @@ -208,13 +208,11 @@ public class KeyValueBackupReporter { void onAgentIllegalKey(PackageInfo packageInfo, String key) { String packageName = packageInfo.packageName; EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, "bad key"); - mMonitor = - BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY, packageInfo, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra( + mBackupManagerMonitorEventSender.putMonitoringExtra( null, BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY, key)); BackupObserverUtils.sendBackupOnPackageResult( mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE); @@ -254,13 +252,11 @@ public class KeyValueBackupReporter { if (MORE_DEBUG) { Slog.i(TAG, "No backup data written, not calling transport"); } - mMonitor = - BackupManagerMonitorUtils.monitorEvent( - mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND, - packageInfo, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); + mBackupManagerMonitorEventSender.monitorEvent( + BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND, + packageInfo, + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + null); } void onPackageBackupComplete(String packageName, long size) { @@ -291,8 +287,7 @@ public class KeyValueBackupReporter { void onPackageBackupNonIncrementalRequired(PackageInfo packageInfo) { Slog.i(TAG, "Transport lost data, retrying package"); - BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, packageInfo, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, @@ -335,28 +330,24 @@ public class KeyValueBackupReporter { EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName); // Time-out used to be implemented as cancel w/ cancelAll = false. // TODO: Change monitoring event to reflect time-out as an event itself. - mMonitor = - BackupManagerMonitorUtils.monitorEvent( - mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, - packageInfo, - BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, - BackupManagerMonitorUtils.putMonitoringExtra( - null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, false)); + mBackupManagerMonitorEventSender.monitorEvent( + BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, + packageInfo, + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + mBackupManagerMonitorEventSender.putMonitoringExtra( + null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, false)); } void onAgentCancelled(@Nullable PackageInfo packageInfo) { String packageName = getPackageName(packageInfo); Slog.i(TAG, "Cancel backing up " + packageName); EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName); - mMonitor = - BackupManagerMonitorUtils.monitorEvent( - mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, - packageInfo, - BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, - BackupManagerMonitorUtils.putMonitoringExtra( - null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, true)); + mBackupManagerMonitorEventSender.monitorEvent( + BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, + packageInfo, + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + mBackupManagerMonitorEventSender.putMonitoringExtra( + null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, true)); } void onAgentResultError(@Nullable PackageInfo packageInfo) { diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java index 41e8092436b5..3a6e1cafa505 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -68,7 +68,7 @@ import com.android.server.backup.transport.BackupTransportClient; import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.transport.TransportNotAvailableException; import com.android.server.backup.utils.BackupEligibilityRules; -import com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import libcore.io.IoUtils; @@ -225,7 +225,8 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { boolean nonIncremental, BackupEligibilityRules backupEligibilityRules) { KeyValueBackupReporter reporter = - new KeyValueBackupReporter(backupManagerService, observer, monitor); + new KeyValueBackupReporter(backupManagerService, observer, + new BackupManagerMonitorEventSender(monitor)); KeyValueBackupTask task = new KeyValueBackupTask( backupManagerService, @@ -698,8 +699,9 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { try { extractAgentData(mCurrentPackage); - BackupManagerMonitorUtils.monitorAgentLoggingResults( - mReporter.getMonitor(), mCurrentPackage, mAgent); + BackupManagerMonitorEventSender mBackupManagerMonitorEventSender = + new BackupManagerMonitorEventSender(mReporter.getMonitor()); + mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mCurrentPackage, mAgent); int status = sendDataToTransport(mCurrentPackage); cleanUpAgentForTransportStatus(status); } catch (AgentException | TaskException e) { diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 8cbb5dc03b9e..e04bf11dad9f 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -16,6 +16,8 @@ package com.android.server.backup.restore; +import static android.app.backup.BackupAnnotations.OperationType.RESTORE; + import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; @@ -30,6 +32,7 @@ import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import android.annotation.Nullable; import android.app.ApplicationThreadConstants; import android.app.IBackupAgent; +import android.app.backup.BackupAnnotations; import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; import android.app.backup.BackupManagerMonitor; @@ -70,7 +73,7 @@ import com.android.server.backup.internal.OnTaskFinishedListener; import com.android.server.backup.transport.BackupTransportClient; import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.utils.BackupEligibilityRules; -import com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import libcore.io.IoUtils; @@ -84,7 +87,6 @@ import java.util.Objects; import java.util.Set; public class PerformUnifiedRestoreTask implements BackupRestoreTask { - private UserBackupManagerService backupManagerService; private final OperationStorage mOperationStorage; private final int mUserId; @@ -98,8 +100,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Restore observer; may be null private IRestoreObserver mObserver; - // BackuoManagerMonitor; may be null - private IBackupManagerMonitor mMonitor; + private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; // Token identifying the dataset to the transport private long mToken; @@ -181,6 +182,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { mUserId = 0; mBackupEligibilityRules = null; this.backupManagerService = backupManagerService; + mBackupManagerMonitorEventSender = + new BackupManagerMonitorEventSender(/*monitor*/null); } // This task can assume that the wakelock is properly held for it and doesn't have to worry @@ -208,7 +211,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { mTransportConnection = transportConnection; mObserver = observer; - mMonitor = monitor; + mBackupManagerMonitorEventSender = + new BackupManagerMonitorEventSender(monitor); mToken = restoreSetToken; mPmToken = pmToken; mTargetPackage = targetPackage; @@ -410,8 +414,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // If the requester of the restore has not passed in a monitor, we ask the transport // for one. - if (mMonitor == null) { - mMonitor = transport.getBackupManagerMonitor(); + if (mBackupManagerMonitorEventSender.getMonitor() == null) { + mBackupManagerMonitorEventSender.setMonitor(transport.getBackupManagerMonitor()); } mStatus = transport.startRestore(mToken, packages); @@ -425,10 +429,12 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { RestoreDescription desc = transport.nextRestorePackage(); if (desc == null) { Slog.e(TAG, "No restore metadata available; halting"); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE, mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + monitoringExtras); mStatus = BackupTransport.TRANSPORT_ERROR; executeNextState(UnifiedRestoreState.FINAL); return; @@ -437,10 +443,12 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { desc.getPackageName())) { Slog.e(TAG, "Required package metadata but got " + desc.getPackageName()); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_NO_PM_METADATA_RECEIVED, mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + monitoringExtras); mStatus = BackupTransport.TRANSPORT_ERROR; executeNextState(UnifiedRestoreState.FINAL); return; @@ -472,10 +480,12 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // the restore operation. if (!mPmAgent.hasMetadata()) { Slog.e(TAG, "PM agent has no metadata, so not restoring"); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA, mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + monitoringExtras); EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, PACKAGE_MANAGER_SENTINEL, "Package manager restore metadata missing"); @@ -492,10 +502,12 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } catch (Exception e) { // If we lost the transport at any time, halt Slog.e(TAG, "Unable to contact transport for restore: " + e.getMessage()); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_LOST_TRANSPORT, null, - BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null); + BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, + monitoringExtras); mStatus = BackupTransport.TRANSPORT_ERROR; backupManagerService.getBackupHandler().removeMessages( MSG_BACKUP_RESTORE_STEP, this); @@ -552,11 +564,12 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Whoops, we thought we could restore this package but it // turns out not to be present. Skip it. Slog.e(TAG, "Package not present: " + pkgName); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_PRESENT, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); + monitoringExtras); EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName, "Package missing on device"); nextState = UnifiedRestoreState.RUNNING_QUEUE; @@ -572,13 +585,15 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { String message = "Source version " + metaInfo.versionCode + " > installed version " + mCurrentPackage.getLongVersionCode(); Slog.w(TAG, "Package " + pkgName + ": " + message); - Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, + Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( + null, BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION, metaInfo.versionCode); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( + monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( monitoringExtras, BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, false); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + monitoringExtras = addRestoreOperationTypeToEvent(monitoringExtras); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -593,13 +608,15 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { + " > installed version " + mCurrentPackage.getLongVersionCode() + " but restoreAnyVersion"); } - Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, + Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( + null, BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION, metaInfo.versionCode); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( + monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( monitoringExtras, BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, true); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + monitoringExtras = addRestoreOperationTypeToEvent(monitoringExtras); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER, mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -652,9 +669,10 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { Slog.i(TAG, "Data exists for package " + packageName + " but app has no agent; skipping"); } - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_APP_HAS_NO_AGENT, mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null); + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, monitoringExtras); EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, "Package has no agent"); executeNextState(UnifiedRestoreState.RUNNING_QUEUE); @@ -665,9 +683,11 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); if (!BackupUtils.signaturesMatch(metaInfo.sigHashes, mCurrentPackage, pmi)) { Slog.w(TAG, "Signature mismatch restoring " + packageName); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_SIGNATURE_MISMATCH, mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + monitoringExtras); EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, "Signature mismatch"); executeNextState(UnifiedRestoreState.RUNNING_QUEUE); @@ -681,9 +701,11 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { mBackupEligibilityRules.getBackupDestination()); if (mAgent == null) { Slog.w(TAG, "Can't find backup agent for " + packageName); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_CANT_FIND_AGENT, mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null); + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + monitoringExtras); EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, "Restore agent missing"); executeNextState(UnifiedRestoreState.RUNNING_QUEUE); @@ -941,8 +963,9 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { EventLog.writeEvent(EventLogTags.FULL_RESTORE_PACKAGE, mCurrentPackage.packageName); - mEngine = new FullRestoreEngine(backupManagerService, mOperationStorage, this, null, - mMonitor, mCurrentPackage, false, mEphemeralOpToken, false, + mEngine = new FullRestoreEngine(backupManagerService, mOperationStorage, + this, null, mBackupManagerMonitorEventSender.getMonitor(), + mCurrentPackage, false, mEphemeralOpToken, false, mBackupEligibilityRules); mEngineThread = new FullRestoreEngineThread(mEngine, mEnginePipes[0]); @@ -1095,10 +1118,11 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { if (DEBUG) { Slog.w(TAG, "Full-data restore target timed out; shutting down"); } - - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT, - mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null); + mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + monitoringExtras); mEngineThread.handleTimeout(); IoUtils.closeQuietly(mEnginePipes[1]); @@ -1322,7 +1346,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Ask the agent for logs after doRestoreFinished() has completed executing to allow // it to finalize its logs. - BackupManagerMonitorUtils.monitorAgentLoggingResults(mMonitor, mCurrentPackage, + mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mCurrentPackage, mAgent); // Just go back to running the restore queue @@ -1358,9 +1382,10 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { public void handleCancel(boolean cancelAll) { mOperationStorage.removeOperation(mEphemeralOpToken); Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT, - mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null); + mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, monitoringExtras); EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, mCurrentPackage.packageName, "restore timeout"); // Handle like an agent that threw on invocation: wipe it and go on to the next @@ -1433,4 +1458,10 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } } } + + private Bundle addRestoreOperationTypeToEvent (@Nullable Bundle extra) { + return mBackupManagerMonitorEventSender.putMonitoringExtra( + extra, + BackupManagerMonitor.EXTRA_LOG_OPERATION_TYPE, RESTORE); + } } diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java new file mode 100644 index 000000000000..0b55ca21371b --- /dev/null +++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java @@ -0,0 +1,260 @@ +/* + * 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.backup.utils; + +import android.app.backup.BackupAnnotations; +import android.app.backup.BackupManagerMonitor; +import android.app.backup.BackupRestoreEventLogger; +import android.os.Bundle; +import android.os.Environment; +import android.util.Slog; + +import com.android.internal.util.FastPrintWriter; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; + + +/* + * Util class to parse a BMM event and write it to a text file, to be the printed in + * the backup dumpsys + * + * Note: this class is note thread safe + */ +public class BackupManagerMonitorDumpsysUtils { + + private static final String TAG = "BackupManagerMonitorDumpsysUtils"; + // Name of the subdirectory where the text file containing the BMM events will be stored. + // Same as {@link UserBackupManagerFiles} + private static final String BACKUP_PERSISTENT_DIR = "backup"; + + /** + * Parses the BackupManagerMonitor bundle for a RESTORE event in a series of strings that + * will be persisted in a text file and printed in the dumpsys. + * + * If the evenntBundle passed is not a RESTORE event, return early + * + * Key information related to the event: + * - Timestamp (HAS TO ALWAYS BE THE FIRST LINE OF EACH EVENT) + * - Event ID + * - Event Category + * - Operation type + * - Package name (can be null) + * - Agent logs (if available) + * + * Example of formatting: + * RESTORE Event: [2023-08-18 17:16:00.735] Agent - Agent logging results + * Package name: com.android.wallpaperbackup + * Agent Logs: + * Data Type: wlp_img_system + * Item restored: 0/1 + * Agent Error - Category: no_wallpaper, Count: 1 + * Data Type: wlp_img_lock + * Item restored: 0/1 + * Agent Error - Category: no_wallpaper, Count: 1 + */ + public void parseBackupManagerMonitorRestoreEventForDumpsys(Bundle eventBundle) { + if (eventBundle == null) { + return; + } + + if (!isOpTypeRestore(eventBundle)) { + //We only log Restore events + return; + } + + if (!eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_EVENT_ID) + || !eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY)) { + Slog.w(TAG, "Event id and category are not optional fields."); + return; + } + File bmmEvents = getBMMEventsFile(); + + try (FileOutputStream out = new FileOutputStream(bmmEvents, /*append*/ true); + PrintWriter pw = new FastPrintWriter(out);) { + + int eventCategory = eventBundle.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY); + int eventId = eventBundle.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID); + + if (eventId == BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS && + !hasAgentLogging(eventBundle)) { + // Do not record an empty agent logging event + return; + } + + pw.println("RESTORE Event: [" + timestamp() + "] " + + getCategory(eventCategory) + " - " + + getId(eventId)); + + if (eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME)) { + pw.println("\tPackage name: " + + eventBundle.getString(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME)); + } + + // TODO(b/296818666): add extras to the events + addAgentLogsIfAvailable(eventBundle, pw); + } catch (java.io.IOException e) { + Slog.e(TAG, "IO Exception when writing BMM events to file: " + e); + } + + } + + private boolean hasAgentLogging(Bundle eventBundle) { + if (eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS)) { + ArrayList<BackupRestoreEventLogger.DataTypeResult> agentLogs = + eventBundle.getParcelableArrayList( + BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS); + + return !agentLogs.isEmpty(); + } + return false; + } + + /** + * Extracts agent logs from the BackupManagerMonitor event. These logs detail: + * - the data type for the agent + * - the count of successfully restored items + * - the count of items that failed to restore + * - the metadata associated with this datatype + * - any errors + */ + private void addAgentLogsIfAvailable(Bundle eventBundle, PrintWriter pw) { + if (hasAgentLogging(eventBundle)) { + pw.println("\tAgent Logs:"); + ArrayList<BackupRestoreEventLogger.DataTypeResult> agentLogs = + eventBundle.getParcelableArrayList( + BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS); + for (BackupRestoreEventLogger.DataTypeResult result : agentLogs) { + int totalItems = result.getFailCount() + result.getSuccessCount(); + pw.println("\t\tData Type: " + result.getDataType()); + pw.println("\t\t\tItem restored: " + result.getSuccessCount() + "/" + + totalItems); + for (Map.Entry<String, Integer> entry : result.getErrors().entrySet()) { + pw.println("\t\t\tAgent Error - Category: " + + entry.getKey() + ", Count: " + entry.getValue()); + } + } + } + } + + /* + * Get the path of the text files which stores the BMM events + */ + public File getBMMEventsFile() { + File dataDir = new File(Environment.getDataDirectory(), BACKUP_PERSISTENT_DIR); + File fname = new File(dataDir, "bmmevents.txt"); + return fname; + } + + private String timestamp() { + long currentTime = System.currentTimeMillis(); + Date date = new Date(currentTime); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + return dateFormat.format(date); + } + + private String getCategory(int code) { + String category = switch (code) { + case BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT -> "Transport"; + case BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT -> "Agent"; + case BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY -> + "Backup Manager Policy"; + default -> "Unknown category code: " + code; + }; + return category; + } + + private String getId(int code) { + String id = switch (code) { + case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL -> "Full backup cancel"; + case BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY -> "Illegal key"; + case BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND -> "No data to send"; + case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE -> "Package ineligible"; + case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT -> + "Package key-value participant"; + case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED -> "Package stopped"; + case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND -> "Package not found"; + case BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED -> "Backup disabled"; + case BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED -> + "Device not provisioned"; + case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT -> + "Package transport not present"; + case BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT -> "Error preflight"; + case BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT -> "Quota hit preflight"; + case BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP -> "Exception full backup"; + case BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL -> + "Key-value backup cancel"; + case BackupManagerMonitor.LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE -> + "No restore metadata available"; + case BackupManagerMonitor.LOG_EVENT_ID_NO_PM_METADATA_RECEIVED -> + "No PM metadata received"; + case BackupManagerMonitor.LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA -> + "PM agent has no metadata"; + case BackupManagerMonitor.LOG_EVENT_ID_LOST_TRANSPORT -> "Lost transport"; + case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_PRESENT -> "Package not present"; + case BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER -> + "Restore version higher"; + case BackupManagerMonitor.LOG_EVENT_ID_APP_HAS_NO_AGENT -> "App has no agent"; + case BackupManagerMonitor.LOG_EVENT_ID_SIGNATURE_MISMATCH -> "Signature mismatch"; + case BackupManagerMonitor.LOG_EVENT_ID_CANT_FIND_AGENT -> "Can't find agent"; + case BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT -> + "Key-value restore timeout"; + case BackupManagerMonitor.LOG_EVENT_ID_RESTORE_ANY_VERSION -> "Restore any version"; + case BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH -> "Versions match"; + case BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER -> + "Version of backup older"; + case BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH -> + "Full restore signature mismatch"; + case BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO_AGENT -> "System app no agent"; + case BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE -> + "Full restore allow backup false"; + case BackupManagerMonitor.LOG_EVENT_ID_APK_NOT_INSTALLED -> "APK not installed"; + case BackupManagerMonitor.LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK -> + "Cannot restore without APK"; + case BackupManagerMonitor.LOG_EVENT_ID_MISSING_SIGNATURE -> "Missing signature"; + case BackupManagerMonitor.LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE -> + "Expected different package"; + case BackupManagerMonitor.LOG_EVENT_ID_UNKNOWN_VERSION -> "Unknown version"; + case BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT -> "Full restore timeout"; + case BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST -> "Corrupt manifest"; + case BackupManagerMonitor.LOG_EVENT_ID_WIDGET_METADATA_MISMATCH -> + "Widget metadata mismatch"; + case BackupManagerMonitor.LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION -> + "Widget unknown version"; + case BackupManagerMonitor.LOG_EVENT_ID_NO_PACKAGES -> "No packages"; + case BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL -> "Transport is null"; + case BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED -> + "Transport non-incremental backup required"; + case BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS -> "Agent logging results"; + default -> "Unknown log event ID: " + code; + }; + return id; + } + + private boolean isOpTypeRestore(Bundle eventBundle) { + return switch (eventBundle.getInt( + BackupManagerMonitor.EXTRA_LOG_OPERATION_TYPE, -1)) { + case BackupAnnotations.OperationType.RESTORE -> true; + default -> false; + }; + } +} diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java index 439b83687b8f..92e3107b6977 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java +++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java @@ -25,7 +25,6 @@ import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; -import android.annotation.NonNull; import android.annotation.Nullable; import android.app.IBackupAgent; import android.app.backup.BackupAnnotations.OperationType; @@ -37,6 +36,7 @@ import android.os.Bundle; import android.os.RemoteException; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AndroidFuture; import java.util.List; @@ -44,9 +44,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** - * Utility methods to communicate with BackupManagerMonitor. + * Utility methods to log BackupManagerMonitor events. */ -public class BackupManagerMonitorUtils { +public class BackupManagerMonitorEventSender { /** * Timeout for how long we wait before we give up on getting logs from a {@link IBackupAgent}. * We expect this to be very fast since the agent immediately returns whatever logs have been @@ -54,51 +54,77 @@ public class BackupManagerMonitorUtils { * for non-essential logs. */ private static final int AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS = 500; + @Nullable private IBackupManagerMonitor mMonitor; + private final BackupManagerMonitorDumpsysUtils mBackupManagerMonitorDumpsysUtils; + public BackupManagerMonitorEventSender(@Nullable IBackupManagerMonitor monitor) { + mMonitor = monitor; + mBackupManagerMonitorDumpsysUtils = new BackupManagerMonitorDumpsysUtils(); + } + + @VisibleForTesting + BackupManagerMonitorEventSender(@Nullable IBackupManagerMonitor monitor, + BackupManagerMonitorDumpsysUtils backupManagerMonitorDumpsysUtils) { + mMonitor = monitor; + mBackupManagerMonitorDumpsysUtils = backupManagerMonitorDumpsysUtils; + } + + public void setMonitor(IBackupManagerMonitor monitor) { + mMonitor = monitor; + } + + public IBackupManagerMonitor getMonitor() { + return mMonitor; + } /** * Notifies monitor about the event. * * Calls {@link IBackupManagerMonitor#onEvent(Bundle)} with a bundle representing current event. * - * @param monitor - implementation of {@link IBackupManagerMonitor} to notify. * @param id - event id. * @param pkg - package event is related to. * @param category - event category. * @param extras - additional event data. - * @return <code>monitor</code> if call succeeded and <code>null</code> otherwise. */ - @Nullable - public static IBackupManagerMonitor monitorEvent( - @Nullable IBackupManagerMonitor monitor, + public void monitorEvent( int id, PackageInfo pkg, int category, Bundle extras) { - if (monitor != null) { - try { - Bundle bundle = new Bundle(); - bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id); - bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, category); - if (pkg != null) { - bundle.putString(EXTRA_LOG_EVENT_PACKAGE_NAME, - pkg.packageName); - bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION, - pkg.versionCode); - bundle.putLong(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION, - pkg.getLongVersionCode()); - } - if (extras != null) { - bundle.putAll(extras); + try { + Bundle bundle = new Bundle(); + bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id); + bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, category); + if (pkg != null) { + bundle.putString(EXTRA_LOG_EVENT_PACKAGE_NAME, + pkg.packageName); + bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION, + pkg.versionCode); + bundle.putLong(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION, + pkg.getLongVersionCode()); + } + if (extras != null) { + bundle.putAll(extras); + if (extras.containsKey(EXTRA_LOG_OPERATION_TYPE) && + extras.getInt(EXTRA_LOG_OPERATION_TYPE) == OperationType.RESTORE){ + mBackupManagerMonitorDumpsysUtils + .parseBackupManagerMonitorRestoreEventForDumpsys(bundle); } - monitor.onEvent(bundle); - return monitor; - } catch (RemoteException e) { + } + + if (mMonitor != null) { + mMonitor.onEvent(bundle); + } else { if (DEBUG) { - Slog.w(TAG, "backup manager monitor went away"); + Slog.w(TAG, "backup manager monitor is null unable to send event"); } } + } catch (RemoteException e) { + mMonitor = null; + if (DEBUG) { + Slog.w(TAG, "backup manager monitor went away"); + } } - return null; } /** @@ -108,17 +134,12 @@ public class BackupManagerMonitorUtils { * <p>Note that this method does two separate binder calls (one to the agent and one to the * monitor). * - * @param monitor - implementation of {@link IBackupManagerMonitor} to notify. * @param pkg - package the {@code agent} belongs to. * @param agent - the {@link IBackupAgent} to retrieve logs from. - * @return {@code null} if the monitor is null. {@code monitor} if we fail to retrieve the logs - * from the {@code agent}. Otherwise, the result of {@link - * #monitorEvent(IBackupManagerMonitor, int, PackageInfo, int, Bundle)}. */ - public static IBackupManagerMonitor monitorAgentLoggingResults( - @Nullable IBackupManagerMonitor monitor, PackageInfo pkg, IBackupAgent agent) { - if (monitor == null) { - return null; + public void monitorAgentLoggingResults(PackageInfo pkg, IBackupAgent agent) { + if (mMonitor == null) { + Slog.i(TAG, "backup manager monitor is null unable to send event"+pkg); } try { @@ -127,7 +148,7 @@ public class BackupManagerMonitorUtils { AndroidFuture<Integer> operationTypeFuture = new AndroidFuture<>(); agent.getLoggerResults(resultsFuture); agent.getOperationType(operationTypeFuture); - return sendAgentLoggingResults(monitor, pkg, + sendAgentLoggingResults(pkg, resultsFuture.get(AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS), operationTypeFuture.get(AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); @@ -136,18 +157,15 @@ public class BackupManagerMonitorUtils { } catch (Exception e) { Slog.w(TAG, "Failed to retrieve logging results from agent", e); } - return monitor; } - public static IBackupManagerMonitor sendAgentLoggingResults( - @NonNull IBackupManagerMonitor monitor, PackageInfo pkg, List<DataTypeResult> results, + public void sendAgentLoggingResults(PackageInfo pkg, List<DataTypeResult> results, @OperationType int operationType) { Bundle loggerResultsBundle = new Bundle(); loggerResultsBundle.putParcelableList( EXTRA_LOG_AGENT_LOGGING_RESULTS, results); loggerResultsBundle.putInt(EXTRA_LOG_OPERATION_TYPE, operationType); - return monitorEvent( - monitor, + monitorEvent( LOG_EVENT_ID_AGENT_LOGGING_RESULTS, pkg, LOG_EVENT_CATEGORY_AGENT, diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java index 71ca8ca24604..78a9952d066d 100644 --- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java +++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java @@ -85,7 +85,8 @@ public class TarBackupReader { private final InputStream mInputStream; private final BytesReadListener mBytesReadListener; - private IBackupManagerMonitor mMonitor; + + private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; // Widget blob to be restored out-of-band. private byte[] mWidgetData = null; @@ -94,7 +95,7 @@ public class TarBackupReader { IBackupManagerMonitor monitor) { mInputStream = inputStream; mBytesReadListener = bytesReadListener; - mMonitor = monitor; + mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(monitor); } /** @@ -323,24 +324,22 @@ public class TarBackupReader { return sigs; } else { Slog.i(TAG, "Missing signature on backed-up package " + info.packageName); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_MISSING_SIGNATURE, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra(null, + mBackupManagerMonitorEventSender.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName)); } } else { Slog.i(TAG, "Expected package " + info.packageName + " but restore manifest claims " + manifestPackage); - Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, - EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( + Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( + null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); + monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( monitoringExtras, EXTRA_LOG_MANIFEST_PACKAGE_NAME, manifestPackage); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -349,12 +348,11 @@ public class TarBackupReader { } else { Slog.i(TAG, "Unknown restore manifest version " + version + " for package " + info.packageName); - Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, - EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras, - EXTRA_LOG_EVENT_PACKAGE_VERSION, version); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( + null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); + monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( + monitoringExtras, EXTRA_LOG_EVENT_PACKAGE_VERSION, version); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_UNKNOWN_VERSION, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -363,12 +361,12 @@ public class TarBackupReader { } } catch (NumberFormatException e) { Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, + mBackupManagerMonitorEventSender.putMonitoringExtra(null, + EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName)); } catch (IllegalArgumentException e) { Slog.w(TAG, e.getMessage()); @@ -436,8 +434,7 @@ public class TarBackupReader { if ((pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) { Slog.i(TAG, "Package has restoreAnyVersion; taking data"); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_RESTORE_ANY_VERSION, pkgInfo, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -446,8 +443,7 @@ public class TarBackupReader { } else if (pkgInfo.getLongVersionCode() >= info.version) { Slog.i(TAG, "Sig + version match; taking data"); policy = RestorePolicy.ACCEPT; - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_VERSIONS_MATCH, pkgInfo, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -466,12 +462,11 @@ public class TarBackupReader { } else { Slog.i(TAG, "Data requires newer version " + info.version + "; ignoring"); - mMonitor = BackupManagerMonitorUtils - .monitorEvent(mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER, pkgInfo, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils + mBackupManagerMonitorEventSender .putMonitoringExtra( null, EXTRA_LOG_OLD_VERSION, @@ -484,8 +479,7 @@ public class TarBackupReader { Slog.w(TAG, "Restore manifest signatures do not match " + "installed application for " + info.packageName); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH, pkgInfo, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -494,8 +488,7 @@ public class TarBackupReader { } else { Slog.w(TAG, "Package " + info.packageName + " is system level with no agent"); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_SYSTEM_APP_NO_AGENT, pkgInfo, LOG_EVENT_CATEGORY_AGENT, @@ -506,8 +499,7 @@ public class TarBackupReader { Slog.i(TAG, "Restore manifest from " + info.packageName + " but allowBackup=false"); } - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE, pkgInfo, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -526,14 +518,13 @@ public class TarBackupReader { } else { policy = RestorePolicy.IGNORE; } - Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( + Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( + monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( monitoringExtras, EXTRA_LOG_POLICY_ALLOW_APKS, allowApks); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_APK_NOT_INSTALLED, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -543,12 +534,11 @@ public class TarBackupReader { if (policy == RestorePolicy.ACCEPT_IF_APK && !info.hasApk) { Slog.i(TAG, "Cannot restore package " + info.packageName + " without the matching .apk"); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra(null, + mBackupManagerMonitorEventSender.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName)); } @@ -632,12 +622,11 @@ public class TarBackupReader { "Metadata mismatch: package " + info.packageName + " but widget data for " + pkg); - Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, + Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras, - BackupManagerMonitor.EXTRA_LOG_WIDGET_PACKAGE_NAME, pkg); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra( + monitoringExtras, BackupManagerMonitor.EXTRA_LOG_WIDGET_PACKAGE_NAME, pkg); + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_WIDGET_METADATA_MISMATCH, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -646,13 +635,12 @@ public class TarBackupReader { } else { Slog.w(TAG, "Unsupported metadata version " + version); - Bundle monitoringExtras = BackupManagerMonitorUtils + Bundle monitoringExtras = mBackupManagerMonitorEventSender .putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras, + monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(monitoringExtras, EXTRA_LOG_EVENT_PACKAGE_VERSION, version); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, + mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION, null, LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, @@ -810,7 +798,7 @@ public class TarBackupReader { } public IBackupManagerMonitor getMonitor() { - return mMonitor; + return mBackupManagerMonitorEventSender.getMonitor(); } public byte[] getWidgetData() { diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 315972cde76b..f59417046c85 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -1225,15 +1225,13 @@ public class ContentCaptureManagerService extends public ContentCaptureOptions getOptions(@UserIdInt int userId, @NonNull String packageName) { boolean isContentCaptureReceiverEnabled; - boolean isContentProtectionReceiverEnabled; + boolean isContentProtectionReceiverEnabled = + isContentProtectionReceiverEnabled(userId, packageName); ArraySet<ComponentName> whitelistedComponents = null; synchronized (mGlobalWhitelistStateLock) { isContentCaptureReceiverEnabled = isContentCaptureReceiverEnabled(userId, packageName); - isContentProtectionReceiverEnabled = - isContentProtectionReceiverEnabled(userId, packageName); - if (!isContentCaptureReceiverEnabled) { // Full package is not allowlisted: check individual components next whitelistedComponents = getWhitelistedComponents(userId, packageName); diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags index b67e62703067..d47a3991a966 100644 --- a/services/core/java/com/android/server/EventLogTags.logtags +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -81,7 +81,7 @@ option java_package com.android.server # when a notification has been clicked 27520 notification_clicked (key|3),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1) # when a notification action button has been clicked -27521 notification_action_clicked (key|3),(action_index|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1) +27521 notification_action_clicked (key|3),(piIdentifier|3),(pendingIntent|3),(action_index|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1) # when a notification has been canceled 27530 notification_canceled (key|3),(reason|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1),(count|1),(listener|3) # replaces 27510 with a row per notification diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 73dbb86ae7cc..d47573d52767 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -73,8 +73,8 @@ import android.content.pm.ProviderInfo; import android.content.pm.UserInfo; import android.content.res.ObbInfo; import android.database.ContentObserver; -import android.media.MediaCodecList; import android.media.MediaCodecInfo; +import android.media.MediaCodecList; import android.media.MediaFormat; import android.net.Uri; import android.os.BatteryManager; @@ -219,6 +219,8 @@ class StorageManagerService extends IStorageManager.Stub @GuardedBy("mLock") private final Set<Integer> mCeStoragePreparedUsers = new ArraySet<>(); + private volatile long mInternalStorageSize = 0; + public static class Lifecycle extends SystemService { private StorageManagerService mStorageManagerService; @@ -3479,6 +3481,15 @@ class StorageManagerService extends IStorageManager.Stub return authority; } + @Override + public long getInternalStorageBlockDeviceSize() throws RemoteException { + if (mInternalStorageSize == 0) { + mInternalStorageSize = mVold.getStorageSize(); + } + + return mInternalStorageSize; + } + /** * Enforces that the caller is the {@link ExternalStorageService} * diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 9f9e2eb38288..b5cab17043c7 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -391,7 +391,6 @@ public class AudioDeviceBroker { final boolean wasBtScoRequested = isBluetoothScoRequested(); CommunicationRouteClient client; - // Save previous client route in case of failure to start BT SCO audio AudioDeviceAttributes prevClientDevice = null; boolean prevPrivileged = false; @@ -1043,7 +1042,7 @@ public class AudioDeviceBroker { synchronized (mBluetoothAudioStateLock) { mBluetoothScoOn = on; updateAudioHalBluetoothState(); - postUpdateCommunicationRouteClient(eventSource); + postUpdateCommunicationRouteClient(isBluetoothScoRequested(), eventSource); } } @@ -1395,8 +1394,10 @@ public class AudioDeviceBroker { MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset); } - /*package*/ void postUpdateCommunicationRouteClient(String eventSource) { - sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, eventSource); + /*package*/ void postUpdateCommunicationRouteClient( + boolean wasBtScoRequested, String eventSource) { + sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, + wasBtScoRequested ? 1 : 0, eventSource); } /*package*/ void postSetCommunicationDeviceForClient(CommunicationDeviceInfo info) { @@ -1708,7 +1709,8 @@ public class AudioDeviceBroker { : AudioSystem.STREAM_DEFAULT); if (btInfo.mProfile == BluetoothProfile.LE_AUDIO || btInfo.mProfile == BluetoothProfile.HEARING_AID) { - onUpdateCommunicationRouteClient("setBluetoothActiveDevice"); + onUpdateCommunicationRouteClient(isBluetoothScoRequested(), + "setBluetoothActiveDevice"); } } } @@ -1762,9 +1764,11 @@ public class AudioDeviceBroker { case MSG_I_SET_MODE_OWNER: synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { + boolean wasBtScoRequested = isBluetoothScoRequested(); mAudioModeOwner = (AudioModeInfo) msg.obj; if (mAudioModeOwner.mMode != AudioSystem.MODE_RINGTONE) { - onUpdateCommunicationRouteClient("setNewModeOwner"); + onUpdateCommunicationRouteClient( + wasBtScoRequested, "setNewModeOwner"); } } } @@ -1787,10 +1791,10 @@ public class AudioDeviceBroker { } break; - case MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT: + case MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT: synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { - onUpdateCommunicationRouteClient((String) msg.obj); + onUpdateCommunicationRouteClient(msg.arg1 == 1, (String) msg.obj); } } break; @@ -1973,7 +1977,7 @@ public class AudioDeviceBroker { private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38; private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42; - private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43; + private static final int MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43; private static final int MSG_I_SCO_AUDIO_STATE_CHANGED = 44; private static final int MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT = 45; @@ -2330,16 +2334,20 @@ public class AudioDeviceBroker { */ // @GuardedBy("mSetModeLock") @GuardedBy("mDeviceStateLock") - private void onUpdateCommunicationRouteClient(String eventSource) { - updateCommunicationRoute(eventSource); + private void onUpdateCommunicationRouteClient(boolean wasBtScoRequested, String eventSource) { CommunicationRouteClient crc = topCommunicationRouteClient(); if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " - + crc + " eventSource: " + eventSource); + Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " + crc + + " wasBtScoRequested: " + wasBtScoRequested + " eventSource: " + eventSource); } if (crc != null) { setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(), BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource); + } else { + if (!isBluetoothScoRequested() && wasBtScoRequested) { + mBtHelper.stopBluetoothSco(eventSource); + } + updateCommunicationRoute(eventSource); } } @@ -2403,7 +2411,11 @@ public class AudioDeviceBroker { } @GuardedBy("mDeviceStateLock") - private boolean communnicationDeviceCompatOn() { + // LE Audio: For system server (Telecom) and APKs targeting S and above, we let the audio + // policy routing rules select the default communication device. + // For older APKs, we force LE Audio headset when connected as those APKs cannot select a LE + // Audiodevice explicitly. + private boolean communnicationDeviceLeAudioCompatOn() { return mAudioModeOwner.mMode == AudioSystem.MODE_IN_COMMUNICATION && !(CompatChanges.isChangeEnabled( USE_SET_COMMUNICATION_DEVICE, mAudioModeOwner.mUid) @@ -2411,19 +2423,25 @@ public class AudioDeviceBroker { } @GuardedBy("mDeviceStateLock") + // Hearing Aid: For system server (Telecom) and IN_CALL mode we let the audio + // policy routing rules select the default communication device. + // For 3p apps and IN_COMMUNICATION mode we force Hearing aid when connected to maintain + // backwards compatibility + private boolean communnicationDeviceHaCompatOn() { + return mAudioModeOwner.mMode == AudioSystem.MODE_IN_COMMUNICATION + && !(mAudioModeOwner.mUid == android.os.Process.SYSTEM_UID); + } + + @GuardedBy("mDeviceStateLock") AudioDeviceAttributes getDefaultCommunicationDevice() { - // For system server (Telecom) and APKs targeting S and above, we let the audio - // policy routing rules select the default communication device. - // For older APKs, we force Hearing Aid or LE Audio headset when connected as - // those APKs cannot select a LE Audio or Hearing Aid device explicitly. AudioDeviceAttributes device = null; - if (communnicationDeviceCompatOn()) { - // If both LE and Hearing Aid are active (thie should not happen), - // priority to Hearing Aid. + // If both LE and Hearing Aid are active (thie should not happen), + // priority to Hearing Aid. + if (communnicationDeviceHaCompatOn()) { device = mDeviceInventory.getDeviceOfType(AudioSystem.DEVICE_OUT_HEARING_AID); - if (device == null) { - device = mDeviceInventory.getDeviceOfType(AudioSystem.DEVICE_OUT_BLE_HEADSET); - } + } + if (device == null && communnicationDeviceLeAudioCompatOn()) { + device = mDeviceInventory.getDeviceOfType(AudioSystem.DEVICE_OUT_BLE_HEADSET); } return device; } @@ -2433,6 +2451,7 @@ public class AudioDeviceBroker { List<AudioRecordingConfiguration> recordConfigs) { synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { + final boolean wasBtScoRequested = isBluetoothScoRequested(); boolean updateCommunicationRoute = false; for (CommunicationRouteClient crc : mCommunicationRouteClients) { boolean wasActive = crc.isActive(); @@ -2461,7 +2480,8 @@ public class AudioDeviceBroker { } } if (updateCommunicationRoute) { - postUpdateCommunicationRouteClient("updateCommunicationRouteClientsActivity"); + postUpdateCommunicationRouteClient( + wasBtScoRequested, "updateCommunicationRouteClientsActivity"); } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6a73c2b85aaa..ceb96efd2e72 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -10691,6 +10691,27 @@ public class AudioService extends IAudioService.Stub @Override @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public boolean isCsdAsAFeatureAvailable() { + super.isCsdAsAFeatureAvailable_enforcePermission(); + return mSoundDoseHelper.isCsdAsAFeatureAvailable(); + } + + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public boolean isCsdAsAFeatureEnabled() { + super.isCsdAsAFeatureEnabled_enforcePermission(); + return mSoundDoseHelper.isCsdAsAFeatureEnabled(); + } + + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void setCsdAsAFeatureEnabled(boolean csdToggleValue) { + super.setCsdAsAFeatureEnabled_enforcePermission(); + mSoundDoseHelper.setCsdAsAFeatureEnabled(csdToggleValue); + } + + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void setBluetoothAudioDeviceCategory(@NonNull String address, boolean isBle, @AudioDeviceCategory int btAudioDeviceCategory) { super.setBluetoothAudioDeviceCategory_enforcePermission(); diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 3560797ce2cf..a4d26d3e96c0 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -329,7 +329,7 @@ public class BtHelper { default: break; } - if(broadcast) { + if (broadcast) { broadcastScoConnectionState(scoAudioState); //FIXME: this is to maintain compatibility with deprecated intent // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate. @@ -718,8 +718,10 @@ public class BtHelper { checkScoAudioState(); if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { // Make sure that the state transitions to CONNECTING even if we cannot initiate - // the connection. - broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING); + // the connection except if already connected internally + if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL) { + broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING); + } switch (mScoAudioState) { case SCO_STATE_INACTIVE: mScoAudioMode = scoAudioMode; @@ -775,7 +777,7 @@ public class BtHelper { broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED); break; case SCO_STATE_ACTIVE_INTERNAL: - Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return"); + // Already in ACTIVE mode, simply return break; case SCO_STATE_ACTIVE_EXTERNAL: /* Confirm SCO Audio connection to requesting app as it is already connected diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index 851c5c3cb73c..5ebc1c055b50 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -37,13 +37,11 @@ import android.media.ISoundDose; import android.media.ISoundDoseCallback; import android.media.SoundDoseRecord; import android.os.Binder; -import android.os.HandlerExecutor; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; -import android.provider.DeviceConfig; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; @@ -143,8 +141,6 @@ public class SoundDoseHelper { private static final int SAFE_MEDIA_VOLUME_UNINITIALIZED = -1; - private static final String FEATURE_FLAG_ENABLE_CSD = "enable_csd"; - private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE, "CSD updates"); @@ -193,7 +189,15 @@ public class SoundDoseHelper { private final AtomicBoolean mEnableCsd = new AtomicBoolean(false); - private ArrayList<ISoundDose.AudioDeviceCategory> mCachedAudioDeviceCategories = + private final Object mCsdAsAFeatureLock = new Object(); + + @GuardedBy("mCsdAsAFeatureLock") + private boolean mIsCsdAsAFeatureAvailable = false; + + @GuardedBy("mCsdAsAFeatureLock") + private boolean mIsCsdAsAFeatureEnabled = false; + + private final ArrayList<ISoundDose.AudioDeviceCategory> mCachedAudioDeviceCategories = new ArrayList<>(); private final Object mCsdStateLock = new Object(); @@ -315,10 +319,6 @@ public class SoundDoseHelper { mAlarmManager = (AlarmManager) mContext.getSystemService( Context.ALARM_SERVICE); - - DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_MEDIA, - new HandlerExecutor(mAudioHandler), - p -> updateCsdEnabled("onPropertiesChanged")); } void initSafeVolumes() { @@ -494,6 +494,38 @@ public class SoundDoseHelper { return false; } + boolean isCsdAsAFeatureAvailable() { + synchronized (mCsdAsAFeatureLock) { + return mIsCsdAsAFeatureAvailable; + } + } + + boolean isCsdAsAFeatureEnabled() { + synchronized (mCsdAsAFeatureLock) { + return mIsCsdAsAFeatureEnabled; + } + } + + void setCsdAsAFeatureEnabled(boolean csdAsAFeatureEnabled) { + boolean doUpdate; + synchronized (mCsdAsAFeatureLock) { + doUpdate = mIsCsdAsAFeatureEnabled != csdAsAFeatureEnabled && mIsCsdAsAFeatureAvailable; + mIsCsdAsAFeatureEnabled = csdAsAFeatureEnabled; + final long callingIdentity = Binder.clearCallingIdentity(); + try { + mSettings.putSecureIntForUser(mAudioService.getContentResolver(), + Settings.Secure.AUDIO_SAFE_CSD_AS_A_FEATURE_ENABLED, + mIsCsdAsAFeatureEnabled ? 1 : 0, UserHandle.USER_CURRENT); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + if (doUpdate) { + updateCsdEnabled("setCsdAsAFeatureEnabled"); + } + } + void setAudioDeviceCategory(String address, int internalAudioType, boolean isHeadphone) { if (!mEnableCsd.get()) { return; @@ -864,6 +896,13 @@ public class SoundDoseHelper { Log.e(TAG, "Exception while forcing the internal MEL computation", e); } + synchronized (mCsdAsAFeatureLock) { + mIsCsdAsAFeatureEnabled = mSettings.getSecureIntForUser( + mAudioService.getContentResolver(), + Settings.Secure.AUDIO_SAFE_CSD_AS_A_FEATURE_ENABLED, 0, + UserHandle.USER_CURRENT) != 0; + } + synchronized (mCsdStateLock) { if (mGlobalTimeOffsetInSecs == GLOBAL_TIME_OFFSET_UNINITIALIZED) { mGlobalTimeOffsetInSecs = System.currentTimeMillis() / 1000L; @@ -908,18 +947,23 @@ public class SoundDoseHelper { @GuardedBy("mSafeMediaVolumeStateLock") private void updateSafeMediaVolume_l(String caller) { - boolean safeMediaVolumeEnabled = - SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_FORCE, false) - || (mContext.getResources().getBoolean( - com.android.internal.R.bool.config_safe_media_volume_enabled) - && !mEnableCsd.get()); boolean safeMediaVolumeBypass = - SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_BYPASS, false); + SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_BYPASS, false) + || mEnableCsd.get(); + boolean safeMediaVolumeForce = SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_FORCE, + false); + // we are using the MCC overlaid legacy flag used for the safe volume enablement + // to determine whether the MCC enforces any safe hearing standard. + boolean mccEnforcedSafeMediaVolume = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_safe_media_volume_enabled); + + boolean safeVolumeEnabled = + (mccEnforcedSafeMediaVolume || safeMediaVolumeForce) && !safeMediaVolumeBypass; // The persisted state is either "disabled" or "active": this is the state applied // next time we boot and cannot be "inactive" int persistedState; - if (safeMediaVolumeEnabled && !safeMediaVolumeBypass) { + if (safeVolumeEnabled) { persistedState = SAFE_MEDIA_VOLUME_ACTIVE; // The state can already be "inactive" here if the user has forced it before // the 30 seconds timeout for forced configuration. In this case we don't reset @@ -946,22 +990,28 @@ public class SoundDoseHelper { } private void updateCsdEnabled(String caller) { - boolean newEnableCsd = SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE, - false); - if (!newEnableCsd) { - final String featureFlagEnableCsdValue = DeviceConfig.getProperty( - DeviceConfig.NAMESPACE_MEDIA, - FEATURE_FLAG_ENABLE_CSD); - if (featureFlagEnableCsdValue != null) { - newEnableCsd = Boolean.parseBoolean(featureFlagEnableCsdValue); + boolean csdForce = SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE, false); + // we are using the MCC overlaid legacy flag used for the safe volume enablement + // to determine whether the MCC enforces any safe hearing standard. + boolean mccEnforcedSafeMedia = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_safe_media_volume_enabled); + boolean csdEnable = mContext.getResources().getBoolean( + R.bool.config_safe_sound_dosage_enabled); + boolean newEnabledCsd = (mccEnforcedSafeMedia && csdEnable) || csdForce; + + synchronized (mCsdAsAFeatureLock) { + if (!mccEnforcedSafeMedia && csdEnable) { + mIsCsdAsAFeatureAvailable = true; + newEnabledCsd = mIsCsdAsAFeatureEnabled || csdForce; + Log.v(TAG, caller + ": CSD as a feature is not enforced and enabled: " + + newEnabledCsd); } else { - newEnableCsd = mContext.getResources().getBoolean( - R.bool.config_safe_sound_dosage_enabled); + mIsCsdAsAFeatureAvailable = false; } } - if (mEnableCsd.compareAndSet(!newEnableCsd, newEnableCsd)) { - Log.i(TAG, caller + ": enable CSD " + newEnableCsd); + if (mEnableCsd.compareAndSet(!newEnabledCsd, newEnabledCsd)) { + Log.i(TAG, caller + ": enabled CSD " + newEnabledCsd); initCsd(); synchronized (mSafeMediaVolumeStateLock) { diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java index 97e5c6fbd8c3..356b30103c2f 100644 --- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java +++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java @@ -26,6 +26,7 @@ import android.content.pm.PackageManager; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.UserHandle; +import android.util.Slog; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -54,6 +55,7 @@ public class AuthenticationStatsCollector { private final float mThreshold; private final int mModality; + private boolean mPersisterInitialized = false; @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap; @@ -85,9 +87,15 @@ public class AuthenticationStatsCollector { } private void initializeUserAuthenticationStatsMap() { - mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext); - for (AuthenticationStats stats : mAuthenticationStatsPersister.getAllFrrStats(mModality)) { - mUserAuthenticationStatsMap.put(stats.getUserId(), stats); + try { + mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext); + for (AuthenticationStats stats : + mAuthenticationStatsPersister.getAllFrrStats(mModality)) { + mUserAuthenticationStatsMap.put(stats.getUserId(), stats); + } + mPersisterInitialized = true; + } catch (IllegalStateException e) { + Slog.w(TAG, "Failed to initialize AuthenticationStatsPersister.", e); } } @@ -108,7 +116,9 @@ public class AuthenticationStatsCollector { authenticationStats.authenticate(authenticated); - persistDataIfNeeded(userId); + if (mPersisterInitialized) { + persistDataIfNeeded(userId); + } sendNotificationIfNeeded(userId); } @@ -166,11 +176,13 @@ public class AuthenticationStatsCollector { } private void onUserRemoved(final int userId) { - if (mAuthenticationStatsPersister == null) { + if (!mPersisterInitialized) { initializeUserAuthenticationStatsMap(); } - mUserAuthenticationStatsMap.remove(userId); - mAuthenticationStatsPersister.removeFrrStats(userId); + if (mPersisterInitialized) { + mUserAuthenticationStatsMap.remove(userId); + mAuthenticationStatsPersister.removeFrrStats(userId); + } } /** diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java index d57dc471694e..8642fb888556 100644 --- a/services/core/java/com/android/server/display/DisplayDevice.java +++ b/services/core/java/com/android/server/display/DisplayDevice.java @@ -16,6 +16,9 @@ package com.android.server.display; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + import android.annotation.Nullable; import android.content.Context; import android.graphics.Point; @@ -132,12 +135,15 @@ abstract class DisplayDevice { /** * Returns the default size of the surface associated with the display, or null if the surface * is not provided for layer mirroring by SurfaceFlinger. For non virtual displays, this will - * be the actual display device's size. + * be the actual display device's size, reflecting the current rotation. */ @Nullable public Point getDisplaySurfaceDefaultSizeLocked() { DisplayDeviceInfo displayDeviceInfo = getDisplayDeviceInfoLocked(); - return new Point(displayDeviceInfo.width, displayDeviceInfo.height); + final boolean isRotated = mCurrentOrientation == ROTATION_90 + || mCurrentOrientation == ROTATION_270; + return isRotated ? new Point(displayDeviceInfo.height, displayDeviceInfo.width) + : new Point(displayDeviceInfo.width, displayDeviceInfo.height); } /** @@ -358,7 +364,7 @@ abstract class DisplayDevice { } boolean isRotated = (mCurrentOrientation == Surface.ROTATION_90 - || mCurrentOrientation == Surface.ROTATION_270); + || mCurrentOrientation == ROTATION_270); DisplayDeviceInfo info = getDisplayDeviceInfoLocked(); viewport.deviceWidth = isRotated ? info.height : info.width; viewport.deviceHeight = isRotated ? info.width : info.height; diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index e5965eff7070..486cd288753e 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -2080,7 +2080,7 @@ public class DisplayDeviceConfig { /** Loads the refresh rate profiles. */ private void loadRefreshRateZoneProfiles(RefreshRateConfigs refreshRateConfigs) { - if (refreshRateConfigs == null) { + if (refreshRateConfigs == null || refreshRateConfigs.getRefreshRateZoneProfiles() == null) { return; } for (RefreshRateZone zone : diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index 4edc8bc3eceb..9c271ff54d6b 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -441,6 +441,9 @@ final class LogicalDisplay { if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED) != 0) { mBaseDisplayInfo.flags |= Display.FLAG_ALWAYS_UNLOCKED; } + if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT) != 0) { + mBaseDisplayInfo.flags |= Display.FLAG_ROTATES_WITH_CONTENT; + } if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_TOUCH_FEEDBACK_DISABLED) != 0) { mBaseDisplayInfo.flags |= Display.FLAG_TOUCH_FEEDBACK_DISABLED; } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 2a617c529f38..6509126dcc60 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -6132,6 +6132,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVisibilityStateComputer.dump(pw); p.println(" mInFullscreenMode=" + mInFullscreenMode); p.println(" mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive); + p.println(" ENABLE_HIDE_IME_CAPTION_BAR=" + + InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR); p.println(" mSettingsObserver=" + mSettingsObserver); p.println(" mStylusIds=" + (mStylusIds != null ? Arrays.toString(mStylusIds.toArray()) : "")); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 15a8c0fb8290..c24e729cbff5 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1194,7 +1194,9 @@ public class NotificationManagerService extends SystemService { mNotificationRecordLogger.log( NotificationRecordLogger.NotificationEvent.fromAction(actionIndex, generatedByAssistant, action.isContextual()), r); - EventLogTags.writeNotificationActionClicked(key, actionIndex, + EventLogTags.writeNotificationActionClicked(key, + action.actionIntent.getTarget().toString(), + action.actionIntent.getIntent().toString(), actionIndex, r.getLifespanMs(now), r.getFreshnessMs(now), r.getExposureMs(now), nv.rank, nv.count); nv.recycle(); diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index f0e38955f050..d662aaedb774 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -67,6 +67,7 @@ import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.Context; @@ -311,6 +312,19 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final long SILENT_INSTALL_ALLOWED = 265131695L; /** + * The system supports pre-approval and update ownership features from + * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE API 34}. The change id is used to make sure + * the system includes the fix of pre-approval with update ownership case. When checking the + * change id, if it is disabled, it means the build includes the fix. The more detail is on + * b/293644536. + * See {@link PackageInstaller.SessionParams#setRequestUpdateOwnership(boolean)} and + * {@link #requestUserPreapproval(PreapprovalDetails, IntentSender)} for more details. + */ + @Disabled + @ChangeId + private static final long PRE_APPROVAL_WITH_UPDATE_OWNERSHIP_FIX = 293644536L; + + /** * The default value of {@link #mValidatedTargetSdk} is {@link Integer#MAX_VALUE}. If {@link * #mValidatedTargetSdk} is compared with {@link Build.VERSION_CODES#S} before getting the * target sdk version from a validated apk in {@link #validateApkInstallLocked()}, the compared @@ -893,16 +907,27 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (mPermissionsManuallyAccepted) { return USER_ACTION_NOT_NEEDED; } - packageName = mPackageName; + // For pre-pappvoal case, the mPackageName would be null. + if (mPackageName != null) { + packageName = mPackageName; + } else if (mPreapprovalRequested.get() && mPreapprovalDetails != null) { + packageName = mPreapprovalDetails.getPackageName(); + } else { + packageName = null; + } hasDeviceAdminReceiver = mHasDeviceAdminReceiver; } - final boolean forcePermissionPrompt = + // For the below cases, force user action prompt + // 1. installFlags includes INSTALL_FORCE_PERMISSION_PROMPT + // 2. params.requireUserAction is USER_ACTION_REQUIRED + final boolean forceUserActionPrompt = (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0 || params.requireUserAction == SessionParams.USER_ACTION_REQUIRED; - if (forcePermissionPrompt) { - return USER_ACTION_REQUIRED; - } + final int userActionNotTypicallyNeededResponse = forceUserActionPrompt + ? USER_ACTION_REQUIRED + : USER_ACTION_NOT_NEEDED; + // It is safe to access mInstallerUid and mInstallSource without lock // because they are immutable after sealing. final Computer snapshot = mPm.snapshotComputer(); @@ -956,7 +981,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { || isInstallerDeviceOwnerOrAffiliatedProfileOwner(); if (noUserActionNecessary) { - return USER_ACTION_NOT_NEEDED; + return userActionNotTypicallyNeededResponse; } if (isUpdateOwnershipEnforcementEnabled @@ -969,7 +994,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } if (isPermissionGranted) { - return USER_ACTION_NOT_NEEDED; + return userActionNotTypicallyNeededResponse; } if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid, diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index d0f86c0a9239..71502c667f02 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4375,7 +4375,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Reset the last saved PiP snap fraction on removal. mDisplayContent.mPinnedTaskController.onActivityHidden(mActivityComponent); mDisplayContent.onRunningActivityChanged(); - mWmService.mEmbeddedWindowController.onActivityRemoved(this); mRemovingFromDisplay = false; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index fcf65872e0af..ff2c71999188 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2596,9 +2596,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { - // When a task is locked, dismiss the root pinned task if it exists - mRootWindowContainer.removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED); - getLockTaskController().startLockTaskMode(task, isSystemCaller, callingUid); } finally { Binder.restoreCallingIdentity(ident); diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 2eceeccd9d8f..025047588ea5 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -172,10 +172,9 @@ class AsyncRotationController extends FadeAnimationController implements Consume if (recents != null && recents.isNavigationBarAttachedToApp()) { return; } - } else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS) { + } else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS + || mDisplayContent.mTransitionController.mNavigationBarAttachedToApp) { action = Operation.ACTION_SEAMLESS; - } else if (mDisplayContent.mTransitionController.mNavigationBarAttachedToApp) { - return; } mTargetWindowTokens.put(w.mToken, new Operation(action)); return; @@ -294,6 +293,11 @@ class AsyncRotationController extends FadeAnimationController implements Consume finishOp(mTargetWindowTokens.keyAt(i)); } mTargetWindowTokens.clear(); + onAllCompleted(); + } + + private void onAllCompleted() { + if (DEBUG) Slog.d(TAG, "onAllCompleted"); if (mTimeoutRunnable != null) { mService.mH.removeCallbacks(mTimeoutRunnable); } @@ -333,7 +337,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume if (DEBUG) Slog.d(TAG, "Complete directly " + token.getTopChild()); finishOp(token); if (mTargetWindowTokens.isEmpty()) { - if (mTimeoutRunnable != null) mService.mH.removeCallbacks(mTimeoutRunnable); + onAllCompleted(); return true; } } @@ -411,14 +415,18 @@ class AsyncRotationController extends FadeAnimationController implements Consume if (mDisplayContent.mInputMethodWindow == null) return; final WindowToken imeWindowToken = mDisplayContent.mInputMethodWindow.mToken; if (isTargetToken(imeWindowToken)) return; + hideImmediately(imeWindowToken, Operation.ACTION_TOGGLE_IME); + if (DEBUG) Slog.d(TAG, "hideImeImmediately " + imeWindowToken.getTopChild()); + } + + private void hideImmediately(WindowToken token, @Operation.Action int action) { final boolean original = mHideImmediately; mHideImmediately = true; - final Operation op = new Operation(Operation.ACTION_TOGGLE_IME); - mTargetWindowTokens.put(imeWindowToken, op); - fadeWindowToken(false /* show */, imeWindowToken, ANIMATION_TYPE_TOKEN_TRANSFORM); - op.mLeash = imeWindowToken.getAnimationLeash(); + final Operation op = new Operation(action); + mTargetWindowTokens.put(token, op); + fadeWindowToken(false /* show */, token, ANIMATION_TYPE_TOKEN_TRANSFORM); + op.mLeash = token.getAnimationLeash(); mHideImmediately = original; - if (DEBUG) Slog.d(TAG, "hideImeImmediately " + imeWindowToken.getTopChild()); } /** Returns {@code true} if the window will rotate independently. */ @@ -428,11 +436,20 @@ class AsyncRotationController extends FadeAnimationController implements Consume || isTargetToken(w.mToken); } - /** Returns {@code true} if the controller will run fade animations on the window. */ + /** + * Returns {@code true} if the rotation transition appearance of the window is currently + * managed by this controller. + */ boolean isTargetToken(WindowToken token) { return mTargetWindowTokens.containsKey(token); } + /** Returns {@code true} if the controller will run fade animations on the window. */ + boolean hasFadeOperation(WindowToken token) { + final Operation op = mTargetWindowTokens.get(token); + return op != null && op.mAction == Operation.ACTION_FADE; + } + /** * Whether the insets animation leash should use previous position when running fade animation * or seamless transformation in a rotated display. @@ -564,7 +581,18 @@ class AsyncRotationController extends FadeAnimationController implements Consume return false; } final Operation op = mTargetWindowTokens.get(w.mToken); - if (op == null) return false; + if (op == null) { + // If a window becomes visible after the rotation transition is requested but before + // the transition is ready, hide it by an animation leash so it won't be flickering + // by drawing the rotated content before applying projection transaction of display. + // And it will fade in after the display transition is finished. + if (mTransitionOp == OP_APP_SWITCH && !mIsStartTransactionCommitted + && canBeAsync(w.mToken)) { + hideImmediately(w.mToken, Operation.ACTION_FADE); + if (DEBUG) Slog.d(TAG, "Hide on finishDrawing " + w.mToken.getTopChild()); + } + return false; + } if (DEBUG) Slog.d(TAG, "handleFinishDrawing " + w); if (postDrawTransaction == null || !mIsSyncDrawRequested || canDrawBeforeStartTransaction(op)) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index ee3014cadb0d..4cb4fe292173 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -6499,6 +6499,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * @return whether the physical display has a fixed orientation and cannot be rotated. + */ + boolean isDisplayOrientationFixed() { + return (mDisplayInfo.flags & Display.FLAG_ROTATES_WITH_CONTENT) == 0; + } + + /** * @return whether AOD is showing on this display */ boolean isAodShowing() { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index ed3fad0dd3e7..94df77a58eb3 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -434,7 +434,8 @@ public class DisplayRotation { final boolean isTv = mContext.getPackageManager().hasSystemFeature( PackageManager.FEATURE_LEANBACK); mDefaultFixedToUserRotation = - (isCar || isTv || mService.mIsPc || mDisplayContent.forceDesktopMode()) + (isCar || isTv || mService.mIsPc || mDisplayContent.forceDesktopMode() + || mDisplayContent.isDisplayOrientationFixed()) // For debug purposes the next line turns this feature off with: // $ adb shell setprop config.override_forced_orient true // $ adb shell wm size reset diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 98027bbed37f..c9bae127b800 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -135,19 +135,6 @@ class EmbeddedWindowController { return mWindowsByWindowToken.get(windowToken); } - void onActivityRemoved(ActivityRecord activityRecord) { - for (int i = mWindows.size() - 1; i >= 0; i--) { - final EmbeddedWindow window = mWindows.valueAt(i); - if (window.mHostActivityRecord == activityRecord) { - final WindowProcessController processController = - mAtmService.getProcessController(window.mOwnerPid, window.mOwnerUid); - if (processController != null) { - processController.removeHostActivity(activityRecord); - } - } - } - } - static class EmbeddedWindow implements InputTarget { final IWindow mClient; @Nullable final WindowState mHostWindowState; @@ -230,6 +217,13 @@ class EmbeddedWindowController { mInputChannel.dispose(); mInputChannel = null; } + if (mHostActivityRecord != null) { + final WindowProcessController wpc = + mWmService.mAtmService.getProcessController(mOwnerPid, mOwnerUid); + if (wpc != null) { + wpc.removeHostActivity(mHostActivityRecord); + } + } } @Override diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java index 0c98fb5000d5..830f7859b2f5 100644 --- a/services/core/java/com/android/server/wm/LockTaskController.java +++ b/services/core/java/com/android/server/wm/LockTaskController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Context.DEVICE_POLICY_SERVICE; import static android.content.Context.STATUS_BAR_SERVICE; @@ -669,6 +670,9 @@ public class LockTaskController { } } + // When a task is locked, dismiss the root pinned task if it exists + mSupervisor.mRootWindowContainer.removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED); + // System can only initiate screen pinning, not full lock task mode ProtoLog.w(WM_DEBUG_LOCKTASK, "%s", isSystemCaller ? "Locking pinned" : "Locking fully"); setLockTaskMode(task, isSystemCaller ? LOCK_TASK_MODE_PINNED : LOCK_TASK_MODE_LOCKED, diff --git a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java index 2e5474e5e415..79b26d2ee994 100644 --- a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java +++ b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java @@ -86,7 +86,7 @@ public class NavBarFadeAnimationController extends FadeAnimationController{ ANIMATION_TYPE_TOKEN_TRANSFORM); if (controller == null) { fadeAnim.run(); - } else if (!controller.isTargetToken(mNavigationBar.mToken)) { + } else if (!controller.hasFadeOperation(mNavigationBar.mToken)) { // If fade rotation animation is running and the nav bar is not controlled by it: // - For fade-in animation, defer the animation until fade rotation animation finishes. // - For fade-out animation, just play the animation. diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 05f95f813e55..d4fdc1290649 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2264,7 +2264,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> int finishTopCrashedActivities(WindowProcessController app, String reason) { Task focusedRootTask = getTopDisplayFocusedRootTask(); final Task[] finishedTask = new Task[1]; - forAllTasks(rootTask -> { + forAllRootTasks(rootTask -> { final Task t = rootTask.finishTopCrashedActivityLocked(app, reason); if (rootTask == focusedRootTask || finishedTask[0] == null) { finishedTask[0] = t; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index f1fb17bbf316..44632c910a6e 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1930,11 +1930,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { break; } - final AsyncRotationController asyncRotationController = dc.getAsyncRotationController(); - if (asyncRotationController != null) { - asyncRotationController.accept(navWindow); - } - if (animate) { final NavBarFadeAnimationController controller = new NavBarFadeAnimationController(dc); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 48cca3291650..5b466a017cac 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -1124,14 +1124,15 @@ class TransitionController { + "track #%d", transition.getSyncId(), track); } } - if (sync) { + transition.mAnimationTrack = track; + info.setTrack(track); + mTrackCount = Math.max(mTrackCount, track + 1); + if (sync && mTrackCount > 1) { + // If there are >1 tracks, mark as sync so that all tracks finish. info.setFlags(info.getFlags() | TransitionInfo.FLAG_SYNC); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Marking #%d animation as SYNC.", transition.getSyncId()); } - transition.mAnimationTrack = track; - info.setTrack(track); - mTrackCount = Math.max(mTrackCount, track + 1); } void updateAnimatingState(SurfaceControl.Transaction t) { diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java index 14b4dc3dc7d0..2db2438b9a21 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java @@ -27,6 +27,7 @@ import android.platform.test.annotations.Presubmit; import android.util.Log; import com.android.server.backup.UserBackupManagerService; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.android.server.testing.shadows.ShadowEventLog; import com.android.server.testing.shadows.ShadowSlog; @@ -46,10 +47,13 @@ public class KeyValueBackupReporterTest { @Mock private IBackupManagerMonitor mMonitor; private KeyValueBackupReporter mReporter; + private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; @Before public void setUp() { - mReporter = new KeyValueBackupReporter(mBackupManagerService, mObserver, mMonitor); + mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(mMonitor); + mReporter = new KeyValueBackupReporter( + mBackupManagerService, mObserver, mBackupManagerMonitorEventSender); } @Test diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java index bfbc0f50f67a..7349c14ef62b 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -122,6 +122,7 @@ import com.android.server.backup.testing.TransportData; import com.android.server.backup.testing.TransportTestUtils; import com.android.server.backup.testing.TransportTestUtils.TransportMock; import com.android.server.backup.utils.BackupEligibilityRules; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.android.server.testing.shadows.FrameworkShadowLooper; import com.android.server.testing.shadows.ShadowApplicationPackageManager; import com.android.server.testing.shadows.ShadowBackupDataInput; @@ -260,7 +261,8 @@ public class KeyValueBackupTaskTest { mBackupHandler = mBackupManagerService.getBackupHandler(); mShadowBackupLooper = shadowOf(mBackupHandler.getLooper()); ShadowEventLog.setUp(); - mReporter = spy(new KeyValueBackupReporter(mBackupManagerService, mObserver, mMonitor)); + mReporter = spy(new KeyValueBackupReporter(mBackupManagerService, mObserver, + new BackupManagerMonitorEventSender(mMonitor))); when(mPackageManagerInternal.getApplicationEnabledState(any(), anyInt())) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED); diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java index dc92376263a6..ba13c992edcf 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java @@ -56,7 +56,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.StatFs; import android.os.SystemClock; -import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.Postsubmit; import android.provider.DeviceConfig; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; @@ -93,7 +93,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; -@Presubmit +@Postsubmit public class PackageManagerTests extends AndroidTestCase { private static final boolean localLOGV = true; diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java new file mode 100644 index 000000000000..4fd8f26d91a8 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.view.SurfaceControl; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the {@link DisplayDevice} class. + * + * Build/Install/Run: + * atest DisplayServicesTests:DisplayDeviceTest + */ +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class DisplayDeviceTest { + private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo(); + private static final int WIDTH = 500; + private static final int HEIGHT = 900; + private static final Point PORTRAIT_SIZE = new Point(WIDTH, HEIGHT); + private static final Point LANDSCAPE_SIZE = new Point(HEIGHT, WIDTH); + + @Mock + private SurfaceControl.Transaction mMockTransaction; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mDisplayDeviceInfo.width = WIDTH; + mDisplayDeviceInfo.height = HEIGHT; + mDisplayDeviceInfo.rotation = ROTATION_0; + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_notRotated() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation0() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_0, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation90() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_90, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation180() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_180, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation270() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_270, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE); + } + + private static class FakeDisplayDevice extends DisplayDevice { + private final DisplayDeviceInfo mDisplayDeviceInfo; + + FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo) { + super(null, null, "", InstrumentationRegistry.getInstrumentation().getContext()); + mDisplayDeviceInfo = displayDeviceInfo; + } + + @Override + public boolean hasStableUniqueId() { + return false; + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + return mDisplayDeviceInfo; + } + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index d16c9c59bb1b..50cf1696ba83 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -477,6 +477,41 @@ public class DisplayManagerServiceTest { } @Test + public void testCreateVirtualRotatesWithContent() throws RemoteException { + DisplayManagerService displayManager = + new DisplayManagerService(mContext, mBasicInjector); + registerDefaultDisplays(displayManager); + + // This is effectively the DisplayManager service published to ServiceManager. + DisplayManagerService.BinderService bs = displayManager.new BinderService(); + + String uniqueId = "uniqueId --- Rotates with Content Test"; + int width = 600; + int height = 800; + int dpi = 320; + int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; + + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder( + VIRTUAL_DISPLAY_NAME, width, height, dpi); + builder.setFlags(flags); + builder.setUniqueId(uniqueId); + int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken, + /* projection= */ null, PACKAGE_NAME); + verify(mMockProjectionService, never()).setContentRecordingSession(any(), + nullable(IMediaProjection.class)); + + displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); + + // flush the handler + displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0); + + DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); + assertNotNull(ddi); + assertTrue((ddi.flags & DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT) != 0); + } + + @Test public void testCreateVirtualDisplayOwnFocus() throws RemoteException { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java index 20654797a5d2..c0128ae38a28 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java @@ -69,7 +69,6 @@ public class LogicalDisplayTest { mDisplayDeviceInfo.copyFrom(new DisplayDeviceInfo()); mDisplayDeviceInfo.width = DISPLAY_WIDTH; mDisplayDeviceInfo.height = DISPLAY_HEIGHT; - mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT; mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL; mDisplayDeviceInfo.modeId = MODE_ID; mDisplayDeviceInfo.supportedModes = new Display.Mode[] {new Display.Mode(MODE_ID, @@ -112,8 +111,18 @@ public class LogicalDisplayTest { mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition()); - expectedPosition.set(40, -20); DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = DISPLAY_WIDTH; + displayInfo.logicalHeight = DISPLAY_HEIGHT; + // Rotation doesn't matter when the FLAG_ROTATES_WITH_CONTENT is absent. + displayInfo.rotation = Surface.ROTATION_90; + mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition()); + + expectedPosition.set(40, -20); + mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT; + mLogicalDisplay.updateLocked(mDeviceRepo); displayInfo.logicalWidth = DISPLAY_HEIGHT; displayInfo.logicalHeight = DISPLAY_WIDTH; displayInfo.rotation = Surface.ROTATION_90; diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java index dc1c6d57dfdb..c942cf4b8ee4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java @@ -52,7 +52,7 @@ import com.android.server.backup.params.BackupParams; import com.android.server.backup.transport.BackupTransportClient; import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.utils.BackupEligibilityRules; -import com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupManagerMonitorEventSender; import com.google.common.collect.ImmutableSet; @@ -86,6 +86,7 @@ public class UserBackupManagerServiceTest { @Mock BackupTransportClient mBackupTransport; @Mock BackupEligibilityRules mBackupEligibilityRules; @Mock LifecycleOperationStorage mOperationStorage; + @Mock BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; private MockitoSession mSession; private TestBackupService mService; @@ -94,7 +95,7 @@ public class UserBackupManagerServiceTest { public void setUp() throws Exception { mSession = mockitoSession() .initMocks(this) - .mockStatic(BackupManagerMonitorUtils.class) + .mockStatic(BackupManagerMonitorEventSender.class) .mockStatic(FeatureFlagUtils.class) // TODO(b/263239775): Remove unnecessary stubbing. .strictness(Strictness.LENIENT) @@ -246,9 +247,9 @@ public class UserBackupManagerServiceTest { new DataTypeResult(/* dataType */ "type_2")); mService.reportDelayedRestoreResult(TEST_PACKAGE, results); - verify(() -> BackupManagerMonitorUtils.sendAgentLoggingResults( - eq(mBackupManagerMonitor), eq(packageInfo), eq(results), eq( - BackupAnnotations.OperationType.RESTORE))); + + verify(mBackupManagerMonitorEventSender).sendAgentLoggingResults( + eq(packageInfo), eq(results), eq(BackupAnnotations.OperationType.RESTORE)); } private static PackageInfo getPackageInfo(String packageName) { @@ -258,7 +259,7 @@ public class UserBackupManagerServiceTest { return packageInfo; } - private static class TestBackupService extends UserBackupManagerService { + private class TestBackupService extends UserBackupManagerService { boolean isEnabledStatePersisted = false; boolean shouldUseNewBackupEligibilityRules = false; @@ -293,6 +294,11 @@ public class UserBackupManagerServiceTest { return mWorkerThread; } + @Override + BackupManagerMonitorEventSender getBMMEventSender(IBackupManagerMonitor monitor) { + return mBackupManagerMonitorEventSender; + } + private void waitForAsyncOperation() { if (mWorkerThread == null) { return; diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java new file mode 100644 index 000000000000..8e17b3a58769 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup.utils; + +import static org.junit.Assert.assertTrue; + +import android.app.backup.BackupManagerMonitor; +import android.os.Bundle; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; + +public class BackupManagerMonitorDumpsysUtilsTest { + private File mTempFile; + private TestBackupManagerMonitorDumpsysUtils mBackupManagerMonitorDumpsysUtils; + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + mTempFile = tmp.newFile("testbmmevents.txt"); + mBackupManagerMonitorDumpsysUtils = new TestBackupManagerMonitorDumpsysUtils(); + } + + + @Test + public void parseBackupManagerMonitorEventForDumpsys_bundleIsNull_noLogsWrittenToFile() + throws Exception { + mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(null); + + assertTrue(mTempFile.length() == 0); + + } + + @Test + public void parseBackupManagerMonitorEventForDumpsys_missingID_noLogsWrittenToFile() + throws Exception { + Bundle event = new Bundle(); + event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, 1); + mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event); + + assertTrue(mTempFile.length() == 0); + } + + @Test + public void parseBackupManagerMonitorEventForDumpsys_missingCategory_noLogsWrittenToFile() + throws Exception { + Bundle event = new Bundle(); + event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, 1); + mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event); + + assertTrue(mTempFile.length() == 0); + } + + private class TestBackupManagerMonitorDumpsysUtils + extends BackupManagerMonitorDumpsysUtils { + TestBackupManagerMonitorDumpsysUtils() { + super(); + } + + @Override + public File getBMMEventsFile() { + return mTempFile; + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorEventSenderTest.java index 093ad3cc7bb3..3af2932ee937 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorEventSenderTest.java @@ -30,11 +30,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.IBackupAgent; -import android.app.backup.BackupAnnotations; import android.app.backup.BackupAnnotations.OperationType; import android.app.backup.BackupManagerMonitor; import android.app.backup.BackupRestoreEventLogger; @@ -62,39 +62,65 @@ import java.util.List; @SmallTest @Presubmit @RunWith(AndroidJUnit4.class) -public class BackupManagerMonitorUtilsTest { +public class BackupManagerMonitorEventSenderTest { @Mock private IBackupManagerMonitor mMonitorMock; + @Mock private BackupManagerMonitorDumpsysUtils mBackupManagerMonitorDumpsysUtilsMock; + + private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(mMonitorMock, + mBackupManagerMonitorDumpsysUtilsMock); + } + + @Test + public void monitorEvent_monitorIsNull_sendBundleToDumpsys() throws Exception { + Bundle extras = new Bundle(); + extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.RESTORE); + mBackupManagerMonitorEventSender.setMonitor(null); + mBackupManagerMonitorEventSender.monitorEvent(0, null, 0, extras); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); + + verify(mBackupManagerMonitorDumpsysUtilsMock).parseBackupManagerMonitorRestoreEventForDumpsys(any( + Bundle.class)); } @Test - public void monitorEvent_monitorIsNull_returnsNull() throws Exception { - IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(null, 0, null, 0, - null); + public void monitorEvent_monitorIsNull_doNotCallOnEvent() throws Exception { + mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(null); + mBackupManagerMonitorEventSender.monitorEvent(0, null, 0, null); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); - assertThat(result).isNull(); + verify(mMonitorMock, never()).onEvent(any(Bundle.class)); } @Test - public void monitorEvent_monitorOnEventThrows_returnsNull() throws Exception { + public void monitorEvent_monitorOnEventThrows_setsMonitorToNull() throws Exception { doThrow(new RemoteException()).when(mMonitorMock).onEvent(any(Bundle.class)); - IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 0, null, - 0, null); + mBackupManagerMonitorEventSender.monitorEvent(0, null, 0, null); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); verify(mMonitorMock).onEvent(any(Bundle.class)); - assertThat(result).isNull(); + assertThat(monitor).isNull(); + } + + @Test + public void monitorEvent_extrasAreNull_doNotSendBundleToDumpsys() throws Exception { + mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, null); + + verify(mBackupManagerMonitorDumpsysUtilsMock, never()) + .parseBackupManagerMonitorRestoreEventForDumpsys(any(Bundle.class)); } @Test public void monitorEvent_packageAndExtrasAreNull_fillsBundleCorrectly() throws Exception { - IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 1, null, - 2, null); + mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, null); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); - assertThat(result).isEqualTo(mMonitorMock); + assertThat(monitor).isEqualTo(mMonitorMock); ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(mMonitorMock).onEvent(bundleCaptor.capture()); Bundle eventBundle = bundleCaptor.getValue(); @@ -112,10 +138,10 @@ public class BackupManagerMonitorUtilsTest { extras.putInt("key1", 4); extras.putString("key2", "value2"); - IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 1, - packageInfo, 2, extras); + mBackupManagerMonitorEventSender.monitorEvent(1, packageInfo, 2, extras); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); - assertThat(result).isEqualTo(mMonitorMock); + assertThat(monitor).isEqualTo(mMonitorMock); ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(mMonitorMock).onEvent(bundleCaptor.capture()); Bundle eventBundle = bundleCaptor.getValue(); @@ -130,7 +156,8 @@ public class BackupManagerMonitorUtilsTest { } @Test - public void monitorEvent_packageAndExtrasAreNotNull_fillsBundleCorrectlyLong() throws Exception { + public void monitorEvent_packageAndExtrasAreNotNull_fillsBundleCorrectlyLong() + throws Exception { PackageInfo packageInfo = new PackageInfo(); packageInfo.packageName = "test.package"; packageInfo.versionCode = 3; @@ -139,10 +166,10 @@ public class BackupManagerMonitorUtilsTest { extras.putInt("key1", 4); extras.putString("key2", "value2"); - IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 1, - packageInfo, 2, extras); + mBackupManagerMonitorEventSender.monitorEvent(1, packageInfo, 2, extras); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); - assertThat(result).isEqualTo(mMonitorMock); + assertThat(monitor).isEqualTo(mMonitorMock); ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(mMonitorMock).onEvent(bundleCaptor.capture()); Bundle eventBundle = bundleCaptor.getValue(); @@ -158,15 +185,45 @@ public class BackupManagerMonitorUtilsTest { } @Test + public void monitorEvent_eventOpTypeIsRestore_sendBundleToDumpsys() throws Exception { + Bundle extras = new Bundle(); + extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.RESTORE); + mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, extras); + + verify(mBackupManagerMonitorDumpsysUtilsMock).parseBackupManagerMonitorRestoreEventForDumpsys(any( + Bundle.class)); + } + + @Test + public void monitorEvent_eventOpTypeIsBackup_doNotSendBundleToDumpsys() throws Exception { + Bundle extras = new Bundle(); + extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.BACKUP); + mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, extras); + + verify(mBackupManagerMonitorDumpsysUtilsMock, never()) + .parseBackupManagerMonitorRestoreEventForDumpsys(any(Bundle.class)); + } + + @Test + public void monitorEvent_eventOpTypeIsUnknown_doNotSendBundleToDumpsys() throws Exception { + Bundle extras = new Bundle(); + extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.UNKNOWN); + mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, extras); + + verify(mBackupManagerMonitorDumpsysUtilsMock, never()) + .parseBackupManagerMonitorRestoreEventForDumpsys(any(Bundle.class)); + } + + @Test public void monitorAgentLoggingResults_onBackup_fillsBundleCorrectly() throws Exception { PackageInfo packageInfo = new PackageInfo(); packageInfo.packageName = "test.package"; // Mock an agent that returns a logging result. IBackupAgent agent = setUpLoggingAgentForOperation(OperationType.BACKUP); - IBackupManagerMonitor monitor = - BackupManagerMonitorUtils.monitorAgentLoggingResults( - mMonitorMock, packageInfo, agent); + + mBackupManagerMonitorEventSender.monitorAgentLoggingResults(packageInfo, agent); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); assertCorrectBundleSentToMonitor(monitor, OperationType.BACKUP); } @@ -178,9 +235,8 @@ public class BackupManagerMonitorUtilsTest { // Mock an agent that returns a logging result. IBackupAgent agent = setUpLoggingAgentForOperation(OperationType.RESTORE); - IBackupManagerMonitor monitor = - BackupManagerMonitorUtils.monitorAgentLoggingResults( - mMonitorMock, packageInfo, agent); + mBackupManagerMonitorEventSender.monitorAgentLoggingResults(packageInfo, agent); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); assertCorrectBundleSentToMonitor(monitor, OperationType.RESTORE); } @@ -217,9 +273,9 @@ public class BackupManagerMonitorUtilsTest { List<BackupRestoreEventLogger.DataTypeResult> loggingResults = new ArrayList<>(); loggingResults.add(new BackupRestoreEventLogger.DataTypeResult("testLoggingResult")); - IBackupManagerMonitor monitor = BackupManagerMonitorUtils.sendAgentLoggingResults( - mMonitorMock, packageInfo, loggingResults, OperationType.BACKUP); - + mBackupManagerMonitorEventSender.sendAgentLoggingResults( + packageInfo, loggingResults, OperationType.BACKUP); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); assertCorrectBundleSentToMonitor(monitor, OperationType.BACKUP); } @@ -230,8 +286,9 @@ public class BackupManagerMonitorUtilsTest { List<BackupRestoreEventLogger.DataTypeResult> loggingResults = new ArrayList<>(); loggingResults.add(new BackupRestoreEventLogger.DataTypeResult("testLoggingResult")); - IBackupManagerMonitor monitor = BackupManagerMonitorUtils.sendAgentLoggingResults( - mMonitorMock, packageInfo, loggingResults, OperationType.RESTORE); + mBackupManagerMonitorEventSender.sendAgentLoggingResults( + packageInfo, loggingResults, OperationType.RESTORE); + IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor(); assertCorrectBundleSentToMonitor(monitor, OperationType.RESTORE); } @@ -262,7 +319,7 @@ public class BackupManagerMonitorUtilsTest { public void putMonitoringExtraString_bundleExists_fillsBundleCorrectly() throws Exception { Bundle bundle = new Bundle(); - Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(bundle, "key", "value"); + Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(bundle, "key", "value"); assertThat(result).isEqualTo(bundle); assertThat(result.size()).isEqualTo(1); @@ -272,7 +329,7 @@ public class BackupManagerMonitorUtilsTest { @Test public void putMonitoringExtraString_bundleDoesNotExist_fillsBundleCorrectly() throws Exception { - Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(null, "key", "value"); + Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(null, "key", "value"); assertThat(result).isNotNull(); assertThat(result.size()).isEqualTo(1); @@ -284,7 +341,7 @@ public class BackupManagerMonitorUtilsTest { public void putMonitoringExtraLong_bundleExists_fillsBundleCorrectly() throws Exception { Bundle bundle = new Bundle(); - Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(bundle, "key", 123); + Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(bundle, "key", 123); assertThat(result).isEqualTo(bundle); assertThat(result.size()).isEqualTo(1); @@ -293,7 +350,7 @@ public class BackupManagerMonitorUtilsTest { @Test public void putMonitoringExtraLong_bundleDoesNotExist_fillsBundleCorrectly() throws Exception { - Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(null, "key", 123); + Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(null, "key", 123); assertThat(result).isNotNull(); assertThat(result.size()).isEqualTo(1); @@ -304,7 +361,7 @@ public class BackupManagerMonitorUtilsTest { public void putMonitoringExtraBoolean_bundleExists_fillsBundleCorrectly() throws Exception { Bundle bundle = new Bundle(); - Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(bundle, "key", true); + Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(bundle, "key", true); assertThat(result).isEqualTo(bundle); assertThat(result.size()).isEqualTo(1); @@ -314,10 +371,10 @@ public class BackupManagerMonitorUtilsTest { @Test public void putMonitoringExtraBoolean_bundleDoesNotExist_fillsBundleCorrectly() throws Exception { - Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(null, "key", true); + Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(null, "key", true); assertThat(result).isNotNull(); assertThat(result.size()).isEqualTo(1); assertThat(result.getBoolean("key")).isTrue(); } -}
\ No newline at end of file +} diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java index 24029b1113ea..fc27edcb7bda 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -35,6 +35,7 @@ import android.util.Pair; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -68,6 +69,27 @@ public class BugreportManagerServiceImplTest { mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(); } + @After + public void tearDown() throws Exception { + // Changes to RoleManager persist between tests, so we need to clear out any funny + // business we did in previous tests. + RoleManager roleManager = mContext.getSystemService(RoleManager.class); + CallbackFuture future = new CallbackFuture(); + runWithShellPermissionIdentity( + () -> { + roleManager.setBypassingRoleQualification(false); + roleManager.removeRoleHolderAsUser( + "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION", + mContext.getPackageName(), + /* flags= */ 0, + Process.myUserHandle(), + mContext.getMainExecutor(), + future); + }); + + assertThat(future.get()).isEqualTo(true); + } + @Test public void testBugreportFileManagerFileExists() { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); @@ -131,14 +153,17 @@ public class BugreportManagerServiceImplTest { new BugreportManagerServiceImpl.Injector(mContext, new ArraySet<>())); RoleManager roleManager = mContext.getSystemService(RoleManager.class); CallbackFuture future = new CallbackFuture(); - runWithShellPermissionIdentity(() -> roleManager.setBypassingRoleQualification(true)); - runWithShellPermissionIdentity(() -> roleManager.addRoleHolderAsUser( - "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION", - mContext.getPackageName(), - /* flags= */ 0, - Process.myUserHandle(), - mContext.getMainExecutor(), - future)); + runWithShellPermissionIdentity( + () -> { + roleManager.setBypassingRoleQualification(true); + roleManager.addRoleHolderAsUser( + "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION", + mContext.getPackageName(), + /* flags= */ 0, + Process.myUserHandle(), + mContext.getMainExecutor(), + future); + }); assertThat(future.get()).isEqualTo(true); mService.cancelBugreport(Binder.getCallingUid(), mContext.getPackageName()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index bdee99b484b9..6a6fa3f91a22 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -6043,6 +6043,49 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testVisitUris_styleExtrasWithoutStyle() { + Notification notification = new Notification.Builder(mContext, "a") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .build(); + + Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle( + personWithIcon("content://user")) + .addHistoricMessage(new Notification.MessagingStyle.Message("Heyhey!", + System.currentTimeMillis(), + personWithIcon("content://historicalMessenger"))) + .addMessage(new Notification.MessagingStyle.Message("Are you there", + System.currentTimeMillis(), + personWithIcon("content://messenger"))) + .setShortcutIcon( + Icon.createWithContentUri("content://conversationShortcut")); + messagingStyle.addExtras(notification.extras); // Instead of Builder.setStyle(style). + + Notification.CallStyle callStyle = Notification.CallStyle.forOngoingCall( + personWithIcon("content://caller"), + PendingIntent.getActivity(mContext, 0, new Intent(), + PendingIntent.FLAG_IMMUTABLE)) + .setVerificationIcon(Icon.createWithContentUri("content://callVerification")); + callStyle.addExtras(notification.extras); // Same. + + Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class); + notification.visitUris(visitor); + + verify(visitor).accept(eq(Uri.parse("content://user"))); + verify(visitor).accept(eq(Uri.parse("content://historicalMessenger"))); + verify(visitor).accept(eq(Uri.parse("content://messenger"))); + verify(visitor).accept(eq(Uri.parse("content://conversationShortcut"))); + verify(visitor).accept(eq(Uri.parse("content://caller"))); + verify(visitor).accept(eq(Uri.parse("content://callVerification"))); + } + + private static Person personWithIcon(String iconUri) { + return new Person.Builder() + .setName("Mr " + iconUri) + .setIcon(Icon.createWithContentUri(iconUri)) + .build(); + } + + @Test public void testVisitUris_wearableExtender() { Icon actionIcon = Icon.createWithContentUri("content://media/action"); Icon wearActionIcon = Icon.createWithContentUri("content://media/wearAction"); @@ -7752,7 +7795,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testOnNotificationActionClick() { final int actionIndex = 2; final Notification.Action action = - new Notification.Action.Builder(null, "text", null).build(); + new Notification.Action.Builder(null, "text", PendingIntent.getActivity( + mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build(); final boolean generatedByAssistant = false; NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); @@ -7776,7 +7820,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testOnAssistantNotificationActionClick() { final int actionIndex = 1; final Notification.Action action = - new Notification.Action.Builder(null, "text", null).build(); + new Notification.Action.Builder(null, "text", PendingIntent.getActivity( + mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build(); final boolean generatedByAssistant = true; NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java index f7573303d887..161709365b59 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -1176,6 +1176,15 @@ public class DisplayRotationTests { + " fixed to user rotation.", mTarget.isFixedToUserRotation()); } + @Test + public void testIsFixedToUserRotation_displayContentOrientationFixed() throws Exception { + mBuilder.build(); + when(mMockDisplayContent.isDisplayOrientationFixed()).thenReturn(true); + + assertFalse("Display rotation should respect app requested orientation if" + + " the display has fixed orientation.", mTarget.isFixedToUserRotation()); + } + private void moveTimeForward(long timeMillis) { sCurrentUptimeMillis += timeMillis; sClock.fastForward(timeMillis); 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 b4f1176f71e1..7544fdaa1fb6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -39,6 +39,7 @@ import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; +import static android.window.TransitionInfo.FLAG_SYNC; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static android.window.TransitionInfo.isIndependent; @@ -1186,6 +1187,7 @@ public class TransitionTests extends WindowTestsBase { final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, "statusBar"); makeWindowVisible(statusBar); mDisplayContent.getDisplayPolicy().addWindowLw(statusBar, statusBar.mAttrs); + final WindowState navBar = createWindow(null, TYPE_NAVIGATION_BAR, "navBar"); final ActivityRecord app = createActivityRecord(mDisplayContent); final Transition transition = app.mTransitionController.createTransition(TRANSIT_OPEN); app.mTransitionController.requestStartTransition(transition, app.getTask(), @@ -1215,9 +1217,17 @@ public class TransitionTests extends WindowTestsBase { mDisplayContent.mTransitionController.dispatchLegacyAppTransitionFinished(app); assertTrue(mDisplayContent.hasTopFixedRotationLaunchingApp()); + // The bar was invisible so it is not handled by the controller. But if it becomes visible + // and drawn before the transition starts, + assertFalse(asyncRotationController.isTargetToken(navBar.mToken)); + navBar.finishDrawing(null /* postDrawTransaction */, Integer.MAX_VALUE); + assertTrue(asyncRotationController.isTargetToken(navBar.mToken)); + player.startTransition(); // Non-app windows should not be collected. assertFalse(mDisplayContent.mTransitionController.isCollecting(statusBar.mToken)); + // Avoid DeviceStateController disturbing the test by triggering another rotation change. + doReturn(false).when(mDisplayContent).updateRotationUnchecked(); onRotationTransactionReady(player, mWm.mTransactionFactory.get()).onTransactionCommitted(); assertEquals(ROTATION_ANIMATION_SEAMLESS, player.mLastReady.getChange( @@ -2384,6 +2394,37 @@ public class TransitionTests extends WindowTestsBase { assertFalse(controller.isCollecting()); } + @Test + public void testNoSyncFlagIfOneTrack() { + final TransitionController controller = mAtm.getTransitionController(); + final TestTransitionPlayer player = registerTestTransitionPlayer(); + + mSyncEngine = createTestBLASTSyncEngine(); + controller.setSyncEngine(mSyncEngine); + + final Transition transitA = createTestTransition(TRANSIT_OPEN, controller); + final Transition transitB = createTestTransition(TRANSIT_OPEN, controller); + final Transition transitC = createTestTransition(TRANSIT_OPEN, controller); + + controller.startCollectOrQueue(transitA, (deferred) -> {}); + controller.startCollectOrQueue(transitB, (deferred) -> {}); + controller.startCollectOrQueue(transitC, (deferred) -> {}); + + // Verify that, as-long as there is <= 1 track, we won't get a SYNC flag + transitA.start(); + transitA.setAllReady(); + mSyncEngine.tryFinishForTest(transitA.getSyncId()); + assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); + transitB.start(); + transitB.setAllReady(); + mSyncEngine.tryFinishForTest(transitB.getSyncId()); + assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); + transitC.start(); + transitC.setAllReady(); + mSyncEngine.tryFinishForTest(transitC.getSyncId()); + assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) { diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java index 0d88a0d485ff..030615fd7527 100644 --- a/services/usage/java/com/android/server/usage/StorageStatsService.java +++ b/services/usage/java/com/android/server/usage/StorageStatsService.java @@ -259,7 +259,24 @@ public class StorageStatsService extends IStorageStatsManager.Stub { // NOTE: No permissions required if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) { - return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize()); + // As a safety measure, use the original implementation for the devices + // with storage size <= 512GB to prevent any potential regressions + final long roundedUserspaceBytes = mStorage.getPrimaryStorageSize(); + if (roundedUserspaceBytes <= DataUnit.GIGABYTES.toBytes(512)) { + return roundedUserspaceBytes; + } + + // Since 1TB devices can actually have either 1000GB or 1024GB, + // get the block device size and do just a small rounding if any at all + final long totalBytes = mStorage.getInternalStorageBlockDeviceSize(); + final long totalBytesRounded = FileUtils.roundStorageSize(totalBytes); + // If the storage size is 997GB-999GB, round it to a 1000GB to show + // 1TB in UI instead of 0.99TB. Same for 2TB, 4TB, 8TB etc. + if (totalBytesRounded - totalBytes <= DataUnit.GIGABYTES.toBytes(3)) { + return totalBytesRounded; + } else { + return totalBytes; + } } else { final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid); if (vol == null) { @@ -286,15 +303,19 @@ public class StorageStatsService extends IStorageStatsManager.Stub { // Free space is usable bytes plus any cached data that we're // willing to automatically clear. To avoid user confusion, this // logic should be kept in sync with getAllocatableBytes(). + long freeBytes; if (isQuotaSupported(volumeUuid, PLATFORM_PACKAGE_NAME)) { final long cacheTotal = getCacheBytes(volumeUuid, PLATFORM_PACKAGE_NAME); final long cacheReserved = mStorage.getStorageCacheBytes(path, 0); final long cacheClearable = Math.max(0, cacheTotal - cacheReserved); - return path.getUsableSpace() + cacheClearable; + freeBytes = path.getUsableSpace() + cacheClearable; } else { - return path.getUsableSpace(); + freeBytes = path.getUsableSpace(); } + + Slog.d(TAG, "getFreeBytes: " + freeBytes); + return freeBytes; } finally { Binder.restoreCallingIdentity(token); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 58da4b43aa05..3d78a1dd8943 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -54,6 +54,7 @@ import android.provider.DeviceConfig; import android.service.voice.HotwordDetectionService; import android.service.voice.HotwordDetectionServiceFailure; import android.service.voice.HotwordDetector; +import android.service.voice.IDetectorSessionStorageService; import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; import android.service.voice.ISandboxedDetectionService; import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback; @@ -69,6 +70,7 @@ import android.view.contentcapture.IContentCaptureManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import com.android.server.LocalServices; import com.android.server.pm.permission.PermissionManagerServiceInternal; @@ -157,6 +159,8 @@ final class HotwordDetectionConnection { @NonNull private ServiceConnection mRemoteVisualQueryDetectionService; @GuardedBy("mLock") @Nullable private IBinder mAudioFlinger; + + @Nullable private IHotwordRecognitionStatusCallback mHotwordRecognitionCallback; @GuardedBy("mLock") private boolean mDebugHotwordLogging = false; @@ -694,6 +698,7 @@ final class HotwordDetectionConnection { updateContentCaptureManager(connection); updateSpeechService(connection); updateServiceIdentity(connection); + updateStorageService(connection); return connection; } } @@ -910,6 +915,7 @@ final class HotwordDetectionConnection { mVoiceInteractionServiceUid, mVoiceInteractorIdentity, mScheduledExecutorService, mDebugHotwordLogging, mRemoteExceptionListener); } + mHotwordRecognitionCallback = callback; mDetectorSessions.put(detectorType, session); session.initialize(options, sharedMemory); } @@ -1035,6 +1041,23 @@ final class HotwordDetectionConnection { })); } + private void updateStorageService(ServiceConnection connection) { + connection.run(service -> { + service.registerRemoteStorageService(new IDetectorSessionStorageService.Stub() { + @Override + public void openFile(String filename, AndroidFuture future) + throws RemoteException { + Slog.v(TAG, "BinderCallback#onFileOpen"); + try { + mHotwordRecognitionCallback.onOpenFile(filename, future); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + }); + }); + } + private void addServiceUidForAudioPolicy(int uid) { mScheduledExecutorService.execute(() -> { AudioManagerInternal audioManager = diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp index 1d423ca6aa36..2ccc0fa9e1e7 100644 --- a/tests/FlickerTests/Android.bp +++ b/tests/FlickerTests/Android.bp @@ -97,6 +97,7 @@ java_defaults { "flickerlib-helpers", "platform-test-annotations", "wm-flicker-common-app-helpers", + "wm-shell-flicker-utils", ], data: [ ":FlickerTestApp", diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt new file mode 100644 index 000000000000..87231c86ef19 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt @@ -0,0 +1,189 @@ +/* + * 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.wm.flicker.activityembedding.splitscreen + +import android.platform.test.annotations.Presubmit +import android.platform.test.annotations.RequiresDevice +import android.tools.common.datatypes.Rect +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.LegacyFlickerTest +import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import android.tools.device.traces.parsers.toFlickerComponent +import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper +import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.wm.shell.flicker.utils.* +import org.junit.FixMethodOrder +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/*** + * Test entering System SplitScreen with Activity Embedding Split and another app. + * + * Setup: Launch A|B in split and secondaryApp, return to home. + * Transitions: Let AE Split A|B enter splitscreen with secondaryApp. Resulting in A|B|secondaryApp. + * + * To run this test: `atest FlickerTestsOther:EnterSystemSplitTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterSystemSplitTest(flicker: LegacyFlickerTest) : + ActivityEmbeddingTestBase(flicker) { + + private val secondaryApp = SplitScreenUtils.getPrimary(instrumentation) + override val transition: FlickerBuilder.() -> Unit = { + setup { + testApp.launchViaIntent(wmHelper) + testApp.launchSecondaryActivity(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + tapl.goHome() + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + startDisplayBounds = + wmHelper.currentState.layerState.physicalDisplayBounds ?: + error("Display not found") + } + transitions { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, testApp, secondaryApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, testApp, secondaryApp) + } + } + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun activityEmbeddingSplitLayerBecomesVisible() { + flicker.splitAppLayerBoundsIsVisibleAtEnd( + testApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + } + + @Presubmit + @Test + fun activityEmbeddingSplitWindowBecomesVisible() = flicker.appWindowIsVisibleAtEnd(testApp) + + @Presubmit + @Test + fun secondaryLayerBecomesVisible() { + flicker.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + } + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.appWindowIsVisibleAtEnd(secondaryApp) + + /** + * After the transition there should be both ActivityEmbedding activities, + * SplitScreenPrimaryActivity and the system split divider on screen. + * Verify the layers are in expected sizes. + */ + @Presubmit + @Test + fun activityEmbeddingSplitSurfaceAreEven() { + flicker.assertLayersEnd { + val leftAELayerRegion = + visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + val rightAELayerRegion = + visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + val secondaryAppLayerRegion = + visibleRegion( + ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()) + val systemDivider = visibleRegion(SPLIT_SCREEN_DIVIDER_COMPONENT) + leftAELayerRegion + .plus(rightAELayerRegion.region) + .plus(secondaryAppLayerRegion.region) + .plus(systemDivider.region) + .coversExactly(startDisplayBounds) + check { "ActivityEmbeddingSplitHeight" } + .that(leftAELayerRegion.region.height) + .isEqual(rightAELayerRegion.region.height) + check { "SystemSplitHeight" } + .that(rightAELayerRegion.region.height) + .isEqual(secondaryAppLayerRegion.region.height) + // TODO(b/292283182): Remove this special case handling. + check { "ActivityEmbeddingSplitWidth" } + .that(Math.abs( + leftAELayerRegion.region.width - rightAELayerRegion.region.width)) + .isLower(2) + check { "SystemSplitWidth" } + .that(Math.abs(secondaryAppLayerRegion.region.width - + 2 * rightAELayerRegion.region.width)) + .isLower(2) + } + } + + /** + * Verify the windows are in expected sizes. + */ + @Presubmit + @Test + fun activityEmbeddingSplitWindowsAreEven() { + flicker.assertWmEnd { + val leftAEWindowRegion = + visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + val rightAEWindowRegion = + visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + // There's no window for the divider bar. + val secondaryAppLayerRegion = + visibleRegion( + ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()) + check { "ActivityEmbeddingSplitHeight" } + .that(leftAEWindowRegion.region.height) + .isEqual(rightAEWindowRegion.region.height) + check { "SystemSplitHeight" } + .that(rightAEWindowRegion.region.height) + .isEqual(secondaryAppLayerRegion.region.height) + check { "ActivityEmbeddingSplitWidth" } + .that(Math.abs( + leftAEWindowRegion.region.width - rightAEWindowRegion.region.width)) + .isLower(2) + check { "SystemSplitWidth" } + .that(Math.abs(secondaryAppLayerRegion.region.width - + 2 * rightAEWindowRegion.region.width)) + .isLower(2) + } + } + + @Ignore("Not applicable to this CUJ.") + override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} + + companion object { + /** {@inheritDoc} */ + private var startDisplayBounds = Rect.EMPTY + /** + * Creates the test configurations. + * + * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = LegacyFlickerTestFactory.nonRotationTests() + } +}
\ No newline at end of file diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java index c9b5c96c9920..12556bcf7ded 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java @@ -154,7 +154,7 @@ public final class ImeStressTestUtil { * <p>The given {@code pred} will be called on the main thread. */ public static void waitOnMainUntil(String message, Callable<Boolean> pred) { - eventually(() -> assertWithMessage(message).that(pred.call()).isTrue(), TIMEOUT); + eventually(() -> assertWithMessage(message).that(callOnMainSync(pred)).isTrue(), TIMEOUT); } /** Waits until IME is shown, or throws on timeout. */ |