Merge "Switch "postsubmit" to "presubmit" after soak" into main
diff --git a/libs/WindowManager/Shell/res/values/config_tv.xml b/libs/WindowManager/Shell/res/values/config_tv.xml
new file mode 100644
index 0000000..3da5539
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/config_tv.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.
+-->
+<resources>
+ <integer name="config_tvPipEnterFadeOutDuration">500</integer>
+ <integer name="config_tvPipEnterFadeInDuration">1500</integer>
+ <integer name="config_tvPipExitFadeOutDuration">500</integer>
+ <integer name="config_tvPipExitFadeInDuration">500</integer>
+</resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index d520ff7..8b6c7b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -258,7 +258,7 @@
ActivityTaskManager.getService().onPictureInPictureStateChanged(
new PictureInPictureUiState(stashedState != STASH_TYPE_NONE /* isStashed */)
);
- } catch (RemoteException e) {
+ } catch (RemoteException | IllegalStateException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Unable to set alert PiP state change.", TAG);
}
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 a9675f9..1947097 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
@@ -20,6 +20,8 @@
import android.os.Handler;
import android.os.SystemClock;
+import androidx.annotation.NonNull;
+
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.common.DisplayController;
@@ -41,7 +43,6 @@
import com.android.wm.shell.pip.PipParamsChangedForwarder;
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.tv.TvPipBoundsAlgorithm;
import com.android.wm.shell.pip.tv.TvPipBoundsController;
@@ -78,11 +79,12 @@
PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
+ PipTransitionState pipTransitionState,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
TvPipMenuController tvPipMenuController,
PipMediaController pipMediaController,
- PipTransitionController pipTransitionController,
+ TvPipTransition tvPipTransition,
TvPipNotificationController tvPipNotificationController,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
@@ -99,9 +101,10 @@
pipDisplayLayoutState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
+ pipTransitionState,
pipAppOpsListener,
pipTaskOrganizer,
- pipTransitionController,
+ tvPipTransition,
tvPipMenuController,
pipMediaController,
tvPipNotificationController,
@@ -151,25 +154,23 @@
return new LegacySizeSpecSource(context, pipDisplayLayoutState);
}
- // Handler needed for loadDrawableAsync() in PipControlsViewController
@WMSingleton
@Provides
- static PipTransitionController provideTvPipTransition(
+ static TvPipTransition provideTvPipTransition(
Context context,
- ShellInit shellInit,
- ShellTaskOrganizer shellTaskOrganizer,
- Transitions transitions,
+ @NonNull ShellInit shellInit,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer,
+ @NonNull Transitions transitions,
TvPipBoundsState tvPipBoundsState,
- PipDisplayLayoutState pipDisplayLayoutState,
- PipTransitionState pipTransitionState,
- TvPipMenuController pipMenuController,
+ TvPipMenuController tvPipMenuController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+ PipTransitionState pipTransitionState,
PipAnimationController pipAnimationController,
- PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
+ PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
+ PipDisplayLayoutState pipDisplayLayoutState) {
return new TvPipTransition(context, shellInit, shellTaskOrganizer, transitions,
- tvPipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController,
- tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
- Optional.empty());
+ tvPipBoundsState, tvPipMenuController, tvPipBoundsAlgorithm, pipTransitionState,
+ pipAnimationController, pipSurfaceTransactionHelper, pipDisplayLayoutState);
}
@WMSingleton
@@ -207,7 +208,7 @@
PipTransitionState pipTransitionState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipAnimationController pipAnimationController,
- PipTransitionController pipTransitionController,
+ TvPipTransition tvPipTransition,
PipParamsChangedForwarder pipParamsChangedForwarder,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
Optional<SplitScreenController> splitScreenControllerOptional,
@@ -217,7 +218,7 @@
return new TvPipTaskOrganizer(context,
syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState,
tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController,
- pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+ pipSurfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
splitScreenControllerOptional, displayController, pipUiEventLogger,
shellTaskOrganizer, mainExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index cbed4b5..a58d94e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -81,15 +81,35 @@
*/
public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
Rect sourceBounds, Rect destinationBounds) {
+ mTmpDestinationRectF.set(destinationBounds);
+ return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */);
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, RectF destinationBounds) {
return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */);
}
/**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, Rect destinationBounds, float degrees) {
+ mTmpDestinationRectF.set(destinationBounds);
+ return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees);
+ }
+
+ /**
* Operates the scale (setMatrix) on a given transaction and leash, along with a rotation.
* @return same {@link PipSurfaceTransactionHelper} instance for method chaining
*/
public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
- Rect sourceBounds, Rect destinationBounds, float degrees) {
+ Rect sourceBounds, RectF destinationBounds, float degrees) {
mTmpSourceRectF.set(sourceBounds);
// We want the matrix to position the surface relative to the screen coordinates so offset
// the source to 0,0
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 c05601b..b4067d0 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
@@ -297,9 +297,9 @@
// changed RunningTaskInfo when it finishes.
private ActivityManager.RunningTaskInfo mDeferredTaskInfo;
private WindowContainerToken mToken;
- private SurfaceControl mLeash;
+ protected SurfaceControl mLeash;
protected PipTransitionState mPipTransitionState;
- private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ protected PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
protected PictureInPictureParams mPictureInPictureParams;
private IntConsumer mOnDisplayIdChangeCallback;
@@ -973,7 +973,7 @@
return;
}
- cancelCurrentAnimator();
+ cancelAnimationOnTaskVanished();
onExitPipFinished(info);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -981,6 +981,10 @@
}
}
+ protected void cancelAnimationOnTaskVanished() {
+ cancelCurrentAnimator();
+ }
+
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken");
@@ -1100,7 +1104,7 @@
}
/** Called when exiting PIP transition is finished to do the state cleanup. */
- void onExitPipFinished(TaskInfo info) {
+ public void onExitPipFinished(TaskInfo info) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"onExitPipFinished: %s, state=%s leash=%s",
info.topActivity, mPipTransitionState, mLeash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index a48e969f..72c0cd7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -44,6 +44,7 @@
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import java.util.Collections;
import java.util.Set;
/**
@@ -101,12 +102,29 @@
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
&& !mTvPipBoundsState.isTvPipManuallyCollapsed();
if (isPipExpanded) {
- updateGravityOnExpansionToggled(/* expanding= */ true);
+ updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded);
}
mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
}
+ @Override
+ public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: getEntryDestinationBoundsIgnoringKeepClearAreas()", TAG);
+
+ updateExpandedPipSize();
+ final boolean isPipExpanded = mTvPipBoundsState.isTvExpandedPipSupported()
+ && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
+ && !mTvPipBoundsState.isTvPipManuallyCollapsed();
+ if (isPipExpanded) {
+ updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded);
+ }
+ mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
+ return adjustBoundsForTemporaryDecor(getTvPipPlacement(Collections.emptySet(),
+ Collections.emptySet()).getUnstashedBounds());
+ }
+
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
@Override
public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
@@ -133,16 +151,25 @@
*/
@NonNull
public Placement getTvPipPlacement() {
+ final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
+ final Set<Rect> unrestrictedKeepClearAreas =
+ mTvPipBoundsState.getUnrestrictedKeepClearAreas();
+
+ return getTvPipPlacement(restrictedKeepClearAreas, unrestrictedKeepClearAreas);
+ }
+
+ /**
+ * Calculates the PiP bounds.
+ */
+ @NonNull
+ private Placement getTvPipPlacement(Set<Rect> restrictedKeepClearAreas,
+ Set<Rect> unrestrictedKeepClearAreas) {
final Size pipSize = getPipSize();
final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
final Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
- final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
- final Set<Rect> unrestrictedKeepClearAreas =
- mTvPipBoundsState.getUnrestrictedKeepClearAreas();
-
mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity());
mKeepClearAlgorithm.setScreenSize(screenSize);
mKeepClearAlgorithm.setMovementBounds(insetBounds);
@@ -189,8 +216,11 @@
int updatedGravity;
if (expanding) {
- // Save collapsed gravity.
- mTvPipBoundsState.setTvPipPreviousCollapsedGravity(mTvPipBoundsState.getTvPipGravity());
+ if (!mTvPipBoundsState.isTvPipExpanded()) {
+ // Save collapsed gravity.
+ mTvPipBoundsState.setTvPipPreviousCollapsedGravity(
+ mTvPipBoundsState.getTvPipGravity());
+ }
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
updatedGravity = Gravity.CENTER_HORIZONTAL | currentY;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
index 2b3a93e..5ee3734e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
@@ -131,6 +131,7 @@
mTvFixedPipOrientation = ORIENTATION_UNDETERMINED;
mTvPipGravity = mDefaultGravity;
mPreviousCollapsedGravity = mDefaultGravity;
+ mIsTvPipExpanded = false;
mTvPipManuallyCollapsed = false;
}
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 72115fd..cd3d38b 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
@@ -56,6 +56,7 @@
import com.android.wm.shell.pip.PipParamsChangedForwarder;
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.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellController;
@@ -122,6 +123,7 @@
private final PipDisplayLayoutState mPipDisplayLayoutState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final TvPipBoundsController mTvPipBoundsController;
+ private final PipTransitionState mPipTransitionState;
private final PipAppOpsListener mAppOpsListener;
private final PipTaskOrganizer mPipTaskOrganizer;
private final PipMediaController mPipMediaController;
@@ -157,6 +159,7 @@
PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
+ PipTransitionState pipTransitionState,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
@@ -177,6 +180,7 @@
pipDisplayLayoutState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
+ pipTransitionState,
pipAppOpsListener,
pipTaskOrganizer,
pipTransitionController,
@@ -199,6 +203,7 @@
PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
+ PipTransitionState pipTransitionState,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
@@ -212,6 +217,7 @@
Handler mainHandler,
ShellExecutor mainExecutor) {
mContext = context;
+ mPipTransitionState = pipTransitionState;
mMainHandler = mainHandler;
mMainExecutor = mainExecutor;
mShellController = shellController;
@@ -365,7 +371,6 @@
"%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */);
- onPipDisappeared();
}
private void togglePipExpansion() {
@@ -420,6 +425,11 @@
@Override
public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+ if (!mPipTransitionState.hasEnteredPip()) {
+ // Do not schedule a move animation while we're still transitioning into/out of PiP
+ return;
+ }
+
mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
animationDuration, null);
mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
@@ -447,7 +457,7 @@
return;
}
mPipTaskOrganizer.removePip();
- onPipDisappeared();
+ mTvPipMenuController.closeMenu();
}
@Override
@@ -477,7 +487,7 @@
mPipNotificationController.dismiss();
mActionBroadcastReceiver.unregister();
- mTvPipMenuController.closeMenu();
+ mTvPipMenuController.detach();
mTvPipActionsProvider.reset();
mTvPipBoundsState.resetTvPipState();
mTvPipBoundsController.reset();
@@ -501,8 +511,6 @@
public void onPipTransitionCanceled(int direction) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.onPipTransitionFinished(
- PipAnimationController.isInPipDirection(direction));
mTvPipActionsProvider.updatePipExpansionState(mTvPipBoundsState.isTvPipExpanded());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index ee55211..c6803f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -262,8 +262,8 @@
@Override
public void detach() {
- closeMenu();
detachPipMenu();
+ switchToMenuMode(MODE_NO_MENU);
mLeash = null;
}
@@ -320,10 +320,21 @@
@Override
public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction pipTx,
Rect pipBounds, float alpha) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: movePipMenu: %s, alpha %s", TAG, pipBounds.toShortString(), alpha);
+ movePipMenu(pipTx, pipBounds, alpha);
+ }
- if (pipBounds.isEmpty()) {
+ /**
+ * Move the PiP menu with the given bounds and update its opacity.
+ * The PiP SurfaceControl is given if there is a need to synchronize the movements
+ * on the same frame as PiP.
+ */
+ public void movePipMenu(@Nullable SurfaceControl.Transaction pipTx, @Nullable Rect pipBounds,
+ float alpha) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: movePipMenu: %s, alpha %s", TAG,
+ pipBounds != null ? pipBounds.toShortString() : null, alpha);
+
+ if ((pipBounds == null || pipBounds.isEmpty()) && alpha == ALPHA_NO_CHANGE) {
if (pipTx == null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: no transaction given", TAG);
@@ -334,28 +345,36 @@
return;
}
- final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
- final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
- final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds);
if (pipTx == null) {
pipTx = new SurfaceControl.Transaction();
}
- pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top);
- pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top);
+
+ final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
+ final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
+
+ if (pipBounds != null) {
+ final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds);
+ pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top);
+ pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top);
+ updateMenuBounds(pipBounds);
+ }
if (alpha != ALPHA_NO_CHANGE) {
pipTx.setAlpha(frontSurface, alpha);
pipTx.setAlpha(backSurface, alpha);
}
- // Synchronize drawing the content in the front and back surfaces together with the pip
- // transaction and the position change for the front and back surfaces
- final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip");
- syncGroup.add(mPipMenuView.getRootSurfaceControl(), null);
- syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null);
- updateMenuBounds(pipBounds);
- syncGroup.addTransaction(pipTx);
- syncGroup.markSyncReady();
+ if (pipBounds != null) {
+ // Synchronize drawing the content in the front and back surfaces together with the pip
+ // transaction and the position change for the front and back surfaces
+ final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip");
+ syncGroup.add(mPipMenuView.getRootSurfaceControl(), null);
+ syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null);
+ syncGroup.addTransaction(pipTx);
+ syncGroup.markSyncReady();
+ } else {
+ pipTx.apply();
+ }
}
private boolean isMenuAttached() {
@@ -388,14 +407,19 @@
final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: updateMenuBounds: %s", TAG, menuBounds.toShortString());
- mSystemWindows.updateViewLayout(mPipBackgroundView,
- getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
- menuBounds.height()));
- mSystemWindows.updateViewLayout(mPipMenuView,
- getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
- menuBounds.height()));
- if (mPipMenuView != null) {
- mPipMenuView.setPipBounds(pipBounds);
+
+ boolean needsRelayout = mPipBackgroundView.getLayoutParams().width != menuBounds.width()
+ || mPipBackgroundView.getLayoutParams().height != menuBounds.height();
+ if (needsRelayout) {
+ mSystemWindows.updateViewLayout(mPipBackgroundView,
+ getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
+ menuBounds.height()));
+ mSystemWindows.updateViewLayout(mPipMenuView,
+ getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
+ menuBounds.height()));
+ if (mPipMenuView != null) {
+ mPipMenuView.setPipBounds(pipBounds);
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
index f86f987..202d36f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -168,6 +168,9 @@
* that the edu text will be marqueed
*/
private boolean isEduTextMarqueed() {
+ if (mEduTextView.getLayout() == null) {
+ return false;
+ }
final int availableWidth = (int) mEduTextView.getWidth()
- mEduTextView.getCompoundPaddingLeft()
- mEduTextView.getCompoundPaddingRight();
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 f315afb..21223c9a 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
@@ -35,7 +35,6 @@
import com.android.wm.shell.pip.PipParamsChangedForwarder;
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.splitscreen.SplitScreenController;
@@ -46,6 +45,7 @@
* TV specific changes to the PipTaskOrganizer.
*/
public class TvPipTaskOrganizer extends PipTaskOrganizer {
+ private final TvPipTransition mTvPipTransition;
public TvPipTaskOrganizer(Context context,
@NonNull SyncTransactionQueue syncTransactionQueue,
@@ -56,7 +56,7 @@
@NonNull PipMenuController pipMenuController,
@NonNull PipAnimationController pipAnimationController,
@NonNull PipSurfaceTransactionHelper surfaceTransactionHelper,
- @NonNull PipTransitionController pipTransitionController,
+ @NonNull TvPipTransition tvPipTransition,
@NonNull PipParamsChangedForwarder pipParamsChangedForwarder,
Optional<SplitScreenController> splitScreenOptional,
@NonNull DisplayController displayController,
@@ -65,9 +65,10 @@
ShellExecutor mainExecutor) {
super(context, syncTransactionQueue, pipTransitionState, pipBoundsState,
pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController,
- surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+ surfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer,
mainExecutor);
+ mTvPipTransition = tvPipTransition;
}
@Override
@@ -105,4 +106,14 @@
// when the menu alpha is 0 (e.g. when a fade-in animation starts).
return true;
}
+
+ @Override
+ protected void cancelAnimationOnTaskVanished() {
+ mTvPipTransition.cancelAnimations();
+ if (mLeash != null) {
+ mSurfaceControlTransactionFactory.getTransaction()
+ .setAlpha(mLeash, 0f)
+ .apply();
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index f24b2b3..571c839 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -16,43 +16,822 @@
package com.android.wm.shell.pip.tv;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PIP;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.transitTypeToString;
+
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE;
+import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
+
+import android.animation.AnimationHandler;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.TaskInfo;
import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.IBinder;
+import android.os.Trace;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+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.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
-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.splitscreen.SplitScreenController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
/**
* PiP Transition for TV.
*/
-public class TvPipTransition extends PipTransition {
+public class TvPipTransition extends PipTransitionController {
+ private static final String TAG = "TvPipTransition";
+ private static final float ZOOM_ANIMATION_SCALE_FACTOR = 0.97f;
+
+ private final PipTransitionState mPipTransitionState;
+ private final PipAnimationController mPipAnimationController;
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final TvPipMenuController mTvPipMenuController;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+ private final PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory
+ mTransactionFactory;
+
+ private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
+ ThreadLocal.withInitial(() -> {
+ AnimationHandler handler = new AnimationHandler();
+ handler.setProvider(new SfVsyncFrameCallbackProvider());
+ return handler;
+ });
+
+ private final long mEnterFadeOutDuration;
+ private final long mEnterFadeInDuration;
+ private final long mExitFadeOutDuration;
+ private final long mExitFadeInDuration;
+
+ @Nullable
+ private Animator mCurrentAnimator;
+
+ /**
+ * The Task window that is currently in PIP windowing mode.
+ */
+ @Nullable
+ private WindowContainerToken mCurrentPipTaskToken;
+
+ @Nullable
+ private IBinder mPendingExitTransition;
public TvPipTransition(Context context,
@NonNull ShellInit shellInit,
@NonNull ShellTaskOrganizer shellTaskOrganizer,
@NonNull Transitions transitions,
TvPipBoundsState tvPipBoundsState,
- PipDisplayLayoutState pipDisplayLayoutState,
- PipTransitionState pipTransitionState,
TvPipMenuController tvPipMenuController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+ PipTransitionState pipTransitionState,
PipAnimationController pipAnimationController,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
- Optional<SplitScreenController> splitScreenOptional) {
- super(context, shellInit, shellTaskOrganizer, transitions, tvPipBoundsState,
- pipDisplayLayoutState, pipTransitionState, tvPipMenuController,
- tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
- splitScreenOptional);
+ PipDisplayLayoutState pipDisplayLayoutState) {
+ super(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, tvPipMenuController,
+ tvPipBoundsAlgorithm);
+ mPipTransitionState = pipTransitionState;
+ mPipAnimationController = pipAnimationController;
+ mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
+ mTvPipMenuController = tvPipMenuController;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+ mTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+
+ mEnterFadeOutDuration = context.getResources().getInteger(
+ R.integer.config_tvPipEnterFadeOutDuration);
+ mEnterFadeInDuration = context.getResources().getInteger(
+ R.integer.config_tvPipEnterFadeInDuration);
+ mExitFadeOutDuration = context.getResources().getInteger(
+ R.integer.config_tvPipExitFadeOutDuration);
+ mExitFadeInDuration = context.getResources().getInteger(
+ R.integer.config_tvPipExitFadeInDuration);
}
+ @Override
+ public void startExitTransition(int type, WindowContainerTransaction out,
+ @Nullable Rect destinationBounds) {
+ cancelAnimations();
+ mPendingExitTransition = mTransitions.startTransition(type, out, this);
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+
+ if (isCloseTransition(info)) {
+ // PiP is closing (without reentering fullscreen activity)
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Starting close animation", TAG);
+ cancelAnimations();
+ startCloseAnimation(info, startTransaction, finishTransaction, finishCallback);
+ mCurrentPipTaskToken = null;
+ return true;
+
+ } else if (transition.equals(mPendingExitTransition)) {
+ // PiP is exiting (reentering fullscreen activity)
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Starting exit animation", TAG);
+
+ final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
+ mPendingExitTransition = null;
+ // PipTaskChange can be null if the PIP task has been detached, for example, when the
+ // task contains multiple activities, the PIP will be moved to a new PIP task when
+ // entering, and be moved back when exiting. In that case, the PIP task will be removed
+ // immediately.
+ final TaskInfo pipTaskInfo = currentPipTaskChange != null
+ ? currentPipTaskChange.getTaskInfo()
+ : mPipOrganizer.getTaskInfo();
+ if (pipTaskInfo == null) {
+ throw new RuntimeException("Cannot find the pip task for exit-pip transition.");
+ }
+
+ final int type = info.getType();
+ switch (type) {
+ case TRANSIT_EXIT_PIP -> {
+ TransitionInfo.Change pipChange = currentPipTaskChange;
+ SurfaceControl activitySc = null;
+ if (mCurrentPipTaskToken == null) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG);
+ } else if (pipChange == null) {
+ // The pipTaskChange is null, this can happen if we are reparenting the
+ // PIP activity back to its original Task. In that case, we should animate
+ // the activity leash instead, which should be the change whose last parent
+ // is the recorded PiP Task.
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (mCurrentPipTaskToken.equals(change.getLastParent())) {
+ // Find the activity that is exiting PiP.
+ pipChange = change;
+ activitySc = change.getLeash();
+ break;
+ }
+ }
+ }
+ if (pipChange == null) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: No window of exiting PIP is found. Can't play expand "
+ + "animation",
+ TAG);
+ removePipImmediately(info, pipTaskInfo, startTransaction, finishTransaction,
+ finishCallback);
+ return true;
+ }
+ final TransitionInfo.Root root = TransitionUtil.getRootFor(pipChange, info);
+ final SurfaceControl pipLeash;
+ if (activitySc != null) {
+ // Use a local leash to animate activity in case the activity has
+ // letterbox which may be broken by PiP animation, e.g. always end at 0,0
+ // in parent and unable to include letterbox area in crop bounds.
+ final SurfaceControl activitySurface = pipChange.getLeash();
+ pipLeash = new SurfaceControl.Builder()
+ .setName(activitySc + "_pip-leash")
+ .setContainerLayer()
+ .setHidden(false)
+ .setParent(root.getLeash())
+ .build();
+ startTransaction.reparent(activitySurface, pipLeash);
+ // Put the activity at local position with offset in case it is letterboxed.
+ final Point activityOffset = pipChange.getEndRelOffset();
+ startTransaction.setPosition(activitySc, activityOffset.x,
+ activityOffset.y);
+ } else {
+ pipLeash = pipChange.getLeash();
+ startTransaction.reparent(pipLeash, root.getLeash());
+ }
+ startTransaction.setLayer(pipLeash, Integer.MAX_VALUE);
+ final Rect currentBounds = mPipBoundsState.getBounds();
+ final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds());
+ cancelAnimations();
+ startExitAnimation(pipTaskInfo, pipLeash, currentBounds, destinationBounds,
+ startTransaction,
+ finishTransaction, finishCallback);
+ }
+ // pass through here is intended
+ case TRANSIT_TO_BACK, TRANSIT_REMOVE_PIP -> removePipImmediately(info, pipTaskInfo,
+ startTransaction, finishTransaction,
+ finishCallback
+ );
+ default -> {
+ return false;
+ }
+ }
+ mCurrentPipTaskToken = null;
+ return true;
+
+ } else if (isEnteringPip(info)) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Starting enter animation", TAG);
+
+ // Search for an Enter PiP transition
+ TransitionInfo.Change enterPip = null;
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
+ enterPip = change;
+ }
+ }
+ if (enterPip == null) {
+ throw new IllegalStateException("Trying to start PiP animation without a pip"
+ + "participant");
+ }
+
+ // Make sure other open changes are visible as entering PIP. Some may be hidden in
+ // Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change == enterPip) continue;
+ if (TransitionUtil.isOpeningType(change.getMode())) {
+ final SurfaceControl leash = change.getLeash();
+ startTransaction.show(leash).setAlpha(leash, 1.f);
+ }
+ }
+
+ cancelAnimations();
+ startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * For {@link Transitions#TRANSIT_REMOVE_PIP}, we just immediately remove the PIP Task.
+ */
+ private void removePipImmediately(@NonNull TransitionInfo info,
+ @NonNull TaskInfo taskInfo, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: removePipImmediately", TAG);
+ cancelAnimations();
+ startTransaction.apply();
+ finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
+ mPipDisplayLayoutState.getDisplayBounds());
+ mTvPipMenuController.detach();
+ mPipOrganizer.onExitPipFinished(taskInfo);
+ finishCallback.onTransitionFinished(/* wct= */ null);
+
+ mPipTransitionState.setTransitionState(UNDEFINED);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
+ }
+
+ private void startCloseAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final TransitionInfo.Change pipTaskChange = findCurrentPipTaskChange(info);
+ final SurfaceControl pipLeash = pipTaskChange.getLeash();
+
+ final List<SurfaceControl> closeLeashes = new ArrayList<>();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (TransitionUtil.isClosingType(change.getMode()) && change != pipTaskChange) {
+ closeLeashes.add(change.getLeash());
+ }
+ }
+
+ final Rect pipBounds = mPipBoundsState.getBounds();
+ mSurfaceTransactionHelper
+ .resetScale(startTransaction, pipLeash, pipBounds)
+ .crop(startTransaction, pipLeash, pipBounds)
+ .shadow(startTransaction, pipLeash, false);
+
+ final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction();
+ for (SurfaceControl leash : closeLeashes) {
+ startTransaction.setShadowRadius(leash, 0f);
+ }
+
+ ValueAnimator closeFadeOutAnimator = createAnimator();
+ closeFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+ closeFadeOutAnimator.setDuration(mExitFadeOutDuration);
+ closeFadeOutAnimator.addUpdateListener(
+ animationUpdateListener(pipLeash).fadingOut().withMenu());
+ for (SurfaceControl leash : closeLeashes) {
+ closeFadeOutAnimator.addUpdateListener(animationUpdateListener(leash).fadingOut());
+ }
+
+ closeFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close animation: start", TAG);
+ for (SurfaceControl leash : closeLeashes) {
+ startTransaction.setShadowRadius(leash, 0f);
+ }
+ startTransaction.apply();
+
+ mPipTransitionState.setTransitionState(EXITING_PIP);
+ sendOnPipTransitionStarted(TRANSITION_DIRECTION_REMOVE_STACK);
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close animation: cancel", TAG);
+ sendOnPipTransitionCancelled(TRANSITION_DIRECTION_REMOVE_STACK);
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close animation: end", TAG);
+ mTvPipMenuController.detach();
+ finishCallback.onTransitionFinished(null /* wct */);
+ transaction.close();
+ mPipTransitionState.setTransitionState(UNDEFINED);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
+
+ mCurrentAnimator = null;
+ }
+ });
+
+ closeFadeOutAnimator.start();
+ mCurrentAnimator = closeFadeOutAnimator;
+ }
+
+ @Override
+ public void startEnterAnimation(@NonNull TransitionInfo.Change pipChange,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ // Keep track of the PIP task
+ mCurrentPipTaskToken = pipChange.getContainer();
+ final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo();
+ final SurfaceControl leash = pipChange.getLeash();
+
+ mTvPipMenuController.attach(leash);
+ setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
+ taskInfo.topActivityInfo);
+
+ final Rect pipBounds =
+ mPipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas();
+ mPipBoundsState.setBounds(pipBounds);
+ mTvPipMenuController.movePipMenu(null, pipBounds, 0f);
+
+ final WindowContainerTransaction resizePipWct = new WindowContainerTransaction();
+ resizePipWct.setWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
+ resizePipWct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
+ resizePipWct.setBounds(taskInfo.token, pipBounds);
+
+ mSurfaceTransactionHelper
+ .resetScale(finishTransaction, leash, pipBounds)
+ .crop(finishTransaction, leash, pipBounds)
+ .shadow(finishTransaction, leash, false);
+
+ final Rect currentBounds = pipChange.getStartAbsBounds();
+ final Rect fadeOutCurrentBounds = scaledRect(currentBounds, ZOOM_ANIMATION_SCALE_FACTOR);
+
+ final ValueAnimator enterFadeOutAnimator = createAnimator();
+ enterFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+ enterFadeOutAnimator.setDuration(mEnterFadeOutDuration);
+ enterFadeOutAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingOut()
+ .animateBounds(currentBounds, fadeOutCurrentBounds, currentBounds));
+
+ enterFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @SuppressLint("MissingPermission")
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter fade out animation: end", TAG);
+ SurfaceControl.Transaction tx = mTransactionFactory.getTransaction();
+ mSurfaceTransactionHelper
+ .resetScale(tx, leash, pipBounds)
+ .crop(tx, leash, pipBounds)
+ .shadow(tx, leash, false);
+ mShellTaskOrganizer.applyTransaction(resizePipWct);
+ tx.apply();
+ }
+ });
+
+ final ValueAnimator enterFadeInAnimator = createAnimator();
+ enterFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
+ enterFadeInAnimator.setDuration(mEnterFadeInDuration);
+ enterFadeInAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingIn()
+ .withMenu()
+ .atBounds(pipBounds));
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet
+ .play(enterFadeInAnimator)
+ .after(500)
+ .after(enterFadeOutAnimator);
+
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter animation: start", TAG);
+ startTransaction.apply();
+ mPipTransitionState.setTransitionState(ENTERING_PIP);
+ sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter animation: cancel", TAG);
+ enterFadeInAnimator.setCurrentFraction(1f);
+ sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter animation: end", TAG);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ wct.setBounds(taskInfo.token, pipBounds);
+ finishCallback.onTransitionFinished(wct);
+
+ mPipTransitionState.setTransitionState(ENTERED_PIP);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ mCurrentAnimator = null;
+ }
+ });
+
+ animatorSet.start();
+ mCurrentAnimator = animatorSet;
+ }
+
+ private void startExitAnimation(@NonNull TaskInfo taskInfo, SurfaceControl leash,
+ Rect currentBounds, Rect destinationBounds,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final Rect fadeInStartBounds = scaledRect(destinationBounds, ZOOM_ANIMATION_SCALE_FACTOR);
+
+ final ValueAnimator exitFadeOutAnimator = createAnimator();
+ exitFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+ exitFadeOutAnimator.setDuration(mExitFadeOutDuration);
+ exitFadeOutAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingOut()
+ .withMenu()
+ .atBounds(currentBounds));
+ exitFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit fade out animation: end", TAG);
+ startTransaction.apply();
+ mPipMenuController.detach();
+ }
+ });
+
+ final ValueAnimator exitFadeInAnimator = createAnimator();
+ exitFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
+ exitFadeInAnimator.setDuration(mExitFadeInDuration);
+ exitFadeInAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingIn()
+ .animateBounds(fadeInStartBounds, destinationBounds, destinationBounds));
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playSequentially(
+ exitFadeOutAnimator,
+ exitFadeInAnimator
+ );
+
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit animation: start", TAG);
+ mPipTransitionState.setTransitionState(EXITING_PIP);
+ sendOnPipTransitionStarted(TRANSITION_DIRECTION_LEAVE_PIP);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit animation: cancel", TAG);
+ sendOnPipTransitionCancelled(TRANSITION_DIRECTION_LEAVE_PIP);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit animation: end", TAG);
+ mPipOrganizer.onExitPipFinished(taskInfo);
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ wct.setBounds(taskInfo.token, destinationBounds);
+ finishCallback.onTransitionFinished(wct);
+
+ mPipTransitionState.setTransitionState(UNDEFINED);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_LEAVE_PIP);
+
+ mCurrentAnimator = null;
+ }
+ });
+
+ animatorSet.start();
+ mCurrentAnimator = animatorSet;
+ }
+
+ @NonNull
+ private ValueAnimator createAnimator() {
+ final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+ animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
+ return animator;
+ }
+
+ @NonNull
+ private TvPipTransitionAnimatorUpdateListener animationUpdateListener(
+ @NonNull SurfaceControl leash) {
+ return new TvPipTransitionAnimatorUpdateListener(leash, mTvPipMenuController,
+ mTransactionFactory.getTransaction(), mSurfaceTransactionHelper);
+ }
+
+ @NonNull
+ private static Rect scaledRect(@NonNull Rect rect, float scale) {
+ final Rect out = new Rect(rect);
+ out.inset((int) (rect.width() * (1 - scale) / 2), (int) (rect.height() * (1 - scale) / 2));
+ return out;
+ }
+
+ private boolean isCloseTransition(TransitionInfo info) {
+ final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
+ return currentPipTaskChange != null && info.getType() == TRANSIT_CLOSE;
+ }
+
+ @Nullable
+ private TransitionInfo.Change findCurrentPipTaskChange(@NonNull TransitionInfo info) {
+ if (mCurrentPipTaskToken == null) {
+ return null;
+ }
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (mCurrentPipTaskToken.equals(change.getContainer())) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Whether we should handle the given {@link TransitionInfo} animation as entering PIP.
+ */
+ private boolean isEnteringPip(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (isEnteringPip(change, info.getType())) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Whether a particular change is a window that is entering pip.
+ */
+ @Override
+ public boolean isEnteringPip(@NonNull TransitionInfo.Change change,
+ @WindowManager.TransitionType int transitType) {
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED
+ && !Objects.equals(change.getContainer(), mCurrentPipTaskToken)) {
+ if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN
+ || transitType == TRANSIT_CHANGE) {
+ return true;
+ }
+ // Please file a bug to handle the unexpected transition type.
+ android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type="
+ + transitTypeToString(transitType), new Throwable());
+ }
+ return false;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG);
+ if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
+ mCurrentAnimator.end();
+ }
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ if (requestHasPipEnter(request)) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: handle PiP enter request", TAG);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ augmentRequest(transition, request, wct);
+ return wct;
+ } else if (request.getType() == TRANSIT_TO_BACK && request.getTriggerTask() != null
+ && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_PINNED) {
+ // if we receive a TRANSIT_TO_BACK type of request while in PiP
+ mPendingExitTransition = transition;
+
+ // update the transition state to avoid {@link PipTaskOrganizer#onTaskVanished()} calls
+ mPipTransitionState.setTransitionState(EXITING_PIP);
+
+ // return an empty WindowContainerTransaction so that we don't check other handlers
+ return new WindowContainerTransaction();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request,
+ @NonNull WindowContainerTransaction outWCT) {
+ if (!requestHasPipEnter(request)) {
+ throw new IllegalStateException("Called PiP augmentRequest when request has no PiP");
+ }
+ outWCT.setActivityWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_UNDEFINED);
+ }
+
+ /**
+ * Cancel any ongoing PiP transitions/animations.
+ */
+ public void cancelAnimations() {
+ if (mPipAnimationController.isAnimating()) {
+ mPipAnimationController.getCurrentAnimator().cancel();
+ mPipAnimationController.resetAnimatorState();
+ }
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.cancel();
+ }
+ }
+
+ @Override
+ public void end() {
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.end();
+ }
+ }
+
+ private static class TvPipTransitionAnimatorUpdateListener implements
+ ValueAnimator.AnimatorUpdateListener {
+ private final SurfaceControl mLeash;
+ private final TvPipMenuController mTvPipMenuController;
+ private final SurfaceControl.Transaction mTransaction;
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final RectF mTmpRectF = new RectF();
+ private final Rect mTmpRect = new Rect();
+
+ private float mStartAlpha = ALPHA_NO_CHANGE;
+ private float mEndAlpha = ALPHA_NO_CHANGE;
+
+ @Nullable
+ private Rect mStartBounds;
+ @Nullable
+ private Rect mEndBounds;
+ private Rect mWindowContainerBounds;
+ private boolean mShowMenu;
+
+ TvPipTransitionAnimatorUpdateListener(@NonNull SurfaceControl leash,
+ @NonNull TvPipMenuController tvPipMenuController,
+ @NonNull SurfaceControl.Transaction transaction,
+ @NonNull PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
+ mLeash = leash;
+ mTvPipMenuController = tvPipMenuController;
+ mTransaction = transaction;
+ mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
+ }
+
+ public TvPipTransitionAnimatorUpdateListener animateAlpha(
+ @FloatRange(from = 0.0, to = 1.0) float startAlpha,
+ @FloatRange(from = 0.0, to = 1.0) float endAlpha) {
+ mStartAlpha = startAlpha;
+ mEndAlpha = endAlpha;
+ return this;
+ }
+
+ public TvPipTransitionAnimatorUpdateListener animateBounds(@NonNull Rect startBounds,
+ @NonNull Rect endBounds, @NonNull Rect windowContainerBounds) {
+ mStartBounds = startBounds;
+ mEndBounds = endBounds;
+ mWindowContainerBounds = windowContainerBounds;
+ return this;
+ }
+
+ public TvPipTransitionAnimatorUpdateListener atBounds(@NonNull Rect bounds) {
+ return animateBounds(bounds, bounds, bounds);
+ }
+
+ public TvPipTransitionAnimatorUpdateListener fadingOut() {
+ return animateAlpha(1f, 0f);
+ }
+
+ public TvPipTransitionAnimatorUpdateListener fadingIn() {
+ return animateAlpha(0f, 1f);
+ }
+
+ public TvPipTransitionAnimatorUpdateListener withMenu() {
+ mShowMenu = true;
+ return this;
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+ final float fraction = animation.getAnimatedFraction();
+ final float alpha = lerp(mStartAlpha, mEndAlpha, fraction);
+ if (mStartBounds != null && mEndBounds != null) {
+ lerp(mStartBounds, mEndBounds, fraction, mTmpRectF);
+ applyAnimatedValue(alpha, mTmpRectF);
+ } else {
+ applyAnimatedValue(alpha, null);
+ }
+ }
+
+ private void applyAnimatedValue(float alpha, @Nullable RectF bounds) {
+ Trace.beginSection("applyAnimatedValue");
+ final SurfaceControl.Transaction tx = mTransaction;
+
+ Trace.beginSection("leash scale and alpha");
+ if (alpha != ALPHA_NO_CHANGE) {
+ mSurfaceTransactionHelper.alpha(tx, mLeash, alpha);
+ }
+ if (bounds != null) {
+ mSurfaceTransactionHelper.scale(tx, mLeash, mWindowContainerBounds, bounds);
+ }
+ mSurfaceTransactionHelper.shadow(tx, mLeash, false);
+ tx.show(mLeash);
+ Trace.endSection();
+
+ if (mShowMenu) {
+ Trace.beginSection("movePipMenu");
+ if (bounds != null) {
+ mTmpRect.set((int) bounds.left, (int) bounds.top, (int) bounds.right,
+ (int) bounds.bottom);
+ mTvPipMenuController.movePipMenu(tx, mTmpRect, alpha);
+ } else {
+ mTvPipMenuController.movePipMenu(tx, null, alpha);
+ }
+ Trace.endSection();
+ } else {
+ mTvPipMenuController.movePipMenu(tx, null, 0f);
+ }
+
+ tx.apply();
+ Trace.endSection();
+ }
+
+ private float lerp(float start, float end, float fraction) {
+ return start * (1 - fraction) + end * fraction;
+ }
+
+ private void lerp(@NonNull Rect start, @NonNull Rect end, float fraction,
+ @NonNull RectF out) {
+ out.set(
+ start.left * (1 - fraction) + end.left * fraction,
+ start.top * (1 - fraction) + end.top * fraction,
+ start.right * (1 - fraction) + end.right * fraction,
+ start.bottom * (1 - fraction) + end.bottom * fraction);
+ }
+ }
}
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 4e2b7f6..800f9e4 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
@@ -66,6 +66,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -283,7 +284,7 @@
doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
.round(any(), any(), anyBoolean());
doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
- .scale(any(), any(), any(), any(), anyFloat());
+ .scale(any(), any(), any(), ArgumentMatchers.<Rect>any(), anyFloat());
doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
.alpha(any(), any(), anyFloat());
doNothing().when(mMockPipSurfaceTransactionHelper).onDensityOrFontScaleChanged(any());
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index b1e1585..90c7d46 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -49,6 +49,7 @@
import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
+import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver
@@ -100,6 +101,7 @@
SettingsTextFieldPasswordPageProvider,
SearchScaffoldPageProvider,
CardPageProvider,
+ CopyablePageProvider,
),
rootPages = listOf(
HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index f52ceec..1d897f7 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -44,6 +44,7 @@
import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
+import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.widget.scaffold.HomeScaffold
@@ -71,6 +72,7 @@
AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
CardPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ CopyablePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
)
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
new file mode 100644
index 0000000..f897d8c
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.ui
+
+import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CopyableBody
+
+private const val TITLE = "Sample Copyable"
+
+object CopyablePageProvider : SettingsPageProvider {
+ override val name = "Copyable"
+
+ private val owner = createSettingsPage()
+
+ fun buildInjectEntry(): SettingsEntryBuilder {
+ return SettingsEntryBuilder.createInject(owner)
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
+ .setSearchDataFn { EntrySearchData(title = TITLE) }
+ }
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ RegularScaffold(title = TITLE) {
+ Box(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
+ CopyableBody(body = "Copyable body")
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index 905640f..b40e911 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
#
[versions]
-agp = "8.1.3"
+agp = "8.1.4"
compose-compiler = "1.5.1"
dexmaker-mockito = "2.28.3"
jvm = "17"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
new file mode 100644
index 0000000..930d0a1
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.DpOffset
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+fun CopyableBody(body: String) {
+ var expanded by remember { mutableStateOf(false) }
+ var dpOffset by remember { mutableStateOf(DpOffset.Unspecified) }
+
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onLongPress = {
+ dpOffset = DpOffset(it.x.toDp(), it.y.toDp())
+ expanded = true
+ },
+ )
+ }
+ ) {
+ SettingsBody(body)
+
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ offset = dpOffset,
+ ) {
+ DropdownMenuTitle(body)
+ DropdownMenuCopy(body) { expanded = false }
+ }
+ }
+}
+
+@Composable
+private fun DropdownMenuTitle(text: String) {
+ Text(
+ text = text,
+ modifier = Modifier
+ .padding(MenuDefaults.DropdownMenuItemContentPadding)
+ .padding(
+ top = SettingsDimension.itemPaddingAround,
+ bottom = SettingsDimension.buttonPaddingVertical,
+ ),
+ color = SettingsTheme.colorScheme.categoryTitle,
+ style = MaterialTheme.typography.labelMedium,
+ )
+}
+
+@Composable
+private fun DropdownMenuCopy(body: String, onCopy: () -> Unit) {
+ val clipboardManager = LocalClipboardManager.current
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = stringResource(android.R.string.copy),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ onClick = {
+ onCopy()
+ clipboardManager.setText(AnnotatedString(body))
+ }
+ )
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
new file mode 100644
index 0000000..71072a5
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CopyableBodyTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun text_isDisplayed() {
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).assertIsDisplayed()
+ }
+
+ @Test
+ fun onLongPress_contextMenuDisplayed() {
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).performTouchInput {
+ longClick()
+ }
+
+ composeTestRule.onNodeWithText(context.getString(android.R.string.copy)).assertIsDisplayed()
+ }
+
+ @Test
+ fun onCopy_saveToClipboard() {
+ val clipboardManager = context.getSystemService(ClipboardManager::class.java)!!
+ clipboardManager.setPrimaryClip(ClipData.newPlainText("", ""))
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).performTouchInput {
+ longClick()
+ }
+ composeTestRule.onNodeWithText(context.getString(android.R.string.copy)).performClick()
+
+ assertThat(clipboardManager.primaryClip!!.getItemAt(0).text.toString()).isEqualTo(TEXT)
+ }
+
+ private companion object {
+ const val TEXT = "Text"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index fc10a27..45295b0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -36,10 +36,10 @@
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
import com.android.settingslib.development.DevelopmentSettingsEnabler
import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.CopyableBody
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SettingsTitle
import com.android.settingslib.spaprivileged.R
@@ -71,26 +71,38 @@
@Composable
private fun InstallType(app: ApplicationInfo) {
if (!app.isInstantApp) return
- Spacer(modifier = Modifier.height(4.dp))
- SettingsBody(stringResource(com.android.settingslib.widget.preference.app.R.string.install_type_instant))
+ Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
+ SettingsBody(
+ stringResource(
+ com.android.settingslib.widget.preference.app.R.string.install_type_instant
+ )
+ )
}
@Composable
private fun AppVersion() {
- if (packageInfo.versionName == null) return
- Spacer(modifier = Modifier.height(4.dp))
- SettingsBody(packageInfo.versionNameBidiWrapped)
+ val versionName = packageInfo.versionNameBidiWrapped ?: return
+ Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
+ SettingsBody(versionName)
}
@Composable
fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) {
- if (packageInfo.versionName == null) return
+ val context = LocalContext.current
+ val footer = remember(showPackageName) {
+ val list = mutableListOf<String>()
+ packageInfo.versionNameBidiWrapped?.let {
+ list += context.getString(R.string.version_text, it)
+ }
+ if (showPackageName) {
+ list += packageInfo.packageName
+ }
+ list.joinToString(separator = System.lineSeparator())
+ }
+ if (footer.isBlank()) return
HorizontalDivider()
Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
- SettingsBody(stringResource(R.string.version_text, packageInfo.versionNameBidiWrapped))
- if (showPackageName) {
- SettingsBody(packageInfo.packageName)
- }
+ CopyableBody(footer)
}
}
@@ -104,7 +116,7 @@
private companion object {
/** Wrapped the version name, so its directionality still keep same when RTL. */
- val PackageInfo.versionNameBidiWrapped: String
+ val PackageInfo.versionNameBidiWrapped: String?
get() = BidiFormatter.getInstance().unicodeWrap(versionName)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
index ab34f68..72a5bd7 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
@@ -105,7 +105,8 @@
}
}
- composeTestRule.onNodeWithText("version $VERSION_NAME").assertIsDisplayed()
+ composeTestRule.onNodeWithText(text = "version $VERSION_NAME", substring = true)
+ .assertIsDisplayed()
}
@Test
@@ -119,10 +120,10 @@
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
- appInfoProvider.FooterAppVersion(true)
+ appInfoProvider.FooterAppVersion(showPackageName = true)
}
}
- composeTestRule.onNodeWithText(PACKAGE_NAME).assertIsDisplayed()
+ composeTestRule.onNodeWithText(text = PACKAGE_NAME, substring = true).assertIsDisplayed()
}
@@ -137,10 +138,10 @@
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
- appInfoProvider.FooterAppVersion(false)
+ appInfoProvider.FooterAppVersion(showPackageName = false)
}
}
- composeTestRule.onNodeWithText(PACKAGE_NAME).assertDoesNotExist()
+ composeTestRule.onNodeWithText(text = PACKAGE_NAME, substring = true).assertDoesNotExist()
}
private companion object {
diff --git a/packages/SettingsLib/res/drawable/ic_bt_untethered_earbuds.xml b/packages/SettingsLib/res/drawable/ic_bt_untethered_earbuds.xml
new file mode 100644
index 0000000..bcf5b91
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_untethered_earbuds.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorControlNormal" >
+ <path
+ android:fillColor="#4E4639"
+ android:pathData="M21.818,18.14V15.227C21.818,12.522 19.615,10.318 16.909,10.318C14.204,10.318 12,12.522 12,15.227V19.591C12,22.296 14.204,24.5 16.909,24.5C17.662,24.5 18.382,24.326 19.025,24.02C19.538,24.326 20.127,24.5 20.727,24.5C22.527,24.5 24,23.028 24,21.228C24,19.809 23.084,18.587 21.818,18.14ZM16.909,12.5C18.414,12.5 19.636,13.722 19.636,15.227C19.636,16.733 18.414,17.955 16.909,17.955C15.404,17.955 14.182,16.733 14.182,15.227C14.182,13.722 15.404,12.5 16.909,12.5ZM14.182,19.591V19.308C14.967,19.831 15.906,20.136 16.909,20.136C17.913,20.136 18.851,19.831 19.636,19.308V19.591C19.636,19.635 19.636,19.678 19.636,19.711C19.636,19.722 19.636,19.733 19.636,19.744C19.636,19.777 19.636,19.82 19.625,19.853C19.625,19.864 19.625,19.886 19.625,19.896C19.625,19.918 19.615,19.951 19.615,19.973C19.615,19.995 19.604,20.028 19.604,20.049C19.604,20.06 19.593,20.082 19.593,20.093C19.549,20.3 19.494,20.497 19.407,20.693C19.385,20.736 19.364,20.78 19.342,20.824C19.342,20.835 19.331,20.846 19.331,20.846C19.32,20.867 19.309,20.889 19.298,20.911C19.287,20.933 19.265,20.966 19.254,20.987C19.244,20.998 19.244,21.009 19.233,21.02C19.211,21.053 19.2,21.075 19.178,21.107C19.178,21.107 19.178,21.108 19.178,21.118C18.687,21.838 17.858,22.318 16.92,22.318C15.404,22.318 14.182,21.097 14.182,19.591Z" />
+ <path
+ android:fillColor="#4E4639"
+ android:pathData="M12,5.409C12,2.704 9.796,0.5 7.091,0.5C4.385,0.5 2.182,2.704 2.182,5.409V8.322C0.916,8.769 0,9.991 0,11.409C0,13.209 1.473,14.682 3.273,14.682C3.873,14.682 4.462,14.507 4.975,14.202C5.618,14.507 6.338,14.682 7.091,14.682C9.796,14.682 12,12.478 12,9.773V5.409ZM7.091,2.682C8.596,2.682 9.818,3.904 9.818,5.409C9.818,6.915 8.596,8.136 7.091,8.136C5.585,8.136 4.364,6.915 4.364,5.409C4.364,3.904 5.585,2.682 7.091,2.682ZM7.091,12.5C6.153,12.5 5.324,12.02 4.833,11.3C4.833,11.3 4.833,11.3 4.833,11.289C4.811,11.256 4.8,11.234 4.778,11.202C4.767,11.191 4.767,11.18 4.756,11.169C4.745,11.147 4.724,11.115 4.713,11.093C4.702,11.071 4.691,11.049 4.68,11.027C4.68,11.016 4.669,11.005 4.669,11.005C4.647,10.962 4.625,10.918 4.604,10.875C4.516,10.678 4.451,10.482 4.418,10.274C4.418,10.264 4.407,10.242 4.407,10.231C4.407,10.209 4.396,10.176 4.396,10.155C4.396,10.133 4.385,10.1 4.385,10.078C4.385,10.067 4.385,10.045 4.385,10.035C4.385,10.002 4.375,9.958 4.375,9.925C4.375,9.915 4.375,9.904 4.375,9.893C4.364,9.86 4.364,9.816 4.364,9.773V9.489C5.149,10.013 6.087,10.318 7.091,10.318C8.095,10.318 9.033,10.013 9.818,9.489V9.773C9.818,11.278 8.596,12.5 7.091,12.5Z" />
+</vector>
+
diff --git a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
rename to packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
index 5e6caf0..27c9b3f 100644
--- a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
@@ -11,20 +11,17 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.common
-import com.android.systemui.common.domain.interactor.ConfigurationInteractor
-import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
+package com.android.systemui.common.data
+
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
import dagger.Binds
import dagger.Module
@Module
-abstract class CommonModule {
+abstract class CommonDataLayerModule {
@Binds abstract fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository
-
- @Binds abstract fun bindInteractor(impl: ConfigurationInteractorImpl): ConfigurationInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt b/packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
similarity index 71%
copy from packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
copy to packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
index 5e6caf0..7be2eaf 100644
--- a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
@@ -11,20 +11,17 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.common
+
+package com.android.systemui.common.domain
import com.android.systemui.common.domain.interactor.ConfigurationInteractor
import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
-import com.android.systemui.common.ui.data.repository.ConfigurationRepository
-import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
import dagger.Binds
import dagger.Module
@Module
-abstract class CommonModule {
- @Binds abstract fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository
-
+abstract class CommonDomainLayerModule {
@Binds abstract fun bindInteractor(impl: ConfigurationInteractorImpl): ConfigurationInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt
new file mode 100644
index 0000000..8d04e3d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.common.ui.view
+
+import android.view.View
+
+/**
+ * Set this view's [View#importantForAccessibility] to [View#IMPORTANT_FOR_ACCESSIBILITY_YES] or
+ * [View#IMPORTANT_FOR_ACCESSIBILITY_NO] based on [value].
+ */
+fun View.setImportantForAccessibilityYesNo(value: Boolean) {
+ importantForAccessibility =
+ if (value) View.IMPORTANT_FOR_ACCESSIBILITY_YES else View.IMPORTANT_FOR_ACCESSIBILITY_NO
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 482832b..0405ca4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -42,7 +42,8 @@
import com.android.systemui.bouncer.ui.BouncerViewModule;
import com.android.systemui.classifier.FalsingModule;
import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
-import com.android.systemui.common.CommonModule;
+import com.android.systemui.common.data.CommonDataLayerModule;
+import com.android.systemui.common.domain.CommonDomainLayerModule;
import com.android.systemui.communal.dagger.CommunalModule;
import com.android.systemui.complication.dagger.ComplicationComponent;
import com.android.systemui.controls.dagger.ControlsModule;
@@ -176,7 +177,8 @@
ClipboardOverlayModule.class,
ClockRegistryModule.class,
CommunalModule.class,
- CommonModule.class,
+ CommonDataLayerModule.class,
+ CommonDomainLayerModule.class,
ConnectivityModule.class,
ControlsModule.class,
CoroutinesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index a44b4b4..c810786 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -184,6 +184,8 @@
import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -604,6 +606,7 @@
private final LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
private final PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
private final SharedNotificationContainerInteractor mSharedNotificationContainerInteractor;
+ private final ActiveNotificationsInteractor mActiveNotificationsInteractor;
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final KeyguardInteractor mKeyguardInteractor;
private final PowerInteractor mPowerInteractor;
@@ -774,6 +777,7 @@
KeyguardInteractor keyguardInteractor,
ActivityStarter activityStarter,
SharedNotificationContainerInteractor sharedNotificationContainerInteractor,
+ ActiveNotificationsInteractor activeNotificationsInteractor,
KeyguardViewConfigurator keyguardViewConfigurator,
KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
SplitShadeStateController splitShadeStateController,
@@ -804,6 +808,7 @@
mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mSharedNotificationContainerInteractor = sharedNotificationContainerInteractor;
+ mActiveNotificationsInteractor = activeNotificationsInteractor;
mKeyguardInteractor = keyguardInteractor;
mPowerInteractor = powerInteractor;
mKeyguardViewConfigurator = keyguardViewConfigurator;
@@ -1795,9 +1800,14 @@
}
private boolean hasVisibleNotifications() {
- return mNotificationStackScrollLayoutController
- .getVisibleNotificationCount() != 0
- || mMediaDataManager.hasActiveMediaOrRecommendation();
+ if (FooterViewRefactor.isEnabled()) {
+ return mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()
+ || mMediaDataManager.hasActiveMediaOrRecommendation();
+ } else {
+ return mNotificationStackScrollLayoutController
+ .getVisibleNotificationCount() != 0
+ || mMediaDataManager.hasActiveMediaOrRecommendation();
+ }
}
/** Returns space between top of lock icon and bottom of NotificationStackScrollLayout. */
@@ -3004,7 +3014,9 @@
@Override
public void setBouncerShowing(boolean bouncerShowing) {
mBouncerShowing = bouncerShowing;
- mNotificationStackScrollLayoutController.updateShowEmptyShadeView();
+ if (!FooterViewRefactor.isEnabled()) {
+ mNotificationStackScrollLayoutController.updateShowEmptyShadeView();
+ }
updateVisibility();
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 868fbce..e84bfc5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -87,6 +87,8 @@
import com.android.systemui.statusbar.PulseExpansionHandler;
import com.android.systemui.statusbar.QsFrameTranslateController;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
@@ -106,12 +108,12 @@
import dalvik.annotation.optimization.NeverCompile;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import javax.inject.Inject;
-import dagger.Lazy;
-
/** Handles QuickSettings touch handling, expansion and animation state
* TODO (b/264460656) make this dumpable
*/
@@ -157,6 +159,7 @@
private final InteractionJankMonitor mInteractionJankMonitor;
private final ShadeRepository mShadeRepository;
private final ShadeInteractor mShadeInteractor;
+ private final ActiveNotificationsInteractor mActiveNotificationsInteractor;
private final JavaAdapter mJavaAdapter;
private final FalsingManager mFalsingManager;
private final AccessibilityManager mAccessibilityManager;
@@ -339,6 +342,7 @@
KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
ShadeRepository shadeRepository,
ShadeInteractor shadeInteractor,
+ ActiveNotificationsInteractor activeNotificationsInteractor,
JavaAdapter javaAdapter,
CastController castController,
SplitShadeStateController splitShadeStateController
@@ -386,6 +390,7 @@
mInteractionJankMonitor = interactionJankMonitor;
mShadeRepository = shadeRepository;
mShadeInteractor = shadeInteractor;
+ mActiveNotificationsInteractor = activeNotificationsInteractor;
mJavaAdapter = javaAdapter;
mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback());
@@ -983,7 +988,9 @@
void updateQsState() {
boolean qsFullScreen = getExpanded() && !mSplitShadeEnabled;
mShadeRepository.setLegacyQsFullscreen(qsFullScreen);
- mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
+ if (!FooterViewRefactor.isEnabled()) {
+ mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
+ }
mNotificationStackScrollLayoutController.setScrollingEnabled(
mBarState != KEYGUARD && (!qsFullScreen || mExpansionFromOverscroll));
@@ -2230,8 +2237,12 @@
mLockscreenShadeTransitionController.getQSDragProgress());
setExpansionHeight(qsHeight);
}
- if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() == 0
- && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
+
+ boolean hasNotifications = FooterViewRefactor.isEnabled()
+ ? mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()
+ : mNotificationStackScrollLayoutController.getVisibleNotificationCount()
+ != 0;
+ if (!hasNotifications && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
// No notifications are visible, let's animate to the height of qs instead
if (isQsFragmentCreated()) {
// Let's interpolate to the header height instead of the top padding,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
index de334bb..2338be2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
@@ -85,7 +85,9 @@
public void setFooterVisibility(@Visibility int visibility) {
mFooterVisibility = visibility;
- setSecondaryVisible(visibility == View.VISIBLE, false);
+ setSecondaryVisible(/* visible = */ visibility == View.VISIBLE,
+ /* animate = */false,
+ /* onAnimationEnded = */ null);
}
public void setFooterText(@StringRes int text) {
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 64970e4..fa2748c 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
@@ -16,17 +16,19 @@
package com.android.systemui.statusbar.notification.collection.coordinator
+import com.android.app.tracing.traceSection
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
import com.android.systemui.statusbar.phone.NotificationIconAreaController
-import com.android.app.tracing.traceSection
import javax.inject.Inject
/**
@@ -40,6 +42,7 @@
private val groupExpansionManagerImpl: GroupExpansionManagerImpl,
private val notificationIconAreaController: NotificationIconAreaController,
private val renderListInteractor: RenderNotificationListInteractor,
+ private val activeNotificationsInteractor: ActiveNotificationsInteractor,
) : Coordinator {
override fun attach(pipeline: NotifPipeline) {
@@ -49,8 +52,14 @@
fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
traceSection("StackCoordinator.onAfterRenderList") {
- controller.setNotifStats(calculateNotifStats(entries))
- if (NotificationIconContainerRefactor.isEnabled) {
+ val notifStats = calculateNotifStats(entries)
+ if (FooterViewRefactor.isEnabled) {
+ activeNotificationsInteractor.setNotifStats(notifStats)
+ }
+ // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the footer
+ // visibility is handled in the new stack.
+ controller.setNotifStats(notifStats)
+ if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) {
renderListInteractor.setRenderedList(entries)
} else {
notificationIconAreaController.updateNotificationIcons(entries)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
index fde4ecb..a37937a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
@@ -26,6 +26,7 @@
/** Data provided to the NotificationRootController whenever the pipeline runs */
data class NotifStats(
+ // TODO(b/293167744): The count can be removed from here when we remove the FooterView flag.
val numActiveNotifs: Int,
val hasNonClearableAlertingNotifs: Boolean,
val hasClearableAlertingNotifs: Boolean,
@@ -33,17 +34,16 @@
val hasClearableSilentNotifs: Boolean
) {
companion object {
- @JvmStatic
- val empty = NotifStats(0, false, false, false, false)
+ @JvmStatic val empty = NotifStats(0, false, false, false, false)
}
}
/**
* An implementation of NotifStackController which provides default, no-op implementations of each
- * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding
- * methods, rather than forcing us to add no-op implementations in their implementation every time
- * a method is added.
+ * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding methods,
+ * rather than forcing us to add no-op implementations in their implementation every time a method
+ * is added.
*/
open class DefaultNotifStackController @Inject constructor() : NotifStackController {
override fun setNotifStats(stats: NotifStats) {}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
index 12ee54d..5ed82cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.data.repository
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key
import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
@@ -37,6 +38,9 @@
/** Are any already-seen notifications currently filtered out of the active list? */
val hasFilteredOutSeenNotifications = MutableStateFlow(false)
+
+ /** Stats about the list of notifications attached to the shade */
+ val notifStats = MutableStateFlow(NotifStats.empty)
}
/** Represents the notification list, comprised of groups and individual notifications. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 85ba205..31893b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -15,17 +15,19 @@
package com.android.systemui.statusbar.notification.domain.interactor
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
class ActiveNotificationsInteractor
@Inject
constructor(
- repository: ActiveNotificationListRepository,
+ private val repository: ActiveNotificationListRepository,
) {
/** Notifications actively presented to the user in the notification stack, in order. */
val topLevelRepresentativeNotifications: Flow<List<ActiveNotificationModel>> =
@@ -40,4 +42,25 @@
}
}
}
+
+ /** Are any notifications being actively presented in the notification stack? */
+ val areAnyNotificationsPresent: Flow<Boolean> =
+ repository.activeNotifications.map { it.renderList.isNotEmpty() }.distinctUntilChanged()
+
+ /**
+ * The same as [areAnyNotificationsPresent], but without flows, for easy access in synchronous
+ * code.
+ */
+ val areAnyNotificationsPresentValue: Boolean
+ get() = repository.activeNotifications.value.renderList.isNotEmpty()
+
+ /** Are there are any notifications that can be cleared by the "Clear all" button? */
+ val hasClearableNotifications: Flow<Boolean> =
+ repository.notifStats
+ .map { it.hasClearableAlertingNotifs || it.hasClearableSilentNotifs }
+ .distinctUntilChanged()
+
+ fun setNotifStats(notifStats: NotifStats) {
+ repository.notifStats.value = notifStats
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index 10a43d5..3184d5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -46,6 +46,7 @@
import com.android.systemui.util.DumpUtilsKt;
import java.io.PrintWriter;
+import java.util.function.Consumer;
public class FooterView extends StackScrollerDecorView {
private static final String TAG = "FooterView";
@@ -63,9 +64,13 @@
private String mSeenNotifsFilteredText;
private Drawable mSeenNotifsFilteredIcon;
+ private @StringRes int mClearAllButtonTextId;
+ private @StringRes int mClearAllButtonDescriptionId;
private @StringRes int mMessageStringId;
private @DrawableRes int mMessageIconId;
+ private OnClickListener mClearAllButtonClickListener;
+
public FooterView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -84,12 +89,18 @@
return isSecondaryVisible();
}
+ /** See {@link this#setClearAllButtonVisible(boolean, boolean, Consumer)}. */
+ public void setClearAllButtonVisible(boolean visible, boolean animate) {
+ setClearAllButtonVisible(visible, animate, /* onAnimationEnded = */ null);
+ }
+
/**
* Set the visibility of the "Clear all" button to {@code visible}. Animate the change if
* {@code animate} is true.
*/
- public void setClearAllButtonVisible(boolean visible, boolean animate) {
- setSecondaryVisible(visible, animate);
+ public void setClearAllButtonVisible(boolean visible, boolean animate,
+ Consumer<Boolean> onAnimationEnded) {
+ setSecondaryVisible(visible, animate, onAnimationEnded);
}
@Override
@@ -106,6 +117,42 @@
});
}
+ /** Set the text label for the "Clear all" button. */
+ public void setClearAllButtonText(@StringRes int textId) {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
+ if (mClearAllButtonTextId == textId) {
+ return; // nothing changed
+ }
+ mClearAllButtonTextId = textId;
+ updateClearAllButtonText();
+ }
+
+ private void updateClearAllButtonText() {
+ if (mClearAllButtonTextId == 0) {
+ return; // not initialized yet
+ }
+ mClearAllButton.setText(getContext().getString(mClearAllButtonTextId));
+ }
+
+ /** Set the accessibility content description for the "Clear all" button. */
+ public void setClearAllButtonDescription(@StringRes int contentDescriptionId) {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ return;
+ }
+ if (mClearAllButtonDescriptionId == contentDescriptionId) {
+ return; // nothing changed
+ }
+ mClearAllButtonDescriptionId = contentDescriptionId;
+ updateClearAllButtonDescription();
+ }
+
+ private void updateClearAllButtonDescription() {
+ if (mClearAllButtonDescriptionId == 0) {
+ return; // not initialized yet
+ }
+ mClearAllButton.setContentDescription(getContext().getString(mClearAllButtonDescriptionId));
+ }
+
/** Set the string for a message to be shown instead of the buttons. */
public void setMessageString(@StringRes int messageId) {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
@@ -181,6 +228,10 @@
/** Set onClickListener for the clear all (end) button. */
public void setClearAllButtonClickListener(OnClickListener listener) {
+ if (FooterViewRefactor.isEnabled()) {
+ if (mClearAllButtonClickListener == listener) return;
+ mClearAllButtonClickListener = listener;
+ }
mClearAllButton.setOnClickListener(listener);
}
@@ -214,7 +265,28 @@
mManageButton.setText(mManageNotificationText);
mManageButton.setContentDescription(mManageNotificationText);
}
- if (!FooterViewRefactor.isEnabled()) {
+ if (FooterViewRefactor.isEnabled()) {
+ updateClearAllButtonText();
+ updateClearAllButtonDescription();
+
+ updateMessageString();
+ updateMessageIcon();
+ } else {
+ // NOTE: Prior to the refactor, `updateResources` set the class properties to the right
+ // string values. It was always being called together with `updateContent`, which
+ // deals with actually associating those string values with the correct views
+ // (buttons or text).
+ // In the new code, the resource IDs are being set in the view binder (through
+ // setMessageString and similar setters). The setters themselves now deal with
+ // updating both the resource IDs and the views where appropriate (as in, calling
+ // `updateMessageString` when the resource ID changes). This eliminates the need for
+ // `updateResources`, which will eventually be removed. There are, however, still
+ // situations in which we want to update the views even if the resource IDs didn't
+ // change, such as configuration changes.
+ mClearAllButton.setText(R.string.clear_all_notifications_text);
+ mClearAllButton.setContentDescription(
+ mContext.getString(R.string.accessibility_clear_all));
+
mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
mSeenNotifsFooterTextView
.setCompoundDrawablesRelative(mSeenNotifsFilteredIcon, null, null, null);
@@ -230,16 +302,8 @@
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateColors();
- mClearAllButton.setText(R.string.clear_all_notifications_text);
- mClearAllButton.setContentDescription(
- mContext.getString(R.string.accessibility_clear_all));
updateResources();
updateContent();
-
- if (FooterViewRefactor.isEnabled()) {
- updateMessageString();
- updateMessageIcon();
- }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 6d823437..0299114 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -16,10 +16,14 @@
package com.android.systemui.statusbar.notification.footer.ui.viewbinder
+import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
+import com.android.systemui.util.ui.value
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
@@ -28,9 +32,31 @@
fun bind(
footer: FooterView,
viewModel: FooterViewModel,
+ clearAllNotifications: View.OnClickListener,
): DisposableHandle {
+ // Listen for changes when the view is attached.
return footer.repeatWhenAttached {
- // Listen for changes when the view is attached.
+ lifecycleScope.launch {
+ viewModel.clearAllButton.collect { button ->
+ if (button.isVisible.isAnimating) {
+ footer.setClearAllButtonVisible(
+ button.isVisible.value,
+ /* animate = */ true,
+ ) { _ ->
+ button.isVisible.stopAnimating()
+ }
+ } else {
+ footer.setClearAllButtonVisible(
+ button.isVisible.value,
+ /* animate = */ false,
+ )
+ }
+ footer.setClearAllButtonText(button.labelId)
+ footer.setClearAllButtonDescription(button.accessibilityDescriptionId)
+ footer.setClearAllButtonClickListener(clearAllNotifications)
+ }
+ }
+
lifecycleScope.launch {
viewModel.message.collect { message ->
footer.setFooterLabelVisible(message.visible)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
new file mode 100644
index 0000000..ea5abef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.footer.ui.viewmodel
+
+import android.annotation.StringRes
+import com.android.systemui.util.ui.AnimatedValue
+
+data class FooterButtonViewModel(
+ @StringRes val labelId: Int,
+ @StringRes val accessibilityDescriptionId: Int,
+ val isVisible: AnimatedValue<Boolean>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 7390485..721bea1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -18,21 +18,50 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.ui.AnimatableEvent
+import com.android.systemui.util.ui.toAnimatedValueFlow
import dagger.Module
import dagger.Provides
import java.util.Optional
import javax.inject.Provider
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
/** ViewModel for [FooterView]. */
-class FooterViewModel(seenNotificationsInteractor: SeenNotificationsInteractor) {
- init {
- /* Check if */ FooterViewRefactor.isUnexpectedlyInLegacyMode()
- }
+class FooterViewModel(
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ seenNotificationsInteractor: SeenNotificationsInteractor,
+ shadeInteractor: ShadeInteractor,
+) {
+ val clearAllButton: Flow<FooterButtonViewModel> =
+ activeNotificationsInteractor.hasClearableNotifications
+ .sample(
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) ->
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+ AnimatableEvent(hasClearableNotifications, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ .map { visible ->
+ FooterButtonViewModel(
+ labelId = R.string.clear_all_notifications_text,
+ accessibilityDescriptionId = R.string.accessibility_clear_all,
+ isVisible = visible,
+ )
+ }
val message: Flow<FooterMessageViewModel> =
seenNotificationsInteractor.hasFilteredOutSeenNotifications.map { hasFilteredOutNotifs ->
@@ -49,10 +78,18 @@
@Provides
@SysUISingleton
fun provideOptional(
+ activeNotificationsInteractor: Provider<ActiveNotificationsInteractor>,
seenNotificationsInteractor: Provider<SeenNotificationsInteractor>,
+ shadeInteractor: Provider<ShadeInteractor>,
): Optional<FooterViewModel> {
return if (FooterViewRefactor.isEnabled) {
- Optional.of(FooterViewModel(seenNotificationsInteractor.get()))
+ Optional.of(
+ FooterViewModel(
+ activeNotificationsInteractor.get(),
+ seenNotificationsInteractor.get(),
+ shadeInteractor.get()
+ )
+ )
} else {
Optional.empty()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index ec90a8d..162e8af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -54,7 +54,7 @@
mContent = findContentView();
mSecondaryView = findSecondaryView();
setVisible(false /* visible */, false /* animate */);
- setSecondaryVisible(false /* visible */, false /* animate */);
+ setSecondaryVisible(false /* visible */, false /* animate */, null /* onAnimationEnd */);
setOutlineProvider(null);
}
@@ -155,15 +155,23 @@
/**
* Set the secondary view of this layout to visible.
*
- * @param visible should the secondary view be visible
- * @param animate should the change be animated
+ * @param visible True if the contents should be visible.
+ * @param animate True if we should fade to new visibility.
+ * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
+ * parameter that represents whether the animation was cancelled.
*/
- protected void setSecondaryVisible(boolean visible, boolean animate) {
+ protected void setSecondaryVisible(boolean visible, boolean animate,
+ Consumer<Boolean> onAnimationEnded) {
if (mIsSecondaryVisible != visible) {
mSecondaryAnimating = animate;
mIsSecondaryVisible = visible;
- setViewVisible(mSecondaryView, visible, animate,
- (cancelled) -> onSecondaryVisibilityAnimationEnd());
+ Consumer<Boolean> onAnimationEndedWrapper = (cancelled) -> {
+ onContentVisibilityAnimationEnd();
+ if (onAnimationEnded != null) {
+ onAnimationEnded.accept(cancelled);
+ }
+ };
+ setViewVisible(mSecondaryView, visible, animate, onAnimationEndedWrapper);
}
if (!mSecondaryAnimating) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 38d782b..283a593 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -740,7 +740,9 @@
updateFooter();
}
- void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
+ /** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */
+ public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
+ FooterViewRefactor.assertInLegacyMode();
mHasFilteredOutSeenNotifications = hasFilteredOutSeenNotifications;
}
@@ -749,7 +751,6 @@
if (mFooterView == null || mController == null) {
return;
}
- // TODO: move this logic to controller, which will invoke updateFooterView directly
final boolean showHistory = mController.isHistoryEnabled();
final boolean showDismissView = shouldShowDismissView();
@@ -773,13 +774,6 @@
&& !mIsRemoteInputActive;
}
- /**
- * Return whether there are any clearable notifications
- */
- boolean hasActiveClearableNotifications(@SelectedRows int selection) {
- return mController.hasActiveClearableNotifications(selection);
- }
-
public NotificationSwipeActionHelper getSwipeActionHelper() {
return mSwipeHelper;
}
@@ -1658,8 +1652,44 @@
/**
* @return the position from where the appear transition ends when expanding.
* Measured in absolute height.
+ *
+ * TODO(b/308591475): This entire logic can probably be improved as part of the empty shade
+ * refactor, but for now:
+ * - if the empty shade is visible, we can assume that the visible notif count is not 0;
+ * - if the shelf is visible, we can assume there are at least 2 notifications present (this
+ * is not true for AOD, but this logic refers to the expansion of the shade, where we never
+ * have the shelf on its own)
*/
private float getAppearEndPosition() {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ return getAppearEndPositionLegacy();
+ }
+
+ int appearPosition = mAmbientState.getStackTopMargin();
+ if (mEmptyShadeView.getVisibility() == GONE) {
+ if (isHeadsUpTransition()
+ || (mInHeadsUpPinnedMode && !mAmbientState.isDozing())) {
+ if (mShelf.getVisibility() != GONE) {
+ appearPosition += mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
+ }
+ appearPosition += getTopHeadsUpPinnedHeight()
+ + getPositionInLinearLayout(mAmbientState.getTrackedHeadsUpRow());
+ } else if (mShelf.getVisibility() != GONE) {
+ appearPosition += mShelf.getIntrinsicHeight();
+ }
+ } else {
+ appearPosition = mEmptyShadeView.getHeight();
+ }
+ return appearPosition + (onKeyguard() ? mTopPadding : mIntrinsicPadding);
+ }
+
+ /**
+ * The version of {@code getAppearEndPosition} that uses the notif count. The view shouldn't
+ * need to know about that, so we want to phase this out with the footer view refactor.
+ */
+ private float getAppearEndPositionLegacy() {
+ FooterViewRefactor.assertInLegacyMode();
+
int appearPosition = mAmbientState.getStackTopMargin();
int visibleNotifCount = mController.getVisibleNotificationCount();
if (mEmptyShadeView.getVisibility() == GONE && visibleNotifCount > 0) {
@@ -1698,7 +1728,8 @@
// This can't use expansion fraction as that goes only from 0 to 1. Also when
// appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3
// and that makes translation jump immediately.
- float appearEndPosition = getAppearEndPosition();
+ float appearEndPosition = FooterViewRefactor.isEnabled() ? getAppearEndPosition()
+ : getAppearEndPositionLegacy();
float appearStartPosition = getAppearStartPosition();
float hunAppearFraction = (height - appearStartPosition)
/ (appearEndPosition - appearStartPosition);
@@ -4577,13 +4608,15 @@
if (mManageButtonClickListener != null) {
mFooterView.setManageButtonClickListener(mManageButtonClickListener);
}
- mFooterView.setClearAllButtonClickListener(v -> {
- if (mFooterClearAllListener != null) {
- mFooterClearAllListener.onClearAll();
- }
- clearNotifications(ROWS_ALL, true /* closeShade */);
- footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
- });
+ if (!FooterViewRefactor.isEnabled()) {
+ mFooterView.setClearAllButtonClickListener(v -> {
+ if (mFooterClearAllListener != null) {
+ mFooterClearAllListener.onClearAll();
+ }
+ clearNotifications(ROWS_ALL, true /* closeShade */);
+ footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
+ });
+ }
if (FooterViewRefactor.isEnabled()) {
updateFooter();
}
@@ -4599,12 +4632,21 @@
addView(mEmptyShadeView, index);
}
- void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
+ /** Legacy version, should be removed with the footer refactor flag. */
+ public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
+ FooterViewRefactor.assertInLegacyMode();
+ updateEmptyShadeView(visible, areNotificationsHiddenInShade,
+ mHasFilteredOutSeenNotifications);
+ }
+
+ /** Trigger an update for the empty shade resources and visibility. */
+ public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
+ boolean hasFilteredOutSeenNotifications) {
mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
if (areNotificationsHiddenInShade) {
updateEmptyShadeView(R.string.dnd_suppressing_shade_text, 0, 0);
- } else if (mHasFilteredOutSeenNotifications) {
+ } else if (hasFilteredOutSeenNotifications) {
updateEmptyShadeView(
R.string.no_unseen_notif_text,
R.string.unlock_to_see_notif_text,
@@ -4647,9 +4689,9 @@
}
boolean animate = mIsExpanded && mAnimationsEnabled;
mFooterView.setVisible(visible, animate);
- mFooterView.setClearAllButtonVisible(showDismissView, animate);
mFooterView.showHistory(showHistory);
if (!FooterViewRefactor.isEnabled()) {
+ mFooterView.setClearAllButtonVisible(showDismissView, animate);
mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
}
}
@@ -4781,10 +4823,13 @@
public void removeContainerView(View v) {
Assert.isMainThread();
removeView(v);
- if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) {
- mController.updateShowEmptyShadeView();
- updateFooter();
- mController.updateImportantForAccessibility();
+ if (!FooterViewRefactor.isEnabled()) {
+ // A notification was removed, and we're not currently showing the empty shade view.
+ if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) {
+ mController.updateShowEmptyShadeView();
+ updateFooter();
+ mController.updateImportantForAccessibility();
+ }
}
updateSpeedBumpIndex();
@@ -4793,10 +4838,13 @@
public void addContainerView(View v) {
Assert.isMainThread();
addView(v);
- if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) {
- mController.updateShowEmptyShadeView();
- updateFooter();
- mController.updateImportantForAccessibility();
+ if (!FooterViewRefactor.isEnabled()) {
+ // A notification was added, and we're currently showing the empty shade view.
+ if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) {
+ mController.updateShowEmptyShadeView();
+ updateFooter();
+ mController.updateImportantForAccessibility();
+ }
}
updateSpeedBumpIndex();
@@ -4806,7 +4854,9 @@
Assert.isMainThread();
ensureRemovedFromTransientContainer(v);
addView(v, index);
- if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) {
+ // A notification was added, and we're currently showing the empty shade view.
+ if (!FooterViewRefactor.isEnabled() && v instanceof ExpandableNotificationRow
+ && mController.isShowingEmptyShadeView()) {
mController.updateShowEmptyShadeView();
updateFooter();
mController.updateImportantForAccessibility();
@@ -5079,7 +5129,8 @@
if (mEmptyShadeView.getVisibility() == GONE) {
return getMinExpansionHeight();
} else {
- return getAppearEndPosition();
+ return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
+ : getAppearEndPositionLegacy();
}
}
@@ -5306,11 +5357,15 @@
return viewsToRemove;
}
+ /** Clear all clearable notifications when the user requests it. */
+ public void clearAllNotifications() {
+ clearNotifications(ROWS_ALL, /* closeShade = */ true);
+ }
+
/**
* Collects a list of visible rows, and animates them away in a staggered fashion as if they
* were dismissed. Notifications are dismissed in the backend via onClearAllAnimationsEnd.
*/
- @VisibleForTesting
void clearNotifications(@SelectedRows int selection, boolean closeShade) {
// Animate-swipe all dismissable notifications, then animate the shade closed
final ArrayList<View> viewsToAnimateAway = getVisibleViewsToAnimateAway(selection);
@@ -5610,6 +5665,7 @@
}
void setFooterClearAllListener(FooterClearAllListener listener) {
+ FooterViewRefactor.assertInLegacyMode();
mFooterClearAllListener = listener;
}
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 3e140a4..e6315fd 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
@@ -20,7 +20,6 @@
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
import static com.android.app.animation.Interpolators.STANDARD;
-
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -108,7 +107,9 @@
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
import com.android.systemui.statusbar.notification.dagger.SilentHeader;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -223,7 +224,9 @@
@Override
public void onViewAttachedToWindow(View v) {
mConfigurationController.addCallback(mConfigurationListener);
- mZenModeController.addCallback(mZenModeControllerCallback);
+ if (!FooterViewRefactor.isEnabled()) {
+ mZenModeController.addCallback(mZenModeControllerCallback);
+ }
final int newBarState = mStatusBarStateController.getState();
if (newBarState != mBarState) {
mStateListener.onStateChanged(newBarState);
@@ -236,7 +239,9 @@
@Override
public void onViewDetachedFromWindow(View v) {
mConfigurationController.removeCallback(mConfigurationListener);
- mZenModeController.removeCallback(mZenModeControllerCallback);
+ if (!FooterViewRefactor.isEnabled()) {
+ mZenModeController.removeCallback(mZenModeControllerCallback);
+ }
mStatusBarStateController.removeCallback(mStateListener);
}
};
@@ -292,7 +297,9 @@
final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
@Override
public void onDensityOrFontScaleChanged() {
- updateShowEmptyShadeView();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateShowEmptyShadeView();
+ }
mView.reinflateViews();
}
@@ -308,7 +315,9 @@
mView.updateBgColor();
mView.updateDecorViews();
mView.reinflateViews();
- updateShowEmptyShadeView();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateShowEmptyShadeView();
+ }
updateFooter();
}
@@ -357,7 +366,9 @@
mView.updateSensitiveness(mStatusBarStateController.goingToFullShade(),
mLockscreenUserManager.isAnyProfilePublicMode());
mView.onStatePostChange(mStatusBarStateController.fromShadeLocked());
- updateImportantForAccessibility();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateImportantForAccessibility();
+ }
}
};
@@ -459,7 +470,7 @@
@Override
public void onSnooze(StatusBarNotification sbn,
- NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
+ NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
mNotificationsController.setNotificationSnoozed(sbn, snoozeOption);
}
@@ -581,7 +592,7 @@
@Override
public boolean updateSwipeProgress(View animView, boolean dismissable,
- float swipeProgress) {
+ float swipeProgress) {
// Returning true prevents alpha fading.
return false;
}
@@ -673,6 +684,7 @@
UiEventLogger uiEventLogger,
NotificationRemoteInputManager remoteInputManager,
VisibilityLocationProviderDelegator visibilityLocationProviderDelegator,
+ ActiveNotificationsInteractor activeNotificationsInteractor,
SeenNotificationsInteractor seenNotificationsInteractor,
NotificationListViewBinder viewBinder,
ShadeController shadeController,
@@ -747,8 +759,10 @@
mView.setClearAllAnimationListener(this::onAnimationEnd);
mView.setClearAllListener((selection) -> mUiEventLogger.log(
NotificationPanelEvent.fromSelection(selection)));
- mView.setFooterClearAllListener(() ->
- mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+ if (!FooterViewRefactor.isEnabled()) {
+ mView.setFooterClearAllListener(() ->
+ mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+ }
mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
@Override
@@ -840,8 +854,10 @@
mViewBinder.bind(mView, this);
- collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
- this::onKeyguardTransitionChanged);
+ if (!FooterViewRefactor.isEnabled()) {
+ collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
+ this::onKeyguardTransitionChanged);
+ }
}
private boolean isInVisibleLocation(NotificationEntry entry) {
@@ -1031,6 +1047,8 @@
}
public int getVisibleNotificationCount() {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle footer
+ // visibility in the refactored code
return mNotifStats.getNumActiveNotifs();
}
@@ -1074,7 +1092,7 @@
}
public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
- boolean cancelAnimators) {
+ boolean cancelAnimators) {
mView.setOverScrollAmount(amount, onTop, animate, cancelAnimators);
}
@@ -1124,6 +1142,7 @@
}
public void setQsFullScreen(boolean fullScreen) {
+ FooterViewRefactor.assertInLegacyMode();
mView.setQsFullScreen(fullScreen);
updateShowEmptyShadeView();
}
@@ -1273,7 +1292,10 @@
public void updateVisibility(boolean visible) {
mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
- if (mView.getVisibility() == View.VISIBLE) {
+ // Refactor note: the empty shade's visibility doesn't seem to actually depend on the
+ // parent visibility (so this update seemingly doesn't do anything). Therefore, this is not
+ // modeled in the refactored code.
+ if (!FooterViewRefactor.isEnabled() && mView.getVisibility() == View.VISIBLE) {
// Synchronize EmptyShadeView visibility with the parent container.
updateShowEmptyShadeView();
updateImportantForAccessibility();
@@ -1288,6 +1310,8 @@
* are true.
*/
public void updateShowEmptyShadeView() {
+ FooterViewRefactor.assertInLegacyMode();
+
Trace.beginSection("NSSLC.updateShowEmptyShadeView");
final boolean shouldShow = getVisibleNotificationCount() == 0
@@ -1331,6 +1355,7 @@
* auto-scrolling in NSSL.
*/
public void updateImportantForAccessibility() {
+ FooterViewRefactor.assertInLegacyMode();
if (getVisibleNotificationCount() == 0 && mView.onKeyguard()) {
mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
} else {
@@ -1338,16 +1363,6 @@
}
}
- /**
- * @return true if {@link StatusBarStateController} is in transition to the KEYGUARD
- * and false otherwise.
- */
- private boolean isInTransitionToKeyguard() {
- final int currentState = mStatusBarStateController.getState();
- final int upcomingState = mStatusBarStateController.getCurrentOrUpcomingState();
- return (currentState != upcomingState && upcomingState == KEYGUARD);
- }
-
public boolean isShowingEmptyShadeView() {
return mView.isEmptyShadeViewVisible();
}
@@ -1395,10 +1410,14 @@
* Return whether there are any clearable notifications
*/
public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
+ // visibility in the refactored code
return hasNotifications(selection, true /* clearable */);
}
public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
+ // visibility in the refactored code
boolean hasAlertingMatchingClearable = isClearable
? mNotifStats.getHasClearableAlertingNotifs()
: mNotifStats.getHasNonClearableAlertingNotifs();
@@ -1437,7 +1456,7 @@
public RemoteInputController.Delegate createDelegate() {
return new RemoteInputController.Delegate() {
public void setRemoteInputActive(NotificationEntry entry,
- boolean remoteInputActive) {
+ boolean remoteInputActive) {
mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
entry.notifyHeightChanged(true /* needsAnimation */);
updateFooter();
@@ -1556,7 +1575,7 @@
}
private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove,
- @SelectedRows int selectedRows) {
+ @SelectedRows int selectedRows) {
if (selectedRows == ROWS_ALL) {
mNotifCollection.dismissAllNotifications(
mLockscreenUserManager.getCurrentUserId());
@@ -1648,7 +1667,7 @@
* Set rounded rect clipping bounds on this view.
*/
public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
- int bottomRadius) {
+ int bottomRadius) {
mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius);
}
@@ -1678,6 +1697,7 @@
@VisibleForTesting
void onKeyguardTransitionChanged(TransitionStep transitionStep) {
+ FooterViewRefactor.assertInLegacyMode();
boolean isTransitionToAod = transitionStep.getTo().equals(KeyguardState.AOD)
&& (transitionStep.getFrom().equals(KeyguardState.GONE)
|| transitionStep.getFrom().equals(KeyguardState.OCCLUDED));
@@ -2003,11 +2023,21 @@
private class NotifStackControllerImpl implements NotifStackController {
@Override
public void setNotifStats(@NonNull NotifStats notifStats) {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once footer visibility
+ // is handled in the refactored stack.
mNotifStats = notifStats;
- mView.setHasFilteredOutSeenNotifications(
- mSeenNotificationsInteractor.getHasFilteredOutSeenNotifications().getValue());
+
+ if (!FooterViewRefactor.isEnabled()) {
+ mView.setHasFilteredOutSeenNotifications(
+ mSeenNotificationsInteractor
+ .getHasFilteredOutSeenNotifications().getValue());
+ }
+
updateFooter();
- updateShowEmptyShadeView();
+
+ if (!FooterViewRefactor.isEnabled()) {
+ updateShowEmptyShadeView();
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index a5b87f0..4554085 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -17,9 +17,13 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.view.LayoutInflater
+import androidx.lifecycle.lifecycleScope
import com.android.app.tracing.traceSection
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.nano.MetricsProto
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.reinflateAndBindLatest
+import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
@@ -36,12 +40,15 @@
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.statusbar.policy.ConfigurationController
import javax.inject.Inject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
/** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
class NotificationListViewBinder
@Inject
constructor(
private val viewModel: NotificationListViewModel,
+ private val metricsLogger: MetricsLogger,
private val configuration: ConfigurationState,
private val configurationController: ConfigurationController,
private val falsingManager: FalsingManager,
@@ -56,7 +63,16 @@
) {
bindShelf(view)
bindFooter(view)
+ bindEmptyShade(view)
bindHideList(viewController, viewModel)
+
+ view.repeatWhenAttached {
+ lifecycleScope.launch {
+ viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
+ view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+ }
+ }
+ }
}
private fun bindShelf(parentView: NotificationStackScrollLayout) {
@@ -87,7 +103,17 @@
attachToRoot = false,
) { footerView: FooterView ->
traceSection("bind FooterView") {
- val disposableHandle = FooterViewBinder.bind(footerView, footerViewModel)
+ val disposableHandle =
+ FooterViewBinder.bind(
+ footerView,
+ footerViewModel,
+ clearAllNotifications = {
+ metricsLogger.action(
+ MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
+ )
+ parentView.clearAllNotifications()
+ },
+ )
parentView.setFooterView(footerView)
return@reinflateAndBindLatest disposableHandle
}
@@ -95,4 +121,26 @@
}
}
}
+
+ private fun bindEmptyShade(
+ parentView: NotificationStackScrollLayout,
+ ) {
+ parentView.repeatWhenAttached {
+ lifecycleScope.launch {
+ combine(
+ viewModel.shouldShowEmptyShadeView,
+ viewModel.areNotificationsHiddenInShade,
+ viewModel.hasFilteredOutSeenNotifications,
+ ::Triple
+ )
+ .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
+ parentView.updateEmptyShadeView(
+ shouldShow,
+ areNotifsHidden,
+ hasFilteredNotifs,
+ )
+ }
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4f76680..569ae24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -16,10 +16,22 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.onStart
/** ViewModel for the list of notifications. */
class NotificationListViewModel
@@ -27,5 +39,78 @@
constructor(
val shelf: NotificationShelfViewModel,
val hideListViewModel: HideListViewModel,
- val footer: Optional<FooterViewModel>
-)
+ val footer: Optional<FooterViewModel>,
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ seenNotificationsInteractor: SeenNotificationsInteractor,
+ shadeInteractor: ShadeInteractor,
+ zenModeInteractor: ZenModeInteractor,
+) {
+ /**
+ * We want the NSSL to be unimportant for accessibility when there are no notifications in it
+ * while the device is on lock screen, to avoid an unlabelled NSSL view in TalkBack. Otherwise,
+ * we want it to be important for accessibility to enable accessibility auto-scrolling in NSSL.
+ * See b/242235264 for more details.
+ */
+ val isImportantForAccessibility: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(true)
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ keyguardTransitionInteractor.isFinishedInStateWhere {
+ KeyguardState.lockscreenVisibleInState(it)
+ }
+ ) { hasNotifications, isOnKeyguard ->
+ hasNotifications || !isOnKeyguard
+ }
+ .distinctUntilChanged()
+ }
+ }
+
+ val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ shadeInteractor.isQsFullscreen,
+ keyguardTransitionInteractor.isInTransitionToState(KeyguardState.AOD).onStart {
+ emit(false)
+ },
+ keyguardTransitionInteractor
+ .isFinishedInState(KeyguardState.PRIMARY_BOUNCER)
+ .onStart { emit(false) }
+ ) { hasNotifications, isQsFullScreen, transitioningToAOD, isBouncerShowing ->
+ !hasNotifications &&
+ !isQsFullScreen &&
+ // Hide empty shade view when in transition to AOD.
+ // That avoids "No Notifications" blinking when transitioning to AOD.
+ // For more details, see b/228790482.
+ !transitioningToAOD &&
+ // Don't show any notification content if the bouncer is showing. See
+ // b/267060171.
+ !isBouncerShowing
+ }
+ .distinctUntilChanged()
+ }
+ }
+
+ // TODO(b/308591475): This should be tracked separately by the empty shade.
+ val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ zenModeInteractor.areNotificationsHiddenInShade
+ }
+ }
+
+ // TODO(b/308591475): This should be tracked separately by the empty shade.
+ val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ seenNotificationsInteractor.hasFilteredOutSeenNotifications
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index ba8a666..03878b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -147,6 +147,7 @@
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -335,6 +336,7 @@
@Mock private JavaAdapter mJavaAdapter;
@Mock private CastController mCastController;
@Mock private SharedNotificationContainerInteractor mSharedNotificationContainerInteractor;
+ @Mock private ActiveNotificationsInteractor mActiveNotificationsInteractor;
@Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm;
@Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
@@ -709,6 +711,7 @@
mKeyguardInteractor,
mActivityStarter,
mSharedNotificationContainerInteractor,
+ mActiveNotificationsInteractor,
mKeyguardViewConfigurator,
mKeyguardFaceAuthInteractor,
new ResourcesSplitShadeStateController(),
@@ -783,6 +786,7 @@
mKeyguardFaceAuthInteractor,
mShadeRepository,
mShadeInteractor,
+ mActiveNotificationsInteractor,
mJavaAdapter,
mCastController,
new ResourcesSplitShadeStateController()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 26b84e3..bff47f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -80,6 +80,8 @@
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor;
@@ -178,6 +180,8 @@
protected SysuiStatusBarStateController mStatusBarStateController;
protected ShadeInteractor mShadeInteractor;
+ protected ActiveNotificationsInteractor mActiveNotificationsInteractor;
+
protected Handler mMainHandler;
protected LockscreenShadeTransitionController.Callback mLockscreenShadeTransitionCallback;
@@ -290,6 +294,9 @@
)
);
+ mActiveNotificationsInteractor =
+ new ActiveNotificationsInteractor(new ActiveNotificationListRepository());
+
KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -362,6 +369,7 @@
mock(KeyguardFaceAuthInteractor.class),
mShadeRepository,
mShadeInteractor,
+ mActiveNotificationsInteractor,
new JavaAdapter(mTestScope.getBackgroundScope()),
mCastController,
splitShadeStateController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 428574b..fa5fad0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
@@ -57,6 +58,7 @@
@Mock private lateinit var groupExpansionManagerImpl: GroupExpansionManagerImpl
@Mock private lateinit var notificationIconAreaController: NotificationIconAreaController
@Mock private lateinit var renderListInteractor: RenderNotificationListInteractor
+ @Mock private lateinit var activeNotificationsInteractor: ActiveNotificationsInteractor
@Mock private lateinit var stackController: NotifStackController
@Mock private lateinit var section: NotifSection
@@ -75,6 +77,7 @@
groupExpansionManagerImpl,
notificationIconAreaController,
renderListInteractor,
+ activeNotificationsInteractor,
)
coordinator.attach(pipeline)
afterRenderListListener = withArgCaptor {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
new file mode 100644
index 0000000..4ab3cd4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.collectLastValue
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import org.junit.Test
+
+@SmallTest
+class ActiveNotificationsInteractorTest : SysuiTestCase() {
+
+ @Component(modules = [SysUITestModule::class])
+ @SysUISingleton
+ interface TestComponent : SysUITestComponent<ActiveNotificationsInteractor> {
+ val activeNotificationListRepository: ActiveNotificationListRepository
+
+ @Component.Factory
+ interface Factory {
+ fun create(@BindsInstance test: SysuiTestCase): TestComponent
+ }
+ }
+
+ private val testComponent: TestComponent =
+ DaggerActiveNotificationsInteractorTest_TestComponent.factory().create(test = this)
+
+ @Test
+ fun testAreAnyNotificationsPresent_isTrue() =
+ testComponent.runTest {
+ val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
+
+ activeNotificationListRepository.setActiveNotifs(2)
+ runCurrent()
+
+ assertThat(areAnyNotificationsPresent).isTrue()
+ assertThat(underTest.areAnyNotificationsPresentValue).isTrue()
+ }
+
+ @Test
+ fun testAreAnyNotificationsPresent_isFalse() =
+ testComponent.runTest {
+ val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
+
+ activeNotificationListRepository.setActiveNotifs(0)
+ runCurrent()
+
+ assertThat(areAnyNotificationsPresent).isFalse()
+ assertThat(underTest.areAnyNotificationsPresentValue).isFalse()
+ }
+
+ @Test
+ fun testHasClearableNotifications_whenHasClearableAlertingNotifs() =
+ testComponent.runTest {
+ val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = false,
+ )
+ runCurrent()
+
+ assertThat(hasClearable).isTrue()
+ }
+
+ @Test
+ fun testHasClearableNotifications_whenHasClearableSilentNotifs() =
+ testComponent.runTest {
+ val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = false,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ assertThat(hasClearable).isTrue()
+ }
+
+ @Test
+ fun testHasClearableNotifications_whenHasNoClearableNotifs() =
+ testComponent.runTest {
+ val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = false,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = false,
+ )
+ runCurrent()
+
+ assertThat(hasClearable).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
index a64ac67..22c5bae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
@@ -114,9 +114,46 @@
}
@Test
+ public void testSetClearAllButtonText_resourceOnlyFetchedOnce() {
+ int resId = R.string.clear_all_notifications_text;
+ mView.setClearAllButtonText(resId);
+ verify(mSpyContext).getString(eq(resId));
+
+ clearInvocations(mSpyContext);
+
+ assertThat(((TextView) mView.findViewById(R.id.dismiss_text))
+ .getText().toString()).contains("Clear all");
+
+ // Set it a few more times, it shouldn't lead to the resource being fetched again
+ mView.setClearAllButtonText(resId);
+ mView.setClearAllButtonText(resId);
+
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
+ public void testSetClearAllButtonDescription_resourceOnlyFetchedOnce() {
+ int resId = R.string.accessibility_clear_all;
+ mView.setClearAllButtonDescription(resId);
+ verify(mSpyContext).getString(eq(resId));
+
+ clearInvocations(mSpyContext);
+
+ assertThat(((TextView) mView.findViewById(R.id.dismiss_text))
+ .getContentDescription().toString()).contains("Clear all notifications");
+
+ // Set it a few more times, it shouldn't lead to the resource being fetched again
+ mView.setClearAllButtonDescription(resId);
+ mView.setClearAllButtonDescription(resId);
+
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
public void testSetMessageString_resourceOnlyFetchedOnce() {
- mView.setMessageString(R.string.unlock_to_see_notif_text);
- verify(mSpyContext).getString(eq(R.string.unlock_to_see_notif_text));
+ int resId = R.string.unlock_to_see_notif_text;
+ mView.setMessageString(resId);
+ verify(mSpyContext).getString(eq(resId));
clearInvocations(mSpyContext);
@@ -124,22 +161,23 @@
.getText().toString()).contains("Unlock");
// Set it a few more times, it shouldn't lead to the resource being fetched again
- mView.setMessageString(R.string.unlock_to_see_notif_text);
- mView.setMessageString(R.string.unlock_to_see_notif_text);
+ mView.setMessageString(resId);
+ mView.setMessageString(resId);
verify(mSpyContext, never()).getString(anyInt());
}
@Test
public void testSetMessageIcon_resourceOnlyFetchedOnce() {
- mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
- verify(mSpyContext).getDrawable(eq(R.drawable.ic_friction_lock_closed));
+ int resId = R.drawable.ic_friction_lock_closed;
+ mView.setMessageIcon(resId);
+ verify(mSpyContext).getDrawable(eq(resId));
clearInvocations(mSpyContext);
// Set it a few more times, it shouldn't lead to the resource being fetched again
- mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
- mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
+ mView.setMessageIcon(resId);
+ mView.setMessageIcon(resId);
verify(mSpyContext, never()).getDrawable(anyInt());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 57a7c3c..94dcf7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -18,37 +18,222 @@
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
+import dagger.BindsInstance
+import dagger.Component
+import java.util.Optional
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class FooterViewModelTest : SysuiTestCase() {
- private val repository = ActiveNotificationListRepository()
- private val interactor = SeenNotificationsInteractor(repository)
- private val underTest = FooterViewModel(interactor)
+ private lateinit var footerViewModel: FooterViewModel
- @Test
- fun testMessageVisible_whenFilteredNotifications() = runTest {
- val message by collectLastValue(underTest.message)
+ @SysUISingleton
+ @Component(
+ modules =
+ [
+ SysUITestModule::class,
+ ActivatableNotificationViewModelModule::class,
+ FooterViewModelModule::class,
+ HeadlessSystemUserModeModule::class,
+ ]
+ )
+ interface TestComponent : SysUITestComponent<Optional<FooterViewModel>> {
+ val activeNotificationListRepository: ActiveNotificationListRepository
+ val configurationRepository: FakeConfigurationRepository
+ val keyguardRepository: FakeKeyguardRepository
+ val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ val shadeRepository: FakeShadeRepository
+ val powerRepository: FakePowerRepository
- repository.hasFilteredOutSeenNotifications.value = true
+ @Component.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance test: SysuiTestCase,
+ featureFlags: FakeFeatureFlagsClassicModule,
+ mocks: TestMocksModule,
+ ): TestComponent
+ }
+ }
- assertThat(message?.visible).isTrue()
+ private val dozeParameters: DozeParameters = mock()
+
+ private val testComponent: TestComponent =
+ DaggerFooterViewModelTest_TestComponent.factory()
+ .create(
+ test = this,
+ featureFlags =
+ FakeFeatureFlagsClassicModule {
+ set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true)
+ },
+ mocks =
+ TestMocksModule(
+ dozeParameters = dozeParameters,
+ )
+ )
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
+
+ // The underTest in the component is Optional, because that matches the provider we
+ // currently have for the footer view model.
+ footerViewModel = testComponent.underTest.get()
}
@Test
- fun testMessageVisible_whenNoFilteredNotifications() = runTest {
- val message by collectLastValue(underTest.message)
+ fun testMessageVisible_whenFilteredNotifications() =
+ testComponent.runTest {
+ val message by collectLastValue(footerViewModel.message)
- repository.hasFilteredOutSeenNotifications.value = false
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
- assertThat(message?.visible).isFalse()
- }
+ assertThat(message?.visible).isTrue()
+ }
+
+ @Test
+ fun testMessageVisible_whenNoFilteredNotifications() =
+ testComponent.runTest {
+ val message by collectLastValue(footerViewModel.message)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+
+ assertThat(message?.visible).isFalse()
+ }
+
+ @Test
+ fun testClearAllButtonVisible_whenHasClearableNotifs() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ assertThat(button?.isVisible?.value).isTrue()
+ }
+
+ @Test
+ fun testClearAllButtonVisible_whenHasNoClearableNotifs() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = false,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = false,
+ )
+ runCurrent()
+
+ assertThat(button?.isVisible?.value).isFalse()
+ }
+
+ @Test
+ fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+ runCurrent()
+
+ // WHEN shade is expanded
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ shadeRepository.setLegacyShadeExpansion(1f)
+ // AND QS not expanded
+ shadeRepository.setQsExpansion(0f)
+ // AND device is awake
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ runCurrent()
+
+ // AND there are clearable notifications
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ // THEN button visibility should animate
+ assertThat(button?.isVisible?.isAnimating).isTrue()
+ }
+
+ @Test
+ fun testClearAllButtonAnimating_whenShadeNotExpanded() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+ runCurrent()
+
+ // WHEN shade is collapsed
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ shadeRepository.setLegacyShadeExpansion(0f)
+ // AND QS not expanded
+ shadeRepository.setQsExpansion(0f)
+ // AND device is awake
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ runCurrent()
+
+ // AND there are clearable notifications
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ // THEN button visibility should not animate
+ assertThat(button?.isVisible?.isAnimating).isFalse()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index 0341035..360a373 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -26,10 +26,10 @@
import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
import com.android.systemui.runTest
import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
-import com.android.systemui.statusbar.notification.shared.activeNotificationModel
import com.android.systemui.statusbar.notification.shared.byIsAmbient
import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
import com.android.systemui.statusbar.notification.shared.byIsPulsing
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index c2a1519..e264fc0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -43,10 +43,10 @@
import com.android.systemui.runCurrent
import com.android.systemui.runTest
import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationIconViewStateRepository
-import com.android.systemui.statusbar.notification.shared.activeNotificationModel
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher
import com.android.systemui.statusbar.phone.data.repository.FakeDarkIconRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
index ca105f3..16c5c8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
@@ -15,7 +15,6 @@
package com.android.systemui.statusbar.notification.shared
-import android.graphics.drawable.Icon
import com.google.common.truth.Correspondence
val byKey: Correspondence<ActiveNotificationModel, String> =
@@ -38,30 +37,3 @@
)
val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> =
Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of")
-
-fun activeNotificationModel(
- key: String,
- groupKey: String? = null,
- isAmbient: Boolean = false,
- isRowDismissed: Boolean = false,
- isSilent: Boolean = false,
- isLastMessageFromReply: Boolean = false,
- isSuppressedFromStatusBar: Boolean = false,
- isPulsing: Boolean = false,
- aodIcon: Icon? = null,
- shelfIcon: Icon? = null,
- statusBarIcon: Icon? = null,
-) =
- ActiveNotificationModel(
- key = key,
- groupKey = groupKey,
- isAmbient = isAmbient,
- isRowDismissed = isRowDismissed,
- isSilent = isSilent,
- isLastMessageFromReply = isLastMessageFromReply,
- isSuppressedFromStatusBar = isSuppressedFromStatusBar,
- isPulsing = isPulsing,
- aodIcon = aodIcon,
- shelfIcon = shelfIcon,
- statusBarIcon = statusBarIcon,
- )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 5903890..ff5c026 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -36,7 +36,6 @@
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-import android.content.res.Resources;
import android.metrics.LogMaker;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -53,7 +52,6 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.common.ui.ConfigurationState;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
@@ -74,7 +72,6 @@
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
@@ -84,17 +81,15 @@
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore;
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -170,8 +165,14 @@
@Captor
private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
+ private final ActiveNotificationListRepository mActiveNotificationsRepository =
+ new ActiveNotificationListRepository();
+
+ private final ActiveNotificationsInteractor mActiveNotificationsInteractor =
+ new ActiveNotificationsInteractor(mActiveNotificationsRepository);
+
private final SeenNotificationsInteractor mSeenNotificationsInteractor =
- new SeenNotificationsInteractor(new ActiveNotificationListRepository());
+ new SeenNotificationsInteractor(mActiveNotificationsRepository);
private NotificationStackScrollLayoutController mController;
@@ -701,6 +702,7 @@
mUiEventLogger,
mRemoteInputManager,
mVisibilityLocationProviderDelegator,
+ mActiveNotificationsInteractor,
mSeenNotificationsInteractor,
mViewBinder,
mShadeController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
index 46e8453..ac20683 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
@@ -34,9 +34,11 @@
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl
-import com.android.systemui.util.animation.FakeAnimationStatusRepository
+import com.android.systemui.util.animation.data.repository.FakeAnimationStatusRepository
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.test.TestScope
@@ -47,8 +49,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
-import java.time.Duration
-import java.util.Optional
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
new file mode 100644
index 0000000..f00abc9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import android.app.NotificationManager.Policy
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.common.domain.CommonDomainLayerModule
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModelModule
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepository
+import com.android.systemui.unfold.UnfoldTransitionModule
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationListViewModelTest : SysuiTestCase() {
+
+ @SysUISingleton
+ @Component(
+ modules =
+ [
+ SysUITestModule::class,
+ ActivatableNotificationViewModelModule::class,
+ CommonDomainLayerModule::class,
+ FooterViewModelModule::class,
+ HeadlessSystemUserModeModule::class,
+ UnfoldTransitionModule.Bindings::class,
+ ]
+ )
+ interface TestComponent : SysUITestComponent<NotificationListViewModel> {
+ val activeNotificationListRepository: ActiveNotificationListRepository
+ val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ val shadeRepository: FakeShadeRepository
+ val zenModeRepository: FakeZenModeRepository
+ val configurationController: FakeConfigurationController
+
+ @Component.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance test: SysuiTestCase,
+ featureFlags: FakeFeatureFlagsClassicModule,
+ mocks: TestMocksModule,
+ ): TestComponent
+ }
+ }
+
+ private val testComponent: TestComponent =
+ DaggerNotificationListViewModelTest_TestComponent.factory()
+ .create(
+ test = this,
+ featureFlags =
+ FakeFeatureFlagsClassicModule {
+ set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true)
+ },
+ mocks = TestMocksModule()
+ )
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
+ }
+
+ @Test
+ fun testIsImportantForAccessibility_falseWhenNoNotifs() =
+ testComponent.runTest {
+ val important by collectLastValue(underTest.isImportantForAccessibility)
+
+ // WHEN on lockscreen
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope,
+ )
+ // AND has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ testScope.runCurrent()
+
+ // THEN not important
+ assertThat(important).isFalse()
+ }
+
+ @Test
+ fun testIsImportantForAccessibility_trueWhenNotifs() =
+ testComponent.runTest {
+ val important by collectLastValue(underTest.isImportantForAccessibility)
+
+ // WHEN on lockscreen
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope,
+ )
+ // AND has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ runCurrent()
+
+ // THEN is important
+ assertThat(important).isTrue()
+ }
+
+ @Test
+ fun testIsImportantForAccessibility_trueWhenNotKeyguard() =
+ testComponent.runTest {
+ val important by collectLastValue(underTest.isImportantForAccessibility)
+
+ // WHEN not on lockscreen
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope,
+ )
+ // AND has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ runCurrent()
+
+ // THEN is still important
+ assertThat(important).isTrue()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_trueWhenNoNotifs() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ runCurrent()
+
+ // THEN should show
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenNotifs() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND quick settings are expanded
+ shadeRepository.legacyQsFullscreen.value = true
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND quick settings are expanded
+ shadeRepository.setQsExpansion(1f)
+ // AND split shade is enabled
+ overrideResource(R.bool.config_use_split_notification_shade, true)
+ configurationController.notifyConfigurationChanged()
+ runCurrent()
+
+ // THEN should show
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenTransitioningToAOD() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND transitioning to AOD
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 0f,
+ )
+ )
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenBouncerShowing() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND is on bouncer
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ testScope,
+ )
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testAreNotificationsHiddenInShade_true() =
+ testComponent.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ zenModeRepository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+ runCurrent()
+
+ assertThat(hidden).isTrue()
+ }
+
+ @Test
+ fun testAreNotificationsHiddenInShade_false() =
+ testComponent.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ zenModeRepository.zenMode.value = Settings.Global.ZEN_MODE_OFF
+ runCurrent()
+
+ assertThat(hidden).isFalse()
+ }
+
+ @Test
+ fun testHasFilteredOutSeenNotifications_true() =
+ testComponent.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isTrue()
+ }
+
+ @Test
+ fun testHasFilteredOutSeenNotifications_false() =
+ testComponent.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 99e62ee..dbb1062 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -128,8 +128,7 @@
testComponent.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
- repository.consolidatedNotificationPolicy.value =
- policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
repository.zenMode.value = Settings.Global.ZEN_MODE_OFF
runCurrent()
@@ -141,8 +140,7 @@
testComponent.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
- repository.consolidatedNotificationPolicy.value =
- policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_STATUS_BAR)
+ repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_STATUS_BAR)
repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
runCurrent()
@@ -154,19 +152,10 @@
testComponent.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
- repository.consolidatedNotificationPolicy.value =
- policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
runCurrent()
assertThat(hidden).isTrue()
}
}
-
-fun policyWithSuppressedVisualEffects(suppressedVisualEffects: Int) =
- Policy(
- /* priorityCategories = */ 0,
- /* priorityCallSenders = */ 0,
- /* priorityMessageSenders = */ 0,
- /* suppressedVisualEffects = */ suppressedVisualEffects
- )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
index d0c1267..3724291 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
@@ -15,6 +15,7 @@
*/
package com.android.systemui
+import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.testing.TestableContext
@@ -80,6 +81,9 @@
test.fakeBroadcastDispatcher
@Provides
+ fun provideContentResolver(context: Context): ContentResolver = context.contentResolver
+
+ @Provides
fun provideBaseShadeInteractor(
sceneContainerFlags: SceneContainerFlags,
sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 37a4f61..f57ace9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -55,7 +55,9 @@
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.ScrimController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.util.mockito.mock
import com.android.wm.shell.bubbles.Bubbles
import dagger.Binds
@@ -100,6 +102,10 @@
@get:Provides val keyguardViewController: KeyguardViewController = mock(),
@get:Provides val dialogLaunchAnimator: DialogLaunchAnimator = mock(),
@get:Provides val sysuiState: SysUiState = mock(),
+ @get:Provides
+ val unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> =
+ Optional.empty(),
+ @get:Provides val zenModeController: ZenModeController = mock(),
// log buffers
@get:[Provides BroadcastDispatcherLog]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
index 36f0882..8c653a5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
@@ -26,12 +26,14 @@
import com.android.systemui.statusbar.data.FakeStatusBarDataLayerModule
import com.android.systemui.telephony.data.FakeTelephonyDataLayerModule
import com.android.systemui.user.data.FakeUserDataLayerModule
+import com.android.systemui.util.animation.data.FakeAnimationUtilDataLayerModule
import dagger.Module
@Module(
includes =
[
FakeAccessibilityDataLayerModule::class,
+ FakeAnimationUtilDataLayerModule::class,
FakeAuthenticationDataLayerModule::class,
FakeBouncerDataLayerModule::class,
FakeCommonDataLayerModule::class,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
new file mode 100644
index 0000000..9851b0e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.data.model
+
+import android.graphics.drawable.Icon
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+
+/** Simple ActiveNotificationModel builder for use in tests. */
+fun activeNotificationModel(
+ key: String,
+ groupKey: String? = null,
+ isAmbient: Boolean = false,
+ isRowDismissed: Boolean = false,
+ isSilent: Boolean = false,
+ isLastMessageFromReply: Boolean = false,
+ isSuppressedFromStatusBar: Boolean = false,
+ isPulsing: Boolean = false,
+ aodIcon: Icon? = null,
+ shelfIcon: Icon? = null,
+ statusBarIcon: Icon? = null,
+) =
+ ActiveNotificationModel(
+ key = key,
+ groupKey = groupKey,
+ isAmbient = isAmbient,
+ isRowDismissed = isRowDismissed,
+ isSilent = isSilent,
+ isLastMessageFromReply = isLastMessageFromReply,
+ isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+ isPulsing = isPulsing,
+ aodIcon = aodIcon,
+ shelfIcon = shelfIcon,
+ statusBarIcon = statusBarIcon,
+ )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
new file mode 100644
index 0000000..cb1ba20
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.data.repository
+
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+
+/**
+ * Make the repository hold [count] active notifications for testing. The keys of the notifications
+ * are "0", "1", ..., (count - 1).toString().
+ */
+fun ActiveNotificationListRepository.setActiveNotifs(count: Int) {
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply { repeat(count) { i -> addEntry(activeNotificationModel(key = i.toString())) } }
+ .build()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
index 4059930..c4d7867 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.policy.data.repository
-import android.app.NotificationManager
+import android.app.NotificationManager.Policy
import android.provider.Settings
import com.android.systemui.dagger.SysUISingleton
import dagger.Binds
@@ -27,14 +27,24 @@
@SysUISingleton
class FakeZenModeRepository @Inject constructor() : ZenModeRepository {
override val zenMode: MutableStateFlow<Int> = MutableStateFlow(Settings.Global.ZEN_MODE_OFF)
- override val consolidatedNotificationPolicy: MutableStateFlow<NotificationManager.Policy?> =
+ override val consolidatedNotificationPolicy: MutableStateFlow<Policy?> =
MutableStateFlow(
- NotificationManager.Policy(
+ Policy(
/* priorityCategories = */ 0,
/* priorityCallSenders = */ 0,
/* priorityMessageSenders = */ 0,
)
)
+
+ fun setSuppressedVisualEffects(suppressedVisualEffects: Int) {
+ consolidatedNotificationPolicy.value =
+ Policy(
+ /* priorityCategories = */ 0,
+ /* priorityCallSenders = */ 0,
+ /* priorityMessageSenders = */ 0,
+ /* suppressedVisualEffects = */ suppressedVisualEffects,
+ )
+ }
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/FakeAnimationUtilDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/FakeAnimationUtilDataLayerModule.kt
new file mode 100644
index 0000000..f7830d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/FakeAnimationUtilDataLayerModule.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.animation.data
+
+import com.android.systemui.util.animation.data.repository.FakeAnimationStatusRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeAnimationStatusRepositoryModule::class])
+object FakeAnimationUtilDataLayerModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/repository/FakeAnimationStatusRepository.kt
similarity index 72%
rename from packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/repository/FakeAnimationStatusRepository.kt
index e72235c..ca6628b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/repository/FakeAnimationStatusRepository.kt
@@ -11,15 +11,17 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.util.animation
+package com.android.systemui.util.animation.data.repository
-import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
-class FakeAnimationStatusRepository : AnimationStatusRepository {
+class FakeAnimationStatusRepository @Inject constructor() : AnimationStatusRepository {
// Replay 1 element as real repository always emits current status as a first element
private val animationsEnabled: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = 1)
@@ -30,3 +32,8 @@
animationsEnabled.tryEmit(enabled)
}
}
+
+@Module
+interface FakeAnimationStatusRepositoryModule {
+ @Binds fun bindFake(fake: FakeAnimationStatusRepository): AnimationStatusRepository
+}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a158b18..c24d6a0 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -527,12 +527,27 @@
@Override
public boolean canHandleVolumeKey() {
if (isPlaybackTypeLocal()) {
+ if (DEBUG) {
+ Log.d(TAG, "Local MediaSessionRecord can handle volume key");
+ }
return true;
}
if (mVolumeControlType == VOLUME_CONTROL_FIXED) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Local MediaSessionRecord with FIXED volume control can't handle volume"
+ + " key");
+ }
return false;
}
if (mVolumeAdjustmentForRemoteGroupSessions) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Volume adjustment for remote group sessions allowed so MediaSessionRecord"
+ + " can handle volume key");
+ }
return true;
}
// See b/228021646 for details.
@@ -540,7 +555,18 @@
List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName);
boolean foundNonSystemSession = false;
boolean remoteSessionAllowVolumeAdjustment = true;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Found "
+ + sessions.size()
+ + " routing sessions for package name "
+ + mPackageName);
+ }
for (RoutingSessionInfo session : sessions) {
+ if (DEBUG) {
+ Log.d(TAG, "Found routingSessionInfo: " + session);
+ }
if (!session.isSystemSession()) {
foundNonSystemSession = true;
if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) {
@@ -549,9 +575,15 @@
}
}
if (!foundNonSystemSession) {
- Log.d(TAG, "Package " + mPackageName
- + " has a remote media session but no associated routing session");
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Package "
+ + mPackageName
+ + " has a remote media session but no associated routing session");
+ }
}
+
return foundNonSystemSession && remoteSessionAllowVolumeAdjustment;
}