diff options
102 files changed, 2568 insertions, 824 deletions
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b4698dee6019..d21ba014e166 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -77,6 +77,7 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; @@ -502,6 +503,13 @@ public final class ViewRootImpl implements ViewParent, Region mTouchableRegion; Region mPreviousTouchableRegion; + private int mMeasuredWidth; + private int mMeasuredHeight; + + // This indicates that we've already known the window size but without measuring the views. + // If this is true, we must measure the views before laying out them. + private boolean mViewMeasureDeferred; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) int mWidth; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -2566,7 +2574,8 @@ public final class ViewRootImpl implements ViewParent, } private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, - final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { + final Resources res, final int desiredWindowWidth, final int desiredWindowHeight, + boolean forRootSizeOnly) { int childWidthMeasureSpec; int childHeightMeasureSpec; boolean windowSizeMayChange = false; @@ -2622,7 +2631,15 @@ public final class ViewRootImpl implements ViewParent, lp.privateFlags); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height, lp.privateFlags); - performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + if (!forRootSizeOnly || !setMeasuredRootSizeFromSpec( + childWidthMeasureSpec, childHeightMeasureSpec)) { + performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + } else { + // We already know how big the window should be before measuring the views. + // We can measure the views before laying out them. This is to avoid unnecessary + // measure. + mViewMeasureDeferred = true; + } if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } @@ -2638,6 +2655,25 @@ public final class ViewRootImpl implements ViewParent, } /** + * Sets the measured root size for requesting the window frame. + * + * @param widthMeasureSpec contains the size and the mode of the width. + * @param heightMeasureSpec contains the size and the mode of the height. + * @return {@code true} if we actually set the measured size; {@code false} otherwise. + */ + private boolean setMeasuredRootSizeFromSpec(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { + // We don't know the exact size. We need to measure the hierarchy to know that. + return false; + } + mMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec); + mMeasuredHeight = MeasureSpec.getSize(heightMeasureSpec); + return true; + } + + /** * Modifies the input matrix such that it maps view-local coordinates to * on-screen coordinates. * @@ -2724,6 +2760,14 @@ public final class ViewRootImpl implements ViewParent, || lp.type == TYPE_VOLUME_OVERLAY; } + /** + * @return {@code true} if we should reduce unnecessary measure for the window. + * TODO(b/260382739): Apply this to all windows. + */ + private static boolean shouldOptimizeMeasure(final WindowManager.LayoutParams lp) { + return lp.type == TYPE_NOTIFICATION_SHADE; + } + private Rect getWindowBoundsInsetSystemBars() { final Rect bounds = new Rect( mContext.getResources().getConfiguration().windowConfiguration.getBounds()); @@ -2774,6 +2818,7 @@ public final class ViewRootImpl implements ViewParent, mAppVisibilityChanged = false; final boolean viewUserVisibilityChanged = !mFirst && ((mViewVisibility == View.VISIBLE) != (viewVisibility == View.VISIBLE)); + final boolean shouldOptimizeMeasure = shouldOptimizeMeasure(lp); WindowManager.LayoutParams params = null; CompatibilityInfo compatibilityInfo = @@ -2895,7 +2940,7 @@ public final class ViewRootImpl implements ViewParent, // Ask host how big it wants to be windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(), - desiredWindowWidth, desiredWindowHeight); + desiredWindowWidth, desiredWindowHeight, shouldOptimizeMeasure); } if (collectViewAttributes()) { @@ -2935,8 +2980,8 @@ public final class ViewRootImpl implements ViewParent, // we don't need to go through two layout passes when things // change due to fitting system windows, which can happen a lot. windowSizeMayChange |= measureHierarchy(host, lp, - mView.getContext().getResources(), - desiredWindowWidth, desiredWindowHeight); + mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight, + shouldOptimizeMeasure); } } @@ -3351,6 +3396,13 @@ public final class ViewRootImpl implements ViewParent, maybeHandleWindowMove(frame); } + if (mViewMeasureDeferred) { + // It's time to measure the views since we are going to layout them. + performMeasure( + MeasureSpec.makeMeasureSpec(frame.width(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(frame.height(), MeasureSpec.EXACTLY)); + } + if (!mRelayoutRequested && mCheckIfCanDraw) { // We had a sync previously, but we didn't call IWindowSession#relayout in this // traversal. So we don't know if the sync is complete that we can continue to draw. @@ -3940,6 +3992,9 @@ public final class ViewRootImpl implements ViewParent, } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } + mMeasuredWidth = mView.getMeasuredWidth(); + mMeasuredHeight = mView.getMeasuredHeight(); + mViewMeasureDeferred = false; } /** @@ -4035,7 +4090,7 @@ public final class ViewRootImpl implements ViewParent, view.requestLayout(); } measureHierarchy(host, lp, mView.getContext().getResources(), - desiredWindowWidth, desiredWindowHeight); + desiredWindowWidth, desiredWindowHeight, false /* forRootSizeOnly */); mInLayout = true; host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); @@ -8101,8 +8156,8 @@ public final class ViewRootImpl implements ViewParent, final WindowConfiguration winConfigFromWm = mLastReportedMergedConfiguration.getGlobalConfiguration().windowConfiguration; final WindowConfiguration winConfig = getCompatWindowConfiguration(); - final int measuredWidth = mView.getMeasuredWidth(); - final int measuredHeight = mView.getMeasuredHeight(); + final int measuredWidth = mMeasuredWidth; + final int measuredHeight = mMeasuredHeight; final boolean relayoutAsync; if (LOCAL_LAYOUT && (mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0 diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 15be5f563d88..f90353adffc1 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -827,6 +827,26 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Sets/removes the reparent leaf task flag for this {@code windowContainer}. + * When this is set, the server side will try to reparent the leaf task to task display area + * if there is an existing activity in history during the activity launch. This operation only + * support on the organized root task. + * @hide + */ + @NonNull + public WindowContainerTransaction setReparentLeafTaskIfRelaunch( + @NonNull WindowContainerToken windowContainer, boolean reparentLeafTaskIfRelaunch) { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder( + HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH) + .setContainer(windowContainer.asBinder()) + .setReparentLeafTaskIfRelaunch(reparentLeafTaskIfRelaunch) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + + /** * Merges another WCT into this one. * @param transfer When true, this will transfer everything from other potentially leaving * other in an unusable state. When false, other is left alone, but @@ -1241,6 +1261,7 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 20; public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21; public static final int HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 22; + public static final int HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH = 23; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. @@ -1293,6 +1314,8 @@ public final class WindowContainerTransaction implements Parcelable { private boolean mAlwaysOnTop; + private boolean mReparentLeafTaskIfRelaunch; + public static HierarchyOp createForReparent( @NonNull IBinder container, @Nullable IBinder reparent, boolean toTop) { return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_REPARENT) @@ -1398,6 +1421,7 @@ public final class WindowContainerTransaction implements Parcelable { mPendingIntent = copy.mPendingIntent; mShortcutInfo = copy.mShortcutInfo; mAlwaysOnTop = copy.mAlwaysOnTop; + mReparentLeafTaskIfRelaunch = copy.mReparentLeafTaskIfRelaunch; } protected HierarchyOp(Parcel in) { @@ -1420,6 +1444,7 @@ public final class WindowContainerTransaction implements Parcelable { mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); mShortcutInfo = in.readTypedObject(ShortcutInfo.CREATOR); mAlwaysOnTop = in.readBoolean(); + mReparentLeafTaskIfRelaunch = in.readBoolean(); } public int getType() { @@ -1494,6 +1519,10 @@ public final class WindowContainerTransaction implements Parcelable { return mAlwaysOnTop; } + public boolean isReparentLeafTaskIfRelaunch() { + return mReparentLeafTaskIfRelaunch; + } + @Nullable public TaskFragmentCreationParams getTaskFragmentCreationOptions() { return mTaskFragmentCreationOptions; @@ -1572,6 +1601,9 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: return "{setCompanionTaskFragment: container = " + mContainer + " companion = " + mReparent + "}"; + case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH: + return "{setReparentLeafTaskIfRelaunch: container= " + mContainer + + " reparentLeafTaskIfRelaunch= " + mReparentLeafTaskIfRelaunch + "}"; default: return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent + " mToTop=" + mToTop @@ -1602,6 +1634,7 @@ public final class WindowContainerTransaction implements Parcelable { dest.writeTypedObject(mPendingIntent, flags); dest.writeTypedObject(mShortcutInfo, flags); dest.writeBoolean(mAlwaysOnTop); + dest.writeBoolean(mReparentLeafTaskIfRelaunch); } @Override @@ -1662,6 +1695,8 @@ public final class WindowContainerTransaction implements Parcelable { private boolean mAlwaysOnTop; + private boolean mReparentLeafTaskIfRelaunch; + Builder(int type) { mType = type; } @@ -1732,6 +1767,11 @@ public final class WindowContainerTransaction implements Parcelable { return this; } + Builder setReparentLeafTaskIfRelaunch(boolean reparentLeafTaskIfRelaunch) { + mReparentLeafTaskIfRelaunch = reparentLeafTaskIfRelaunch; + return this; + } + Builder setShortcutInfo(@Nullable ShortcutInfo shortcutInfo) { mShortcutInfo = shortcutInfo; return this; @@ -1757,6 +1797,7 @@ public final class WindowContainerTransaction implements Parcelable { hierarchyOp.mAlwaysOnTop = mAlwaysOnTop; hierarchyOp.mTaskFragmentCreationOptions = mTaskFragmentCreationOptions; hierarchyOp.mShortcutInfo = mShortcutInfo; + hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch; return hierarchyOp; } diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml index e6ae28207970..29945937788b 100644 --- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright (C) 2019 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" @@ -25,13 +25,12 @@ android:fillAlpha="0.8" android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/> <group - android:translateX="12" - android:translateY="12"> + android:scaleX="0.8" + android:scaleY="0.8" + android:translateX="10" + android:translateY="10"> <path - android:fillColor="@color/compat_controls_text" - android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/> - <path - android:fillColor="@color/compat_controls_text" - android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/> + android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z" + android:fillColor="@color/compat_controls_text"/> </group> </vector> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 6e116b958ac9..c836b95ffab8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -51,8 +51,6 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.SurfaceUtils; -import java.util.function.Consumer; - /** * Handles split decor like showing resizing hint for a specific split. */ @@ -72,17 +70,18 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mIconLeash; private SurfaceControl mBackgroundLeash; private SurfaceControl mGapBackgroundLeash; + private SurfaceControl mScreenshot; private boolean mShown; private boolean mIsResizing; private final Rect mBounds = new Rect(); - private final Rect mResizingBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; private int mIconSize; private int mOffsetX; private int mOffsetY; + private int mRunningAnimationCount = 0; public SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession) { @@ -173,7 +172,6 @@ public class SplitDecorManager extends WindowlessWindowManager { mIsResizing = true; mBounds.set(newBounds); } - mResizingBounds.set(newBounds); mOffsetX = offsetX; mOffsetY = offsetY; @@ -227,33 +225,41 @@ public class SplitDecorManager extends WindowlessWindowManager { t.setVisibility(mBackgroundLeash, show); t.setVisibility(mIconLeash, show); } else { - startFadeAnimation(show, null /* finishedConsumer */); + startFadeAnimation(show, false, null); } mShown = show; } } /** Stops showing resizing hint. */ - public void onResized(SurfaceControl.Transaction t) { - if (!mShown && mIsResizing) { - mTempRect.set(mResizingBounds); - mTempRect.offsetTo(-mOffsetX, -mOffsetY); - final SurfaceControl screenshot = ScreenshotUtils.takeScreenshot(t, - mHostLeash, mTempRect, Integer.MAX_VALUE - 1); + public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) { + if (mScreenshot != null) { + t.setPosition(mScreenshot, mOffsetX, mOffsetY); final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); final ValueAnimator va = ValueAnimator.ofFloat(1, 0); va.addUpdateListener(valueAnimator -> { final float progress = (float) valueAnimator.getAnimatedValue(); - animT.setAlpha(screenshot, progress); + animT.setAlpha(mScreenshot, progress); animT.apply(); }); va.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + mRunningAnimationCount++; + } + + @Override public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) { - animT.remove(screenshot); + mRunningAnimationCount--; + animT.remove(mScreenshot); animT.apply(); animT.close(); + mScreenshot = null; + + if (mRunningAnimationCount == 0 && animFinishedCallback != null) { + animFinishedCallback.run(); + } } }); va.start(); @@ -285,10 +291,34 @@ public class SplitDecorManager extends WindowlessWindowManager { mFadeAnimator.cancel(); } if (mShown) { - fadeOutDecor(null /* finishedCallback */); + fadeOutDecor(animFinishedCallback); } else { // Decor surface is hidden so release it directly. releaseDecor(t); + if (mRunningAnimationCount == 0 && animFinishedCallback != null) { + animFinishedCallback.run(); + } + } + } + + /** Screenshot host leash and attach on it if meet some conditions */ + public void screenshotIfNeeded(SurfaceControl.Transaction t) { + if (!mShown && mIsResizing) { + mTempRect.set(mBounds); + mTempRect.offsetTo(0, 0); + mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, + Integer.MAX_VALUE - 1); + } + } + + /** Set screenshot and attach on host leash it if meet some conditions */ + public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { + if (screenshot == null || !screenshot.isValid()) return; + + if (!mShown && mIsResizing) { + mScreenshot = screenshot; + t.reparent(screenshot, mHostLeash); + t.setLayer(screenshot, Integer.MAX_VALUE - 1); } } @@ -296,18 +326,15 @@ public class SplitDecorManager extends WindowlessWindowManager { * directly. */ public void fadeOutDecor(Runnable finishedCallback) { if (mShown) { - startFadeAnimation(false /* show */, transaction -> { - releaseDecor(transaction); - if (finishedCallback != null) finishedCallback.run(); - }); + startFadeAnimation(false /* show */, true, finishedCallback); mShown = false; } else { if (finishedCallback != null) finishedCallback.run(); } } - private void startFadeAnimation(boolean show, - Consumer<SurfaceControl.Transaction> finishedConsumer) { + private void startFadeAnimation(boolean show, boolean releaseSurface, + Runnable finishedCallback) { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); mFadeAnimator.setDuration(FADE_DURATION); @@ -324,6 +351,7 @@ public class SplitDecorManager extends WindowlessWindowManager { mFadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(@NonNull Animator animation) { + mRunningAnimationCount++; if (show) { animT.show(mBackgroundLeash).show(mIconLeash); } @@ -335,6 +363,7 @@ public class SplitDecorManager extends WindowlessWindowManager { @Override public void onAnimationEnd(@NonNull Animator animation) { + mRunningAnimationCount--; if (!show) { if (mBackgroundLeash != null) { animT.hide(mBackgroundLeash); @@ -343,11 +372,15 @@ public class SplitDecorManager extends WindowlessWindowManager { animT.hide(mIconLeash); } } - if (finishedConsumer != null) { - finishedConsumer.accept(animT); + if (releaseSurface) { + releaseDecor(animT); } animT.apply(); animT.close(); + + if (mRunningAnimationCount == 0 && finishedCallback != null) { + finishedCallback.run(); + } } }); mFadeAnimator.start(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 962be9da2111..ad0adc6c84c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -687,10 +687,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<DesktopModeController> providesDesktopModeController( - @DynamicOverride Optional<DesktopModeController> desktopModeController) { + static Optional<DesktopModeController> provideDesktopModeController( + @DynamicOverride Optional<Lazy<DesktopModeController>> desktopModeController) { + // Use optional-of-lazy for the dependency that this provider relies on. + // Lazy ensures that this provider will not be the cause the dependency is created + // when it will not be returned due to the condition below. if (DesktopModeStatus.IS_SUPPORTED) { - return desktopModeController; + return desktopModeController.map(Lazy::get); } return Optional.empty(); } @@ -701,10 +704,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<DesktopModeTaskRepository> providesDesktopTaskRepository( - @DynamicOverride Optional<DesktopModeTaskRepository> desktopModeTaskRepository) { + static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository( + @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) { + // Use optional-of-lazy for the dependency that this provider relies on. + // Lazy ensures that this provider will not be the cause the dependency is created + // when it will not be returned due to the condition below. if (DesktopModeStatus.IS_SUPPORTED) { - return desktopModeTaskRepository; + return desktopModeTaskRepository.map(Lazy::get); } return Optional.empty(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index f1670cd792cf..6be83054ae59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -189,7 +189,7 @@ public abstract class WMShellModule { ShellTaskOrganizer taskOrganizer, DisplayController displayController, SyncTransactionQueue syncQueue, - @DynamicOverride DesktopModeController desktopModeController) { + Optional<DesktopModeController> desktopModeController) { return new CaptionWindowDecorViewModel( context, mainHandler, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index 17ba246910d5..5824f51a1d33 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -100,7 +100,9 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll mDesktopModeTaskRepository = desktopModeTaskRepository; mMainExecutor = mainExecutor; mSettingsObserver = new SettingsObserver(mContext, mainHandler); - shellInit.addInitCallback(this::onInit, this); + if (DesktopModeStatus.isSupported()) { + shellInit.addInitCallback(this::onInit, this); + } } private void onInit() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 2fafe67664f8..e3eb2b746969 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -37,6 +37,13 @@ public class DesktopModeStatus { "persist.wm.debug.desktop_mode", false); /** + * Return {@code true} if desktop mode support is enabled + */ + public static boolean isSupported() { + return IS_SUPPORTED; + } + + /** * Check if desktop mode is active * * @return {@code true} if active @@ -54,4 +61,5 @@ public class DesktopModeStatus { return false; } } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS new file mode 100644 index 000000000000..926cfb3b12ef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module desktop owners +madym@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS new file mode 100644 index 000000000000..0c2d5c49f830 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module freeform owners +madym@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 21a13103616c..1cf3a896b68e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -47,6 +47,7 @@ import android.window.WindowContainerTransactionCallback; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; @@ -64,6 +65,7 @@ class SplitScreenTransitions { DismissTransition mPendingDismiss = null; TransitSession mPendingEnter = null; TransitSession mPendingRecent = null; + TransitSession mPendingResize = null; private IBinder mAnimatingTransition = null; OneShotRemoteHandler mPendingRemoteHandler = null; @@ -177,6 +179,43 @@ class SplitScreenTransitions { onFinish(null /* wct */, null /* wctCB */); } + void applyResizeTransition(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, + @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) { + mFinishCallback = finishCallback; + mAnimatingTransition = transition; + mFinishTransaction = finishTransaction; + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer())) { + final SurfaceControl leash = change.getLeash(); + startTransaction.setPosition(leash, change.getEndAbsBounds().left, + change.getEndAbsBounds().top); + startTransaction.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + + SplitDecorManager decor = mainRoot.equals(change.getContainer()) + ? mainDecor : sideDecor; + ValueAnimator va = new ValueAnimator(); + mAnimations.add(va); + decor.setScreenshotIfNeeded(change.getSnapshot(), startTransaction); + decor.onResized(startTransaction, () -> { + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(null /* wct */, null /* wctCB */); + }); + }); + } + } + + startTransaction.apply(); + onFinish(null /* wct */, null /* wctCB */); + } + boolean isPendingTransition(IBinder transition) { return getPendingTransition(transition) != null; } @@ -193,6 +232,10 @@ class SplitScreenTransitions { return mPendingDismiss != null && mPendingDismiss.mTransition == transition; } + boolean isPendingResize(IBinder transition) { + return mPendingResize != null && mPendingResize.mTransition == transition; + } + @Nullable private TransitSession getPendingTransition(IBinder transition) { if (isPendingEnter(transition)) { @@ -201,11 +244,14 @@ class SplitScreenTransitions { return mPendingRecent; } else if (isPendingDismiss(transition)) { return mPendingDismiss; + } else if (isPendingResize(transition)) { + return mPendingResize; } return null; } + /** Starts a transition to enter split with a remote transition animator. */ IBinder startEnterTransition( @WindowManager.TransitionType int transitType, @@ -258,6 +304,21 @@ class SplitScreenTransitions { exitReasonToString(reason), stageTypeToString(dismissTop)); } + IBinder startResizeTransition(WindowContainerTransaction wct, + Transitions.TransitionHandler handler, + @Nullable TransitionFinishedCallback finishCallback) { + IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler); + setResizeTransition(transition, finishCallback); + return transition; + } + + void setResizeTransition(@NonNull IBinder transition, + @Nullable TransitionFinishedCallback finishCallback) { + mPendingResize = new TransitSession(transition, null /* consumedCb */, finishCallback); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced Resize split screen"); + } + void setRecentTransition(@NonNull IBinder transition, @Nullable RemoteTransition remoteTransition, @Nullable TransitionFinishedCallback finishCallback) { @@ -324,6 +385,9 @@ class SplitScreenTransitions { mPendingRecent.onConsumed(aborted); mPendingRecent = null; mPendingRemoteHandler = null; + } else if (isPendingResize(transition)) { + mPendingResize.onConsumed(aborted); + mPendingResize = null; } } @@ -340,6 +404,9 @@ class SplitScreenTransitions { } else if (isPendingDismiss(mAnimatingTransition)) { mPendingDismiss.onFinished(wct, mFinishTransaction); mPendingDismiss = null; + } else if (isPendingResize(mAnimatingTransition)) { + mPendingResize.onFinished(wct, mFinishTransaction); + mPendingResize = null; } mPendingRemoteHandler = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index aa0512b64a16..38fb2df11df9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1667,15 +1667,29 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onLayoutSizeChanged(SplitLayout layout) { // Reset this flag every time onLayoutSizeChanged. mShowDecorImmediately = false; + + if (!ENABLE_SHELL_TRANSITIONS) { + // Only need screenshot for legacy case because shell transition should screenshot + // itself during transition. + final SurfaceControl.Transaction startT = mTransactionPool.acquire(); + mMainStage.screenshotIfNeeded(startT); + mSideStage.screenshotIfNeeded(startT); + mTransactionPool.release(startT); + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); updateWindowBounds(layout, wct); sendOnBoundsChanged(); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(layout, t, false /* applyResizingOffset */); - mMainStage.onResized(t); - mSideStage.onResized(t); - }); + if (ENABLE_SHELL_TRANSITIONS) { + mSplitTransitions.startResizeTransition(wct, this, null /* callback */); + } else { + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + updateSurfaceBounds(layout, t, false /* applyResizingOffset */); + mMainStage.onResized(t); + mSideStage.onResized(t); + }); + } mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } @@ -2029,6 +2043,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } else if (mSplitTransitions.isPendingDismiss(transition)) { shouldAnimate = startPendingDismissAnimation( mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction); + } else if (mSplitTransitions.isPendingResize(transition)) { + mSplitTransitions.applyResizeTransition(transition, info, startTransaction, + finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token, + mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(), + mSideStage.getSplitDecorManager()); + return true; } if (!shouldAnimate) return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 358f712f76b5..8a52c8750ba6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -292,7 +292,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void onResized(SurfaceControl.Transaction t) { if (mSplitDecorManager != null) { - mSplitDecorManager.onResized(t); + mSplitDecorManager.onResized(t, null); + } + } + + void screenshotIfNeeded(SurfaceControl.Transaction t) { + if (mSplitDecorManager != null) { + mSplitDecorManager.screenshotIfNeeded(t); } } @@ -304,6 +310,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + SplitDecorManager getSplitDecorManager() { + return mSplitDecorManager; + } + void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) { // Clear overridden bounds and windowing mode to make sure the child task can inherit // windowing mode and bounds from split root. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index e7036c726a27..56554020f3cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -55,6 +55,7 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.transition.Transitions; +import java.util.Optional; import java.util.function.Supplier; /** @@ -74,7 +75,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; private FreeformTaskTransitionStarter mTransitionStarter; - private DesktopModeController mDesktopModeController; + private Optional<DesktopModeController> mDesktopModeController; private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); @@ -90,7 +91,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { ShellTaskOrganizer taskOrganizer, DisplayController displayController, SyncTransactionQueue syncQueue, - DesktopModeController desktopModeController) { + Optional<DesktopModeController> desktopModeController) { this( context, mainHandler, @@ -110,7 +111,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { ShellTaskOrganizer taskOrganizer, DisplayController displayController, SyncTransactionQueue syncQueue, - DesktopModeController desktopModeController, + Optional<DesktopModeController> desktopModeController, CaptionWindowDecoration.Factory captionWindowDecorFactory, Supplier<InputManager> inputManagerSupplier) { @@ -246,10 +247,10 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle) { decoration.createHandleMenu(); } else if (id == R.id.desktop_button) { - mDesktopModeController.setDesktopModeActive(true); + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true)); decoration.closeHandleMenu(); } else if (id == R.id.fullscreen_button) { - mDesktopModeController.setDesktopModeActive(false); + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false)); decoration.closeHandleMenu(); decoration.setButtonVisibility(); } @@ -304,9 +305,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { */ private void handleEventForMove(MotionEvent e) { RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - int windowingMode = mDesktopModeController - .getDisplayAreaWindowingMode(taskInfo.displayId); - if (windowingMode == WINDOWING_MODE_FULLSCREEN) { + if (mDesktopModeController.isPresent() + && mDesktopModeController.get().getDisplayAreaWindowingMode(taskInfo.displayId) + == WINDOWING_MODE_FULLSCREEN) { return; } switch (e.getActionMasked()) { @@ -331,7 +332,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); if (e.getRawY(dragPointerIdx) <= statusBarHeight && DesktopModeStatus.isActive(mContext)) { - mDesktopModeController.setDesktopModeActive(false); + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false)); } break; } @@ -471,7 +472,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { int statusBarHeight = mDisplayController .getDisplayLayout(focusedDecor.mTaskInfo.displayId).stableInsets().top; if (ev.getY() > statusBarHeight) { - mDesktopModeController.setDesktopModeActive(true); + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true)); return; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index 01584a067cc2..dad913300711 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -36,7 +36,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -100,15 +100,14 @@ public class DesktopModeControllerTest extends ShellTestCase { @Before public void setUp() { mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isSupported()).thenReturn(true); when(DesktopModeStatus.isActive(any())).thenReturn(true); mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); mDesktopModeTaskRepository = new DesktopModeTaskRepository(); - mController = new DesktopModeController(mContext, mShellInit, mShellController, - mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions, - mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor()); + mController = createController(); when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>()); @@ -125,7 +124,17 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void instantiate_addInitCallback() { - verify(mShellInit, times(1)).addInitCallback(any(), any()); + verify(mShellInit).addInitCallback(any(), any()); + } + + @Test + public void instantiate_flagOff_doNotAddInitCallback() { + when(DesktopModeStatus.isSupported()).thenReturn(false); + clearInvocations(mShellInit); + + createController(); + + verify(mShellInit, never()).addInitCallback(any(), any()); } @Test @@ -353,6 +362,12 @@ public class DesktopModeControllerTest extends ShellTestCase { assertThat(wct).isNotNull(); } + private DesktopModeController createController() { + return new DesktopModeController(mContext, mShellInit, mShellController, + mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions, + mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor()); + } + private DisplayAreaInfo createMockDisplayArea() { DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken, mContext.getDisplayId(), 0); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java index 9b37b97a4c24..ad6fcedd3166 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java @@ -54,6 +54,7 @@ import org.mockito.Mock; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; /** Tests of {@link CaptionWindowDecorViewModel} */ @@ -101,7 +102,7 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { mTaskOrganizer, mDisplayController, mSyncQueue, - mDesktopModeController, + Optional.of(mDesktopModeController), mCaptionWindowDecorFactory, new MockObjectSupplier<>(mMockInputManagers, () -> mock(InputManager.class))); mCaptionWindowDecorViewModel.setEventReceiverFactory(mEventReceiverFactory); diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 7732da40aac2..46a94fdfcd17 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -314,4 +314,7 @@ <!-- Whether tilt to bright is enabled by default. --> <bool name="def_wearable_tiltToBrightEnabled">false</bool> + + <!-- Whether vibrate icon is shown in the status bar by default. --> + <integer name="def_statusBarVibrateIconEnabled">0</integer> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index a6edb0f0e2e3..19bbcff0bd79 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3631,7 +3631,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 210; + private static final int SETTINGS_VERSION = 211; private final int mUserId; @@ -5512,7 +5512,17 @@ public class SettingsProvider extends ContentProvider { // removed now that feature is enabled for everyone currentVersion = 210; } - + if (currentVersion == 210) { + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final int defaultValueVibrateIconEnabled = getContext().getResources() + .getInteger(R.integer.def_statusBarVibrateIconEnabled); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON, + String.valueOf(defaultValueVibrateIconEnabled), + null /* tag */, true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + currentVersion = 211; + } // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 844e88a09b05..acaa008f2a0c 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -904,6 +904,29 @@ <service android:name=".controls.controller.AuxiliaryPersistenceWrapper$DeletionJobService" android:permission="android.permission.BIND_JOB_SERVICE"/> + <!-- region Note Task --> + <activity + android:name=".notetask.shortcut.CreateNoteTaskShortcutActivity" + android:enabled="false" + android:exported="true" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay" + android:label="@string/note_task_button_label" + android:icon="@drawable/ic_note_task_button"> + + <intent-filter> + <action android:name="android.intent.action.CREATE_SHORTCUT" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + + <activity + android:name=".notetask.shortcut.LaunchNoteTaskActivity" + android:exported="true" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay" /> + <!-- endregion --> + <!-- started from ControlsRequestReceiver --> <activity android:name=".controls.management.ControlsRequestDialog" diff --git a/packages/SystemUI/res/drawable/ic_note_task_button.xml b/packages/SystemUI/res/drawable/ic_note_task_button.xml new file mode 100644 index 000000000000..bb5e224ea315 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_note_task_button.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="#636C6F" + android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z" /> + <path + android:fillColor="#636C6F" + android:fillType="evenOdd" + android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z" /> +</vector> diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 530db0d0304a..13c9a5ea05b1 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -35,7 +35,6 @@ android:layout_height="@dimen/qs_media_session_height_expanded" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" android:translationZ="0dp" android:scaleType="centerCrop" diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index 49dd574af829..fd2e3249d59a 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -36,4 +36,8 @@ avatar will no longer show on the lockscreen --> <bool name="flag_user_switcher_chip">false</bool> + <!-- Whether the battery icon is allowed to display a shield when battery life is being + protected. --> + <bool name="flag_battery_shield_icon">false</bool> + </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 30788cf90e6a..52a931bdd6ec 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2669,6 +2669,10 @@ <xliff:g id="weather_condition" example="Partly cloudy">%1$s</xliff:g>, <xliff:g id="temperature" example="7°C">%2$s</xliff:g> </string> + <!-- TODO(b/259369672): Replace with final resource. --> + <!-- [CHAR LIMIT=30] Label used to open Note Task --> + <string name="note_task_button_label">Notetaking</string> + <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is broadcasting --> <string name="broadcasting_description_is_broadcasting">Broadcasting</string> <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, title --> diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 7c05356dd5fc..389639d23795 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -83,8 +83,7 @@ object Flags { val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true) @JvmField - val NOTIFICATION_GROUP_CORNER = - unreleasedFlag(116, "notification_group_corner", teamfood = true) + val USE_ROUNDNESS_SOURCETYPES = unreleasedFlag(116, "use_roundness_sourcetype", teamfood = true) // TODO(b/259217907) @JvmField @@ -229,7 +228,9 @@ object Flags { val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend") // TODO(b/256623670): Tracking Bug - @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon") + @JvmField + val BATTERY_SHIELD_ICON = + resourceBooleanFlag(610, R.bool.flag_battery_shield_icon, "battery_shield_icon") // TODO(b/260881289): Tracking Bug val NEW_STATUS_BAR_ICONS_DEBUG_COLORING = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 827ac789073a..df8fb9149f30 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -940,19 +940,9 @@ public class MediaControlPanel { if (mIsSeekBarEnabled) { return ConstraintSet.VISIBLE; } - // If disabled and "neighbours" are visible, set progress bar to INVISIBLE instead of GONE - // so layout weights still work. - return areAnyExpandedBottomActionsVisible() ? ConstraintSet.INVISIBLE : ConstraintSet.GONE; - } - - private boolean areAnyExpandedBottomActionsVisible() { - ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); - for (int id : MediaViewHolder.Companion.getExpandedBottomActionIds()) { - if (expandedSet.getVisibility(id) == ConstraintSet.VISIBLE) { - return true; - } - } - return false; + // Set progress bar to INVISIBLE to keep the positions of text and buttons similar to the + // original positions when seekbar is enabled. + return ConstraintSet.INVISIBLE; } private void setGenericButton( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt index 647beb95a3bc..b10abb569717 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -48,52 +48,66 @@ class MediaTttCommandLineHelper @Inject constructor( /** All commands for the sender device. */ inner class SenderCommand : Command { override fun execute(pw: PrintWriter, args: List<String>) { - val commandName = args[1] + if (args.size < 2) { + help(pw) + return + } + + val senderArgs = processArgs(args) + @StatusBarManager.MediaTransferSenderState val displayState: Int? try { - displayState = ChipStateSender.getSenderStateIdFromName(commandName) + displayState = ChipStateSender.getSenderStateIdFromName(senderArgs.commandName) } catch (ex: IllegalArgumentException) { - pw.println("Invalid command name $commandName") + pw.println("Invalid command name ${senderArgs.commandName}") return } @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager - val routeInfo = MediaRoute2Info.Builder(if (args.size >= 4) args[3] else "id", args[0]) + val routeInfo = MediaRoute2Info.Builder(senderArgs.id, senderArgs.deviceName) .addFeature("feature") - val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false") - if (useAppIcon) { + if (senderArgs.useAppIcon) { routeInfo.setClientPackageName(TEST_PACKAGE_NAME) } + var undoExecutor: Executor? = null + var undoRunnable: Runnable? = null + if (isSucceededState(displayState) && senderArgs.showUndo) { + undoExecutor = mainExecutor + undoRunnable = Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") } + } + statusBarManager.updateMediaTapToTransferSenderDisplay( displayState, routeInfo.build(), - getUndoExecutor(displayState), - getUndoCallback(displayState) + undoExecutor, + undoRunnable, ) } - private fun getUndoExecutor( - @StatusBarManager.MediaTransferSenderState displayState: Int - ): Executor? { - return if (isSucceededState(displayState)) { - mainExecutor - } else { - null + private fun processArgs(args: List<String>): SenderArgs { + val senderArgs = SenderArgs( + deviceName = args[0], + commandName = args[1], + ) + + if (args.size == 2) { + return senderArgs } - } - private fun getUndoCallback( - @StatusBarManager.MediaTransferSenderState displayState: Int - ): Runnable? { - return if (isSucceededState(displayState)) { - Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") } - } else { - null + // Process any optional arguments + args.subList(2, args.size).forEach { + when { + it == "useAppIcon=false" -> senderArgs.useAppIcon = false + it == "showUndo=false" -> senderArgs.showUndo = false + it.substring(0, 3) == "id=" -> senderArgs.id = it.substring(3) + } } + + return senderArgs } private fun isSucceededState( @@ -106,14 +120,31 @@ class MediaTttCommandLineHelper @Inject constructor( } override fun help(pw: PrintWriter) { - pw.println("Usage: adb shell cmd statusbar $SENDER_COMMAND " + - "<deviceName> <chipState> useAppIcon=[true|false] <id>") + pw.println( + "Usage: adb shell cmd statusbar $SENDER_COMMAND " + + "<deviceName> <chipState> " + + "useAppIcon=[true|false] id=<id> showUndo=[true|false]" + ) + pw.println("Note: useAppIcon, id, and showUndo are optional additional commands.") } } + private data class SenderArgs( + val deviceName: String, + val commandName: String, + var id: String = "id", + var useAppIcon: Boolean = true, + var showUndo: Boolean = true, + ) + /** All commands for the receiver device. */ inner class ReceiverCommand : Command { override fun execute(pw: PrintWriter, args: List<String>) { + if (args.isEmpty()) { + help(pw) + return + } + val commandName = args[0] @StatusBarManager.MediaTransferReceiverState val displayState: Int? diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 3fd1aa73c033..e2f55f0d81b8 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -145,7 +145,7 @@ public class NavigationBarController implements boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources()); boolean largeScreenChanged = mIsTablet != isOldConfigTablet; // TODO(b/243765256): Disable this logging once b/243765256 is fixed. - Log.d(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig + Log.i(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized() + " willApplyConfigToNavbars=" + willApplyConfig + " navBarCount=" + mNavigationBars.size()); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index cb0f3e26c6a7..d03ac3b419f6 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -958,7 +958,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } // TODO(b/243765256): Disable this logging once b/243765256 is fixed. - Log.d(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig + Log.i(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig + " lastReportedConfig=" + mLastReportedConfig); mLastReportedConfig.updateFrom(newConfig); updateDisplaySize(); diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index b964b76795b8..6dd60d043a06 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -17,10 +17,12 @@ package com.android.systemui.notetask import android.app.KeyguardManager +import android.content.ComponentName import android.content.Context +import android.content.pm.PackageManager import android.os.UserManager -import android.view.KeyEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.util.kotlin.getOrNull import com.android.wm.shell.bubbles.Bubbles import java.util.Optional @@ -45,15 +47,22 @@ constructor( @NoteTaskEnabledKey private val isEnabled: Boolean, ) { - fun handleSystemKey(keyCode: Int) { + /** + * Shows a note task. How the task is shown will depend on when the method is invoked. + * + * If in multi-window mode, notes will open as a full screen experience. That is particularly + * important for Large screen devices. These devices may support a taskbar that let users to + * drag and drop a shortcut into multi-window mode, and notes should comply with this behaviour. + * + * If the keyguard is locked, notes will open as a full screen experience. A locked device has + * no contextual information which let us use the whole screen space available. + * + * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience. + * That will let users open other apps in full screen, and take contextual notes. + */ + fun showNoteTask(isInMultiWindowMode: Boolean = false) { if (!isEnabled) return - if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { - showNoteTask() - } - } - - private fun showNoteTask() { val bubbles = optionalBubbles.getOrNull() ?: return val keyguardManager = optionalKeyguardManager.getOrNull() ?: return val userManager = optionalUserManager.getOrNull() ?: return @@ -62,11 +71,35 @@ constructor( // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. if (!userManager.isUserUnlocked) return - if (keyguardManager.isKeyguardLocked) { + if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) { context.startActivity(intent) } else { // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. bubbles.showAppBubble(intent) } } + + /** + * Set `android:enabled` property in the `AndroidManifest` associated with the Shortcut + * component to [value]. + * + * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the + * Widget Picker to all users. + */ + fun setNoteTaskShortcutEnabled(value: Boolean) { + val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) + + val enabledState = + if (value) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + + context.packageManager.setComponentEnabledSetting( + componentName, + enabledState, + PackageManager.DONT_KILL_APP, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index 0a5b6008981b..d14b7a766762 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -16,9 +16,10 @@ package com.android.systemui.notetask +import android.view.KeyEvent +import androidx.annotation.VisibleForTesting import com.android.systemui.statusbar.CommandQueue import com.android.wm.shell.bubbles.Bubbles -import dagger.Lazy import java.util.Optional import javax.inject.Inject @@ -27,15 +28,18 @@ internal class NoteTaskInitializer @Inject constructor( private val optionalBubbles: Optional<Bubbles>, - private val lazyNoteTaskController: Lazy<NoteTaskController>, + private val noteTaskController: NoteTaskController, private val commandQueue: CommandQueue, @NoteTaskEnabledKey private val isEnabled: Boolean, ) { - private val callbacks = + @VisibleForTesting + val callbacks = object : CommandQueue.Callbacks { override fun handleSystemKey(keyCode: Int) { - lazyNoteTaskController.get().handleSystemKey(keyCode) + if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { + noteTaskController.showNoteTask() + } } } @@ -43,5 +47,6 @@ constructor( if (isEnabled && optionalBubbles.isPresent) { commandQueue.addCallback(callbacks) } + noteTaskController.setNoteTaskShortcutEnabled(isEnabled) } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index 035396a6fc76..8bdf3195d53b 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -16,32 +16,47 @@ package com.android.systemui.notetask +import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.os.UserManager import androidx.core.content.getSystemService import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity +import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity +import dagger.Binds import dagger.Module import dagger.Provides -import java.util.* +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import java.util.Optional /** Compose all dependencies required by Note Task feature. */ @Module -internal class NoteTaskModule { +internal interface NoteTaskModule { - @[Provides NoteTaskEnabledKey] - fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean { - return featureFlags.isEnabled(Flags.NOTE_TASKS) - } + @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)] + fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity? - @Provides - fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> { - return Optional.ofNullable(context.getSystemService()) - } + @[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)] + fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity? + + companion object { + + @[Provides NoteTaskEnabledKey] + fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean { + return featureFlags.isEnabled(Flags.NOTE_TASKS) + } + + @Provides + fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> { + return Optional.ofNullable(context.getSystemService()) + } - @Provides - fun provideOptionalUserManager(context: Context): Optional<UserManager> { - return Optional.ofNullable(context.getSystemService()) + @Provides + fun provideOptionalUserManager(context: Context): Optional<UserManager> { + return Optional.ofNullable(context.getSystemService()) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt new file mode 100644 index 000000000000..f6a623e4f001 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask.shortcut + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.annotation.DrawableRes +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import com.android.systemui.R +import javax.inject.Inject + +/** + * Activity responsible for create a shortcut for notes action. If the shortcut is enabled, a new + * shortcut will appear in the widget picker. If the shortcut is selected, the Activity here will be + * launched, creating a new shortcut for [CreateNoteTaskShortcutActivity], and will finish. + * + * @see <a + * href="https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts#custom-pinned">Creating + * a custom shortcut activity</a> + */ +internal class CreateNoteTaskShortcutActivity @Inject constructor() : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val intent = + createShortcutIntent( + id = SHORTCUT_ID, + shortLabel = getString(R.string.note_task_button_label), + intent = LaunchNoteTaskActivity.newIntent(context = this), + iconResource = R.drawable.ic_note_task_button, + ) + setResult(Activity.RESULT_OK, intent) + + finish() + } + + private fun createShortcutIntent( + id: String, + shortLabel: String, + intent: Intent, + @DrawableRes iconResource: Int, + ): Intent { + val shortcutInfo = + ShortcutInfoCompat.Builder(this, id) + .setIntent(intent) + .setShortLabel(shortLabel) + .setLongLived(true) + .setIcon(IconCompat.createWithResource(this, iconResource)) + .build() + + return ShortcutManagerCompat.createShortcutResultIntent( + this, + shortcutInfo, + ) + } + + private companion object { + private const val SHORTCUT_ID = "note-task-shortcut-id" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt new file mode 100644 index 000000000000..47fe67638cd0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask.shortcut + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import com.android.systemui.notetask.NoteTaskController +import com.android.systemui.notetask.NoteTaskIntentResolver +import javax.inject.Inject + +/** Activity responsible for launching the note experience, and finish. */ +internal class LaunchNoteTaskActivity +@Inject +constructor( + private val noteTaskController: NoteTaskController, +) : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + noteTaskController.showNoteTask(isInMultiWindowMode) + + finish() + } + + companion object { + + /** Creates a new [Intent] set to start [LaunchNoteTaskActivity]. */ + fun newIntent(context: Context): Intent { + return Intent(context, LaunchNoteTaskActivity::class.java).apply { + // Intent's action must be set in shortcuts, or an exception will be thrown. + // TODO(b/254606432): Use Intent.ACTION_NOTES instead. + action = NoteTaskIntentResolver.NOTES_ACTION + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index 67bc76998597..5dbf0f8dcdce 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -247,7 +247,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { Icon icon; ContentDescription contentDescription = null; - if (isParentalControlsEnabled) { + if (isParentalControlsEnabled && securityModel.getDeviceAdminIcon() != null) { icon = new Icon.Loaded(securityModel.getDeviceAdminIcon(), contentDescription); } else if (vpnName != null || vpnNameWorkProfile != null) { if (securityModel.isVpnBranded()) { @@ -476,7 +476,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { @VisibleForTesting View createDialogView(Context quickSettingsContext) { if (mSecurityController.isParentalControlsEnabled()) { - return createParentalControlsDialogView(); + return createParentalControlsDialogView(quickSettingsContext); } return createOrganizationDialogView(quickSettingsContext); } @@ -579,8 +579,8 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { return dialogView; } - private View createParentalControlsDialogView() { - View dialogView = LayoutInflater.from(mContext) + private View createParentalControlsDialogView(Context quickSettingsContext) { + View dialogView = LayoutInflater.from(quickSettingsContext) .inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false); DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index d7eddf53dea5..56c34a0b3665 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -39,6 +39,7 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; +import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; @@ -66,6 +67,8 @@ public class NotificationShelf extends ActivatableNotificationView implements // the next icon has translated out of the way, to avoid overlapping. private static final Interpolator ICON_ALPHA_INTERPOLATOR = new PathInterpolator(0.6f, 0f, 0.6f, 0f); + private static final SourceType BASE_VALUE = SourceType.from("BaseValue"); + private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll"); private NotificationIconContainer mShelfIcons; private int[] mTmp = new int[2]; @@ -112,19 +115,24 @@ public class NotificationShelf extends ActivatableNotificationView implements setClipChildren(false); setClipToPadding(false); mShelfIcons.setIsStaticLayout(false); - requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue); - requestTopRoundness(1f, false, SourceType.DefaultValue); + requestRoundness(/* top = */ 1f, /* bottom = */ 1f, BASE_VALUE, /* animate = */ false); - // Setting this to first in section to get the clipping to the top roundness correct. This - // value determines the way we are clipping to the top roundness of the overall shade - setFirstInSection(true); + if (!mUseRoundnessSourceTypes) { + // Setting this to first in section to get the clipping to the top roundness correct. + // This value determines the way we are clipping to the top roundness of the overall + // shade + setFirstInSection(true); + } updateResources(); } public void bind(AmbientState ambientState, - NotificationStackScrollLayoutController hostLayoutController) { + NotificationStackScrollLayoutController hostLayoutController) { mAmbientState = ambientState; mHostLayoutController = hostLayoutController; + hostLayoutController.setOnNotificationRemovedListener((child, isTransferInProgress) -> { + child.requestRoundnessReset(SHELF_SCROLL); + }); } private void updateResources() { @@ -185,9 +193,11 @@ public class NotificationShelf extends ActivatableNotificationView implements + " indexOfFirstViewInShelf=" + mIndexOfFirstViewInShelf + ')'; } - /** Update the state of the shelf. */ + /** + * Update the state of the shelf. + */ public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { ExpandableView lastView = ambientState.getLastVisibleBackgroundChild(); ShelfState viewState = (ShelfState) getViewState(); if (mShowNotificationShelf && lastView != null) { @@ -246,7 +256,7 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * @param fractionToShade Fraction of lockscreen to shade transition - * @param shortestWidth Shortest width to use for lockscreen shelf + * @param shortestWidth Shortest width to use for lockscreen shelf */ @VisibleForTesting public void updateActualWidth(float fractionToShade, float shortestWidth) { @@ -281,9 +291,9 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * @param localX Click x from left of screen - * @param slop Margin of error within which we count x for valid click - * @param left Left of shelf, from left of screen - * @param right Right of shelf, from left of screen + * @param slop Margin of error within which we count x for valid click + * @param left Left of shelf, from left of screen + * @param right Right of shelf, from left of screen * @return Whether click x was in view */ @VisibleForTesting @@ -293,8 +303,8 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * @param localY Click y from top of shelf - * @param slop Margin of error within which we count y for valid click - * @param top Top of shelf + * @param slop Margin of error within which we count y for valid click + * @param top Top of shelf * @param bottom Height of shelf * @return Whether click y was in view */ @@ -306,7 +316,7 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * @param localX Click x * @param localY Click y - * @param slop Margin of error for valid click + * @param slop Margin of error for valid click * @return Whether this click was on the visible (non-clipped) part of the shelf */ @Override @@ -478,13 +488,15 @@ public class NotificationShelf extends ActivatableNotificationView implements } } - private void updateCornerRoundnessOnScroll(ActivatableNotificationView anv, float viewStart, + private void updateCornerRoundnessOnScroll( + ActivatableNotificationView anv, + float viewStart, float shelfStart) { final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard() && !mAmbientState.isShadeExpanded() && anv instanceof ExpandableNotificationRow - && ((ExpandableNotificationRow) anv).isHeadsUp(); + && anv.isHeadsUp(); final boolean isHunGoingToShade = mAmbientState.isShadeExpanded() && anv == mAmbientState.getTrackedHeadsUpRow(); @@ -506,41 +518,40 @@ public class NotificationShelf extends ActivatableNotificationView implements * mAmbientState.getExpansionFraction(); final float cornerAnimationTop = shelfStart - cornerAnimationDistance; - if (viewEnd >= cornerAnimationTop) { - // Round bottom corners within animation bounds - final float changeFraction = MathUtils.saturate( - (viewEnd - cornerAnimationTop) / cornerAnimationDistance); - anv.requestBottomRoundness( - /* value = */ anv.isLastInSection() ? 1f : changeFraction, - /* animate = */ false, - SourceType.OnScroll); - - } else if (viewEnd < cornerAnimationTop) { - // Fast scroll skips frames and leaves corners with unfinished rounding. - // Reset top and bottom corners outside of animation bounds. - anv.requestBottomRoundness( - /* value = */ anv.isLastInSection() ? 1f : 0f, - /* animate = */ false, - SourceType.OnScroll); + final SourceType sourceType; + if (mUseRoundnessSourceTypes) { + sourceType = SHELF_SCROLL; + } else { + sourceType = LegacySourceType.OnScroll; } - if (viewStart >= cornerAnimationTop) { + final float topValue; + if (!mUseRoundnessSourceTypes && anv.isFirstInSection()) { + topValue = 1f; + } else if (viewStart >= cornerAnimationTop) { // Round top corners within animation bounds - final float changeFraction = MathUtils.saturate( + topValue = MathUtils.saturate( (viewStart - cornerAnimationTop) / cornerAnimationDistance); - anv.requestTopRoundness( - /* value = */ anv.isFirstInSection() ? 1f : changeFraction, - /* animate = */ false, - SourceType.OnScroll); + } else { + // Fast scroll skips frames and leaves corners with unfinished rounding. + // Reset top and bottom corners outside of animation bounds. + topValue = 0f; + } + anv.requestTopRoundness(topValue, sourceType, /* animate = */ false); - } else if (viewStart < cornerAnimationTop) { + final float bottomValue; + if (!mUseRoundnessSourceTypes && anv.isLastInSection()) { + bottomValue = 1f; + } else if (viewEnd >= cornerAnimationTop) { + // Round bottom corners within animation bounds + bottomValue = MathUtils.saturate( + (viewEnd - cornerAnimationTop) / cornerAnimationDistance); + } else { // Fast scroll skips frames and leaves corners with unfinished rounding. // Reset top and bottom corners outside of animation bounds. - anv.requestTopRoundness( - /* value = */ anv.isFirstInSection() ? 1f : 0f, - /* animate = */ false, - SourceType.OnScroll); + bottomValue = 0f; } + anv.requestBottomRoundness(bottomValue, sourceType, /* animate = */ false); } /** @@ -626,10 +637,11 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * Update the clipping of this view. + * * @return the amount that our own top should be clipped */ private int updateNotificationClipHeight(ExpandableView view, - float notificationClipEnd, int childIndex) { + float notificationClipEnd, int childIndex) { float viewEnd = view.getTranslationY() + view.getActualHeight(); boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway()) && !mAmbientState.isDozingAndNotPulsing(view); @@ -657,7 +669,7 @@ public class NotificationShelf extends ActivatableNotificationView implements @Override public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, - int outlineTranslation) { + int outlineTranslation) { if (!mHasItemsInStableShelf) { shadowIntensity = 0.0f; } @@ -665,18 +677,24 @@ public class NotificationShelf extends ActivatableNotificationView implements } /** - * @param i Index of the view in the host layout. - * @param view The current ExpandableView. - * @param scrollingFast Whether we are scrolling fast. + * @param i Index of the view in the host layout. + * @param view The current ExpandableView. + * @param scrollingFast Whether we are scrolling fast. * @param expandingAnimated Whether we are expanding a notification. - * @param isLastChild Whether this is the last view. - * @param shelfClipStart The point at which notifications start getting clipped by the shelf. + * @param isLastChild Whether this is the last view. + * @param shelfClipStart The point at which notifications start getting clipped by the shelf. * @return The amount how much this notification is in the shelf. - * 0f is not in shelf. 1f is completely in shelf. + * 0f is not in shelf. 1f is completely in shelf. */ @VisibleForTesting - public float getAmountInShelf(int i, ExpandableView view, boolean scrollingFast, - boolean expandingAnimated, boolean isLastChild, float shelfClipStart) { + public float getAmountInShelf( + int i, + ExpandableView view, + boolean scrollingFast, + boolean expandingAnimated, + boolean isLastChild, + float shelfClipStart + ) { // Let's calculate how much the view is in the shelf float viewStart = view.getTranslationY(); @@ -755,8 +773,13 @@ public class NotificationShelf extends ActivatableNotificationView implements return start; } - private void updateIconPositioning(ExpandableView view, float iconTransitionAmount, - boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) { + private void updateIconPositioning( + ExpandableView view, + float iconTransitionAmount, + boolean scrollingFast, + boolean expandingAnimated, + boolean isLastChild + ) { StatusBarIconView icon = view.getShelfIcon(); NotificationIconContainer.IconState iconState = getIconState(icon); if (iconState == null) { @@ -817,7 +840,7 @@ public class NotificationShelf extends ActivatableNotificationView implements || row.showingPulsing() || row.getTranslationZ() > mAmbientState.getBaseZHeight(); - iconState.iconAppearAmount = iconState.hidden? 0f : transitionAmount; + iconState.iconAppearAmount = iconState.hidden ? 0f : transitionAmount; // Fade in icons at shelf start // This is important for conversation icons, which are badged and need x reset @@ -847,7 +870,7 @@ public class NotificationShelf extends ActivatableNotificationView implements } private float getFullyClosedTranslation() { - return - (getIntrinsicHeight() - mStatusBarHeight) / 2; + return -(getIntrinsicHeight() - mStatusBarHeight) / 2; } @Override @@ -904,7 +927,7 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * @return whether the shelf has any icons in it when a potential animation has finished, i.e - * if the current state would be applied right now + * if the current state would be applied right now */ public boolean hasItemsInStableShelf() { return mHasItemsInStableShelf; @@ -962,7 +985,7 @@ public class NotificationShelf extends ActivatableNotificationView implements @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { + int oldTop, int oldRight, int oldBottom) { updateRelativeOffset(); } @@ -981,12 +1004,11 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * This method resets the OnScroll roundness of a view to 0f - * + * <p> * Note: This should be the only class that handles roundness {@code SourceType.OnScroll} */ - public static void resetOnScrollRoundness(ExpandableView expandableView) { - expandableView.requestTopRoundness(0f, false, SourceType.OnScroll); - expandableView.requestBottomRoundness(0f, false, SourceType.OnScroll); + public static void resetLegacyOnScrollRoundness(ExpandableView expandableView) { + expandableView.requestRoundnessReset(LegacySourceType.OnScroll); } public class ShelfState extends ExpandableViewState { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java index 3b1fa1779c17..bb84c758d87d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar; import android.view.View; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope; @@ -42,14 +44,17 @@ public class NotificationShelfController { private AmbientState mAmbientState; @Inject - public NotificationShelfController(NotificationShelf notificationShelf, + public NotificationShelfController( + NotificationShelf notificationShelf, ActivatableNotificationViewController activatableNotificationViewController, KeyguardBypassController keyguardBypassController, - SysuiStatusBarStateController statusBarStateController) { + SysuiStatusBarStateController statusBarStateController, + FeatureFlags featureFlags) { mView = notificationShelf; mActivatableNotificationViewController = activatableNotificationViewController; mKeyguardBypassController = keyguardBypassController; mStatusBarStateController = statusBarStateController; + mView.useRoundnessSourceTypes(featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)); mOnAttachStateChangeListener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { @@ -88,7 +93,7 @@ public class NotificationShelfController { public @View.Visibility int getVisibility() { return mView.getVisibility(); - }; + } public void setCollapsedIcons(NotificationIconContainer notificationIcons) { mView.setCollapsedIcons(notificationIcons); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index 97a47b5d1407..362764d1f89e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -78,6 +78,7 @@ import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DataSaverControllerImpl; @@ -193,6 +194,7 @@ public class NetworkControllerImpl extends BroadcastReceiver private final Executor mBgExecutor; // Handler that all callbacks are made on. private final CallbackHandler mCallbackHandler; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; private int mEmergencySource; private boolean mIsEmergency; @@ -243,6 +245,7 @@ public class NetworkControllerImpl extends BroadcastReceiver TelephonyListenerManager telephonyListenerManager, @Nullable WifiManager wifiManager, AccessPointControllerImpl accessPointController, + StatusBarPipelineFlags statusBarPipelineFlags, DemoModeController demoModeController, CarrierConfigTracker carrierConfigTracker, WifiStatusTrackerFactory trackerFactory, @@ -261,6 +264,7 @@ public class NetworkControllerImpl extends BroadcastReceiver bgExecutor, callbackHandler, accessPointController, + statusBarPipelineFlags, new DataUsageController(context), new SubscriptionDefaults(), deviceProvisionedController, @@ -288,6 +292,7 @@ public class NetworkControllerImpl extends BroadcastReceiver Executor bgExecutor, CallbackHandler callbackHandler, AccessPointControllerImpl accessPointController, + StatusBarPipelineFlags statusBarPipelineFlags, DataUsageController dataUsageController, SubscriptionDefaults defaultsHandler, DeviceProvisionedController deviceProvisionedController, @@ -309,6 +314,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mBgLooper = bgLooper; mBgExecutor = bgExecutor; mCallbackHandler = callbackHandler; + mStatusBarPipelineFlags = statusBarPipelineFlags; mDataSaverController = new DataSaverControllerImpl(context); mBroadcastDispatcher = broadcastDispatcher; mMobileFactory = mobileFactory; @@ -1334,7 +1340,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mWifiSignalController.notifyListeners(); } String sims = args.getString("sims"); - if (sims != null) { + if (sims != null && !mStatusBarPipelineFlags.useNewMobileIcons()) { int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8); List<SubscriptionInfo> subs = new ArrayList<>(); if (num != mMobileSignalControllers.size()) { @@ -1357,7 +1363,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mCallbackHandler.setNoSims(mHasNoSubs, mSimDetected); } String mobile = args.getString("mobile"); - if (mobile != null) { + if (mobile != null && !mStatusBarPipelineFlags.useNewMobileIcons()) { boolean show = mobile.equals("show"); String datatype = args.getString("datatype"); String slotString = args.getString("slot"); @@ -1442,7 +1448,7 @@ public class NetworkControllerImpl extends BroadcastReceiver controller.notifyListeners(); } String carrierNetworkChange = args.getString("carriernetworkchange"); - if (carrierNetworkChange != null) { + if (carrierNetworkChange != null && !mStatusBarPipelineFlags.useNewMobileIcons()) { boolean show = carrierNetworkChange.equals("show"); for (int i = 0; i < mMobileSignalControllers.size(); i++) { MobileSignalController controller = mMobileSignalControllers.valueAt(i); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt index ed7f648081c8..0eb00008e289 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt @@ -74,8 +74,8 @@ interface Roundable { @JvmDefault fun requestTopRoundness( @FloatRange(from = 0.0, to = 1.0) value: Float, - animate: Boolean, sourceType: SourceType, + animate: Boolean, ): Boolean { val roundnessMap = roundableState.topRoundnessMap val lastValue = roundnessMap.values.maxOrNull() ?: 0f @@ -105,6 +105,30 @@ interface Roundable { } /** + * Request the top roundness [value] for a specific [sourceType]. Animate the roundness if the + * view is shown. + * + * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more + * origins require different roundness, for the same property, the maximum value will always be + * chosen. + * + * @param value a value between 0f and 1f. + * @param sourceType the source from which the request for roundness comes. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestTopRoundness( + @FloatRange(from = 0.0, to = 1.0) value: Float, + sourceType: SourceType, + ): Boolean { + return requestTopRoundness( + value = value, + sourceType = sourceType, + animate = roundableState.targetView.isShown + ) + } + + /** * Request the bottom roundness [value] for a specific [sourceType]. * * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more @@ -119,8 +143,8 @@ interface Roundable { @JvmDefault fun requestBottomRoundness( @FloatRange(from = 0.0, to = 1.0) value: Float, - animate: Boolean, sourceType: SourceType, + animate: Boolean, ): Boolean { val roundnessMap = roundableState.bottomRoundnessMap val lastValue = roundnessMap.values.maxOrNull() ?: 0f @@ -149,9 +173,101 @@ interface Roundable { return false } + /** + * Request the bottom roundness [value] for a specific [sourceType]. Animate the roundness if + * the view is shown. + * + * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more + * origins require different roundness, for the same property, the maximum value will always be + * chosen. + * + * @param value value between 0f and 1f. + * @param sourceType the source from which the request for roundness comes. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestBottomRoundness( + @FloatRange(from = 0.0, to = 1.0) value: Float, + sourceType: SourceType, + ): Boolean { + return requestBottomRoundness( + value = value, + sourceType = sourceType, + animate = roundableState.targetView.isShown + ) + } + + /** + * Request the roundness [value] for a specific [sourceType]. + * + * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case + * more origins require different roundness, for the same property, the maximum value will + * always be chosen. + * + * @param top top value between 0f and 1f. + * @param bottom bottom value between 0f and 1f. + * @param sourceType the source from which the request for roundness comes. + * @param animate true if it should animate to that value. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestRoundness( + @FloatRange(from = 0.0, to = 1.0) top: Float, + @FloatRange(from = 0.0, to = 1.0) bottom: Float, + sourceType: SourceType, + animate: Boolean, + ): Boolean { + val hasTopChanged = + requestTopRoundness(value = top, sourceType = sourceType, animate = animate) + val hasBottomChanged = + requestBottomRoundness(value = bottom, sourceType = sourceType, animate = animate) + return hasTopChanged || hasBottomChanged + } + + /** + * Request the roundness [value] for a specific [sourceType]. Animate the roundness if the view + * is shown. + * + * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case + * more origins require different roundness, for the same property, the maximum value will + * always be chosen. + * + * @param top top value between 0f and 1f. + * @param bottom bottom value between 0f and 1f. + * @param sourceType the source from which the request for roundness comes. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestRoundness( + @FloatRange(from = 0.0, to = 1.0) top: Float, + @FloatRange(from = 0.0, to = 1.0) bottom: Float, + sourceType: SourceType, + ): Boolean { + return requestRoundness( + top = top, + bottom = bottom, + sourceType = sourceType, + animate = roundableState.targetView.isShown, + ) + } + + /** + * Request the roundness 0f for a [SourceType]. Animate the roundness if the view is shown. + * + * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case + * more origins require different roundness, for the same property, the maximum value will + * always be chosen. + * + * @param sourceType the source from which the request for roundness comes. + */ + @JvmDefault + fun requestRoundnessReset(sourceType: SourceType) { + requestRoundness(top = 0f, bottom = 0f, sourceType = sourceType) + } + /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */ @JvmDefault - fun applyRoundness() { + fun applyRoundnessAndInvalidate() { roundableState.targetView.invalidate() } @@ -227,7 +343,7 @@ class RoundableState( /** Set the current top roundness */ internal fun setTopRoundness( value: Float, - animated: Boolean = targetView.isShown, + animated: Boolean, ) { PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated) } @@ -235,11 +351,19 @@ class RoundableState( /** Set the current bottom roundness */ internal fun setBottomRoundness( value: Float, - animated: Boolean = targetView.isShown, + animated: Boolean, ) { PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated) } + fun debugString() = buildString { + append("TargetView: ${targetView.hashCode()} ") + append("Top: $topRoundness ") + append(topRoundnessMap.map { "${it.key} ${it.value}" }) + append(" Bottom: $bottomRoundness ") + append(bottomRoundnessMap.map { "${it.key} ${it.value}" }) + } + companion object { private val DURATION: AnimationProperties = AnimationProperties() @@ -252,7 +376,7 @@ class RoundableState( override fun setValue(view: View, value: Float) { roundable.roundableState.topRoundness = value - roundable.applyRoundness() + roundable.applyRoundnessAndInvalidate() } }, R.id.top_roundess_animator_tag, @@ -267,7 +391,7 @@ class RoundableState( override fun setValue(view: View, value: Float) { roundable.roundableState.bottomRoundness = value - roundable.applyRoundness() + roundable.applyRoundnessAndInvalidate() } }, R.id.bottom_roundess_animator_tag, @@ -277,7 +401,31 @@ class RoundableState( } } -enum class SourceType { +/** + * Interface used to define the owner of a roundness. Usually the [SourceType] is defined as a + * private property of a class. + */ +interface SourceType { + companion object { + /** + * This is the most convenient way to define a new [SourceType]. + * + * For example: + * + * ```kotlin + * private val SECTION = SourceType.from("Section") + * ``` + */ + @JvmStatic + fun from(name: String) = + object : SourceType { + override fun toString() = name + } + } +} + +@Deprecated("Use SourceType.from() instead", ReplaceWith("SourceType.from()")) +enum class LegacySourceType : SourceType { DefaultValue, OnDismissAnimation, OnScroll, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index d29298a2f637..fbe88dff07f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -39,9 +39,13 @@ import com.android.systemui.animation.Interpolators; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import java.util.HashSet; +import java.util.Set; + /** * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf} * to implement dimming/activating on Keyguard for the double-tap gesture @@ -91,6 +95,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView = new PathInterpolator(0.6f, 0, 0.5f, 1); private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR = new PathInterpolator(0, 0, 0.5f, 1); + private final Set<SourceType> mOnDetachResetRoundness = new HashSet<>(); private int mTintedRippleColor; private int mNormalRippleColor; private Gefingerpoken mTouchHandler; @@ -134,6 +139,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private boolean mDismissed; private boolean mRefocusOnDismiss; private AccessibilityManager mAccessibilityManager; + protected boolean mUseRoundnessSourceTypes; public ActivatableNotificationView(Context context, AttributeSet attrs) { super(context, attrs); @@ -613,9 +619,9 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView protected void resetAllContentAlphas() {} @Override - public void applyRoundness() { - super.applyRoundness(); + public void applyRoundnessAndInvalidate() { applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius()); + super.applyRoundnessAndInvalidate(); } @Override @@ -775,6 +781,33 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mAccessibilityManager = accessibilityManager; } + /** + * Enable the support for rounded corner based on the SourceType + * @param enabled true if is supported + */ + public void useRoundnessSourceTypes(boolean enabled) { + mUseRoundnessSourceTypes = enabled; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mUseRoundnessSourceTypes && !mOnDetachResetRoundness.isEmpty()) { + for (SourceType sourceType : mOnDetachResetRoundness) { + requestRoundnessReset(sourceType); + } + mOnDetachResetRoundness.clear(); + } + } + + /** + * SourceType which should be reset when this View is detached + * @param sourceType will be reset on View detached + */ + public void addOnDetachResetRoundness(SourceType sourceType) { + mOnDetachResetRoundness.add(sourceType); + } + public interface OnActivatedListener { void onActivated(ActivatableNotificationView view); void onActivationReset(ActivatableNotificationView view); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index d7d5ac961249..c7c1634ea105 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -91,6 +91,7 @@ import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; +import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; @@ -143,6 +144,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private static final int MENU_VIEW_INDEX = 0; public static final float DEFAULT_HEADER_VISIBLE_AMOUNT = 1.0f; private static final long RECENTLY_ALERTED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); + private static final SourceType BASE_VALUE = SourceType.from("BaseValue"); + private static final SourceType FROM_PARENT = SourceType.from("FromParent(ENR)"); + private static final SourceType PINNED = SourceType.from("Pinned"); // We don't correctly track dark mode until the content views are inflated, so always update // the background on first content update just in case it happens to be during a theme change. @@ -150,6 +154,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mNotificationTranslationFinished = false; private boolean mIsSnoozed; private boolean mIsFaded; + private boolean mAnimatePinnedRoundness = false; /** * Listener for when {@link ExpandableNotificationRow} is laid out. @@ -376,7 +381,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private float mTopRoundnessDuringLaunchAnimation; private float mBottomRoundnessDuringLaunchAnimation; - private boolean mIsNotificationGroupCornerEnabled; + private float mSmallRoundness; /** * Returns whether the given {@code statusBarNotification} is a system notification. @@ -844,7 +849,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } onAttachedChildrenCountChanged(); row.setIsChildInGroup(false, null); - row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue); + if (!mUseRoundnessSourceTypes) { + row.requestBottomRoundness(0.0f, LegacySourceType.DefaultValue, /* animate = */ false); + } } /** @@ -860,7 +867,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (child.keepInParentForDismissAnimation()) { mChildrenContainer.removeNotification(child); child.setIsChildInGroup(false, null); - child.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue); + if (!mUseRoundnessSourceTypes) { + LegacySourceType sourceType = LegacySourceType.DefaultValue; + child.requestBottomRoundness(0f, sourceType, /* animate = */ false); + } child.setKeepInParentForDismissAnimation(false); logKeepInParentChildDetached(child); childCountChanged = true; @@ -915,6 +925,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mNotificationParent.updateBackgroundForGroupState(); } updateBackgroundClipping(); + if (mUseRoundnessSourceTypes) { + updateBaseRoundness(); + } } @Override @@ -1033,6 +1046,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (isAboveShelf() != wasAboveShelf) { mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); } + if (mUseRoundnessSourceTypes) { + if (pinned) { + // Should be animated if someone explicitly set it to 0 and the row is shown. + boolean animated = mAnimatePinnedRoundness && isShown(); + requestRoundness(/* top = */ 1f, /* bottom = */ 1f, PINNED, animated); + } else { + requestRoundnessReset(PINNED); + mAnimatePinnedRoundness = true; + } + } } @Override @@ -1607,6 +1630,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView super(context, attrs); mImageResolver = new NotificationInlineImageResolver(context, new NotificationInlineImageCache()); + float radius = getResources().getDimension(R.dimen.notification_corner_radius_small); + mSmallRoundness = radius / getMaxRadius(); initDimens(); } @@ -1839,7 +1864,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setIsLowPriority(mIsLowPriority); mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); mChildrenContainer.onNotificationUpdated(); - mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled); + mChildrenContainer.useRoundnessSourceTypes(mUseRoundnessSourceTypes); mTranslateableViews.add(mChildrenContainer); }); @@ -2271,7 +2296,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public float getTopRoundness() { - if (mExpandAnimationRunning) { + if (!mUseRoundnessSourceTypes && mExpandAnimationRunning) { return mTopRoundnessDuringLaunchAnimation; } @@ -2280,7 +2305,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public float getBottomRoundness() { - if (mExpandAnimationRunning) { + if (!mUseRoundnessSourceTypes && mExpandAnimationRunning) { return mBottomRoundnessDuringLaunchAnimation; } @@ -3436,17 +3461,24 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } @Override - public void applyRoundness() { - super.applyRoundness(); + public void applyRoundnessAndInvalidate() { applyChildrenRoundness(); + super.applyRoundnessAndInvalidate(); } private void applyChildrenRoundness() { if (mIsSummaryWithChildren) { - mChildrenContainer.requestBottomRoundness( - getBottomRoundness(), - /* animate = */ false, - SourceType.DefaultValue); + if (mUseRoundnessSourceTypes) { + mChildrenContainer.requestRoundness( + /* top = */ getTopRoundness(), + /* bottom = */ getBottomRoundness(), + FROM_PARENT); + } else { + mChildrenContainer.requestBottomRoundness( + getBottomRoundness(), + LegacySourceType.DefaultValue, + /* animate = */ false); + } } } @@ -3605,6 +3637,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } else { pw.println("no viewState!!!"); } + pw.println("Roundness: " + getRoundableState().debugString()); if (mIsSummaryWithChildren) { pw.println(); @@ -3649,14 +3682,38 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mTargetPoint; } + /** Update the minimum roundness based on current state */ + private void updateBaseRoundness() { + if (isChildInGroup()) { + requestRoundnessReset(BASE_VALUE); + } else { + requestRoundness(mSmallRoundness, mSmallRoundness, BASE_VALUE); + } + } + /** - * Enable the support for rounded corner in notification group + * Enable the support for rounded corner based on the SourceType * @param enabled true if is supported */ - public void enableNotificationGroupCorner(boolean enabled) { - mIsNotificationGroupCornerEnabled = enabled; + @Override + public void useRoundnessSourceTypes(boolean enabled) { + super.useRoundnessSourceTypes(enabled); if (mChildrenContainer != null) { - mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled); + mChildrenContainer.useRoundnessSourceTypes(mUseRoundnessSourceTypes); + } + } + + @Override + public String toString() { + String roundableStateDebug = "RoundableState = " + getRoundableState().debugString(); + return "ExpandableNotificationRow:" + hashCode() + " { " + roundableStateDebug + " }"; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mUseRoundnessSourceTypes) { + updateBaseRoundness(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 8a400d5fef99..d1138608805b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -255,8 +255,8 @@ public class ExpandableNotificationRowController implements NotifViewController mStatusBarStateController.removeCallback(mStatusBarStateListener); } }); - mView.enableNotificationGroupCorner( - mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)); + mView.useRoundnessSourceTypes( + mFeatureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)); } private final StatusBarStateController.StateListener mStatusBarStateListener = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java index 232462714f7d..0213b969551e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java @@ -219,9 +219,9 @@ public abstract class ExpandableOutlineView extends ExpandableView { } @Override - public void applyRoundness() { + public void applyRoundnessAndInvalidate() { invalidateOutline(); - super.applyRoundness(); + super.applyRoundnessAndInvalidate(); } protected void setBackgroundTop(int backgroundTop) { @@ -233,7 +233,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { public void onDensityOrFontScaleChanged() { initDimens(); - applyRoundness(); + applyRoundnessAndInvalidate(); } @Override @@ -241,7 +241,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { int previousHeight = getActualHeight(); super.setActualHeight(actualHeight, notifyListeners); if (previousHeight != actualHeight) { - applyRoundness(); + applyRoundnessAndInvalidate(); } } @@ -250,7 +250,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { int previousAmount = getClipTopAmount(); super.setClipTopAmount(clipTopAmount); if (previousAmount != clipTopAmount) { - applyRoundness(); + applyRoundnessAndInvalidate(); } } @@ -259,14 +259,14 @@ public abstract class ExpandableOutlineView extends ExpandableView { int previousAmount = getClipBottomAmount(); super.setClipBottomAmount(clipBottomAmount); if (previousAmount != clipBottomAmount) { - applyRoundness(); + applyRoundnessAndInvalidate(); } } protected void setOutlineAlpha(float alpha) { if (alpha != mOutlineAlpha) { mOutlineAlpha = alpha; - applyRoundness(); + applyRoundnessAndInvalidate(); } } @@ -280,7 +280,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { setOutlineRect(rect.left, rect.top, rect.right, rect.bottom); } else { mCustomOutline = false; - applyRoundness(); + applyRoundnessAndInvalidate(); } } @@ -340,7 +340,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { // Outlines need to be at least 1 dp mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom); mOutlineRect.right = (int) Math.max(left, mOutlineRect.right); - applyRoundness(); + applyRoundnessAndInvalidate(); } public Path getCustomClipPath(View child) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index f13e48d55ae4..1f664cb16179 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -71,6 +71,8 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple private View mFeedbackIcon; private boolean mIsLowPriority; private boolean mTransformLowPriorityTitle; + private boolean mUseRoundnessSourceTypes; + private RoundnessChangedListener mRoundnessChangedListener; protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { super(ctx, view, row); @@ -117,6 +119,20 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple return mRoundableState; } + @Override + public void applyRoundnessAndInvalidate() { + if (mUseRoundnessSourceTypes && mRoundnessChangedListener != null) { + // We cannot apply the rounded corner to this View, so our parents (in drawChild()) will + // clip our canvas. So we should invalidate our parent. + mRoundnessChangedListener.applyRoundnessAndInvalidate(); + } + Roundable.super.applyRoundnessAndInvalidate(); + } + + public void setOnRoundnessChangedListener(RoundnessChangedListener listener) { + mRoundnessChangedListener = listener; + } + protected void resolveHeaderViews() { mIcon = mView.findViewById(com.android.internal.R.id.icon); mHeaderText = mView.findViewById(com.android.internal.R.id.header_text); @@ -343,4 +359,23 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple } } } + + /** + * Enable the support for rounded corner based on the SourceType + * + * @param enabled true if is supported + */ + public void useRoundnessSourceTypes(boolean enabled) { + mUseRoundnessSourceTypes = enabled; + } + + /** + * Interface that handle the Roundness changes + */ + public interface RoundnessChangedListener { + /** + * This method will be called when this class call applyRoundnessAndInvalidate() + */ + void applyRoundnessAndInvalidate(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index d43ca823089f..4a8e2dbb5334 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -47,6 +47,7 @@ import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.NotificationGroupingUtil; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.FeedbackIcon; +import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.Roundable; @@ -83,6 +84,7 @@ public class NotificationChildrenContainer extends ViewGroup return mAnimationFilter; } }.setDuration(200); + private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)"); private final List<View> mDividers = new ArrayList<>(); private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>(); @@ -131,7 +133,7 @@ public class NotificationChildrenContainer extends ViewGroup private int mUntruncatedChildCount; private boolean mContainingNotificationIsFaded = false; private RoundableState mRoundableState; - private boolean mIsNotificationGroupCornerEnabled; + private boolean mUseRoundnessSourceTypes; public NotificationChildrenContainer(Context context) { this(context, null); @@ -313,9 +315,12 @@ public class NotificationChildrenContainer extends ViewGroup row.setContentTransformationAmount(0, false /* isLastChild */); row.setNotificationFaded(mContainingNotificationIsFaded); - // This is a workaround, the NotificationShelf should be the owner of `OnScroll` roundness. - // Here we should reset the `OnScroll` roundness only on top-level rows. - NotificationShelf.resetOnScrollRoundness(row); + if (!mUseRoundnessSourceTypes) { + // This is a workaround, the NotificationShelf should be the owner of `OnScroll` + // roundness. + // Here we should reset the `OnScroll` roundness only on top-level rows. + NotificationShelf.resetLegacyOnScrollRoundness(row); + } // It doesn't make sense to keep old animations around, lets cancel them! ExpandableViewState viewState = row.getViewState(); @@ -323,6 +328,10 @@ public class NotificationChildrenContainer extends ViewGroup viewState.cancelAnimations(row); row.cancelAppearDrawing(); } + + if (mUseRoundnessSourceTypes) { + applyRoundnessAndInvalidate(); + } } private void ensureRemovedFromTransientContainer(View v) { @@ -356,6 +365,11 @@ public class NotificationChildrenContainer extends ViewGroup if (!row.isRemoved()) { mGroupingUtil.restoreChildNotification(row); } + + if (mUseRoundnessSourceTypes) { + row.requestRoundnessReset(FROM_PARENT); + applyRoundnessAndInvalidate(); + } } /** @@ -382,6 +396,10 @@ public class NotificationChildrenContainer extends ViewGroup getContext(), mNotificationHeader, mContainingNotification); + mNotificationHeaderWrapper.useRoundnessSourceTypes(mUseRoundnessSourceTypes); + if (mUseRoundnessSourceTypes) { + mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + } addView(mNotificationHeader, 0); invalidate(); } else { @@ -419,6 +437,12 @@ public class NotificationChildrenContainer extends ViewGroup getContext(), mNotificationHeaderLowPriority, mContainingNotification); + mNotificationHeaderWrapperLowPriority.useRoundnessSourceTypes( + mUseRoundnessSourceTypes + ); + if (mUseRoundnessSourceTypes) { + mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + } addView(mNotificationHeaderLowPriority, 0); invalidate(); } else { @@ -841,7 +865,7 @@ public class NotificationChildrenContainer extends ViewGroup isCanvasChanged = true; canvas.save(); - if (mIsNotificationGroupCornerEnabled && translation != 0f) { + if (mUseRoundnessSourceTypes && translation != 0f) { clipPath.offset(translation, 0f); canvas.clipPath(clipPath); clipPath.offset(-translation, 0f); @@ -1392,24 +1416,28 @@ public class NotificationChildrenContainer extends ViewGroup } @Override - public void applyRoundness() { - Roundable.super.applyRoundness(); + public void applyRoundnessAndInvalidate() { boolean last = true; for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } - child.requestTopRoundness( - /* value = */ 0f, - /* animate = */ isShown(), - SourceType.DefaultValue); - child.requestBottomRoundness( - /* value = */ last ? getBottomRoundness() : 0f, - /* animate = */ isShown(), - SourceType.DefaultValue); + if (mUseRoundnessSourceTypes) { + child.requestRoundness( + /* top = */ 0f, + /* bottom = */ last ? getBottomRoundness() : 0f, + FROM_PARENT); + } else { + child.requestRoundness( + /* top = */ 0f, + /* bottom = */ last ? getBottomRoundness() : 0f, + LegacySourceType.DefaultValue, + /* animate = */ isShown()); + } last = false; } + Roundable.super.applyRoundnessAndInvalidate(); } public void setHeaderVisibleAmount(float headerVisibleAmount) { @@ -1467,10 +1495,17 @@ public class NotificationChildrenContainer extends ViewGroup } /** - * Enable the support for rounded corner in notification group + * Enable the support for rounded corner based on the SourceType + * * @param enabled true if is supported */ - public void enableNotificationGroupCorner(boolean enabled) { - mIsNotificationGroupCornerEnabled = enabled; + public void useRoundnessSourceTypes(boolean enabled) { + mUseRoundnessSourceTypes = enabled; + } + + @Override + public String toString() { + String roundableStateDebug = "RoundableState = " + getRoundableState().debugString(); + return "NotificationChildrenContainer:" + hashCode() + " { " + roundableStateDebug + " }"; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java index 6810055ad3bf..fde8c4d453ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java @@ -25,6 +25,9 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; import com.android.systemui.statusbar.notification.Roundable; import com.android.systemui.statusbar.notification.SourceType; @@ -45,6 +48,7 @@ import javax.inject.Inject; public class NotificationRoundnessManager implements Dumpable { private static final String TAG = "NotificationRoundnessManager"; + private static final SourceType DISMISS_ANIMATION = SourceType.from("DismissAnimation"); private final ExpandableView[] mFirstInSectionViews; private final ExpandableView[] mLastInSectionViews; @@ -63,12 +67,14 @@ public class NotificationRoundnessManager implements Dumpable { private ExpandableView mSwipedView = null; private Roundable mViewBeforeSwipedView = null; private Roundable mViewAfterSwipedView = null; + private boolean mUseRoundnessSourceTypes; @Inject NotificationRoundnessManager( NotificationSectionsFeatureManager sectionsFeatureManager, NotificationRoundnessLogger notifLogger, - DumpManager dumpManager) { + DumpManager dumpManager, + FeatureFlags featureFlags) { int numberOfSections = sectionsFeatureManager.getNumberOfBuckets(); mFirstInSectionViews = new ExpandableView[numberOfSections]; mLastInSectionViews = new ExpandableView[numberOfSections]; @@ -76,6 +82,7 @@ public class NotificationRoundnessManager implements Dumpable { mTmpLastInSectionViews = new ExpandableView[numberOfSections]; mNotifLogger = notifLogger; mDumpManager = dumpManager; + mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); mDumpManager.registerDumpable(TAG, this); } @@ -94,6 +101,7 @@ public class NotificationRoundnessManager implements Dumpable { } public void updateView(ExpandableView view, boolean animate) { + if (mUseRoundnessSourceTypes) return; boolean changed = updateViewWithoutCallback(view, animate); if (changed) { mRoundingChangedCallback.run(); @@ -110,6 +118,7 @@ public class NotificationRoundnessManager implements Dumpable { boolean updateViewWithoutCallback( ExpandableView view, boolean animate) { + if (mUseRoundnessSourceTypes) return false; if (view == null || view == mViewBeforeSwipedView || view == mViewAfterSwipedView) { @@ -118,13 +127,13 @@ public class NotificationRoundnessManager implements Dumpable { final boolean isTopChanged = view.requestTopRoundness( getRoundnessDefaultValue(view, true /* top */), - animate, - SourceType.DefaultValue); + LegacySourceType.DefaultValue, + animate); final boolean isBottomChanged = view.requestBottomRoundness( getRoundnessDefaultValue(view, /* top = */ false), - animate, - SourceType.DefaultValue); + LegacySourceType.DefaultValue, + animate); final boolean isFirstInSection = isFirstInSection(view); final boolean isLastInSection = isLastInSection(view); @@ -139,6 +148,7 @@ public class NotificationRoundnessManager implements Dumpable { } private boolean isFirstInSection(ExpandableView view) { + if (mUseRoundnessSourceTypes) return false; for (int i = 0; i < mFirstInSectionViews.length; i++) { if (view == mFirstInSectionViews[i]) { return true; @@ -148,6 +158,7 @@ public class NotificationRoundnessManager implements Dumpable { } private boolean isLastInSection(ExpandableView view) { + if (mUseRoundnessSourceTypes) return false; for (int i = mLastInSectionViews.length - 1; i >= 0; i--) { if (view == mLastInSectionViews[i]) { return true; @@ -160,9 +171,6 @@ public class NotificationRoundnessManager implements Dumpable { Roundable viewBefore, ExpandableView viewSwiped, Roundable viewAfter) { - final boolean animate = true; - final SourceType source = SourceType.OnDismissAnimation; - // This method requires you to change the roundness of the current View targets and reset // the roundness of the old View targets (if any) to 0f. // To avoid conflicts, it generates a set of old Views and removes the current Views @@ -172,31 +180,34 @@ public class NotificationRoundnessManager implements Dumpable { if (mSwipedView != null) oldViews.add(mSwipedView); if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView); + final SourceType source; + if (mUseRoundnessSourceTypes) { + source = DISMISS_ANIMATION; + } else { + source = LegacySourceType.OnDismissAnimation; + } + mViewBeforeSwipedView = viewBefore; if (viewBefore != null) { oldViews.remove(viewBefore); - viewBefore.requestTopRoundness(0f, animate, source); - viewBefore.requestBottomRoundness(1f, animate, source); + viewBefore.requestRoundness(/* top = */ 0f, /* bottom = */ 1f, source); } mSwipedView = viewSwiped; if (viewSwiped != null) { oldViews.remove(viewSwiped); - viewSwiped.requestTopRoundness(1f, animate, source); - viewSwiped.requestBottomRoundness(1f, animate, source); + viewSwiped.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, source); } mViewAfterSwipedView = viewAfter; if (viewAfter != null) { oldViews.remove(viewAfter); - viewAfter.requestTopRoundness(1f, animate, source); - viewAfter.requestBottomRoundness(0f, animate, source); + viewAfter.requestRoundness(/* top = */ 1f, /* bottom = */ 0f, source); } // After setting the current Views, reset the views that are still present in the set. for (Roundable oldView : oldViews) { - oldView.requestTopRoundness(0f, animate, source); - oldView.requestBottomRoundness(0f, animate, source); + oldView.requestRoundnessReset(source); } } @@ -204,7 +215,23 @@ public class NotificationRoundnessManager implements Dumpable { mIsClearAllInProgress = isClearingAll; } + /** + * Check if "Clear all" notifications is in progress. + */ + public boolean isClearAllInProgress() { + return mIsClearAllInProgress; + } + + /** + * Check if we can request the `Pulsing` roundness for notification. + */ + public boolean shouldRoundNotificationPulsing() { + return mRoundForPulsingViews; + } + private float getRoundnessDefaultValue(Roundable view, boolean top) { + if (mUseRoundnessSourceTypes) return 0f; + if (view == null) { return 0f; } @@ -250,6 +277,7 @@ public class NotificationRoundnessManager implements Dumpable { } public void setExpanded(float expandedHeight, float appearFraction) { + if (mUseRoundnessSourceTypes) return; mExpanded = expandedHeight != 0.0f; mAppearFraction = appearFraction; if (mTrackedHeadsUp != null) { @@ -258,6 +286,7 @@ public class NotificationRoundnessManager implements Dumpable { } public void updateRoundedChildren(NotificationSection[] sections) { + if (mUseRoundnessSourceTypes) return; boolean anyChanged = false; for (int i = 0; i < sections.length; i++) { mTmpFirstInSectionViews[i] = mFirstInSectionViews[i]; @@ -280,6 +309,7 @@ public class NotificationRoundnessManager implements Dumpable { NotificationSection[] sections, ExpandableView[] oldViews, boolean first) { + if (mUseRoundnessSourceTypes) return false; boolean anyChanged = false; for (ExpandableView oldView : oldViews) { if (oldView != null) { @@ -313,6 +343,7 @@ public class NotificationRoundnessManager implements Dumpable { NotificationSection[] sections, ExpandableView[] oldViews, boolean first) { + if (mUseRoundnessSourceTypes) return false; boolean anyChanged = false; for (NotificationSection section : sections) { ExpandableView newView = @@ -339,6 +370,15 @@ public class NotificationRoundnessManager implements Dumpable { mAnimatedChildren = animatedChildren; } + /** + * Check if the view should be animated + * @param view target view + * @return true, if is in the AnimatedChildren set + */ + public boolean isAnimatedChild(ExpandableView view) { + return mAnimatedChildren.contains(view); + } + public void setOnRoundingChangedCallback(Runnable roundingChangedCallback) { mRoundingChangedCallback = roundingChangedCallback; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt index a1b77acb9a5e..070b4394291f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -19,8 +19,11 @@ import android.annotation.ColorInt import android.util.Log import android.view.View import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.media.controls.ui.KeyguardMediaController import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager +import com.android.systemui.statusbar.notification.SourceType import com.android.systemui.statusbar.notification.collection.render.MediaContainerController import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController import com.android.systemui.statusbar.notification.dagger.AlertingHeader @@ -44,12 +47,16 @@ class NotificationSectionsManager @Inject internal constructor( private val keyguardMediaController: KeyguardMediaController, private val sectionsFeatureManager: NotificationSectionsFeatureManager, private val mediaContainerController: MediaContainerController, + private val notificationRoundnessManager: NotificationRoundnessManager, @IncomingHeader private val incomingHeaderController: SectionHeaderController, @PeopleHeader private val peopleHeaderController: SectionHeaderController, @AlertingHeader private val alertingHeaderController: SectionHeaderController, - @SilentHeader private val silentHeaderController: SectionHeaderController + @SilentHeader private val silentHeaderController: SectionHeaderController, + featureFlags: FeatureFlags ) : SectionProvider { + private val useRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES) + private val configurationListener = object : ConfigurationController.ConfigurationListener { override fun onLocaleListChanged() { reinflateViews() @@ -177,11 +184,49 @@ class NotificationSectionsManager @Inject internal constructor( size = sections.size, operation = SectionBounds::addNotif ) + + // Build a set of the old first/last Views of the sections + val oldFirstChildren = sections.mapNotNull { it.firstVisibleChild }.toSet().toMutableSet() + val oldLastChildren = sections.mapNotNull { it.lastVisibleChild }.toSet().toMutableSet() + // Update each section with the associated boundary, tracking if there was a change val changed = sections.fold(false) { changed, section -> val bounds = sectionBounds[section.bucket] ?: SectionBounds.None - bounds.updateSection(section) || changed + val isSectionChanged = bounds.updateSection(section) + isSectionChanged || changed + } + + if (useRoundnessSourceTypes) { + val newFirstChildren = sections.mapNotNull { it.firstVisibleChild } + val newLastChildren = sections.mapNotNull { it.lastVisibleChild } + + // Update the roundness of Views that weren't already in the first/last position + newFirstChildren.forEach { firstChild -> + val wasFirstChild = oldFirstChildren.remove(firstChild) + if (!wasFirstChild) { + val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(firstChild) + val animated = firstChild.isShown && notAnimatedChild + firstChild.requestTopRoundness(1f, SECTION, animated) + } + } + newLastChildren.forEach { lastChild -> + val wasLastChild = oldLastChildren.remove(lastChild) + if (!wasLastChild) { + val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(lastChild) + val animated = lastChild.isShown && notAnimatedChild + lastChild.requestBottomRoundness(1f, SECTION, animated) + } + } + + // The Views left in the set are no longer in the first/last position + oldFirstChildren.forEach { noMoreFirstChild -> + noMoreFirstChild.requestTopRoundness(0f, SECTION) + } + oldLastChildren.forEach { noMoreLastChild -> + noMoreLastChild.requestBottomRoundness(0f, SECTION) + } } + if (DEBUG) { logSections(sections) } @@ -215,5 +260,6 @@ class NotificationSectionsManager @Inject internal constructor( companion object { private const val TAG = "NotifSectionsManager" private const val DEBUG = false + private val SECTION = SourceType.from("Section") } } 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 7c3e52cf9630..21e2bd877bae 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 @@ -191,10 +191,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final boolean mDebugLines; private Paint mDebugPaint; - /** Used to track the Y positions that were already used to draw debug text labels. */ + /** + * Used to track the Y positions that were already used to draw debug text labels. + */ private Set<Integer> mDebugTextUsedYPositions; private final boolean mDebugRemoveAnimation; private final boolean mSimplifiedAppearFraction; + private final boolean mUseRoundnessSourceTypes; private int mContentHeight; private float mIntrinsicContentHeight; @@ -355,15 +358,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final Rect mQsHeaderBound = new Rect(); private boolean mContinuousShadowUpdate; private boolean mContinuousBackgroundUpdate; - private final ViewTreeObserver.OnPreDrawListener mShadowUpdater - = () -> { - updateViewShadows(); - return true; - }; + private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> { + updateViewShadows(); + return true; + }; private final ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> { - updateBackground(); - return true; - }; + updateBackground(); + return true; + }; private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> { float endY = view.getTranslationY() + view.getActualHeight(); float otherEndY = otherView.getTranslationY() + otherView.getActualHeight(); @@ -419,7 +421,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ private int mMaxDisplayedNotifications = -1; private float mKeyguardBottomPadding = -1; - @VisibleForTesting int mStatusBarHeight; + @VisibleForTesting + int mStatusBarHeight; private int mMinInteractionHeight; private final Rect mClipRect = new Rect(); private boolean mIsClipped; @@ -568,6 +571,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Nullable private OnClickListener mManageButtonClickListener; + @Nullable + private OnNotificationRemovedListener mOnNotificationRemovedListener; public NotificationStackScrollLayout(Context context, AttributeSet attrs) { super(context, attrs, 0, 0); @@ -576,6 +581,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES); mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION); mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION); + mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); mSectionsManager = Dependency.get(NotificationSectionsManager.class); mScreenOffAnimationController = Dependency.get(ScreenOffAnimationController.class); @@ -741,7 +747,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr, - boolean expected, boolean actual) { + boolean expected, boolean actual) { if (mLogger == null) return; mLogger.hunSkippedForUnexpectedState(enr.getEntry(), expected, actual); } @@ -868,13 +874,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * Draws round rects for each background section. - * + * <p> * We want to draw a round rect for each background section as defined by {@link #mSections}. * However, if two sections are directly adjacent with no gap between them (e.g. on the * lockscreen where the shelf can appear directly below the high priority section, or while * scrolling the shade so that the top of the shelf is right at the bottom of the high priority * section), we don't want to round the adjacent corners. - * + * <p> * Since {@link Canvas} doesn't provide a way to draw a half-rounded rect, this means that we * need to coalesce the backgrounds for adjacent sections and draw them as a single round rect. * This method tracks the top of each rect we need to draw, then iterates through the visible @@ -883,7 +889,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * the current section. When we're done iterating we will always have one rect left to draw. */ private void drawBackgroundRects(Canvas canvas, int left, int right, int top, - int animationYOffset) { + int animationYOffset) { int backgroundRectTop = top; int lastSectionBottom = mSections[0].getCurrentBounds().bottom + animationYOffset; @@ -974,7 +980,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) void initView(Context context, NotificationSwipeHelper swipeHelper, - NotificationStackSizeCalculator notificationStackSizeCalculator) { + NotificationStackSizeCalculator notificationStackSizeCalculator) { mScroller = new OverScroller(getContext()); mSwipeHelper = swipeHelper; mNotificationStackSizeCalculator = notificationStackSizeCalculator; @@ -1304,18 +1310,19 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * @return Whether we should skip stack height updates. * True when - * 1) Unlock hint is running - * 2) Swiping up on lockscreen or flinging down after swipe up + * 1) Unlock hint is running + * 2) Swiping up on lockscreen or flinging down after swipe up */ private boolean shouldSkipHeightUpdate() { return mAmbientState.isOnKeyguard() && (mAmbientState.isUnlockHintRunning() - || mAmbientState.isSwipingUp() - || mAmbientState.isFlingingAfterSwipeUpOnLockscreen()); + || mAmbientState.isSwipingUp() + || mAmbientState.isFlingingAfterSwipeUpOnLockscreen()); } /** * Apply expansion fraction to the y position and height of the notifications panel. + * * @param listenerNeedsAnimation does the listener need to animate? */ private void updateStackPosition(boolean listenerNeedsAnimation) { @@ -1708,7 +1715,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ @ShadeViewRefactor(RefactorComponent.COORDINATOR) ExpandableView getChildAtPosition(float touchX, float touchY, - boolean requireMinHeight, boolean ignoreDecors) { + boolean requireMinHeight, boolean ignoreDecors) { // find the view under the pointer, accounting for GONE views final int count = getChildCount(); for (int childIdx = 0; childIdx < count; childIdx++) { @@ -1868,9 +1875,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void dismissViewAnimated( View child, Consumer<Boolean> endRunnable, int delay, long duration) { if (child instanceof SectionHeaderView) { - ((StackScrollerDecorView) child).setContentVisible( - false /* visible */, true /* animate */, endRunnable); - return; + ((StackScrollerDecorView) child).setContentVisible( + false /* visible */, true /* animate */, endRunnable); + return; } mSwipeHelper.dismissChild( child, @@ -2032,10 +2039,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * Scrolls by the given delta, overscrolling if needed. If called during a fling and the delta * would cause us to exceed the provided maximum overscroll, springs back instead. - * + * <p> * This method performs the determination of whether we're exceeding the overscroll and clamps * the scroll amount if so. The actual scrolling/overscrolling happens in * {@link #onCustomOverScrolled(int, boolean)} + * * @param deltaY The (signed) number of pixels to scroll. * @param scrollY The current scroll position (absolute scrolling only). * @param scrollRangeY The maximum allowable scroll position (absolute scrolling only). @@ -2098,7 +2106,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) public void setOverScrollAmount(float amount, boolean onTop, boolean animate, - boolean cancelAnimators) { + boolean cancelAnimators) { setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop)); } @@ -2114,7 +2122,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) public void setOverScrollAmount(float amount, boolean onTop, boolean animate, - boolean cancelAnimators, boolean isRubberbanded) { + boolean cancelAnimators, boolean isRubberbanded) { if (cancelAnimators) { mStateAnimator.cancelOverScrollAnimators(onTop); } @@ -2123,7 +2131,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate, - boolean isRubberbanded) { + boolean isRubberbanded) { amount = Math.max(0, amount); if (animate) { mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded); @@ -2179,7 +2187,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * Scrolls to the given position, overscrolling if needed. If called during a fling and the * position exceeds the provided maximum overscroll, springs back instead. * - * @param scrollY The target scroll position. + * @param scrollY The target scroll position. * @param clampedY Whether this value was clamped by the calling method, meaning we've reached * the overscroll limit. */ @@ -2288,8 +2296,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (row.isSummaryWithChildren() && row.areChildrenExpanded()) { List<ExpandableNotificationRow> notificationChildren = row.getAttachedChildren(); - for (int childIndex = 0; childIndex < notificationChildren.size(); - childIndex++) { + int childrenSize = notificationChildren.size(); + for (int childIndex = 0; childIndex < childrenSize; childIndex++) { ExpandableNotificationRow rowChild = notificationChildren.get(childIndex); if (rowChild.getTranslationY() + rowTranslation >= translationY) { return rowChild; @@ -2365,10 +2373,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * Calculate the gap height between two different views * - * @param previous the previousView - * @param current the currentView + * @param previous the previousView + * @param current the currentView * @param visibleIndex the visible index in the list - * * @return the gap height needed before the current view */ public float calculateGapHeight( @@ -2376,7 +2383,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable ExpandableView current, int visibleIndex ) { - return mStackScrollAlgorithm.getGapHeightForChild(mSectionsManager, visibleIndex, current, + return mStackScrollAlgorithm.getGapHeightForChild(mSectionsManager, visibleIndex, current, previous, mAmbientState.getFractionToShade(), mAmbientState.isOnKeyguard()); } @@ -2642,15 +2649,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return mScrolledToTopOnFirstDown && !mExpandedInThisMotion && (initialVelocity > mMinimumVelocity - || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0)); + || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0)); } /** * Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into * account. * - * @param qsHeight the top padding imposed by the quick settings panel - * @param animate whether to animate the change + * @param qsHeight the top padding imposed by the quick settings panel + * @param animate whether to animate the change */ @ShadeViewRefactor(RefactorComponent.COORDINATOR) public void updateTopPadding(float qsHeight, boolean animate) { @@ -2724,21 +2731,34 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void setChildTransferInProgress(boolean childTransferInProgress) { Assert.isMainThread(); mChildTransferInProgress = childTransferInProgress; } + /** + * Set the remove notification listener + * @param listener callback for notification removed + */ + public void setOnNotificationRemovedListener(OnNotificationRemovedListener listener) { + mOnNotificationRemovedListener = listener; + } + @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @Override public void onViewRemoved(View child) { super.onViewRemoved(child); // we only call our internal methods if this is actually a removal and not just a // notification which becomes a child notification + ExpandableView expandableView = (ExpandableView) child; if (!mChildTransferInProgress) { - onViewRemovedInternal((ExpandableView) child, this); + onViewRemovedInternal(expandableView, this); + } + if (mOnNotificationRemovedListener != null) { + mOnNotificationRemovedListener.onNotificationRemoved( + expandableView, + mChildTransferInProgress); } } @@ -2998,8 +3018,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mAnimateNextSectionBoundsChange = false; } mAmbientState.setLastVisibleBackgroundChild(lastChild); - // TODO: Refactor SectionManager and put the RoundnessManager there. - mController.getNotificationRoundnessManager().updateRoundedChildren(mSections); + if (!mUseRoundnessSourceTypes) { + // TODO: Refactor SectionManager and put the RoundnessManager there. + mController.getNotificationRoundnessManager().updateRoundedChildren(mSections); + } mAnimateBottomOnLayout = false; invalidate(); } @@ -3206,7 +3228,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // Only animate if we still have pinned heads up, otherwise we just have the // regular collapse animation of the lock screen || (mKeyguardBypassEnabled && onKeyguard() - && mInHeadsUpPinnedMode); + && mInHeadsUpPinnedMode); if (performDisappearAnimation && !isHeadsUp) { type = row.wasJustClicked() ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK @@ -3351,7 +3373,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable AnimationEvent animEvent = duration == null ? new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION) : new AnimationEvent( - child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration); + child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration); mAnimationEvents.add(animEvent); } mChildrenChangingPositions.clear(); @@ -3442,7 +3464,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.LAYOUT_ALGORITHM) protected StackScrollAlgorithm createStackScrollAlgorithm(Context context) { - return new StackScrollAlgorithm(context, this); + StackScrollAlgorithm stackScrollAlgorithm = new StackScrollAlgorithm(context, this); + stackScrollAlgorithm.useRoundnessSourceTypes(mUseRoundnessSourceTypes); + return stackScrollAlgorithm; } /** @@ -3772,7 +3796,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } private void logEmptySpaceClick(MotionEvent ev, boolean isTouchBelowLastNotification, - int statusBarState, boolean touchIsClick) { + int statusBarState, boolean touchIsClick) { if (mLogger == null) { return; } @@ -3957,7 +3981,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mOnEmptySpaceClickListener = listener; } - /** @hide */ + /** + * @hide + */ @Override @ShadeViewRefactor(RefactorComponent.INPUT) public boolean performAccessibilityActionInternal(int action, Bundle arguments) { @@ -4728,7 +4754,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return touchY > mTopPadding + mStackTranslation; } - /** @hide */ + /** + * @hide + */ @Override @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { @@ -5353,7 +5381,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return canChildBeCleared(row) && matchesSelection(row, selection); } - /** Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. */ + /** + * Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. + */ public void setManageButtonClickListener(@Nullable OnClickListener listener) { mManageButtonClickListener = listener; if (mFooterView != null) { @@ -5418,6 +5448,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * Set how far the wake up is when waking up from pulsing. This is a height and will adjust the * notification positions accordingly. + * * @param height the new wake up height * @return the overflow how much the height is further than he lowest notification */ @@ -5649,7 +5680,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * 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) { if (mRoundedRectClippingLeft == left && mRoundedRectClippingRight == right && mRoundedRectClippingBottom == bottom && mRoundedRectClippingTop == top && mBgCornerRadii[0] == topRadius && mBgCornerRadii[5] == bottomRadius) { @@ -5710,7 +5741,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mLaunchingNotification = launching; mLaunchingNotificationNeedsToBeClipped = mLaunchAnimationParams != null && (mLaunchAnimationParams.getStartRoundedTopClipping() > 0 - || mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0); + || mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0); if (!mLaunchingNotificationNeedsToBeClipped || !mLaunchingNotification) { mLaunchedNotificationClipPath.reset(); } @@ -5748,7 +5779,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mLaunchAnimationParams.getProgress(0, NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING)); int top = (int) Math.min(MathUtils.lerp(mRoundedRectClippingTop, - mLaunchAnimationParams.getTop(), expandProgress), + mLaunchAnimationParams.getTop(), expandProgress), mRoundedRectClippingTop); float topRadius = mLaunchAnimationParams.getTopCornerRadius(); float bottomRadius = mLaunchAnimationParams.getBottomCornerRadius(); @@ -5872,25 +5903,25 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public interface OnOverscrollTopChangedListener { - /** - * Notifies a listener that the overscroll has changed. - * - * @param amount the amount of overscroll, in pixels - * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an - * unrubberbanded motion to directly expand overscroll view (e.g - * expand - * QS) - */ - void onOverscrollTopChanged(float amount, boolean isRubberbanded); + /** + * Notifies a listener that the overscroll has changed. + * + * @param amount the amount of overscroll, in pixels + * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an + * unrubberbanded motion to directly expand overscroll view (e.g + * expand + * QS) + */ + void onOverscrollTopChanged(float amount, boolean isRubberbanded); - /** - * Notify a listener that the scroller wants to escape from the scrolling motion and - * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) - * - * @param velocity The velocity that the Scroller had when over flinging - * @param open Should the fling open or close the overscroll view. - */ - void flingTopOverscroll(float velocity, boolean open); + /** + * Notify a listener that the scroller wants to escape from the scrolling motion and + * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) + * + * @param velocity The velocity that the Scroller had when over flinging + * @param open Should the fling open or close the overscroll view. + */ + void flingTopOverscroll(float velocity, boolean open); } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @@ -6252,7 +6283,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } }; - public HeadsUpTouchHelper.Callback getHeadsUpCallback() { return mHeadsUpCallback; } + public HeadsUpTouchHelper.Callback getHeadsUpCallback() { + return mHeadsUpCallback; + } void onGroupExpandChanged(ExpandableNotificationRow changedRow, boolean expanded) { boolean animated = mAnimationsEnabled && (mIsExpanded || changedRow.isPinned()); @@ -6357,15 +6390,25 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return mLastSentExpandedHeight; } - /** Enum for selecting some or all notification rows (does not included non-notif views). */ + /** + * Enum for selecting some or all notification rows (does not included non-notif views). + */ @Retention(SOURCE) @IntDef({ROWS_ALL, ROWS_HIGH_PRIORITY, ROWS_GENTLE}) - @interface SelectedRows {} - /** All rows representing notifs. */ + @interface SelectedRows { + } + + /** + * All rows representing notifs. + */ public static final int ROWS_ALL = 0; - /** Only rows where entry.isHighPriority() is true. */ + /** + * Only rows where entry.isHighPriority() is true. + */ public static final int ROWS_HIGH_PRIORITY = 1; - /** Only rows where entry.isHighPriority() is false. */ + /** + * Only rows where entry.isHighPriority() is false. + */ public static final int ROWS_GENTLE = 2; interface ClearAllListener { @@ -6380,4 +6423,16 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable void onAnimationEnd( List<ExpandableNotificationRow> viewsToRemove, @SelectedRows int selectedRows); } + + /** + * + */ + public interface OnNotificationRemovedListener { + /** + * + * @param child + * @param isTransferInProgress + */ + void onNotificationRemoved(ExpandableView child, boolean isTransferInProgress); + } } 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 2d6d0a9cbeca..4bcc0b6923a1 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 @@ -182,10 +182,12 @@ public class NotificationStackScrollLayoutController { private NotificationStackScrollLayout mView; private boolean mFadeNotificationsOnDismiss; private NotificationSwipeHelper mSwipeHelper; - @Nullable private Boolean mHistoryEnabled; + @Nullable + private Boolean mHistoryEnabled; private int mBarState; private HeadsUpAppearanceController mHeadsUpAppearanceController; private final FeatureFlags mFeatureFlags; + private final boolean mUseRoundnessSourceTypes; private final NotificationTargetsHelper mNotificationTargetsHelper; private View mLongPressedView; @@ -391,7 +393,7 @@ public class NotificationStackScrollLayoutController { if (item != null) { Point origin = provider.getRevealAnimationOrigin(); mNotificationGutsManager.openGuts(row, origin.x, origin.y, item); - } else { + } else { Log.e(TAG, "Provider has shouldShowGutsOnSnapOpen, but provided no " + "menu item in menuItemtoExposeOnSnap. Skipping."); } @@ -420,7 +422,7 @@ public class NotificationStackScrollLayoutController { @Override public void onSnooze(StatusBarNotification sbn, - NotificationSwipeActionHelper.SnoozeOption snoozeOption) { + NotificationSwipeActionHelper.SnoozeOption snoozeOption) { mCentralSurfaces.setNotificationSnoozed(sbn, snoozeOption); } @@ -544,7 +546,7 @@ public class NotificationStackScrollLayoutController { @Override public boolean updateSwipeProgress(View animView, boolean dismissable, - float swipeProgress) { + float swipeProgress) { // Returning true prevents alpha fading. return !mFadeNotificationsOnDismiss; } @@ -584,16 +586,22 @@ public class NotificationStackScrollLayoutController { @Override public void onHeadsUpPinned(NotificationEntry entry) { - mNotificationRoundnessManager.updateView(entry.getRow(), false /* animate */); + if (!mUseRoundnessSourceTypes) { + mNotificationRoundnessManager.updateView( + entry.getRow(), + /* animate = */ false); + } } @Override public void onHeadsUpUnPinned(NotificationEntry entry) { - ExpandableNotificationRow row = entry.getRow(); - // update the roundedness posted, because we might be animating away the - // headsup soon, so no need to set the roundedness to 0 and then back to 1. - row.post(() -> mNotificationRoundnessManager.updateView(row, - true /* animate */)); + if (!mUseRoundnessSourceTypes) { + ExpandableNotificationRow row = entry.getRow(); + // update the roundedness posted, because we might be animating away the + // headsup soon, so no need to set the roundedness to 0 and then back to 1. + row.post(() -> mNotificationRoundnessManager.updateView(row, + true /* animate */)); + } } @Override @@ -603,8 +611,10 @@ public class NotificationStackScrollLayoutController { mView.setNumHeadsUp(numEntries); mView.setTopHeadsUpEntry(topEntry); generateHeadsUpAnimation(entry, isHeadsUp); - ExpandableNotificationRow row = entry.getRow(); - mNotificationRoundnessManager.updateView(row, true /* animate */); + if (!mUseRoundnessSourceTypes) { + ExpandableNotificationRow row = entry.getRow(); + mNotificationRoundnessManager.updateView(row, true /* animate */); + } } }; @@ -697,6 +707,7 @@ public class NotificationStackScrollLayoutController { mSeenNotificationsProvider = seenNotificationsProvider; mShadeController = shadeController; mFeatureFlags = featureFlags; + mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); mNotificationTargetsHelper = notificationTargetsHelper; updateResources(); } @@ -763,8 +774,10 @@ public class NotificationStackScrollLayoutController { mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener); mFadeNotificationsOnDismiss = mFeatureFlags.isEnabled(Flags.NOTIFICATION_DISMISSAL_FADE); - mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate); - mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded); + if (!mUseRoundnessSourceTypes) { + mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate); + mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded); + } mVisibilityLocationProviderDelegator.setDelegate(this::isInVisibleLocation); @@ -997,9 +1010,11 @@ public class NotificationStackScrollLayoutController { Log.wtf(TAG, "isHistoryEnabled failed to initialize its value"); return false; } - mHistoryEnabled = historyEnabled = - Settings.Secure.getIntForUser(mView.getContext().getContentResolver(), - Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, UserHandle.USER_CURRENT) == 1; + mHistoryEnabled = historyEnabled = Settings.Secure.getIntForUser( + mView.getContext().getContentResolver(), + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, + 0, + UserHandle.USER_CURRENT) == 1; } return historyEnabled; } @@ -1029,7 +1044,7 @@ public class NotificationStackScrollLayoutController { } public void setOverScrollAmount(float amount, boolean onTop, boolean animate, - boolean cancelAnimators) { + boolean cancelAnimators) { mView.setOverScrollAmount(amount, onTop, animate, cancelAnimators); } @@ -1207,7 +1222,7 @@ public class NotificationStackScrollLayoutController { /** * Update whether we should show the empty shade view ("no notifications" in the shade). - * + * <p> * When in split mode, notifications are always visible regardless of the state of the * QuickSettings panel. That being the case, empty view is always shown if the other conditions * are true. @@ -1233,7 +1248,7 @@ public class NotificationStackScrollLayoutController { /** * @return true if {@link StatusBarStateController} is in transition to the KEYGUARD - * and false otherwise. + * and false otherwise. */ private boolean isInTransitionToKeyguard() { final int currentState = mStatusBarStateController.getState(); @@ -1265,7 +1280,9 @@ public class NotificationStackScrollLayoutController { mView.setExpandedHeight(expandedHeight); } - /** Sets the QS header. Used to check if a touch is within its bounds. */ + /** + * Sets the QS header. Used to check if a touch is within its bounds. + */ public void setQsHeader(ViewGroup view) { mView.setQsHeader(view); } @@ -1328,7 +1345,7 @@ public class NotificationStackScrollLayoutController { 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(); @@ -1459,7 +1476,7 @@ public class NotificationStackScrollLayoutController { } private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove, - @SelectedRows int selectedRows) { + @SelectedRows int selectedRows) { if (selectedRows == ROWS_ALL) { mNotifCollection.dismissAllNotifications( mLockscreenUserManager.getCurrentUserId()); @@ -1502,8 +1519,8 @@ public class NotificationStackScrollLayoutController { /** * @return the inset during the full shade transition, that needs to be added to the position - * of the quick settings edge. This is relevant for media, that is transitioning - * from the keyguard host to the quick settings one. + * of the quick settings edge. This is relevant for media, that is transitioning + * from the keyguard host to the quick settings one. */ public int getFullShadeTransitionInset() { MediaContainerView view = mKeyguardMediaController.getSinglePaneContainer(); @@ -1517,10 +1534,10 @@ public class NotificationStackScrollLayoutController { /** * @param fraction The fraction of lockscreen to shade transition. * 0f for all other states. - * - * Once the lockscreen to shade transition completes and the shade is 100% open, - * LockscreenShadeTransitionController resets amount and fraction to 0, where they remain - * until the next lockscreen-to-shade transition. + * <p> + * Once the lockscreen to shade transition completes and the shade is 100% open, + * LockscreenShadeTransitionController resets amount and fraction to 0, where + * they remain until the next lockscreen-to-shade transition. */ public void setTransitionToFullShadeAmount(float fraction) { mView.setFractionToShade(fraction); @@ -1533,7 +1550,9 @@ public class NotificationStackScrollLayoutController { mView.setExtraTopInsetForFullShadeTransition(overScrollAmount); } - /** */ + /** + * + */ public void setWillExpand(boolean willExpand) { mView.setWillExpand(willExpand); } @@ -1549,7 +1568,7 @@ public class NotificationStackScrollLayoutController { * 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); } @@ -1569,6 +1588,15 @@ public class NotificationStackScrollLayoutController { } /** + * Set the remove notification listener + * @param listener callback for notification removed + */ + public void setOnNotificationRemovedListener( + NotificationStackScrollLayout.OnNotificationRemovedListener listener) { + mView.setOnNotificationRemovedListener(listener); + } + + /** * Enum for UiEvent logged from this class */ enum NotificationPanelEvent implements UiEventLogger.UiEventEnum { @@ -1578,10 +1606,13 @@ public class NotificationStackScrollLayoutController { @UiEvent(doc = "User dismissed all silent notifications from notification panel.") DISMISS_SILENT_NOTIFICATIONS_PANEL(314); private final int mId; + NotificationPanelEvent(int id) { mId = id; } - @Override public int getId() { + + @Override + public int getId() { return mId; } @@ -1722,8 +1753,12 @@ public class NotificationStackScrollLayoutController { @Override public void bindRow(ExpandableNotificationRow row) { row.setHeadsUpAnimatingAwayListener(animatingAway -> { - mNotificationRoundnessManager.updateView(row, false); - mHeadsUpAppearanceController.updateHeader(row.getEntry()); + if (!mUseRoundnessSourceTypes) { + mNotificationRoundnessManager.updateView(row, false); + } + NotificationEntry entry = row.getEntry(); + mHeadsUpAppearanceController.updateHeader(entry); + mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry); }); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java index ee57411cb495..aaf9300e7cc8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.notification.stack; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_SWIPE; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.res.Resources; import android.graphics.Rect; @@ -34,9 +35,11 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.SwipeHelper; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -49,6 +52,7 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc @VisibleForTesting protected static final long COVER_MENU_DELAY = 4000; private static final String TAG = "NotificationSwipeHelper"; + private static final SourceType SWIPE_DISMISS = SourceType.from("SwipeDismiss"); private final Runnable mFalsingCheck; private View mTranslatingParentView; private View mMenuExposedView; @@ -64,13 +68,21 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc private WeakReference<NotificationMenuRowPlugin> mCurrMenuRowRef; private boolean mIsExpanded; private boolean mPulsing; + private final NotificationRoundnessManager mNotificationRoundnessManager; + private final boolean mUseRoundnessSourceTypes; NotificationSwipeHelper( - Resources resources, ViewConfiguration viewConfiguration, - FalsingManager falsingManager, FeatureFlags featureFlags, - int swipeDirection, NotificationCallback callback, - NotificationMenuRowPlugin.OnMenuEventListener menuListener) { + Resources resources, + ViewConfiguration viewConfiguration, + FalsingManager falsingManager, + FeatureFlags featureFlags, + int swipeDirection, + NotificationCallback callback, + NotificationMenuRowPlugin.OnMenuEventListener menuListener, + NotificationRoundnessManager notificationRoundnessManager) { super(swipeDirection, callback, resources, viewConfiguration, falsingManager, featureFlags); + mNotificationRoundnessManager = notificationRoundnessManager; + mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); mMenuListener = menuListener; mCallback = callback; mFalsingCheck = () -> resetExposedMenuView(true /* animate */, true /* force */); @@ -304,6 +316,33 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc handleMenuCoveredOrDismissed(); } + @Override + protected void prepareDismissAnimation(View view, Animator anim) { + super.prepareDismissAnimation(view, anim); + + if (mUseRoundnessSourceTypes + && view instanceof ExpandableNotificationRow + && mNotificationRoundnessManager.isClearAllInProgress()) { + ExpandableNotificationRow row = (ExpandableNotificationRow) view; + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + row.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, SWIPE_DISMISS); + } + + @Override + public void onAnimationCancel(Animator animation) { + row.requestRoundnessReset(SWIPE_DISMISS); + } + + @Override + public void onAnimationEnd(Animator animation) { + row.requestRoundnessReset(SWIPE_DISMISS); + } + }); + } + } + @VisibleForTesting protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) { super.dismissChild(view, velocity, useAccelerateInterpolator); @@ -521,14 +560,17 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc private int mSwipeDirection; private NotificationCallback mNotificationCallback; private NotificationMenuRowPlugin.OnMenuEventListener mOnMenuEventListener; + private NotificationRoundnessManager mNotificationRoundnessManager; @Inject Builder(@Main Resources resources, ViewConfiguration viewConfiguration, - FalsingManager falsingManager, FeatureFlags featureFlags) { + FalsingManager falsingManager, FeatureFlags featureFlags, + NotificationRoundnessManager notificationRoundnessManager) { mResources = resources; mViewConfiguration = viewConfiguration; mFalsingManager = falsingManager; mFeatureFlags = featureFlags; + mNotificationRoundnessManager = notificationRoundnessManager; } Builder setSwipeDirection(int swipeDirection) { @@ -549,7 +591,8 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc NotificationSwipeHelper build() { return new NotificationSwipeHelper(mResources, mViewConfiguration, mFalsingManager, - mFeatureFlags, mSwipeDirection, mNotificationCallback, mOnMenuEventListener); + mFeatureFlags, mSwipeDirection, mNotificationCallback, mOnMenuEventListener, + mNotificationRoundnessManager); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt index 991a14bb9c2a..548d1a135948 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt @@ -20,8 +20,7 @@ class NotificationTargetsHelper constructor( featureFlags: FeatureFlags, ) { - private val isNotificationGroupCornerEnabled = - featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER) + private val useRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES) /** * This method looks for views that can be rounded (and implement [Roundable]) during a @@ -48,7 +47,7 @@ constructor( if (notificationParent != null && childrenContainer != null) { // We are inside a notification group - if (!isNotificationGroupCornerEnabled) { + if (!useRoundnessSourceTypes) { return RoundableTargets(null, null, null) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index d8c68780951a..aff7b4c6c515 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -31,6 +31,7 @@ import com.android.systemui.R; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -51,6 +52,7 @@ public class StackScrollAlgorithm { private static final String TAG = "StackScrollAlgorithm"; private static final Boolean DEBUG = false; + private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm"); private final ViewGroup mHostView; private float mPaddingBetweenElements; @@ -70,6 +72,7 @@ public class StackScrollAlgorithm { private float mQuickQsOffsetHeight; private float mSmallCornerRadius; private float mLargeCornerRadius; + private boolean mUseRoundnessSourceTypes; public StackScrollAlgorithm( Context context, @@ -129,7 +132,7 @@ public class StackScrollAlgorithm { } private void updateAlphaState(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { for (ExpandableView view : algorithmState.visibleChildren) { final ViewState viewState = view.getViewState(); @@ -229,7 +232,7 @@ public class StackScrollAlgorithm { } private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableView v = algorithmState.visibleChildren.get(i); @@ -241,7 +244,7 @@ public class StackScrollAlgorithm { } private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState, - int speedBumpIndex) { + int speedBumpIndex) { int childCount = algorithmState.visibleChildren.size(); int belowSpeedBump = speedBumpIndex; for (int i = 0; i < childCount; i++) { @@ -268,7 +271,7 @@ public class StackScrollAlgorithm { } private void updateClipping(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { float drawStart = ambientState.isOnKeyguard() ? 0 : ambientState.getStackY() - ambientState.getScrollY(); float clipStart = 0; @@ -314,7 +317,7 @@ public class StackScrollAlgorithm { * Updates the dimmed, activated and hiding sensitive states of the children. */ private void updateDimmedActivatedHideSensitive(AmbientState ambientState, - StackScrollAlgorithmState algorithmState) { + StackScrollAlgorithmState algorithmState) { boolean dimmed = ambientState.isDimmed(); boolean hideSensitive = ambientState.isHideSensitive(); View activatedChild = ambientState.getActivatedChild(); @@ -408,7 +411,7 @@ public class StackScrollAlgorithm { } private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, - ExpandableView v) { + ExpandableView v) { ExpandableViewState viewState = v.getViewState(); viewState.notGoneIndex = notGoneIndex; state.visibleChildren.add(v); @@ -434,7 +437,7 @@ public class StackScrollAlgorithm { * @param ambientState The current ambient state */ protected void updatePositionsForState(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { if (!ambientState.isOnKeyguard() || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) { algorithmState.mCurrentYPosition += mNotificationScrimPadding; @@ -448,7 +451,7 @@ public class StackScrollAlgorithm { } private void setLocation(ExpandableViewState expandableViewState, float currentYPosition, - int i) { + int i) { expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; if (currentYPosition <= 0) { expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; @@ -496,9 +499,13 @@ public class StackScrollAlgorithm { } @VisibleForTesting - void maybeUpdateHeadsUpIsVisible(ExpandableViewState viewState, boolean isShadeExpanded, - boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax) { - + void maybeUpdateHeadsUpIsVisible( + ExpandableViewState viewState, + boolean isShadeExpanded, + boolean mustStayOnScreen, + boolean topVisible, + float viewEnd, + float hunMax) { if (isShadeExpanded && mustStayOnScreen && topVisible) { viewState.headsUpIsVisible = viewEnd < hunMax; } @@ -676,7 +683,7 @@ public class StackScrollAlgorithm { } private void updatePulsingStates(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); @@ -693,7 +700,7 @@ public class StackScrollAlgorithm { } private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); // Move the tracked heads up into position during the appear animation, by interpolating @@ -777,7 +784,7 @@ public class StackScrollAlgorithm { */ @VisibleForTesting void clampHunToTop(float quickQsOffsetHeight, float stackTranslation, float collapsedHeight, - ExpandableViewState viewState) { + ExpandableViewState viewState) { final float newTranslation = Math.max(quickQsOffsetHeight + stackTranslation, viewState.getYTranslation()); @@ -792,7 +799,7 @@ public class StackScrollAlgorithm { // Pin HUN to bottom of expanded QS // while the rest of notifications are scrolled offscreen. private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, - ExpandableViewState childState) { + ExpandableViewState childState) { float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() + ambientState.getStackTranslation(); @@ -807,14 +814,19 @@ public class StackScrollAlgorithm { // Animate pinned HUN bottom corners to and from original roundness. final float originalCornerRadius = row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius); - final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(), + final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(), ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius); - row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll); + if (mUseRoundnessSourceTypes) { + row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO); + row.addOnDetachResetRoundness(STACK_SCROLL_ALGO); + } else { + row.requestBottomRoundness(bottomValue, LegacySourceType.OnScroll); + } } @VisibleForTesting float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY, - float viewMaxHeight, float originalCornerRadius) { + float viewMaxHeight, float originalCornerRadius) { // Compute y where corner roundness should be in its original unpinned state. // We use view max height because the pinned collapsed HUN expands to max height @@ -844,7 +856,7 @@ public class StackScrollAlgorithm { * @param ambientState The ambient state of the algorithm */ private void updateZValuesForState(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); float childrenOnTop = 0.0f; @@ -876,9 +888,9 @@ public class StackScrollAlgorithm { * previous HUNs whose Z positions are greater than 0. */ protected float updateChildZValue(int i, float childrenOnTop, - StackScrollAlgorithmState algorithmState, - AmbientState ambientState, - boolean isTopHun) { + StackScrollAlgorithmState algorithmState, + AmbientState ambientState, + boolean isTopHun) { ExpandableView child = algorithmState.visibleChildren.get(i); ExpandableViewState childViewState = child.getViewState(); float baseZ = ambientState.getBaseZHeight(); @@ -950,6 +962,14 @@ public class StackScrollAlgorithm { this.mIsExpanded = isExpanded; } + /** + * Enable the support for rounded corner based on the SourceType + * @param enabled true if is supported + */ + public void useRoundnessSourceTypes(boolean enabled) { + mUseRoundnessSourceTypes = enabled; + } + public static class StackScrollAlgorithmState { /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index 484441a1e76b..c217ab3b42e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -19,11 +19,14 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentModule.OPERATOR_NAME_FRAME_VIEW; import android.graphics.Rect; +import android.util.MathUtils; import android.view.View; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ViewClippingUtil; import com.android.systemui.R; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; @@ -32,8 +35,10 @@ import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.HeadsUpStatusBarView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope; import com.android.systemui.statusbar.policy.Clock; @@ -51,6 +56,7 @@ import javax.inject.Named; /** * Controls the appearance of heads up notifications in the icon area and the header itself. + * It also controls the roundness of the heads up notifications and the pulsing notifications. */ @StatusBarFragmentScope public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBarView> @@ -59,12 +65,17 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar NotificationWakeUpCoordinator.WakeUpListener { public static final int CONTENT_FADE_DURATION = 110; public static final int CONTENT_FADE_DELAY = 100; + + private static final SourceType HEADS_UP = SourceType.from("HeadsUp"); + private static final SourceType PULSING = SourceType.from("Pulsing"); private final NotificationIconAreaController mNotificationIconAreaController; private final HeadsUpManagerPhone mHeadsUpManager; private final NotificationStackScrollLayoutController mStackScrollerController; private final DarkIconDispatcher mDarkIconDispatcher; private final NotificationPanelViewController mNotificationPanelViewController; + private final NotificationRoundnessManager mNotificationRoundnessManager; + private final boolean mUseRoundnessSourceTypes; private final Consumer<ExpandableNotificationRow> mSetTrackingHeadsUp = this::setTrackingHeadsUp; private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction; @@ -105,11 +116,15 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar CommandQueue commandQueue, NotificationStackScrollLayoutController stackScrollerController, NotificationPanelViewController notificationPanelViewController, + NotificationRoundnessManager notificationRoundnessManager, + FeatureFlags featureFlags, HeadsUpStatusBarView headsUpStatusBarView, Clock clockView, @Named(OPERATOR_NAME_FRAME_VIEW) Optional<View> operatorNameViewOptional) { super(headsUpStatusBarView); mNotificationIconAreaController = notificationIconAreaController; + mNotificationRoundnessManager = notificationRoundnessManager; + mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); mHeadsUpManager = headsUpManager; // We may be mid-HUN-expansion when this controller is re-created (for example, if the user @@ -179,6 +194,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar public void onHeadsUpPinned(NotificationEntry entry) { updateTopEntry(); updateHeader(entry); + updateHeadsUpAndPulsingRoundness(entry); } private void updateTopEntry() { @@ -316,6 +332,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar public void onHeadsUpUnPinned(NotificationEntry entry) { updateTopEntry(); updateHeader(entry); + updateHeadsUpAndPulsingRoundness(entry); } public void setAppearFraction(float expandedHeight, float appearFraction) { @@ -346,7 +363,9 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar ExpandableNotificationRow previousTracked = mTrackedChild; mTrackedChild = trackedChild; if (previousTracked != null) { - updateHeader(previousTracked.getEntry()); + NotificationEntry entry = previousTracked.getEntry(); + updateHeader(entry); + updateHeadsUpAndPulsingRoundness(entry); } } @@ -357,6 +376,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar private void updateHeadsUpHeaders() { mHeadsUpManager.getAllEntries().forEach(entry -> { updateHeader(entry); + updateHeadsUpAndPulsingRoundness(entry); }); } @@ -370,6 +390,31 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar row.setHeaderVisibleAmount(headerVisibleAmount); } + /** + * Update the HeadsUp and the Pulsing roundness based on current state + * @param entry target notification + */ + public void updateHeadsUpAndPulsingRoundness(NotificationEntry entry) { + if (mUseRoundnessSourceTypes) { + ExpandableNotificationRow row = entry.getRow(); + boolean isTrackedChild = row == mTrackedChild; + if (row.isPinned() || row.isHeadsUpAnimatingAway() || isTrackedChild) { + float roundness = MathUtils.saturate(1f - mAppearFraction); + row.requestRoundness(roundness, roundness, HEADS_UP); + } else { + row.requestRoundnessReset(HEADS_UP); + } + if (mNotificationRoundnessManager.shouldRoundNotificationPulsing()) { + if (row.showingPulsing()) { + row.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, PULSING); + } else { + row.requestRoundnessReset(PULSING); + } + } + } + } + + @Override public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) { mView.onDarkChanged(areas, darkIntensity, tint); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index fb67f1a1bf50..e7afc50dbc2c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.dagger +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory @@ -29,6 +30,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupR import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository @@ -40,6 +42,8 @@ import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiIntera import dagger.Binds import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module abstract class StatusBarPipelineModule { @@ -52,8 +56,7 @@ abstract class StatusBarPipelineModule { @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository - @Binds - abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository + @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor @@ -63,15 +66,18 @@ abstract class StatusBarPipelineModule { impl: MobileConnectionsRepositoryImpl ): MobileConnectionsRepository - @Binds - abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository + @Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository - @Binds - abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy + @Binds abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy @Binds abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor + @Binds + @IntoMap + @ClassKey(MobileUiAdapter::class) + abstract fun bindFeature(impl: MobileUiAdapter): CoreStartable + @Module companion object { @JvmStatic diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt index da87f7306e60..5479b92edd22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt @@ -20,6 +20,7 @@ import android.telephony.TelephonyManager.DATA_CONNECTED import android.telephony.TelephonyManager.DATA_CONNECTING import android.telephony.TelephonyManager.DATA_DISCONNECTED import android.telephony.TelephonyManager.DATA_DISCONNECTING +import android.telephony.TelephonyManager.DATA_UNKNOWN import android.telephony.TelephonyManager.DataState /** Internal enum representation of the telephony data connection states */ @@ -28,6 +29,7 @@ enum class DataConnectionState(@DataState val dataState: Int) { Connecting(DATA_CONNECTING), Disconnected(DATA_DISCONNECTED), Disconnecting(DATA_DISCONNECTING), + Unknown(DATA_UNKNOWN), } fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState = @@ -36,5 +38,6 @@ fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState = DATA_CONNECTING -> DataConnectionState.Connecting DATA_DISCONNECTED -> DataConnectionState.Disconnected DATA_DISCONNECTING -> DataConnectionState.Disconnecting - else -> throw IllegalArgumentException("unknown data state received") + DATA_UNKNOWN -> DataConnectionState.Unknown + else -> throw IllegalArgumentException("unknown data state received $this") } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index c7e0ce173ece..d9487bf92260 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarIconController @@ -29,9 +30,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * This class is intended to provide a context to collect on the @@ -50,9 +52,9 @@ constructor( interactor: MobileIconsInteractor, private val iconController: StatusBarIconController, private val iconsViewModelFactory: MobileIconsViewModel.Factory, - @Application scope: CoroutineScope, + @Application private val scope: CoroutineScope, private val statusBarPipelineFlags: StatusBarPipelineFlags, -) { +) : CoreStartable { private val mobileSubIds: Flow<List<Int>> = interactor.filteredSubscriptions.mapLatest { infos -> infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId } @@ -66,18 +68,19 @@ constructor( * NOTE: this should go away as the view presenter learns more about this data pipeline */ private val mobileSubIdsState: StateFlow<List<Int>> = - mobileSubIds - .onEach { - // Only notify the icon controller if we want to *render* the new icons. - // Note that this flow may still run if - // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to - // get the logging data without rendering. - if (statusBarPipelineFlags.useNewMobileIcons()) { - // Notify the icon controller here so that it knows to add icons - iconController.setNewMobileIconSubIds(it) - } + mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + + override fun start() { + // Only notify the icon controller if we want to *render* the new icons. + // Note that this flow may still run if + // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to + // get the logging data without rendering. + if (statusBarPipelineFlags.useNewMobileIcons()) { + scope.launch { + mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) } } - .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + } + } /** * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt index b816364ed4cf..52237605caf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt @@ -24,6 +24,7 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel import javax.inject.Inject @@ -73,7 +74,9 @@ constructor( // Note that this flow may still run if // [statusBarPipelineFlags.runNewWifiIconBackend] is true because we may // want to get the logging data without rendering. - if (wifiIcon != null && statusBarPipelineFlags.useNewWifiIcon()) { + if ( + wifiIcon is WifiIcon.Visible && statusBarPipelineFlags.useNewWifiIcon() + ) { iconController.setNewWifiIcon() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 345f8cb75660..f5b5950d33a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -92,8 +93,10 @@ object WifiViewBinder { launch { viewModel.wifiIcon.collect { wifiIcon -> - view.isVisible = wifiIcon != null - wifiIcon?.let { IconViewBinder.bind(wifiIcon, iconView) } + view.isVisible = wifiIcon is WifiIcon.Visible + if (wifiIcon is WifiIcon.Visible) { + IconViewBinder.bind(wifiIcon.icon, iconView) + } } } @@ -135,7 +138,7 @@ object WifiViewBinder { return object : Binding { override fun getShouldIconBeVisible(): Boolean { - return viewModel.wifiIcon.value != null + return viewModel.wifiIcon.value is WifiIcon.Visible } override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt new file mode 100644 index 000000000000..e491d2bbf0d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.ui.model + +import android.annotation.DrawableRes +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger + +/** Represents the various states of the wifi icon. */ +sealed interface WifiIcon : Diffable<WifiIcon> { + /** Represents a wifi icon that should be hidden (not visible). */ + object Hidden : WifiIcon { + override fun toString() = "hidden" + } + + /** + * Represents a visible wifi icon that uses [res] as its image and [contentDescription] as its + * description. + */ + class Visible( + @DrawableRes res: Int, + val contentDescription: ContentDescription.Loaded, + ) : WifiIcon { + val icon = Icon.Resource(res, contentDescription) + + override fun toString() = contentDescription.description.toString() + } + + override fun logDiffs(prevVal: WifiIcon, row: TableRowLogger) { + if (prevVal.toString() != toString()) { + row.logChange(COL_ICON, toString()) + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_ICON, toString()) + } +} + +private const val COL_ICON = "wifiIcon" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt index 95ab251422b2..a29c9b94e6b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.graphics.Color -import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.StateFlow */ class HomeWifiViewModel( statusBarPipelineFlags: StatusBarPipelineFlags, - wifiIcon: StateFlow<Icon.Resource?>, + wifiIcon: StateFlow<WifiIcon>, isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt index 86535d63f84f..1e190fb898ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt @@ -17,15 +17,15 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.graphics.Color -import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** A view model for the wifi icon shown on keyguard (lockscreen). */ class KeyguardWifiViewModel( statusBarPipelineFlags: StatusBarPipelineFlags, - wifiIcon: StateFlow<Icon.Resource?>, + wifiIcon: StateFlow<WifiIcon>, isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index 7cbdf5dbdf2d..e35a8fef4528 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.graphics.Color -import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf @@ -33,8 +33,8 @@ abstract class LocationBasedWifiViewModel( statusBarPipelineFlags: StatusBarPipelineFlags, debugTint: Int, - /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */ - val wifiIcon: StateFlow<Icon.Resource?>, + /** The wifi icon that should be displayed. */ + val wifiIcon: StateFlow<WifiIcon>, /** True if the activity in view should be visible. */ val isActivityInViewVisible: Flow<Boolean>, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt index fd54c5f5062e..18e62b284cb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt @@ -17,15 +17,15 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.graphics.Color -import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */ class QsWifiViewModel( statusBarPipelineFlags: StatusBarPipelineFlags, - wifiIcon: StateFlow<Icon.Resource?>, + wifiIcon: StateFlow<WifiIcon>, isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 0782bbb774eb..ec7ba653cac7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -17,16 +17,18 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.content.Context -import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.R import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK @@ -71,50 +73,39 @@ constructor( connectivityConstants: ConnectivityConstants, private val context: Context, logger: ConnectivityPipelineLogger, + @WifiTableLog wifiTableLogBuffer: TableLogBuffer, interactor: WifiInteractor, @Application private val scope: CoroutineScope, statusBarPipelineFlags: StatusBarPipelineFlags, wifiConstants: WifiConstants, ) { - /** - * Returns the drawable resource ID to use for the wifi icon based on the given network. - * Null if we can't compute the icon. - */ - @DrawableRes - private fun WifiNetworkModel.iconResId(): Int? { + /** Returns the icon to use based on the given network. */ + private fun WifiNetworkModel.icon(): WifiIcon { return when (this) { - is WifiNetworkModel.CarrierMerged -> null - is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK - is WifiNetworkModel.Active -> - when { - this.level == null -> null - this.isValidated -> WIFI_FULL_ICONS[this.level] - else -> WIFI_NO_INTERNET_ICONS[this.level] - } - } - } - - /** - * Returns the content description for the wifi icon based on the given network. - * Null if we can't compute the content description. - */ - private fun WifiNetworkModel.contentDescription(): ContentDescription? { - return when (this) { - is WifiNetworkModel.CarrierMerged -> null - is WifiNetworkModel.Inactive -> + is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden + is WifiNetworkModel.Inactive -> WifiIcon.Visible( + res = WIFI_NO_NETWORK, ContentDescription.Loaded( "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}" ) + ) is WifiNetworkModel.Active -> when (this.level) { - null -> null + null -> WifiIcon.Hidden else -> { val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level]) when { - this.isValidated -> ContentDescription.Loaded(levelDesc) + this.isValidated -> + WifiIcon.Visible( + WIFI_FULL_ICONS[this.level], + ContentDescription.Loaded(levelDesc) + ) else -> - ContentDescription.Loaded( - "$levelDesc,${context.getString(NO_INTERNET)}" + WifiIcon.Visible( + WIFI_NO_INTERNET_ICONS[this.level], + ContentDescription.Loaded( + "$levelDesc,${context.getString(NO_INTERNET)}" + ) ) } } @@ -122,8 +113,8 @@ constructor( } } - /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */ - private val wifiIcon: StateFlow<Icon.Resource?> = + /** The wifi icon that should be displayed. */ + private val wifiIcon: StateFlow<WifiIcon> = combine( interactor.isEnabled, interactor.isDefault, @@ -131,22 +122,29 @@ constructor( interactor.wifiNetwork, ) { isEnabled, isDefault, isForceHidden, wifiNetwork -> if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) { - return@combine null + return@combine WifiIcon.Hidden } - val iconResId = wifiNetwork.iconResId() ?: return@combine null - val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription()) + val icon = wifiNetwork.icon() return@combine when { isDefault -> icon wifiConstants.alwaysShowIconIfEnabled -> icon !connectivityConstants.hasDataCapabilities -> icon wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon - else -> null + else -> WifiIcon.Hidden } } - .logOutputChange(logger, "icon") { icon -> icon?.contentDescription.toString() } - .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null) + .logDiffsForTable( + wifiTableLogBuffer, + columnPrefix = "", + initialValue = WifiIcon.Hidden, + ) + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + initialValue = WifiIcon.Hidden + ) /** The wifi activity status. Null if we shouldn't display the activity status. */ private val activity: Flow<WifiActivityModel?> = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index 761773b1a345..fdef34449adf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -702,7 +702,7 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test - fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToGone() { + fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToInvisible() { useRealConstraintSets() val state = mediaData.copy(semanticActions = MediaButton()) @@ -711,7 +711,7 @@ public class MediaControlPanelTest : SysuiTestCase() { player.bindPlayer(state, PACKAGE) - assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE) + assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE) } @Test @@ -741,7 +741,7 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test - fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToGone() { + fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToInvisible() { useRealConstraintSets() val state = mediaData.copy(semanticActions = MediaButton()) @@ -752,7 +752,7 @@ public class MediaControlPanelTest : SysuiTestCase() { getEnabledChangeListener().onEnabledChanged(enabled = false) - assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE) + assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 9758842d1e35..4a9c7508b1b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -16,16 +16,21 @@ package com.android.systemui.notetask import android.app.KeyguardManager +import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.UserManager import android.test.suitebuilder.annotation.SmallTest -import android.view.KeyEvent import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.android.wm.shell.bubbles.Bubbles +import com.google.common.truth.Truth.assertThat import java.util.Optional import org.junit.Before import org.junit.Test @@ -48,6 +53,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { private val notesIntent = Intent(NOTES_ACTION) @Mock lateinit var context: Context + @Mock lateinit var packageManager: PackageManager @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver @Mock lateinit var bubbles: Bubbles @Mock lateinit var optionalBubbles: Optional<Bubbles> @@ -60,6 +66,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + whenever(context.packageManager).thenReturn(packageManager) whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent) whenever(optionalBubbles.orElse(null)).thenReturn(bubbles) whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager) @@ -78,89 +85,125 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) } + // region showNoteTask @Test - fun handleSystemKey_keyguardIsLocked_shouldStartActivity() { + fun showNoteTask_keyguardIsLocked_shouldStartActivity() { whenever(keyguardManager.isKeyguardLocked).thenReturn(true) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_keyguardIsUnlocked_shouldStartBubbles() { + fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() { whenever(keyguardManager.isKeyguardLocked).thenReturn(false) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(bubbles).showAppBubble(notesIntent) verify(context, never()).startActivity(notesIntent) } @Test - fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) + fun showNoteTask_isInMultiWindowMode_shouldStartActivity() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(false) - verify(context, never()).startActivity(notesIntent) + createNoteTaskController().showNoteTask(isInMultiWindowMode = true) + + verify(context).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_bubblesIsNull_shouldDoNothing() { + fun showNoteTask_bubblesIsNull_shouldDoNothing() { whenever(optionalBubbles.orElse(null)).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() { + fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() { whenever(optionalKeyguardManager.orElse(null)).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_userManagerIsNull_shouldDoNothing() { + fun showNoteTask_userManagerIsNull_shouldDoNothing() { whenever(optionalUserManager.orElse(null)).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() { + fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() { whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_flagDisabled_shouldDoNothing() { - createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + fun showNoteTask_flagDisabled_shouldDoNothing() { + createNoteTaskController(isEnabled = false).showNoteTask() verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } @Test - fun handleSystemKey_userIsLocked_shouldDoNothing() { + fun showNoteTask_userIsLocked_shouldDoNothing() { whenever(userManager.isUserUnlocked).thenReturn(false) - createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) verify(bubbles, never()).showAppBubble(notesIntent) } + // endregion + + // region setNoteTaskShortcutEnabled + @Test + fun setNoteTaskShortcutEnabled_setTrue() { + createNoteTaskController().setNoteTaskShortcutEnabled(value = true) + + val argument = argumentCaptor<ComponentName>() + verify(context.packageManager) + .setComponentEnabledSetting( + argument.capture(), + eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), + eq(PackageManager.DONT_KILL_APP), + ) + val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) + assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) + } + + @Test + fun setNoteTaskShortcutEnabled_setFalse() { + createNoteTaskController().setNoteTaskShortcutEnabled(value = false) + + val argument = argumentCaptor<ComponentName>() + verify(context.packageManager) + .setComponentEnabledSetting( + argument.capture(), + eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), + eq(PackageManager.DONT_KILL_APP), + ) + val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) + assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) + } + // endregion } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 334089c43e27..538131a4dd73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -16,10 +16,10 @@ package com.android.systemui.notetask import android.test.suitebuilder.annotation.SmallTest +import android.view.KeyEvent import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.wm.shell.bubbles.Bubbles import java.util.Optional @@ -45,6 +45,7 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { @Mock lateinit var commandQueue: CommandQueue @Mock lateinit var bubbles: Bubbles @Mock lateinit var optionalBubbles: Optional<Bubbles> + @Mock lateinit var noteTaskController: NoteTaskController @Before fun setUp() { @@ -57,12 +58,13 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer { return NoteTaskInitializer( optionalBubbles = optionalBubbles, - lazyNoteTaskController = mock(), + noteTaskController = noteTaskController, commandQueue = commandQueue, isEnabled = isEnabled, ) } + // region initializer @Test fun initialize_shouldAddCallbacks() { createNoteTaskInitializer().initialize() @@ -85,4 +87,35 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { verify(commandQueue, never()).addCallback(any()) } + + @Test + fun initialize_flagEnabled_shouldEnableShortcut() { + createNoteTaskInitializer().initialize() + + verify(noteTaskController).setNoteTaskShortcutEnabled(true) + } + + @Test + fun initialize_flagDisabled_shouldDisableShortcut() { + createNoteTaskInitializer(isEnabled = false).initialize() + + verify(noteTaskController).setNoteTaskShortcutEnabled(false) + } + // endregion + + // region handleSystemKey + @Test + fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { + createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(noteTaskController).showNoteTask() + } + + @Test + fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { + createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) + + verify(noteTaskController, never()).showNoteTask() + } + // endregion } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index 5e9c1aaad309..906c20b1d032 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -703,28 +703,32 @@ public class QSSecurityFooterTest extends SysuiTestCase { public void testParentalControls() { // Make sure the security footer is visible, so that the images are updated. when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true); - when(mSecurityController.isParentalControlsEnabled()).thenReturn(true); + // We use the default icon when there is no admin icon. + when(mSecurityController.getIcon(any())).thenReturn(null); + mFooter.refreshState(); + TestableLooper.get(this).processAllMessages(); + assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls), + mFooterText.getText()); + assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); + Drawable testDrawable = new VectorDrawable(); when(mSecurityController.getIcon(any())).thenReturn(testDrawable); assertNotNull(mSecurityController.getIcon(null)); mFooter.refreshState(); - TestableLooper.get(this).processAllMessages(); assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls), mFooterText.getText()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); - assertEquals(testDrawable, mPrimaryFooterIcon.getDrawable()); // Ensure the primary icon is back to default after parental controls are gone when(mSecurityController.isParentalControlsEnabled()).thenReturn(false); mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java index faf4592d26e3..5431eba8441c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java @@ -72,6 +72,7 @@ import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; @@ -245,6 +246,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mMockProvisionController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java index ca75a40300cb..9441d49d454a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java @@ -49,6 +49,7 @@ import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; @@ -150,6 +151,7 @@ public class NetworkControllerDataTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java index 84c242cda459..4c1f0a8a1066 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java @@ -44,6 +44,7 @@ import com.android.settingslib.net.DataUsageController; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; @@ -78,6 +79,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mMockProvisionController, @@ -115,6 +117,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mMockProvisionController, @@ -150,6 +153,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), @@ -188,6 +192,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), @@ -274,6 +279,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mFakeExecutor, mCallbackHandler, mock(AccessPointControllerImpl.class), + mock(StatusBarPipelineFlags.class), mock(DataUsageController.class), mMockSubDefaults, mock(DeviceProvisionedController.class), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt new file mode 100644 index 000000000000..89faa239c5a2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt @@ -0,0 +1,164 @@ +package com.android.systemui.statusbar.notification + +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.mock +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +class RoundableTest : SysuiTestCase() { + val targetView: View = mock() + val roundable = FakeRoundable(targetView) + + @Test + fun defaultConfig_shouldNotHaveRoundedCorner() { + // the expected default value for the roundness is top = 0f, bottom = 0f + assertEquals(0f, roundable.roundableState.topRoundness) + assertEquals(0f, roundable.roundableState.bottomRoundness) + assertEquals(false, roundable.hasRoundedCorner()) + } + + @Test + fun applyRoundnessAndInvalidate_should_invalidate_targetView() { + roundable.applyRoundnessAndInvalidate() + + verify(targetView, times(1)).invalidate() + } + + @Test + fun requestTopRoundness_update_and_invalidate_targetView() { + roundable.requestTopRoundness(value = 1f, sourceType = SOURCE1) + + assertEquals(1f, roundable.roundableState.topRoundness) + verify(targetView, times(1)).invalidate() + } + + @Test + fun requestBottomRoundness_update_and_invalidate_targetView() { + roundable.requestBottomRoundness(value = 1f, sourceType = SOURCE1) + + assertEquals(1f, roundable.roundableState.bottomRoundness) + verify(targetView, times(1)).invalidate() + } + + @Test + fun requestRoundness_update_and_invalidate_targetView() { + roundable.requestRoundness(top = 1f, bottom = 1f, sourceType = SOURCE1) + + assertEquals(1f, roundable.roundableState.topRoundness) + assertEquals(1f, roundable.roundableState.bottomRoundness) + verify(targetView, atLeastOnce()).invalidate() + } + + @Test + fun requestRoundnessReset_update_and_invalidate_targetView() { + roundable.requestRoundness(1f, 1f, SOURCE1) + assertEquals(1f, roundable.roundableState.topRoundness) + assertEquals(1f, roundable.roundableState.bottomRoundness) + + roundable.requestRoundnessReset(sourceType = SOURCE1) + + assertEquals(0f, roundable.roundableState.topRoundness) + assertEquals(0f, roundable.roundableState.bottomRoundness) + verify(targetView, atLeastOnce()).invalidate() + } + + @Test + fun hasRoundedCorner_return_true_ifRoundnessIsGreaterThenZero() { + roundable.requestRoundness(top = 1f, bottom = 1f, sourceType = SOURCE1) + assertEquals(true, roundable.hasRoundedCorner()) + + roundable.requestRoundness(top = 1f, bottom = 0f, sourceType = SOURCE1) + assertEquals(true, roundable.hasRoundedCorner()) + + roundable.requestRoundness(top = 0f, bottom = 1f, sourceType = SOURCE1) + assertEquals(true, roundable.hasRoundedCorner()) + + roundable.requestRoundness(top = 0f, bottom = 0f, sourceType = SOURCE1) + assertEquals(false, roundable.hasRoundedCorner()) + } + + @Test + fun roundness_take_maxValue_onMultipleSources_first_lower() { + roundable.requestRoundness(0.1f, 0.1f, SOURCE1) + assertEquals(0.1f, roundable.roundableState.topRoundness) + assertEquals(0.1f, roundable.roundableState.bottomRoundness) + + roundable.requestRoundness(0.2f, 0.2f, SOURCE2) + // SOURCE1 has 0.1f - SOURCE2 has 0.2f + assertEquals(0.2f, roundable.roundableState.topRoundness) + assertEquals(0.2f, roundable.roundableState.bottomRoundness) + } + + @Test + fun roundness_take_maxValue_onMultipleSources_first_higher() { + roundable.requestRoundness(0.5f, 0.5f, SOURCE1) + assertEquals(0.5f, roundable.roundableState.topRoundness) + assertEquals(0.5f, roundable.roundableState.bottomRoundness) + + roundable.requestRoundness(0.1f, 0.1f, SOURCE2) + // SOURCE1 has 0.5f - SOURCE2 has 0.1f + assertEquals(0.5f, roundable.roundableState.topRoundness) + assertEquals(0.5f, roundable.roundableState.bottomRoundness) + } + + @Test + fun roundness_take_maxValue_onMultipleSources_first_higher_second_step() { + roundable.requestRoundness(0.1f, 0.1f, SOURCE1) + assertEquals(0.1f, roundable.roundableState.topRoundness) + assertEquals(0.1f, roundable.roundableState.bottomRoundness) + + roundable.requestRoundness(0.2f, 0.2f, SOURCE2) + // SOURCE1 has 0.1f - SOURCE2 has 0.2f + assertEquals(0.2f, roundable.roundableState.topRoundness) + assertEquals(0.2f, roundable.roundableState.bottomRoundness) + + roundable.requestRoundness(0.3f, 0.3f, SOURCE1) + // SOURCE1 has 0.3f - SOURCE2 has 0.2f + assertEquals(0.3f, roundable.roundableState.topRoundness) + assertEquals(0.3f, roundable.roundableState.bottomRoundness) + } + + @Test + fun roundness_take_maxValue_onMultipleSources_first_lower_second_step() { + roundable.requestRoundness(0.5f, 0.5f, SOURCE1) + assertEquals(0.5f, roundable.roundableState.topRoundness) + assertEquals(0.5f, roundable.roundableState.bottomRoundness) + + roundable.requestRoundness(0.2f, 0.2f, SOURCE2) + // SOURCE1 has 0.5f - SOURCE2 has 0.2f + assertEquals(0.5f, roundable.roundableState.topRoundness) + assertEquals(0.5f, roundable.roundableState.bottomRoundness) + + roundable.requestRoundness(0.1f, 0.1f, SOURCE1) + // SOURCE1 has 0.1f - SOURCE2 has 0.2f + assertEquals(0.2f, roundable.roundableState.topRoundness) + assertEquals(0.2f, roundable.roundableState.bottomRoundness) + } + + class FakeRoundable( + targetView: View, + radius: Float = MAX_RADIUS, + ) : Roundable { + override val roundableState = + RoundableState( + targetView = targetView, + roundable = this, + maxRadius = radius, + ) + } + + companion object { + private const val MAX_RADIUS = 10f + private val SOURCE1 = SourceType.from("Source1") + private val SOURCE2 = SourceType.from("Source2") + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index fb31bef909e0..728e0265c729 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -215,8 +215,7 @@ public class NotificationTestHelper { SourceType sourceType ) throws Exception { ExpandableNotificationRow row = createRow(); - row.requestTopRoundness(topRoundness, false, sourceType); - row.requestBottomRoundness(bottomRoundness, /*animate = */ false, sourceType); + row.requestRoundness(topRoundness, bottomRoundness, sourceType, /*animate = */ false); assertEquals(topRoundness, row.getTopRoundness(), /* delta = */ 0f); assertEquals(bottomRoundness, row.getBottomRoundness(), /* delta = */ 0f); return row; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 438b528944be..fd1944e7478d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -25,7 +25,7 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.notification.SourceType; +import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; @@ -158,7 +158,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { ExpandableNotificationRow row = mNotificationTestHelper.createRowWithRoundness( /* topRoundness = */ 1f, /* bottomRoundness = */ 1f, - /* sourceType = */ SourceType.OnScroll); + /* sourceType = */ LegacySourceType.OnScroll); mChildrenContainer.addNotification(row, 0); @@ -171,11 +171,11 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { ExpandableNotificationRow row1 = mNotificationTestHelper.createRowWithRoundness( /* topRoundness = */ 1f, /* bottomRoundness = */ 1f, - /* sourceType = */ SourceType.DefaultValue); + /* sourceType = */ LegacySourceType.DefaultValue); ExpandableNotificationRow row2 = mNotificationTestHelper.createRowWithRoundness( /* topRoundness = */ 1f, /* bottomRoundness = */ 1f, - /* sourceType = */ SourceType.OnDismissAnimation); + /* sourceType = */ LegacySourceType.OnDismissAnimation); mChildrenContainer.addNotification(row1, 0); mChildrenContainer.addNotification(row2, 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java index 8c8b64424814..bd0a556ac670 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java @@ -35,6 +35,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger; @@ -73,7 +74,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { mRoundnessManager = new NotificationRoundnessManager( new NotificationSectionsFeatureManager(new DeviceConfigProxy(), mContext), mLogger, - mock(DumpManager.class)); + mock(DumpManager.class), + mock(FeatureFlags.class)); allowTestableLooperAsMainThread(); NotificationTestHelper testHelper = new NotificationTestHelper( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index ecc02246ea1d..30da08ebfe7d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -30,6 +30,7 @@ import android.view.ViewGroup; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -59,10 +60,12 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private NotificationSectionsFeatureManager mSectionsFeatureManager; @Mock private MediaContainerController mMediaContainerController; + @Mock private NotificationRoundnessManager mNotificationRoundnessManager; @Mock private SectionHeaderController mIncomingHeaderController; @Mock private SectionHeaderController mPeopleHeaderController; @Mock private SectionHeaderController mAlertingHeaderController; @Mock private SectionHeaderController mSilentHeaderController; + @Mock private FeatureFlags mFeatureFlag; private NotificationSectionsManager mSectionsManager; @@ -89,10 +92,12 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mKeyguardMediaController, mSectionsFeatureManager, mMediaContainerController, + mNotificationRoundnessManager, mIncomingHeaderController, mPeopleHeaderController, mAlertingHeaderController, - mSilentHeaderController + mSilentHeaderController, + mFeatureFlag ); // Required in order for the header inflation to work properly when(mNssl.generateLayoutParams(any(AttributeSet.class))) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index bda233611158..9d759c4b6016 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -9,7 +9,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.notification.SourceType +import com.android.systemui.statusbar.notification.LegacySourceType import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.row.NotificationTestHelper @@ -314,9 +314,9 @@ class NotificationShelfTest : SysuiTestCase() { val row: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness( /* topRoundness = */ 1f, /* bottomRoundness = */ 1f, - /* sourceType = */ SourceType.OnScroll) + /* sourceType = */ LegacySourceType.OnScroll) - NotificationShelf.resetOnScrollRoundness(row) + NotificationShelf.resetLegacyOnScrollRoundness(row) assertEquals(0f, row.topRoundness) assertEquals(0f, row.bottomRoundness) @@ -327,14 +327,14 @@ class NotificationShelfTest : SysuiTestCase() { val row1: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness( /* topRoundness = */ 1f, /* bottomRoundness = */ 1f, - /* sourceType = */ SourceType.DefaultValue) + /* sourceType = */ LegacySourceType.DefaultValue) val row2: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness( /* topRoundness = */ 1f, /* bottomRoundness = */ 1f, - /* sourceType = */ SourceType.OnDismissAnimation) + /* sourceType = */ LegacySourceType.OnDismissAnimation) - NotificationShelf.resetOnScrollRoundness(row1) - NotificationShelf.resetOnScrollRoundness(row2) + NotificationShelf.resetLegacyOnScrollRoundness(row1) + NotificationShelf.resetLegacyOnScrollRoundness(row2) assertEquals(1f, row1.topRoundness) assertEquals(1f, row1.bottomRoundness) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java index 4ea1c7100557..680a32375988 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java @@ -74,6 +74,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { private NotificationSwipeHelper mSwipeHelper; private NotificationSwipeHelper.NotificationCallback mCallback; private NotificationMenuRowPlugin.OnMenuEventListener mListener; + private NotificationRoundnessManager mNotificationRoundnessManager; private View mView; private MotionEvent mEvent; private NotificationMenuRowPlugin mMenuRow; @@ -92,10 +93,17 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { public void setUp() throws Exception { mCallback = mock(NotificationSwipeHelper.NotificationCallback.class); mListener = mock(NotificationMenuRowPlugin.OnMenuEventListener.class); + mNotificationRoundnessManager = mock(NotificationRoundnessManager.class); mFeatureFlags = mock(FeatureFlags.class); mSwipeHelper = spy(new NotificationSwipeHelper( - mContext.getResources(), ViewConfiguration.get(mContext), - new FalsingManagerFake(), mFeatureFlags, SwipeHelper.X, mCallback, mListener)); + mContext.getResources(), + ViewConfiguration.get(mContext), + new FalsingManagerFake(), + mFeatureFlags, + SwipeHelper.X, + mCallback, + mListener, + mNotificationRoundnessManager)); mView = mock(View.class); mEvent = mock(MotionEvent.class); mMenuRow = mock(NotificationMenuRowPlugin.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt index a2e92305bf27..81a3f1245efe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt @@ -35,7 +35,7 @@ class NotificationTargetsHelperTest : SysuiTestCase() { ) = NotificationTargetsHelper( FakeFeatureFlags().apply { - set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner) + set(Flags.USE_ROUNDNESS_SOURCETYPES, notificationGroupCorner) } ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java index 103b7b4268de..9727b6c5eb83 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java @@ -32,6 +32,7 @@ import android.widget.TextView; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; @@ -40,6 +41,7 @@ import com.android.systemui.statusbar.HeadsUpStatusBarView; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -71,6 +73,8 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { private NotificationWakeUpCoordinator mWakeUpCoordinator; private KeyguardStateController mKeyguardStateController; private CommandQueue mCommandQueue; + private NotificationRoundnessManager mNotificationRoundnessManager; + private FeatureFlags mFeatureFlag; @Before public void setUp() throws Exception { @@ -89,6 +93,8 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mWakeUpCoordinator = mock(NotificationWakeUpCoordinator.class); mKeyguardStateController = mock(KeyguardStateController.class); mCommandQueue = mock(CommandQueue.class); + mNotificationRoundnessManager = mock(NotificationRoundnessManager.class); + mFeatureFlag = mock(FeatureFlags.class); mHeadsUpAppearanceController = new HeadsUpAppearanceController( mock(NotificationIconAreaController.class), mHeadsUpManager, @@ -100,6 +106,8 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mCommandQueue, mStackScrollerController, mPanelView, + mNotificationRoundnessManager, + mFeatureFlag, mHeadsUpStatusBarView, new Clock(mContext, null), Optional.of(mOperatorNameView)); @@ -182,6 +190,8 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mCommandQueue, mStackScrollerController, mPanelView, + mNotificationRoundnessManager, + mFeatureFlag, mHeadsUpStatusBarView, new Clock(mContext, null), Optional.empty()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index 5ce51bb62c78..aa7ab0df2ea3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -31,6 +31,7 @@ import android.telephony.TelephonyManager.DATA_CONNECTED import android.telephony.TelephonyManager.DATA_CONNECTING import android.telephony.TelephonyManager.DATA_DISCONNECTED import android.telephony.TelephonyManager.DATA_DISCONNECTING +import android.telephony.TelephonyManager.DATA_UNKNOWN import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest @@ -221,6 +222,21 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test + fun testFlowForSubId_dataConnectionState_unknown() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */) + + assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Unknown) + + job.cancel() + } + + @Test fun testFlowForSubId_dataActivity() = runBlocking(IMMEDIATE) { var latest: MobileSubscriptionModel? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 5c16e1295b65..3d9fd961222f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.lifecycle.InstantTaskExecutorRule +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON @@ -64,6 +65,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var tableLogBuffer: TableLogBuffer @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock @@ -103,6 +105,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { connectivityConstants, context, logger, + tableLogBuffer, interactor, scope, statusBarPipelineFlags, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index 3001b8162185..12b93819fc5e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -23,6 +23,7 @@ import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_ import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK @@ -40,6 +41,7 @@ import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepo import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope @@ -67,6 +69,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var tableLogBuffer: TableLogBuffer @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants private lateinit var airplaneModeRepository: FakeAirplaneModeRepository @@ -123,6 +126,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase connectivityConstants, context, logger, + tableLogBuffer, interactor, scope, statusBarPipelineFlags, @@ -137,15 +141,21 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase yield() // THEN we get the expected icon - assertThat(iconFlow.value?.res).isEqualTo(testCase.expected?.iconResource) - val expectedContentDescription = - if (testCase.expected == null) { - null - } else { - testCase.expected.contentDescription.invoke(context) + val actualIcon = iconFlow.value + when (testCase.expected) { + null -> { + assertThat(actualIcon).isInstanceOf(WifiIcon.Hidden::class.java) + } + else -> { + assertThat(actualIcon).isInstanceOf(WifiIcon.Visible::class.java) + val actualIconVisible = actualIcon as WifiIcon.Visible + assertThat(actualIconVisible.icon.res).isEqualTo(testCase.expected.iconResource) + val expectedContentDescription = + testCase.expected.contentDescription.invoke(context) + assertThat(actualIconVisible.contentDescription.loadContentDescription(context)) + .isEqualTo(expectedContentDescription) } - assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context)) - .isEqualTo(expectedContentDescription) + } job.cancel() } @@ -174,7 +184,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase val isDefault: Boolean = false, val network: WifiNetworkModel, - /** The expected output. Null if we expect the output to be null. */ + /** The expected output. Null if we expect the output to be hidden. */ val expected: Expected? ) { override fun toString(): String { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 6a6b2a801ab0..7502020286f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.Icon +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiIntera import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel +import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -59,6 +60,7 @@ class WifiViewModelTest : SysuiTestCase() { @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var tableLogBuffer: TableLogBuffer @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants private lateinit var airplaneModeRepository: FakeAirplaneModeRepository @@ -103,21 +105,21 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) { - var latestHome: Icon? = null + var latestHome: WifiIcon? = null val jobHome = underTest .home .wifiIcon .onEach { latestHome = it } .launchIn(this) - var latestKeyguard: Icon? = null + var latestKeyguard: WifiIcon? = null val jobKeyguard = underTest .keyguard .wifiIcon .onEach { latestKeyguard = it } .launchIn(this) - var latestQs: Icon? = null + var latestQs: WifiIcon? = null val jobQs = underTest .qs .wifiIcon @@ -133,7 +135,7 @@ class WifiViewModelTest : SysuiTestCase() { ) yield() - assertThat(latestHome).isInstanceOf(Icon.Resource::class.java) + assertThat(latestHome).isInstanceOf(WifiIcon.Visible::class.java) assertThat(latestHome).isEqualTo(latestKeyguard) assertThat(latestKeyguard).isEqualTo(latestQs) @@ -541,6 +543,7 @@ class WifiViewModelTest : SysuiTestCase() { connectivityConstants, context, logger, + tableLogBuffer, interactor, scope, statusBarPipelineFlags, diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 40e28dadb4da..c0e7ab8a28a5 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -602,6 +602,14 @@ class AutomaticBrightnessController { mAmbientBrightnessThresholdsIdle.dump(pw); } + public float[] getLastSensorValues() { + return mAmbientLightRingBuffer.getAllLuxValues(); + } + + public long[] getLastSensorTimestamps() { + return mAmbientLightRingBuffer.getAllTimestamps(); + } + private String configStateToString(int state) { switch (state) { case AUTO_BRIGHTNESS_ENABLED: @@ -1231,10 +1239,42 @@ class AutomaticBrightnessController { return mRingLux[offsetOf(index)]; } + public float[] getAllLuxValues() { + float[] values = new float[mCount]; + if (mCount == 0) { + return values; + } + + if (mStart < mEnd) { + System.arraycopy(mRingLux, mStart, values, 0, mCount); + } else { + System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart); + System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd); + } + + return values; + } + public long getTime(int index) { return mRingTime[offsetOf(index)]; } + public long[] getAllTimestamps() { + long[] values = new long[mCount]; + if (mCount == 0) { + return values; + } + + if (mStart < mEnd) { + System.arraycopy(mRingTime, mStart, values, 0, mCount); + } else { + System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart); + System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd); + } + + return values; + } + public void push(long time, float lux) { int next = mEnd; if (mCount == mCapacity) { diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java index 8f59ffd30bba..fc8c6da6ad8d 100644 --- a/services/core/java/com/android/server/display/BrightnessTracker.java +++ b/services/core/java/com/android/server/display/BrightnessTracker.java @@ -79,10 +79,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.text.SimpleDateFormat; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Date; -import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -101,8 +99,6 @@ public class BrightnessTracker { private static final int MAX_EVENTS = 100; // Discard events when reading or writing that are older than this. private static final long MAX_EVENT_AGE = TimeUnit.DAYS.toMillis(30); - // Time over which we keep lux sensor readings. - private static final long LUX_EVENT_HORIZON = TimeUnit.SECONDS.toNanos(10); private static final String TAG_EVENTS = "events"; private static final String TAG_EVENT = "event"; @@ -174,8 +170,6 @@ public class BrightnessTracker { // Lock held while collecting data related to brightness changes. private final Object mDataCollectionLock = new Object(); @GuardedBy("mDataCollectionLock") - private Deque<LightData> mLastSensorReadings = new ArrayDeque<>(); - @GuardedBy("mDataCollectionLock") private float mLastBatteryLevel = Float.NaN; @GuardedBy("mDataCollectionLock") private float mLastBrightness = -1; @@ -327,7 +321,8 @@ public class BrightnessTracker { */ public void notifyBrightnessChanged(float brightness, boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness, - boolean isDefaultBrightnessConfig, String uniqueDisplayId) { + boolean isDefaultBrightnessConfig, String uniqueDisplayId, float[] luxValues, + long[] luxTimestamps) { if (DEBUG) { Slog.d(TAG, String.format("notifyBrightnessChanged(brightness=%f, userInitiated=%b)", brightness, userInitiated)); @@ -335,7 +330,7 @@ public class BrightnessTracker { Message m = mBgHandler.obtainMessage(MSG_BRIGHTNESS_CHANGED, userInitiated ? 1 : 0, 0 /*unused*/, new BrightnessChangeValues(brightness, powerBrightnessFactor, isUserSetBrightness, isDefaultBrightnessConfig, - mInjector.currentTimeMillis(), uniqueDisplayId)); + mInjector.currentTimeMillis(), uniqueDisplayId, luxValues, luxTimestamps)); m.sendToTarget(); } @@ -349,7 +344,8 @@ public class BrightnessTracker { private void handleBrightnessChanged(float brightness, boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness, - boolean isDefaultBrightnessConfig, long timestamp, String uniqueDisplayId) { + boolean isDefaultBrightnessConfig, long timestamp, String uniqueDisplayId, + float[] luxValues, long[] luxTimestamps) { BrightnessChangeEvent.Builder builder; synchronized (mDataCollectionLock) { @@ -376,28 +372,22 @@ public class BrightnessTracker { builder.setIsDefaultBrightnessConfig(isDefaultBrightnessConfig); builder.setUniqueDisplayId(uniqueDisplayId); - final int readingCount = mLastSensorReadings.size(); - if (readingCount == 0) { + if (luxValues.length == 0) { // No sensor data so ignore this. return; } - float[] luxValues = new float[readingCount]; - long[] luxTimestamps = new long[readingCount]; - - int pos = 0; + long[] luxTimestampsMillis = new long[luxTimestamps.length]; - // Convert sensor timestamp in elapsed time nanos to current time millis. + // Convert lux timestamp in elapsed time to current time. long currentTimeMillis = mInjector.currentTimeMillis(); long elapsedTimeNanos = mInjector.elapsedRealtimeNanos(); - for (LightData reading : mLastSensorReadings) { - luxValues[pos] = reading.lux; - luxTimestamps[pos] = currentTimeMillis - - TimeUnit.NANOSECONDS.toMillis(elapsedTimeNanos - reading.timestamp); - ++pos; + for (int i = 0; i < luxTimestamps.length; i++) { + luxTimestampsMillis[i] = currentTimeMillis - (TimeUnit.NANOSECONDS.toMillis( + elapsedTimeNanos) - luxTimestamps[i]); } builder.setLuxValues(luxValues); - builder.setLuxTimestamps(luxTimestamps); + builder.setLuxTimestamps(luxTimestampsMillis); builder.setBatteryLevel(mLastBatteryLevel); builder.setLastBrightness(previousBrightness); @@ -452,9 +442,6 @@ public class BrightnessTracker { if (mLightSensor != lightSensor) { mLightSensor = lightSensor; stopSensorListener(); - synchronized (mDataCollectionLock) { - mLastSensorReadings.clear(); - } // Attempt to restart the sensor listener. It will check to see if it should be running // so there is no need to also check here. startSensorListener(); @@ -774,12 +761,6 @@ public class BrightnessTracker { pw.println(" mLightSensor=" + mLightSensor); pw.println(" mLastBatteryLevel=" + mLastBatteryLevel); pw.println(" mLastBrightness=" + mLastBrightness); - pw.println(" mLastSensorReadings.size=" + mLastSensorReadings.size()); - if (!mLastSensorReadings.isEmpty()) { - pw.println(" mLastSensorReadings time span " - + mLastSensorReadings.peekFirst().timestamp + "->" - + mLastSensorReadings.peekLast().timestamp); - } } synchronized (mEventsLock) { pw.println(" mEventsDirty=" + mEventsDirty); @@ -895,43 +876,6 @@ public class BrightnessTracker { return ParceledListSlice.emptyList(); } - // Not allowed to keep the SensorEvent so used to copy the data we care about. - private static class LightData { - public float lux; - // Time in elapsedRealtimeNanos - public long timestamp; - } - - private void recordSensorEvent(SensorEvent event) { - long horizon = mInjector.elapsedRealtimeNanos() - LUX_EVENT_HORIZON; - synchronized (mDataCollectionLock) { - if (DEBUG) { - Slog.v(TAG, "Sensor event " + event); - } - if (!mLastSensorReadings.isEmpty() - && event.timestamp < mLastSensorReadings.getLast().timestamp) { - // Ignore event that came out of order. - return; - } - LightData data = null; - while (!mLastSensorReadings.isEmpty() - && mLastSensorReadings.getFirst().timestamp < horizon) { - // Remove data that has fallen out of the window. - data = mLastSensorReadings.removeFirst(); - } - // We put back the last one we removed so we know how long - // the first sensor reading was valid for. - if (data != null) { - mLastSensorReadings.addFirst(data); - } - - data = new LightData(); - data.timestamp = event.timestamp; - data.lux = event.values[0]; - mLastSensorReadings.addLast(data); - } - } - private void recordAmbientBrightnessStats(SensorEvent event) { mAmbientBrightnessStatsTracker.add(mCurrentUserId, event.values[0]); } @@ -945,7 +889,6 @@ public class BrightnessTracker { private final class SensorListener implements SensorEventListener { @Override public void onSensorChanged(SensorEvent event) { - recordSensorEvent(event); recordAmbientBrightnessStats(event); } @@ -1032,7 +975,7 @@ public class BrightnessTracker { handleBrightnessChanged(values.brightness, userInitiatedChange, values.powerBrightnessFactor, values.isUserSetBrightness, values.isDefaultBrightnessConfig, values.timestamp, - values.uniqueDisplayId); + values.uniqueDisplayId, values.luxValues, values.luxTimestamps); break; case MSG_START_SENSOR_LISTENER: startSensorListener(); @@ -1068,16 +1011,20 @@ public class BrightnessTracker { public final boolean isDefaultBrightnessConfig; public final long timestamp; public final String uniqueDisplayId; + public final float[] luxValues; + public final long[] luxTimestamps; BrightnessChangeValues(float brightness, float powerBrightnessFactor, boolean isUserSetBrightness, boolean isDefaultBrightnessConfig, - long timestamp, String uniqueDisplayId) { + long timestamp, String uniqueDisplayId, float[] luxValues, long[] luxTimestamps) { this.brightness = brightness; this.powerBrightnessFactor = powerBrightnessFactor; this.isUserSetBrightness = isUserSetBrightness; this.isDefaultBrightnessConfig = isDefaultBrightnessConfig; this.timestamp = timestamp; this.uniqueDisplayId = uniqueDisplayId; + this.luxValues = luxValues; + this.luxTimestamps = luxTimestamps; } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index d7bbb109b6f7..bb27651ed0ed 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -2471,7 +2471,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call : 1.0f; mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated, powerFactor, hadUserDataPoint, - mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId); + mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId, + mAutomaticBrightnessController.getLastSensorValues(), + mAutomaticBrightnessController.getLastSensorTimestamps()); } } diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java index a11f1721a4b9..73131a1dc220 100644 --- a/services/core/java/com/android/server/display/PersistentDataStore.java +++ b/services/core/java/com/android/server/display/PersistentDataStore.java @@ -619,7 +619,7 @@ final class PersistentDataStore { private static final class DisplayState { private int mColorMode; - private float mBrightness; + private float mBrightness = Float.NaN; private int mWidth; private int mHeight; private float mRefreshRate; @@ -700,7 +700,11 @@ final class PersistentDataStore { break; case TAG_BRIGHTNESS_VALUE: String brightness = parser.nextText(); - mBrightness = Float.parseFloat(brightness); + try { + mBrightness = Float.parseFloat(brightness); + } catch (NumberFormatException e) { + mBrightness = Float.NaN; + } break; case TAG_BRIGHTNESS_CONFIGURATIONS: mDisplayBrightnessConfigurations.loadFromXml(parser); @@ -727,7 +731,9 @@ final class PersistentDataStore { serializer.endTag(null, TAG_COLOR_MODE); serializer.startTag(null, TAG_BRIGHTNESS_VALUE); - serializer.text(Float.toString(mBrightness)); + if (!Float.isNaN(mBrightness)) { + serializer.text(Float.toString(mBrightness)); + } serializer.endTag(null, TAG_BRIGHTNESS_VALUE); serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java index 61936df4a124..babe4eafd8f3 100644 --- a/services/core/java/com/android/server/notification/SnoozeHelper.java +++ b/services/core/java/com/android/server/notification/SnoozeHelper.java @@ -59,6 +59,9 @@ public final class SnoozeHelper { static final int CONCURRENT_SNOOZE_LIMIT = 500; + // A safe size for strings to be put in persistent storage, to avoid breaking the XML write. + static final int MAX_STRING_LENGTH = 1000; + protected static final String XML_TAG_NAME = "snoozed-notifications"; private static final String XML_SNOOZED_NOTIFICATION = "notification"; @@ -200,7 +203,7 @@ public final class SnoozeHelper { scheduleRepost(key, duration); Long activateAt = System.currentTimeMillis() + duration; synchronized (mLock) { - mPersistedSnoozedNotifications.put(key, activateAt); + mPersistedSnoozedNotifications.put(getTrimmedString(key), activateAt); } } @@ -210,7 +213,10 @@ public final class SnoozeHelper { protected void snooze(NotificationRecord record, String contextId) { if (contextId != null) { synchronized (mLock) { - mPersistedSnoozedNotificationsWithContext.put(record.getKey(), contextId); + mPersistedSnoozedNotificationsWithContext.put( + getTrimmedString(record.getKey()), + getTrimmedString(contextId) + ); } } snooze(record); @@ -225,6 +231,13 @@ public final class SnoozeHelper { } } + private String getTrimmedString(String key) { + if (key != null && key.length() > MAX_STRING_LENGTH) { + return key.substring(0, MAX_STRING_LENGTH); + } + return key; + } + protected boolean cancel(int userId, String pkg, String tag, int id) { synchronized (mLock) { final Set<Map.Entry<String, NotificationRecord>> records = @@ -293,10 +306,12 @@ public final class SnoozeHelper { } protected void repost(String key, int userId, boolean muteOnReturn) { + final String trimmedKey = getTrimmedString(key); + NotificationRecord record; synchronized (mLock) { - mPersistedSnoozedNotifications.remove(key); - mPersistedSnoozedNotificationsWithContext.remove(key); + mPersistedSnoozedNotifications.remove(trimmedKey); + mPersistedSnoozedNotificationsWithContext.remove(trimmedKey); record = mSnoozedNotifications.remove(key); } diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java index 2a65ea2552b3..ced3a45935f8 100644 --- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java +++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java @@ -378,13 +378,14 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, try { conditionSatisfied = mStateConditions.get(state).getAsBoolean(); } catch (IllegalStateException e) { - // Failed to compute the current state based on current available data. Return + // Failed to compute the current state based on current available data. Continue // with the expectation that notifyDeviceStateChangedIfNeeded() will be called - // when a callback with the missing data is triggered. + // when a callback with the missing data is triggered. May trigger another state + // change if another state is satisfied currently. if (DEBUG) { Slog.d(TAG, "Unable to check current state", e); } - return; + continue; } if (conditionSatisfied) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index e776e68f108c..e19e921852ff 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5443,25 +5443,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private void postApplyAnimation(boolean visible, boolean fromTransition) { final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled(); - final boolean delayed = isAnimating(PARENTS | CHILDREN, + final boolean delayed = !usingShellTransitions && isAnimating(PARENTS | CHILDREN, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION | ANIMATION_TYPE_RECENTS); - if (!delayed) { + if (!delayed && !usingShellTransitions) { // We aren't delayed anything, but exiting windows rely on the animation finished // callback being called in case the ActivityRecord was pretending to be delayed, // which we might have done because we were in closing/opening apps list. - if (!usingShellTransitions) { - onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, null /* AnimationAdapter */); - if (visible) { - // The token was made immediately visible, there will be no entrance animation. - // We need to inform the client the enter animation was finished. - mEnteringAnimation = true; - mWmService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked( - token); - } - } else { - // update wallpaper target - setAppLayoutChanges(FINISH_LAYOUT_REDO_WALLPAPER, "ActivityRecord"); + onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, null /* AnimationAdapter */); + if (visible) { + // The token was made immediately visible, there will be no entrance animation. + // We need to inform the client the enter animation was finished. + mEnteringAnimation = true; + mWmService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked( + token); } } @@ -5470,8 +5465,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // updated. // If we're becoming invisible, update the client visibility if we are not running an // animation. Otherwise, we'll update client visibility in onAnimationFinished. - if (visible || !isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS) - || usingShellTransitions) { + if (visible || usingShellTransitions + || !isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)) { setClientVisible(visible); } @@ -7452,8 +7447,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else if (!show && mLastSurfaceShowing) { getSyncTransaction().hide(mSurfaceControl); } - if (show) { - mActivityRecordInputSink.applyChangesToSurfaceIfChanged(getSyncTransaction()); + // Input sink surface is not a part of animation, so just apply in a steady state + // (non-sync) with pending transaction. + if (show && mSyncState == SYNC_STATE_NONE) { + mActivityRecordInputSink.applyChangesToSurfaceIfChanged(getPendingTransaction()); } } if (mThumbnail != null) { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index eaa08fd5eb0b..185e06eecabb 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -1537,6 +1537,7 @@ public class DisplayRotation { private int mHalfFoldSavedRotation = -1; // No saved rotation private DeviceStateController.FoldState mFoldState = DeviceStateController.FoldState.UNKNOWN; + private boolean mInHalfFoldTransition = false; boolean overrideFrozenRotation() { return mFoldState == DeviceStateController.FoldState.HALF_FOLDED; @@ -1544,6 +1545,7 @@ public class DisplayRotation { boolean shouldRevertOverriddenRotation() { return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open. + && mInHalfFoldTransition && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted. && mUserRotationMode == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked. @@ -1552,6 +1554,7 @@ public class DisplayRotation { int revertOverriddenRotation() { int savedRotation = mHalfFoldSavedRotation; mHalfFoldSavedRotation = -1; + mInHalfFoldTransition = false; return savedRotation; } @@ -1577,16 +1580,11 @@ public class DisplayRotation { mService.updateRotation(false /* alwaysSendConfiguration */, false /* forceRelayout */); } else { - // Revert the rotation to our saved value if we transition from HALF_FOLDED. - if (mHalfFoldSavedRotation != -1) { - mRotation = mHalfFoldSavedRotation; - } - // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here - // so we will override USER_ROTATION_LOCKED and allow a rotation). + mInHalfFoldTransition = true; + mFoldState = newState; + // Tell the device to update its orientation. mService.updateRotation(false /* alwaysSendConfiguration */, false /* forceRelayout */); - // Once we are rotated, set mFoldstate, effectively removing the lock override. - mFoldState = newState; } } } @@ -1683,6 +1681,7 @@ public class DisplayRotation { private static class RotationHistory { private static final int MAX_SIZE = 8; + private static final int NO_FOLD_CONTROLLER = -2; private static class Record { final @Surface.Rotation int mFromRotation; final @Surface.Rotation int mToRotation; @@ -1694,6 +1693,9 @@ public class DisplayRotation { final String mLastOrientationSource; final @ActivityInfo.ScreenOrientation int mSourceOrientation; final long mTimestamp = System.currentTimeMillis(); + final int mHalfFoldSavedRotation; + final boolean mInHalfFoldTransition; + final DeviceStateController.FoldState mFoldState; Record(DisplayRotation dr, int fromRotation, int toRotation) { mFromRotation = fromRotation; @@ -1719,6 +1721,15 @@ public class DisplayRotation { mLastOrientationSource = null; mSourceOrientation = SCREEN_ORIENTATION_UNSET; } + if (dr.mFoldController != null) { + mHalfFoldSavedRotation = dr.mFoldController.mHalfFoldSavedRotation; + mInHalfFoldTransition = dr.mFoldController.mInHalfFoldTransition; + mFoldState = dr.mFoldController.mFoldState; + } else { + mHalfFoldSavedRotation = NO_FOLD_CONTROLLER; + mInHalfFoldTransition = false; + mFoldState = DeviceStateController.FoldState.UNKNOWN; + } } void dump(String prefix, PrintWriter pw) { @@ -1735,6 +1746,12 @@ public class DisplayRotation { if (mNonDefaultRequestingTaskDisplayArea != null) { pw.println(prefix + " requestingTda=" + mNonDefaultRequestingTaskDisplayArea); } + if (mHalfFoldSavedRotation != NO_FOLD_CONTROLLER) { + pw.println(prefix + " halfFoldSavedRotation=" + + mHalfFoldSavedRotation + + " mInHalfFoldTransition=" + mInHalfFoldTransition + + " mFoldState=" + mFoldState); + } } } diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index e6a0f4d5d77d..48258a11d13a 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -37,6 +37,7 @@ import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_SHADE; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER; +import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; @@ -651,6 +652,12 @@ class KeyguardController { mRequestDismissKeyguard = lastDismissKeyguardActivity != mDismissingKeyguardActivity && !mOccluded && !mKeyguardGoingAway && mDismissingKeyguardActivity != null; + if (mOccluded && mKeyguardShowing && !display.isSleeping() && !top.fillsParent() + && display.mWallpaperController.getWallpaperTarget() == null) { + // The occluding activity may be translucent or not fill screen. Then let wallpaper + // to check whether it should set itself as target to avoid blank background. + display.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + } if (mTopTurnScreenOnActivity != lastTurnScreenOnActivity && mTopTurnScreenOnActivity != null diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 84e30142f983..51eec03855a5 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -499,6 +499,12 @@ class Task extends TaskFragment { */ boolean mInRemoveTask; + /** + * When set, disassociate the leaf task if relaunched and reparented it to TDA as root task if + * possible. + */ + boolean mReparentLeafTaskIfRelaunch; + private final AnimatingActivityRegistry mAnimatingActivityRegistry = new AnimatingActivityRegistry(); @@ -2177,7 +2183,7 @@ class Task extends TaskFragment { } private boolean shouldStartChangeTransition(int prevWinMode, @NonNull Rect prevBounds) { - if (!isLeafTask() || !canStartChangeTransition()) { + if (!(isLeafTask() || mCreatedByOrganizer) || !canStartChangeTransition()) { return false; } final int newWinMode = getWindowingMode(); @@ -6142,6 +6148,12 @@ class Task extends TaskFragment { } } + void setReparentLeafTaskIfRelaunch(boolean reparentLeafTaskIfRelaunch) { + if (isOrganized()) { + mReparentLeafTaskIfRelaunch = reparentLeafTaskIfRelaunch; + } + } + @Override public void dumpDebug(ProtoOutputStream proto, long fieldId, @WindowTraceLogLevel int logLevel) { diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 1088a2e543eb..e0ed356fbe59 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -899,15 +899,16 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { } } else if (candidateTask != null) { final int position = onTop ? POSITION_TOP : POSITION_BOTTOM; - final Task launchRootTask = getLaunchRootTask(resolvedWindowingMode, activityType, + final Task launchParentTask = getLaunchRootTask(resolvedWindowingMode, activityType, options, sourceTask, launchFlags, candidateTask); - if (launchRootTask != null) { + if (launchParentTask != null) { if (candidateTask.getParent() == null) { - launchRootTask.addChild(candidateTask, position); - } else if (candidateTask.getParent() != launchRootTask) { - candidateTask.reparent(launchRootTask, position); + launchParentTask.addChild(candidateTask, position); + } else if (candidateTask.getParent() != launchParentTask) { + candidateTask.reparent(launchParentTask, position); } - } else if (candidateTask.getDisplayArea() != this) { + } else if (candidateTask.getDisplayArea() != this + || candidateTask.getRootTask().mReparentLeafTaskIfRelaunch) { if (candidateTask.getParent() == null) { addChild(candidateTask, position); } else { diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java index 14a2d0383019..0b90a7ce664c 100644 --- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java @@ -164,9 +164,11 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { } // If the launch windowing mode is still undefined, inherit from the target task if the // task is already on the right display area (otherwise, the task may be on a different - // display area that has incompatible windowing mode). + // display area that has incompatible windowing mode or the task organizer request to + // disassociate the leaf task if relaunched and reparented it to TDA as root task). if (launchMode == WINDOWING_MODE_UNDEFINED - && task != null && task.getTaskDisplayArea() == suggestedDisplayArea) { + && task != null && task.getTaskDisplayArea() == suggestedDisplayArea + && !task.getRootTask().mReparentLeafTaskIfRelaunch) { launchMode = task.getWindowingMode(); if (DEBUG) { appendLog("inherit-from-task=" diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index ec3962c282f6..6b14dba6eb81 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -646,8 +646,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (target.asDisplayContent() != null || target.asActivityRecord() != null) { t.setCrop(targetLeash, null /* crop */); } else { - // Crop to the requested bounds. - final Rect clipRect = target.getRequestedOverrideBounds(); + // Crop to the resolved override bounds. + final Rect clipRect = target.getResolvedOverrideBounds(); t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height()); } t.setCornerRadius(targetLeash, 0); @@ -1645,8 +1645,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, changes, topApp); - // make leash based on highest (z-order) direct child of ancestor with a participant. - WindowContainer leashReference = sortedTargets.get(0); + // Make leash based on highest (z-order) direct child of ancestor with a participant. + // TODO(b/261418859): Handle the case when the target contains window containers which + // belong to a different display. As a workaround we use topApp, from which wallpaper + // window container is removed, instead of sortedTargets here. + WindowContainer leashReference = topApp; while (leashReference.getParent() != ancestor) { leashReference = leashReference.getParent(); } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index d3d1c163aa19..971b619e671f 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -38,6 +38,7 @@ import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import android.view.SurfaceControl; import android.view.WindowManager; import android.window.ITransitionMetricsReporter; import android.window.ITransitionPlayer; @@ -116,6 +117,8 @@ class TransitionController { */ boolean mBuildingFinishLayers = false; + private final SurfaceControl.Transaction mWakeT = new SurfaceControl.Transaction(); + TransitionController(ActivityTaskManagerService atm, TaskSnapshotController taskSnapshotController, TransitionTracer transitionTracer) { @@ -619,8 +622,16 @@ class TransitionController { private void updateRunningRemoteAnimation(Transition transition, boolean isPlaying) { if (mTransitionPlayerProc == null) return; if (isPlaying) { + mWakeT.setEarlyWakeupStart(); + mWakeT.apply(); + // Usually transitions put quite a load onto the system already (with all the things + // happening in app), so pause task snapshot persisting to not increase the load. + mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(true); mTransitionPlayerProc.setRunningRemoteAnimation(true); } else if (mPlayingTransitions.isEmpty()) { + mWakeT.setEarlyWakeupEnd(); + mWakeT.apply(); + mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(false); mTransitionPlayerProc.setRunningRemoteAnimation(false); mRemotePlayer.clear(); return; diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index d85bd830cc11..69c4297ce717 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -42,6 +42,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_SHORTCUT; @@ -1236,7 +1237,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub WindowContainer.fromBinder(hop.getContainer()) .removeLocalInsetsSourceProvider(hop.getInsetsTypes()); break; - case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: + case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: { final WindowContainer container = WindowContainer.fromBinder(hop.getContainer()); if (container == null || container.asDisplayArea() == null || !container.isAttached()) { @@ -1247,7 +1248,26 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub container.setAlwaysOnTop(hop.isAlwaysOnTop()); effects |= TRANSACT_EFFECTS_LIFECYCLE; break; - + } + case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH: { + final WindowContainer container = WindowContainer.fromBinder(hop.getContainer()); + final Task task = container != null ? container.asTask() : null; + if (task == null || !task.isAttached()) { + Slog.e(TAG, "Attempt to operate on unknown or detached container: " + + container); + break; + } + if (!task.mCreatedByOrganizer) { + throw new UnsupportedOperationException( + "Cannot set reparent leaf task flag on non-organized task : " + task); + } + if (!task.isRootTask()) { + throw new UnsupportedOperationException( + "Cannot set reparent leaf task flag on non-root task : " + task); + } + task.setReparentLeafTaskIfRelaunch(hop.isReparentLeafTaskIfRelaunch()); + break; + } } return effects; } diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java index 4f2b613d7aed..bb08ef756a14 100644 --- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java @@ -18,6 +18,7 @@ package com.android.server.display; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; @@ -178,7 +179,7 @@ public class AutomaticBrightnessControllerTest { // Send new sensor value and verify listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1)); - assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), 0.001f); + assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON); // Set up system to return 0.0f (minimum possible brightness) as a brightness value float lux2 = 10.0f; @@ -192,7 +193,7 @@ public class AutomaticBrightnessControllerTest { // Send new sensor value and verify listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2)); - assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), 0.001f); + assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON); } @Test @@ -221,7 +222,7 @@ public class AutomaticBrightnessControllerTest { // Send new sensor value and verify listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1)); - assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), 0.001f); + assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON); // Set up system to return 1.0f as a brightness value (brightness_max) @@ -236,7 +237,7 @@ public class AutomaticBrightnessControllerTest { // Send new sensor value and verify listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2)); - assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), 0.001f); + assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON); } @Test @@ -418,6 +419,12 @@ public class AutomaticBrightnessControllerTest { // ambient lux goes to 0 listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); + + // only the values within the horizon should be kept + assertArrayEquals(new float[] {10000, 10000, 0, 0, 0}, mController.getLastSensorValues(), + EPSILON); + assertArrayEquals(new long[] {4000, 4500, 5000, 5500, 6000}, + mController.getLastSensorTimestamps()); } @Test @@ -489,4 +496,92 @@ public class AutomaticBrightnessControllerTest { 0 /* adjustment */, false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT); assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getAutomaticScreenBrightness(), 0.0f); } + + @Test + public void testGetSensorReadings() throws Exception { + ArgumentCaptor<SensorEventListener> listenerCaptor = + ArgumentCaptor.forClass(SensorEventListener.class); + verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor), + eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class)); + SensorEventListener listener = listenerCaptor.getValue(); + + // Choose values such that the ring buffer's capacity is extended and the buffer is pruned + int increment = 11; + int lux = 5000; + for (int i = 0; i < 1000; i++) { + lux += increment; + mClock.fastForward(increment); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + } + + int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment + 1); + float[] sensorValues = mController.getLastSensorValues(); + long[] sensorTimestamps = mController.getLastSensorTimestamps(); + + // Only the values within the horizon should be kept + assertEquals(valuesCount, sensorValues.length); + assertEquals(valuesCount, sensorTimestamps.length); + + long sensorTimestamp = mClock.now(); + for (int i = valuesCount - 1; i >= 1; i--) { + assertEquals(lux, sensorValues[i], EPSILON); + assertEquals(sensorTimestamp, sensorTimestamps[i]); + lux -= increment; + sensorTimestamp -= increment; + } + assertEquals(lux, sensorValues[0], EPSILON); + assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]); + } + + @Test + public void testGetSensorReadingsFullBuffer() throws Exception { + ArgumentCaptor<SensorEventListener> listenerCaptor = + ArgumentCaptor.forClass(SensorEventListener.class); + verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor), + eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class)); + SensorEventListener listener = listenerCaptor.getValue(); + int initialCapacity = 150; + + // Choose values such that the ring buffer is pruned + int increment1 = 200; + int lux = 5000; + for (int i = 0; i < 20; i++) { + lux += increment1; + mClock.fastForward(increment1); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + } + + int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment1 + 1); + + // Choose values such that the buffer becomes full + int increment2 = 1; + for (int i = 0; i < initialCapacity - valuesCount; i++) { + lux += increment2; + mClock.fastForward(increment2); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + } + + float[] sensorValues = mController.getLastSensorValues(); + long[] sensorTimestamps = mController.getLastSensorTimestamps(); + + // The buffer should be full + assertEquals(initialCapacity, sensorValues.length); + assertEquals(initialCapacity, sensorTimestamps.length); + + long sensorTimestamp = mClock.now(); + for (int i = initialCapacity - 1; i >= 1; i--) { + assertEquals(lux, sensorValues[i], EPSILON); + assertEquals(sensorTimestamp, sensorTimestamps[i]); + + if (i >= valuesCount) { + lux -= increment2; + sensorTimestamp -= increment2; + } else { + lux -= increment1; + sensorTimestamp -= increment1; + } + } + assertEquals(lux, sensorValues[0], EPSILON); + assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]); + } } diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java index 06422281ab25..5780ef3edc67 100644 --- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java @@ -136,28 +136,28 @@ public class BrightnessTrackerTest { assertNull(mInjector.mSensorListener); assertNotNull(mInjector.mBroadcastReceiver); assertTrue(mInjector.mIdleScheduled); - mInjector.sendScreenChange(/*screen on */ true); + mInjector.sendScreenChange(/* screenOn= */ true); assertNotNull(mInjector.mSensorListener); assertTrue(mInjector.mColorSamplingEnabled); - mInjector.sendScreenChange(/*screen on */ false); + mInjector.sendScreenChange(/* screenOn= */ false); assertNull(mInjector.mSensorListener); assertFalse(mInjector.mColorSamplingEnabled); // Turn screen on while brightness mode is manual - mInjector.setBrightnessMode(/* isBrightnessModeAutomatic */ false); - mInjector.sendScreenChange(/*screen on */ true); + mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false); + mInjector.sendScreenChange(/* screenOn= */ true); assertNull(mInjector.mSensorListener); assertFalse(mInjector.mColorSamplingEnabled); // Set brightness mode to automatic while screen is off. - mInjector.sendScreenChange(/*screen on */ false); - mInjector.setBrightnessMode(/* isBrightnessModeAutomatic */ true); + mInjector.sendScreenChange(/* screenOn= */ false); + mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true); assertNull(mInjector.mSensorListener); assertFalse(mInjector.mColorSamplingEnabled); // Turn on screen while brightness mode is automatic. - mInjector.sendScreenChange(/*screen on */ true); + mInjector.sendScreenChange(/* screenOn= */ true); assertNotNull(mInjector.mSensorListener); assertTrue(mInjector.mColorSamplingEnabled); @@ -188,14 +188,14 @@ public class BrightnessTrackerTest { assertFalse(mInjector.mColorSamplingEnabled); // Pretend screen is off, update config to turn on color sampling. - mInjector.sendScreenChange(/*screen on */ false); + mInjector.sendScreenChange(/* screenOn= */ false); mTracker.setBrightnessConfiguration(buildBrightnessConfiguration( /* collectColorSamples= */ true)); mInjector.waitForHandler(); assertFalse(mInjector.mColorSamplingEnabled); // Pretend screen is on. - mInjector.sendScreenChange(/*screen on */ true); + mInjector.sendScreenChange(/* screenOn= */ true); assertTrue(mInjector.mColorSamplingEnabled); mTracker.stop(); @@ -261,7 +261,7 @@ public class BrightnessTrackerTest { assertFalse(mInjector.mColorSamplingEnabled); assertNull(mInjector.mDisplayListener); - mInjector.setBrightnessMode(/*isBrightnessModeAutomatic*/ true); + mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true); assertNotNull(mInjector.mSensorListener); assertTrue(mInjector.mColorSamplingEnabled); assertNotNull(mInjector.mDisplayListener); @@ -272,16 +272,15 @@ public class BrightnessTrackerTest { mInjector.mColorSamplingEnabled = false; mInjector.mDisplayListener = null; // Duplicate notification - mInjector.setBrightnessMode(/*isBrightnessModeAutomatic*/ true); + mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true); // Sensor shouldn't have been registered as it was already registered. assertNull(mInjector.mSensorListener); assertFalse(mInjector.mColorSamplingEnabled); assertNull(mInjector.mDisplayListener); - mInjector.mSensorListener = listener; mInjector.mDisplayListener = displayListener; mInjector.mColorSamplingEnabled = true; - mInjector.setBrightnessMode(/*isBrightnessModeAutomatic*/ false); + mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false); assertNull(mInjector.mSensorListener); assertFalse(mInjector.mColorSamplingEnabled); assertNull(mInjector.mDisplayListener); @@ -301,19 +300,21 @@ public class BrightnessTrackerTest { final String displayId = "1234"; startTracker(mTracker); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f)); + final long sensorTime = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos()); mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2)); - notifyBrightnessChanged(mTracker, brightness, displayId); + final long currentTime = mInjector.currentTimeMillis(); + notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1.0f}, + new long[] {sensorTime}); List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList(); mTracker.stop(); assertEquals(1, events.size()); BrightnessChangeEvent event = events.get(0); - assertEquals(mInjector.currentTimeMillis(), event.timeStamp); + assertEquals(currentTime, event.timeStamp); assertEquals(displayId, event.uniqueDisplayId); assertEquals(1, event.luxValues.length); assertEquals(1.0f, event.luxValues[0], FLOAT_DELTA); - assertEquals(mInjector.currentTimeMillis() - TimeUnit.SECONDS.toMillis(2), + assertEquals(currentTime - TimeUnit.SECONDS.toMillis(2), event.luxTimestamps[0]); assertEquals(brightness, event.brightness, FLOAT_DELTA); assertEquals(DEFAULT_INITIAL_BRIGHTNESS, event.lastBrightness, FLOAT_DELTA); @@ -339,9 +340,9 @@ public class BrightnessTrackerTest { startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED); mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(), batteryChangeEvent(30, 60)); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f)); - final long sensorTime = mInjector.currentTimeMillis(); - notifyBrightnessChanged(mTracker, brightness, displayId); + final long currentTime = mInjector.currentTimeMillis(); + notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1000.0f}, + new long[] {TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos())}); List<BrightnessChangeEvent> eventsNoPackage = mTracker.getEvents(0, false).getList(); List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList(); @@ -349,10 +350,10 @@ public class BrightnessTrackerTest { assertEquals(1, events.size()); BrightnessChangeEvent event = events.get(0); - assertEquals(event.timeStamp, mInjector.currentTimeMillis()); + assertEquals(event.timeStamp, currentTime); assertEquals(displayId, event.uniqueDisplayId); - assertArrayEquals(new float[] {1000.0f}, event.luxValues, 0.01f); - assertArrayEquals(new long[] {sensorTime}, event.luxTimestamps); + assertArrayEquals(new float[] {1000.0f}, event.luxValues, FLOAT_DELTA); + assertArrayEquals(new long[] {currentTime}, event.luxTimestamps); assertEquals(brightness, event.brightness, FLOAT_DELTA); assertEquals(initialBrightness, event.lastBrightness, FLOAT_DELTA); assertEquals(0.5, event.batteryLevel, FLOAT_DELTA); @@ -374,13 +375,12 @@ public class BrightnessTrackerTest { public void testIgnoreAutomaticBrightnessChange() { final int initialBrightness = 30; startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f)); mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1)); final int systemUpdatedBrightness = 20; - notifyBrightnessChanged(mTracker, systemUpdatedBrightness, false /*userInitiated*/, - 0.5f /*powerBrightnessFactor(*/, false /*isUserSetBrightness*/, - false /*isDefaultBrightnessConfig*/, DEFAULT_DISPLAY_ID); + notifyBrightnessChanged(mTracker, systemUpdatedBrightness, /* userInitiated= */ false, + /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ false, + /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID); List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList(); // No events because we filtered out our change. assertEquals(0, events.size()); @@ -408,10 +408,8 @@ public class BrightnessTrackerTest { @Test public void testLimitedBufferSize() { startTracker(mTracker); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f)); for (int brightness = 0; brightness <= 255; ++brightness) { - mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f)); mInjector.incrementTime(TimeUnit.SECONDS.toNanos(1)); notifyBrightnessChanged(mTracker, brightness); } @@ -427,33 +425,6 @@ public class BrightnessTrackerTest { } @Test - public void testLimitedSensorEvents() { - final int brightness = 20; - - startTracker(mTracker); - // 20 Sensor events 1 second apart. - for (int i = 0; i < 20; ++i) { - mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1)); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(i + 1.0f)); - } - notifyBrightnessChanged(mTracker, 20); - List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList(); - mTracker.stop(); - - assertEquals(1, events.size()); - BrightnessChangeEvent event = events.get(0); - assertEquals(mInjector.currentTimeMillis(), event.timeStamp); - - // 12 sensor events, 11 for 0->10 seconds + 1 previous event. - assertEquals(12, event.luxValues.length); - for (int i = 0; i < 12; ++i) { - assertEquals(event.luxTimestamps[11 - i], - mInjector.currentTimeMillis() - i * TimeUnit.SECONDS.toMillis(1)); - } - assertEquals(brightness, event.brightness, FLOAT_DELTA); - } - - @Test public void testReadEvents() throws Exception { BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(), mInjector); @@ -607,15 +578,16 @@ public class BrightnessTrackerTest { startTracker(mTracker); mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(), batteryChangeEvent(30, 100)); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(2000.0f)); - final long firstSensorTime = mInjector.currentTimeMillis(); + final long elapsedTime1 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos()); + final long currentTime1 = mInjector.currentTimeMillis(); mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2)); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(3000.0f)); - final long secondSensorTime = mInjector.currentTimeMillis(); + final long elapsedTime2 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos()); + final long currentTime2 = mInjector.currentTimeMillis(); mInjector.incrementTime(TimeUnit.SECONDS.toMillis(3)); - notifyBrightnessChanged(mTracker, brightness, true /*userInitiated*/, - 0.5f /*powerBrightnessFactor*/, true /*hasUserBrightnessPoints*/, - false /*isDefaultBrightnessConfig*/, displayId); + notifyBrightnessChanged(mTracker, brightness, /* userInitiated= */ true, + /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ true, + /* isDefaultBrightnessConfig= */ false, displayId, new float[] {2000.0f, 3000.0f}, + new long[] {elapsedTime1, elapsedTime2}); ByteArrayOutputStream baos = new ByteArrayOutputStream(); mTracker.writeEventsLocked(baos); mTracker.stop(); @@ -631,7 +603,7 @@ public class BrightnessTrackerTest { BrightnessChangeEvent event = events.get(0); assertEquals(displayId, event.uniqueDisplayId); assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, FLOAT_DELTA); - assertArrayEquals(new long[] {firstSensorTime, secondSensorTime}, event.luxTimestamps); + assertArrayEquals(new long[] {currentTime1, currentTime2}, event.luxTimestamps); assertEquals(brightness, event.brightness, FLOAT_DELTA); assertEquals(0.3, event.batteryLevel, FLOAT_DELTA); assertTrue(event.nightMode); @@ -647,53 +619,6 @@ public class BrightnessTrackerTest { } @Test - public void testWritePrunesOldEvents() throws Exception { - final int brightness = 20; - - mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1); - mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3339); - - mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, 1); - mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL, 40); - - startTracker(mTracker); - mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(), - batteryChangeEvent(30, 100)); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f)); - mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1)); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(2000.0f)); - final long sensorTime = mInjector.currentTimeMillis(); - notifyBrightnessChanged(mTracker, brightness); - - // 31 days later - mInjector.incrementTime(TimeUnit.DAYS.toMillis(31)); - mInjector.mSensorListener.onSensorChanged(createSensorEvent(3000.0f)); - notifyBrightnessChanged(mTracker, brightness); - final long eventTime = mInjector.currentTimeMillis(); - - List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList(); - assertEquals(2, events.size()); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - mTracker.writeEventsLocked(baos); - events = mTracker.getEvents(0, true).getList(); - mTracker.stop(); - - assertEquals(1, events.size()); - BrightnessChangeEvent event = events.get(0); - assertEquals(eventTime, event.timeStamp); - - // We will keep one of the old sensor events because we keep 1 event outside the window. - assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, FLOAT_DELTA); - assertArrayEquals(new long[] {sensorTime, eventTime}, event.luxTimestamps); - assertEquals(brightness, event.brightness, FLOAT_DELTA); - assertEquals(0.3, event.batteryLevel, FLOAT_DELTA); - assertTrue(event.nightMode); - assertTrue(event.reduceBrightColors); - assertEquals(3339, event.colorTemperature); - } - - @Test public void testParcelUnParcel() { Parcel parcel = Parcel.obtain(); BrightnessChangeEvent.Builder builder = new BrightnessChangeEvent.Builder(); @@ -796,9 +721,10 @@ public class BrightnessTrackerTest { // Send an event. long eventTime = mInjector.currentTimeMillis(); - mTracker.notifyBrightnessChanged(brightness, true /*userInitiated*/, - 1.0f /*powerBrightnessFactor*/, false /*isUserSetBrightness*/, - false /*isDefaultBrightnessConfig*/, DEFAULT_DISPLAY_ID); + mTracker.notifyBrightnessChanged(brightness, /* userInitiated= */ true, + /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false, + /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID, new float[10], + new long[10]); // Time passes before handler can run. mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2)); @@ -890,20 +816,33 @@ public class BrightnessTrackerTest { public void testOnlyOneReceiverRegistered() { assertNull(mInjector.mLightSensor); assertNull(mInjector.mSensorListener); + assertNull(mInjector.mContentObserver); + assertNull(mInjector.mBroadcastReceiver); + assertFalse(mInjector.mIdleScheduled); startTracker(mTracker, 0.3f, false); assertNotNull(mInjector.mLightSensor); assertNotNull(mInjector.mSensorListener); + assertNotNull(mInjector.mContentObserver); + assertNotNull(mInjector.mBroadcastReceiver); + assertTrue(mInjector.mIdleScheduled); Sensor registeredLightSensor = mInjector.mLightSensor; SensorEventListener registeredSensorListener = mInjector.mSensorListener; + ContentObserver registeredContentObserver = mInjector.mContentObserver; + BroadcastReceiver registeredBroadcastReceiver = mInjector.mBroadcastReceiver; mTracker.start(0.3f); assertSame(registeredLightSensor, mInjector.mLightSensor); assertSame(registeredSensorListener, mInjector.mSensorListener); + assertSame(registeredContentObserver, mInjector.mContentObserver); + assertSame(registeredBroadcastReceiver, mInjector.mBroadcastReceiver); mTracker.stop(); assertNull(mInjector.mLightSensor); assertNull(mInjector.mSensorListener); + assertNull(mInjector.mContentObserver); + assertNull(mInjector.mBroadcastReceiver); + assertFalse(mInjector.mIdleScheduled); // mInjector asserts that we aren't removing a null receiver mTracker.stop(); @@ -954,23 +893,41 @@ public class BrightnessTrackerTest { private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness, String displayId) { - notifyBrightnessChanged(tracker, brightness, true /*userInitiated*/, - 1.0f /*powerBrightnessFactor*/, false /*isUserSetBrightness*/, - false /*isDefaultBrightnessConfig*/, displayId); + notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true, + /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false, + /* isDefaultBrightnessConfig= */ false, displayId, new float[10], new long[10]); + } + + private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness, + String displayId, float[] luxValues, long[] luxTimestamps) { + notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true, + /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false, + /* isDefaultBrightnessConfig= */ false, displayId, luxValues, luxTimestamps); } private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness, boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness, boolean isDefaultBrightnessConfig, String displayId) { tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor, - isUserSetBrightness, isDefaultBrightnessConfig, displayId); + isUserSetBrightness, isDefaultBrightnessConfig, displayId, new float[10], + new long[10]); + mInjector.waitForHandler(); + } + + private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness, + boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness, + boolean isDefaultBrightnessConfig, String displayId, float[] luxValues, + long[] luxTimestamps) { + tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor, + isUserSetBrightness, isDefaultBrightnessConfig, displayId, luxValues, + luxTimestamps); mInjector.waitForHandler(); } private BrightnessConfiguration buildBrightnessConfiguration(boolean collectColorSamples) { BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder( - /* lux = */ new float[] {0f, 10f, 100f}, - /* nits = */ new float[] {1f, 90f, 100f}); + /* lux= */ new float[] {0f, 10f, 100f}, + /* nits= */ new float[] {1f, 90f, 100f}); builder.setShouldCollectColorSamples(collectColorSamples); return builder.build(); } diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java index 3b0a22f80c30..35a677e0f816 100644 --- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java @@ -344,6 +344,40 @@ public class PersistentDataStoreTest { assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f); } + @Test + public void testBrightnessInitialisesWithInvalidFloat() { + final String uniqueDisplayId = "test:123"; + DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) { + @Override + public boolean hasStableUniqueId() { + return true; + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + return null; + } + }; + + // Set any value which initialises Display state + float refreshRate = 85.3f; + mDataStore.loadIfNeeded(); + mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + mInjector.setWriteStream(baos); + mDataStore.saveIfNeeded(); + mTestLooper.dispatchAll(); + assertTrue(mInjector.wasWriteSuccessful()); + TestInjector newInjector = new TestInjector(); + PersistentDataStore newDataStore = new PersistentDataStore(newInjector); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + newInjector.setReadStream(bais); + newDataStore.loadIfNeeded(); + assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice))); + } + + public class TestInjector extends PersistentDataStore.Injector { private InputStream mReadStream; private OutputStream mWriteStream; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java index 7817e8176e76..1f117b3697c9 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java @@ -234,6 +234,37 @@ public class SnoozeHelperTest extends UiServiceTestCase { } @Test + public void testScheduleRepostsForLongTagPersistedNotification() throws Exception { + String longTag = "A".repeat(66000); + NotificationRecord r = getNotificationRecord("pkg", 1, longTag, UserHandle.SYSTEM); + mSnoozeHelper.snooze(r, 0); + + // We store the full key in temp storage. + ArgumentCaptor<PendingIntent> captor = ArgumentCaptor.forClass(PendingIntent.class); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), anyLong(), captor.capture()); + assertEquals(66010, captor.getValue().getIntent().getStringExtra(EXTRA_KEY).length()); + + TypedXmlSerializer serializer = Xml.newFastSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + mSnoozeHelper.writeXml(serializer); + serializer.endDocument(); + serializer.flush(); + + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), "utf-8"); + mSnoozeHelper.readXml(parser, 4); + + mSnoozeHelper.scheduleRepostsForPersistedNotifications(5); + + // We trim the key in persistent storage. + verify(mAm, times(2)).setExactAndAllowWhileIdle(anyInt(), anyLong(), captor.capture()); + assertEquals(1000, captor.getValue().getIntent().getStringExtra(EXTRA_KEY).length()); + } + + @Test public void testSnoozeForTime() throws Exception { NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM); mSnoozeHelper.snooze(r, 1000); |