Merge "Boost remote-transition animator processes during animation" into sc-v2-dev
diff --git a/apct-tests/perftests/autofill/AndroidManifest.xml b/apct-tests/perftests/autofill/AndroidManifest.xml
index 57595a2..de2a3f2 100644
--- a/apct-tests/perftests/autofill/AndroidManifest.xml
+++ b/apct-tests/perftests/autofill/AndroidManifest.xml
@@ -16,6 +16,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.perftests.autofill">
+ <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
+ <uses-permission android:name="android.permission.DEVICE_POWER" />
+ <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+ <uses-permission android:name="android.permission.MANAGE_USERS" />
+ <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
<application>
<uses-library android:name="android.test.runner" />
<activity android:name="android.perftests.utils.PerfTestActivity"
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index bd9b6e9..ddde272 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -368,7 +368,8 @@
&& Objects.equals(taskDescription, that.taskDescription)
&& isFocused == that.isFocused
&& isVisible == that.isVisible
- && isSleeping == that.isSleeping;
+ && isSleeping == that.isSleeping
+ && Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId);
}
/**
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index b570ae6..11c01e6 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -586,12 +586,12 @@
Rect dimensions = null;
synchronized (this) {
+ ParcelFileDescriptor pfd = null;
try {
Bundle params = new Bundle();
+ pfd = mService.getWallpaperWithFeature(context.getOpPackageName(),
+ context.getAttributionTag(), this, FLAG_SYSTEM, params, userId);
// Let's peek user wallpaper first.
- ParcelFileDescriptor pfd = mService.getWallpaperWithFeature(
- context.getOpPackageName(), context.getAttributionTag(), this,
- FLAG_SYSTEM, params, userId);
if (pfd != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
@@ -600,6 +600,13 @@
}
} catch (RemoteException ex) {
Log.w(TAG, "peek wallpaper dimensions failed", ex);
+ } finally {
+ if (pfd != null) {
+ try {
+ pfd.close();
+ } catch (IOException ignored) {
+ }
+ }
}
}
// If user wallpaper is unavailable, may be the default one instead.
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 0686104..02b2c5d 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -124,7 +124,7 @@
public void setControl(@Nullable InsetsSourceControl control, int[] showTypes,
int[] hideTypes) {
super.setControl(control, showTypes, hideTypes);
- if (control == null && !mIsRequestedVisibleAwaitingControl) {
+ if (control == null && !isRequestedVisibleAwaitingControl()) {
hide();
removeSurface();
}
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 7d8d653..805727c 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -284,8 +284,8 @@
mShownOnFinish, mCurrentAlpha, mCurrentInsets));
mController.notifyFinished(this, mShownOnFinish);
releaseLeashes();
+ if (DEBUG) Log.d(TAG, "Animation finished abruptly.");
}
- if (DEBUG) Log.d(TAG, "Animation finished abruptly.");
return mFinished;
}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 2357d13..57b7d61 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -5746,11 +5746,9 @@
// persisted across change, and has the RemoteViews re-applied in a different situation
// (orientation or size), we throw an exception, since the layouts may be completely
// unrelated.
- if (hasMultipleLayouts()) {
- if (!rvToApply.canRecycleView(v)) {
- throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
- " that does not share the same root layout id.");
- }
+ if (!rvToApply.canRecycleView(v)) {
+ throw new RuntimeException("Attempting to re-apply RemoteViews to a view that"
+ + " that does not share the same root layout id.");
}
rvToApply.performApply(v, (ViewGroup) v.getParent(), handler, colorResources);
@@ -5794,11 +5792,9 @@
// In the case that a view has this RemoteViews applied in one orientation, is persisted
// across orientation change, and has the RemoteViews re-applied in the new orientation,
// we throw an exception, since the layouts may be completely unrelated.
- if (hasMultipleLayouts()) {
- if (!rvToApply.canRecycleView(v)) {
- throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
- " that does not share the same root layout id.");
- }
+ if (!rvToApply.canRecycleView(v)) {
+ throw new RuntimeException("Attempting to re-apply RemoteViews to a view that"
+ + " that does not share the same root layout id.");
}
return new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(),
diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java
index f748d4b..f04155d 100644
--- a/core/java/android/window/SplashScreenView.java
+++ b/core/java/android/window/SplashScreenView.java
@@ -20,6 +20,10 @@
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_AVD;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -54,6 +58,7 @@
import android.widget.ImageView;
import com.android.internal.R;
+import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.policy.DecorView;
import com.android.internal.util.ContrastColorUtil;
@@ -487,6 +492,23 @@
}
IconAnimateListener aniDrawable = (IconAnimateListener) iconDrawable;
aniDrawable.prepareAnimate(duration, this::animationStartCallback);
+ aniDrawable.setAnimationJankMonitoring(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ InteractionJankMonitor.getInstance().cancel(CUJ_SPLASHSCREEN_AVD);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ InteractionJankMonitor.getInstance().end(CUJ_SPLASHSCREEN_AVD);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ InteractionJankMonitor.getInstance().begin(
+ SplashScreenView.this, CUJ_SPLASHSCREEN_AVD);
+ }
+ });
}
private void animationStartCallback() {
@@ -669,6 +691,12 @@
* Stop animation.
*/
void stopAnimation();
+
+ /**
+ * Provides a chance to start interaction jank monitoring in avd animation.
+ * @param listener a listener to start jank monitoring
+ */
+ default void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {}
}
/**
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index c863292..d14054d 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -99,6 +99,7 @@
private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback;
private final Handler mHandler;
private final ChoreographerWrapper mChoreographer;
+ private final Object mLock = InteractionJankMonitor.getInstance().getLock();
@VisibleForTesting
public final boolean mSurfaceOnly;
@@ -181,7 +182,7 @@
mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
@Override
public void surfaceCreated(SurfaceControl.Transaction t) {
- synchronized (FrameTracker.this) {
+ synchronized (mLock) {
if (mSurfaceControl == null) {
mSurfaceControl = mViewRoot.getSurfaceControl();
if (mBeginVsyncId != INVALID_ID) {
@@ -203,12 +204,12 @@
// Wait a while to give the system a chance for the remaining
// frames to arrive, then force finish the session.
mHandler.postDelayed(() -> {
- synchronized (FrameTracker.this) {
+ synchronized (mLock) {
if (DEBUG) {
Log.d(TAG, "surfaceDestroyed: " + mSession.getName()
+ ", finalized=" + mMetricsFinalized
+ ", info=" + mJankInfos.size()
- + ", vsync=" + mBeginVsyncId + "-" + mEndVsyncId);
+ + ", vsync=" + mBeginVsyncId);
}
if (!mMetricsFinalized) {
end(REASON_END_SURFACE_DESTROYED);
@@ -227,20 +228,20 @@
/**
* Begin a trace session of the CUJ.
*/
- public synchronized void begin() {
- mBeginVsyncId = mChoreographer.getVsyncId() + 1;
- if (DEBUG) {
- Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
- }
- if (mSurfaceControl != null) {
- postTraceStartMarker();
- mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
- }
- if (!mSurfaceOnly) {
- mRendererWrapper.addObserver(mObserver);
- }
- if (mListener != null) {
- mListener.onCujEvents(mSession, ACTION_SESSION_BEGIN);
+ public void begin() {
+ synchronized (mLock) {
+ mBeginVsyncId = mChoreographer.getVsyncId() + 1;
+ if (DEBUG) {
+ Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
+ }
+ if (mSurfaceControl != null) {
+ postTraceStartMarker();
+ mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
+ }
+ if (!mSurfaceOnly) {
+ mRendererWrapper.addObserver(mObserver);
+ }
+ notifyCujEvent(ACTION_SESSION_BEGIN);
}
}
@@ -250,7 +251,7 @@
@VisibleForTesting
public void postTraceStartMarker() {
mChoreographer.mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, () -> {
- synchronized (FrameTracker.this) {
+ synchronized (mLock) {
if (mCancelled || mEndVsyncId != INVALID_ID) {
return;
}
@@ -263,88 +264,98 @@
/**
* End the trace session of the CUJ.
*/
- public synchronized void end(@Reasons int reason) {
- if (mEndVsyncId != INVALID_ID) return;
- mEndVsyncId = mChoreographer.getVsyncId();
+ public boolean end(@Reasons int reason) {
+ synchronized (mLock) {
+ if (mCancelled || mEndVsyncId != INVALID_ID) return false;
+ mEndVsyncId = mChoreographer.getVsyncId();
+ // Cancel the session if:
+ // 1. The session begins and ends at the same vsync id.
+ // 2. The session never begun.
+ if (mBeginVsyncId == INVALID_ID) {
+ return cancel(REASON_CANCEL_NOT_BEGUN);
+ } else if (mEndVsyncId <= mBeginVsyncId) {
+ return cancel(REASON_CANCEL_SAME_VSYNC);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "end: " + mSession.getName()
+ + ", end=" + mEndVsyncId + ", reason=" + reason);
+ }
+ Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
+ mSession.setReason(reason);
- // Cancel the session if:
- // 1. The session begins and ends at the same vsync id.
- // 2. The session never begun.
- if (mBeginVsyncId == INVALID_ID) {
- cancel(REASON_CANCEL_NOT_BEGUN);
- } else if (mEndVsyncId <= mBeginVsyncId) {
- cancel(REASON_CANCEL_SAME_VSYNC);
- } else {
- if (DEBUG) {
- Log.d(TAG, "end: " + mSession.getName()
- + ", end=" + mEndVsyncId + ", reason=" + reason);
+ // We don't remove observer here,
+ // will remove it when all the frame metrics in this duration are called back.
+ // See onFrameMetricsAvailable for the logic of removing the observer.
+ // Waiting at most 10 seconds for all callbacks to finish.
+ mWaitForFinishTimedOut = () -> {
+ Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
+ finish(mJankInfos.size() - 1);
+ };
+ mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
+ notifyCujEvent(ACTION_SESSION_END);
+ return true;
}
- Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
- mSession.setReason(reason);
- if (mListener != null) {
- mListener.onCujEvents(mSession, ACTION_SESSION_END);
- }
-
- // We don't remove observer here,
- // will remove it when all the frame metrics in this duration are called back.
- // See onFrameMetricsAvailable for the logic of removing the observer.
- // Waiting at most 10 seconds for all callbacks to finish.
- mWaitForFinishTimedOut = () -> {
- Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
- finish(mJankInfos.size() - 1);
- };
- mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
}
}
/**
* Cancel the trace session of the CUJ.
*/
- public synchronized void cancel(@Reasons int reason) {
- mCancelled = true;
+ public boolean cancel(@Reasons int reason) {
+ synchronized (mLock) {
+ final boolean cancelFromEnd =
+ reason == REASON_CANCEL_NOT_BEGUN || reason == REASON_CANCEL_SAME_VSYNC;
+ if (mCancelled || (mEndVsyncId != INVALID_ID && !cancelFromEnd)) return false;
+ mCancelled = true;
+ // We don't need to end the trace section if it never begun.
+ if (mTracingStarted) {
+ Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
+ }
- // We don't need to end the trace section if it never begun.
- if (mTracingStarted) {
- Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
- }
+ // Always remove the observers in cancel call to avoid leakage.
+ removeObservers();
- // Always remove the observers in cancel call to avoid leakage.
- removeObservers();
+ if (DEBUG) {
+ Log.d(TAG, "cancel: " + mSession.getName() + ", begin=" + mBeginVsyncId
+ + ", end=" + mEndVsyncId + ", reason=" + reason);
+ }
- if (DEBUG) {
- Log.d(TAG, "cancel: " + mSession.getName()
- + ", begin=" + mBeginVsyncId + ", end=" + mEndVsyncId + ", reason=" + reason);
- }
-
- mSession.setReason(reason);
- // Notify the listener the session has been cancelled.
- // We don't notify the listeners if the session never begun.
- if (mListener != null) {
- mListener.onCujEvents(mSession, ACTION_SESSION_CANCEL);
+ mSession.setReason(reason);
+ // Notify the listener the session has been cancelled.
+ // We don't notify the listeners if the session never begun.
+ notifyCujEvent(ACTION_SESSION_CANCEL);
+ return true;
}
}
- @Override
- public synchronized void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
- if (mCancelled) {
- return;
- }
+ private void notifyCujEvent(String action) {
+ if (mListener == null) return;
+ mListener.onCujEvents(mSession, action);
+ }
- for (SurfaceControl.JankData jankStat : jankData) {
- if (!isInRange(jankStat.frameVsyncId)) {
- continue;
+ @Override
+ public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
+ synchronized (mLock) {
+ if (mCancelled) {
+ return;
}
- JankInfo info = findJankInfo(jankStat.frameVsyncId);
- if (info != null) {
- info.surfaceControlCallbackFired = true;
- info.jankType = jankStat.jankType;
- } else {
- mJankInfos.put((int) jankStat.frameVsyncId,
- JankInfo.createFromSurfaceControlCallback(
- jankStat.frameVsyncId, jankStat.jankType));
+
+ for (SurfaceControl.JankData jankStat : jankData) {
+ if (!isInRange(jankStat.frameVsyncId)) {
+ continue;
+ }
+ JankInfo info = findJankInfo(jankStat.frameVsyncId);
+ if (info != null) {
+ info.surfaceControlCallbackFired = true;
+ info.jankType = jankStat.jankType;
+ } else {
+ mJankInfos.put((int) jankStat.frameVsyncId,
+ JankInfo.createFromSurfaceControlCallback(
+ jankStat.frameVsyncId, jankStat.jankType));
+ }
}
+ processJankInfos();
}
- processJankInfos();
}
private @Nullable JankInfo findJankInfo(long frameVsyncId) {
@@ -359,31 +370,34 @@
}
@Override
- public synchronized void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
- if (mCancelled) {
- return;
- }
+ public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
+ synchronized (mLock) {
+ if (mCancelled) {
+ return;
+ }
- // Since this callback might come a little bit late after the end() call.
- // We should keep tracking the begin / end timestamp.
- // Then compare with vsync timestamp to check if the frame is in the duration of the CUJ.
- long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
- boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
- long frameVsyncId = mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
+ // Since this callback might come a little bit late after the end() call.
+ // We should keep tracking the begin / end timestamp that we can compare with
+ // vsync timestamp to check if the frame is in the duration of the CUJ.
+ long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
+ boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
+ long frameVsyncId =
+ mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
- if (!isInRange(frameVsyncId)) {
- return;
+ if (!isInRange(frameVsyncId)) {
+ return;
+ }
+ JankInfo info = findJankInfo(frameVsyncId);
+ if (info != null) {
+ info.hwuiCallbackFired = true;
+ info.totalDurationNanos = totalDurationNanos;
+ info.isFirstFrame = isFirstFrame;
+ } else {
+ mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
+ frameVsyncId, totalDurationNanos, isFirstFrame));
+ }
+ processJankInfos();
}
- JankInfo info = findJankInfo(frameVsyncId);
- if (info != null) {
- info.hwuiCallbackFired = true;
- info.totalDurationNanos = totalDurationNanos;
- info.isFirstFrame = isFirstFrame;
- } else {
- mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
- frameVsyncId, totalDurationNanos, isFirstFrame));
- }
- processJankInfos();
}
/**
@@ -497,11 +511,7 @@
(int) (maxFrameTimeNanos / NANOS_IN_MILLISECOND));
// Trigger perfetto if necessary.
- boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
- && missedFramesCount >= mTraceThresholdMissedFrames;
- boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
- && maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
- if (overMissedFramesThreshold || overFrameTimeThreshold) {
+ if (shouldTriggerPerfetto(missedFramesCount, (int) maxFrameTimeNanos)) {
triggerPerfetto();
}
if (mSession.logToStatsd()) {
@@ -513,9 +523,7 @@
maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */
missedSfFramesCount,
missedAppFramesCount);
- if (mListener != null) {
- mListener.onCujEvents(mSession, ACTION_METRICS_LOGGED);
- }
+ notifyCujEvent(ACTION_METRICS_LOGGED);
}
if (DEBUG) {
Log.i(TAG, "finish: CUJ=" + mSession.getName()
@@ -528,6 +536,14 @@
}
}
+ private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) {
+ boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
+ && missedFramesCount >= mTraceThresholdMissedFrames;
+ boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
+ && maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
+ return overMissedFramesThreshold || overFrameTimeThreshold;
+ }
+
/**
* Remove all the registered listeners, observers and callbacks.
*/
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index f8eb95c..0ba5a39 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -58,6 +58,8 @@
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_ROW_EXPAND;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_ROW_SWIPE;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_SCROLL_FLING;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_AVD;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_EXIT_ANIM;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION;
@@ -173,6 +175,8 @@
public static final int CUJ_PIP_TRANSITION = 35;
public static final int CUJ_WALLPAPER_TRANSITION = 36;
public static final int CUJ_USER_SWITCH = 37;
+ public static final int CUJ_SPLASHSCREEN_AVD = 38;
+ public static final int CUJ_SPLASHSCREEN_EXIT_ANIM = 39;
private static final int NO_STATSD_LOGGING = -1;
@@ -219,6 +223,8 @@
UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION,
UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION,
UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH,
+ UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_AVD,
+ UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_EXIT_ANIM,
};
private static volatile InteractionJankMonitor sInstance;
@@ -230,6 +236,7 @@
private final SparseArray<FrameTracker> mRunningTrackers;
private final SparseArray<Runnable> mTimeoutActions;
private final HandlerThread mWorker;
+ private final Object mLock = new Object();
private boolean mEnabled = DEFAULT_ENABLED;
private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
@@ -276,6 +283,8 @@
CUJ_PIP_TRANSITION,
CUJ_WALLPAPER_TRANSITION,
CUJ_USER_SWITCH,
+ CUJ_SPLASHSCREEN_AVD,
+ CUJ_SPLASHSCREEN_EXIT_ANIM,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {
@@ -325,6 +334,10 @@
mPropertiesChangedListener);
}
+ Object getLock() {
+ return mLock;
+ }
+
/**
* Creates a {@link FrameTracker} instance.
*
@@ -344,7 +357,7 @@
final ChoreographerWrapper choreographer =
new ChoreographerWrapper(Choreographer.getInstance());
- synchronized (this) {
+ synchronized (mLock) {
FrameTrackerListener eventsListener =
(s, act) -> handleCujEvents(config.getContext(), act, s);
return new FrameTracker(session, mWorker.getThreadHandler(),
@@ -372,11 +385,16 @@
final boolean badEnd = action.equals(ACTION_SESSION_END)
&& session.getReason() != REASON_END_NORMAL;
final boolean badCancel = action.equals(ACTION_SESSION_CANCEL)
- && session.getReason() != REASON_CANCEL_NORMAL;
+ && !(session.getReason() == REASON_CANCEL_NORMAL
+ || session.getReason() == REASON_CANCEL_TIMEOUT);
return badEnd || badCancel;
}
- private void notifyEvents(Context context, String action, Session session) {
+ /**
+ * Notifies who may interest in some CUJ events.
+ */
+ @VisibleForTesting
+ public void notifyEvents(Context context, String action, Session session) {
if (action.equals(ACTION_SESSION_CANCEL)
&& session.getReason() == REASON_CANCEL_NOT_BEGUN) {
return;
@@ -389,7 +407,7 @@
}
private void removeTimeout(@CujType int cujType) {
- synchronized (this) {
+ synchronized (mLock) {
Runnable timeout = mTimeoutActions.get(cujType);
if (timeout != null) {
mWorker.getThreadHandler().removeCallbacks(timeout);
@@ -432,17 +450,9 @@
}
private boolean beginInternal(@NonNull Configuration conf) {
- synchronized (this) {
+ synchronized (mLock) {
int cujType = conf.mCujType;
- boolean shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
- if (!mEnabled || !shouldSample) {
- if (DEBUG) {
- Log.d(TAG, "Skip monitoring cuj: " + getNameOfCuj(cujType)
- + ", enable=" + mEnabled + ", debuggable=" + DEFAULT_ENABLED
- + ", sample=" + shouldSample + ", interval=" + mSamplingInterval);
- }
- return false;
- }
+ if (!shouldMonitor(cujType)) return false;
FrameTracker tracker = getTracker(cujType);
// Skip subsequent calls if we already have an ongoing tracing.
if (tracker != null) return false;
@@ -460,6 +470,24 @@
}
/**
+ * Check if the monitoring is enabled and if it should be sampled.
+ */
+ @SuppressWarnings("RandomModInteger")
+ @VisibleForTesting
+ public boolean shouldMonitor(@CujType int cujType) {
+ boolean shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
+ if (!mEnabled || !shouldSample) {
+ if (DEBUG) {
+ Log.d(TAG, "Skip monitoring cuj: " + getNameOfCuj(cujType)
+ + ", enable=" + mEnabled + ", debuggable=" + DEFAULT_ENABLED
+ + ", sample=" + shouldSample + ", interval=" + mSamplingInterval);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Schedules a timeout action.
* @param cuj cuj type
* @param timeout duration to timeout
@@ -478,14 +506,16 @@
* @return boolean true if the tracker is ended successfully, false otherwise.
*/
public boolean end(@CujType int cujType) {
- synchronized (this) {
+ synchronized (mLock) {
// remove the timeout action first.
removeTimeout(cujType);
FrameTracker tracker = getTracker(cujType);
// Skip this call since we haven't started a trace yet.
if (tracker == null) return false;
- tracker.end(REASON_END_NORMAL);
- removeTracker(cujType);
+ // if the end call doesn't return true, another thread is handling end of the cuj.
+ if (tracker.end(REASON_END_NORMAL)) {
+ removeTracker(cujType);
+ }
return true;
}
}
@@ -499,33 +529,37 @@
return cancel(cujType, REASON_CANCEL_NORMAL);
}
- boolean cancel(@CujType int cujType, @Reasons int reason) {
- synchronized (this) {
+ /**
+ * Cancels the trace session.
+ *
+ * @return boolean true if the tracker is cancelled successfully, false otherwise.
+ */
+ @VisibleForTesting
+ public boolean cancel(@CujType int cujType, @Reasons int reason) {
+ synchronized (mLock) {
// remove the timeout action first.
removeTimeout(cujType);
FrameTracker tracker = getTracker(cujType);
// Skip this call since we haven't started a trace yet.
if (tracker == null) return false;
- tracker.cancel(reason);
- removeTracker(cujType);
+ // if the cancel call doesn't return true, another thread is handling cancel of the cuj.
+ if (tracker.cancel(reason)) {
+ removeTracker(cujType);
+ }
return true;
}
}
private FrameTracker getTracker(@CujType int cuj) {
- synchronized (this) {
- return mRunningTrackers.get(cuj);
- }
+ return mRunningTrackers.get(cuj);
}
private void removeTracker(@CujType int cuj) {
- synchronized (this) {
- mRunningTrackers.remove(cuj);
- }
+ mRunningTrackers.remove(cuj);
}
private void updateProperties(DeviceConfig.Properties properties) {
- synchronized (this) {
+ synchronized (mLock) {
mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
DEFAULT_SAMPLING_INTERVAL);
mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
@@ -547,10 +581,8 @@
*/
@VisibleForTesting
public void trigger(Session session) {
- synchronized (this) {
- mWorker.getThreadHandler().post(
- () -> PerfettoTrigger.trigger(session.getPerfettoTrigger()));
- }
+ mWorker.getThreadHandler().post(
+ () -> PerfettoTrigger.trigger(session.getPerfettoTrigger()));
}
/**
@@ -648,6 +680,10 @@
return "WALLPAPER_TRANSITION";
case CUJ_USER_SWITCH:
return "USER_SWITCH";
+ case CUJ_SPLASHSCREEN_AVD:
+ return "SPLASHSCREEN_AVD";
+ case CUJ_SPLASHSCREEN_EXIT_ANIM:
+ return "SPLASHSCREEN_EXIT_ANIM";
}
return "UNKNOWN";
}
diff --git a/core/res/res/layout/alert_dialog.xml b/core/res/res/layout/alert_dialog.xml
index 59e56af..6869c5f 100644
--- a/core/res/res/layout/alert_dialog.xml
+++ b/core/res/res/layout/alert_dialog.xml
@@ -124,21 +124,21 @@
android:layout_width="0dip"
android:layout_gravity="start"
android:layout_weight="1"
- style="?android:attr/buttonBarButtonStyle"
+ style="?android:attr/buttonBarPositiveButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<Button android:id="@+id/button3"
android:layout_width="0dip"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
- style="?android:attr/buttonBarButtonStyle"
+ style="?android:attr/buttonBarNeutralButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<Button android:id="@+id/button2"
android:layout_width="0dip"
android:layout_gravity="end"
android:layout_weight="1"
- style="?android:attr/buttonBarButtonStyle"
+ style="?android:attr/buttonBarNegativeButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<LinearLayout android:id="@+id/rightSpacer"
diff --git a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index d7a5e26..0d2d047 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -16,6 +16,8 @@
package com.android.internal.jank;
+import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT;
+import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_TO_STATSD_INTERACTION_TYPE;
@@ -34,6 +36,7 @@
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.SystemClock;
import android.provider.DeviceConfig;
import android.view.View;
import android.view.ViewAttachTestActivity;
@@ -82,36 +85,23 @@
Handler handler = spy(new Handler(mActivity.getMainLooper()));
doReturn(true).when(handler).sendMessageAtTime(any(), anyLong());
- mWorker = spy(new HandlerThread("Interaction-jank-monitor-test"));
- doNothing().when(mWorker).start();
+ mWorker = mock(HandlerThread.class);
doReturn(handler).when(mWorker).getThreadHandler();
}
@Test
public void testBeginEnd() {
- // Should return false if the view is not attached.
- InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
- verify(mWorker).start();
-
- Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
- Configuration config = mock(Configuration.class);
- when(config.isSurfaceOnly()).thenReturn(false);
- FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
- new ThreadedRendererWrapper(mView.getThreadedRenderer()),
- new ViewRootWrapper(mView.getViewRootImpl()),
- new SurfaceControlWrapper(), mock(ChoreographerWrapper.class),
- new FrameMetricsWrapper(),
- /* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
- /* FrameTrackerListener */ null, config));
+ InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
+ FrameTracker tracker = createMockedFrameTracker(null);
doReturn(tracker).when(monitor).createFrameTracker(any(), any());
- doNothing().when(tracker).triggerPerfetto();
- doNothing().when(tracker).postTraceStartMarker();
+ doNothing().when(tracker).begin();
+ doReturn(true).when(tracker).end(anyInt());
// Simulate a trace session and see if begin / end are invoked.
- assertThat(monitor.begin(mView, session.getCuj())).isTrue();
+ assertThat(monitor.begin(mView, CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
verify(tracker).begin();
- assertThat(monitor.end(session.getCuj())).isTrue();
- verify(tracker).end(FrameTracker.REASON_END_NORMAL);
+ assertThat(monitor.end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
+ verify(tracker).end(REASON_END_NORMAL);
}
@Test
@@ -140,33 +130,23 @@
}
@Test
- public void testBeginCancel() {
- InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
-
+ public void testBeginTimeout() {
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-
- Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
- Configuration config = mock(Configuration.class);
- when(config.isSurfaceOnly()).thenReturn(false);
- FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
- new ThreadedRendererWrapper(mView.getThreadedRenderer()),
- new ViewRootWrapper(mView.getViewRootImpl()),
- new SurfaceControlWrapper(), mock(FrameTracker.ChoreographerWrapper.class),
- new FrameMetricsWrapper(),
- /* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
- /* FrameTrackerListener */ null, config));
+ InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
+ FrameTracker tracker = createMockedFrameTracker(null);
doReturn(tracker).when(monitor).createFrameTracker(any(), any());
- doNothing().when(tracker).triggerPerfetto();
- doNothing().when(tracker).postTraceStartMarker();
+ doNothing().when(tracker).begin();
+ doReturn(true).when(tracker).cancel(anyInt());
- assertThat(monitor.begin(mView, session.getCuj())).isTrue();
+ assertThat(monitor.begin(mView, CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
verify(tracker).begin();
verify(monitor).scheduleTimeoutAction(anyInt(), anyLong(), captor.capture());
Runnable runnable = captor.getValue();
assertThat(runnable).isNotNull();
mWorker.getThreadHandler().removeCallbacks(runnable);
runnable.run();
- verify(tracker).cancel(FrameTracker.REASON_CANCEL_TIMEOUT);
+ verify(monitor).cancel(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, REASON_CANCEL_TIMEOUT);
+ verify(tracker).cancel(REASON_CANCEL_TIMEOUT);
}
@Test
@@ -192,4 +172,43 @@
.isTrue();
}
}
+
+ private InteractionJankMonitor createMockedInteractionJankMonitor() {
+ InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
+ doReturn(true).when(monitor).shouldMonitor(anyInt());
+ doNothing().when(monitor).notifyEvents(any(), any(), any());
+ return monitor;
+ }
+
+ private FrameTracker createMockedFrameTracker(FrameTracker.FrameTrackerListener listener) {
+ Session session = spy(new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX));
+ doReturn(false).when(session).logToStatsd();
+
+ ThreadedRendererWrapper threadedRenderer = mock(ThreadedRendererWrapper.class);
+ doNothing().when(threadedRenderer).addObserver(any());
+ doNothing().when(threadedRenderer).removeObserver(any());
+
+ ViewRootWrapper viewRoot = spy(new ViewRootWrapper(mView.getViewRootImpl()));
+ doNothing().when(viewRoot).addSurfaceChangedCallback(any());
+
+ SurfaceControlWrapper surfaceControl = mock(SurfaceControlWrapper.class);
+ doNothing().when(surfaceControl).addJankStatsListener(any(), any());
+ doNothing().when(surfaceControl).removeJankStatsListener(any());
+
+ final ChoreographerWrapper choreographer = mock(ChoreographerWrapper.class);
+ doReturn(SystemClock.elapsedRealtime()).when(choreographer).getVsyncId();
+
+ Configuration configuration = mock(Configuration.class);
+ when(configuration.isSurfaceOnly()).thenReturn(false);
+
+ FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
+ threadedRenderer, viewRoot, surfaceControl, choreographer,
+ new FrameMetricsWrapper(), /* traceThresholdMissedFrames= */ 1,
+ /* traceThresholdFrameTimeMillis= */ -1, listener, configuration));
+
+ doNothing().when(tracker).postTraceStartMarker();
+ doNothing().when(tracker).triggerPerfetto();
+
+ return tracker;
+ }
}
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml b/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml
new file mode 100644
index 0000000..94165a1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/size_compat_hint_bubble"/>
+ <corners android:radius="@dimen/size_compat_hint_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml b/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
new file mode 100644
index 0000000..a8f0f76
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/size_compat_hint_point_width"
+ android:height="8dp"
+ android:viewportWidth="10"
+ android:viewportHeight="8">
+ <path
+ android:fillColor="@color/size_compat_hint_bubble"
+ android:pathData="M10,0 l-4.1875,6.6875 a1,1 0 0,1 -1.625,0 l-4.1875,-6.6875z"/>
+</vector>
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 73a48d3..3e486df 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
@@ -15,14 +15,21 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
<path
- android:fillColor="#aa000000"
- android:pathData="M0,12 a12,12 0 1,0 24,0 a12,12 0 1,0 -24,0" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M17.65,6.35c-1.63,-1.63 -3.94,-2.57 -6.48,-2.31c-3.67,0.37 -6.69,3.35 -7.1,7.02C3.52,15.91 7.27,20 12,20c3.19,0 5.93,-1.87 7.21,-4.57c0.31,-0.66 -0.16,-1.43 -0.89,-1.43h-0.01c-0.37,0 -0.72,0.2 -0.88,0.53c-1.13,2.43 -3.84,3.97 -6.81,3.32c-2.22,-0.49 -4.01,-2.3 -4.49,-4.52C5.31,9.44 8.26,6 12,6c1.66,0 3.14,0.69 4.22,1.78l-2.37,2.37C13.54,10.46 13.76,11 14.21,11H19c0.55,0 1,-0.45 1,-1V5.21c0,-0.45 -0.54,-0.67 -0.85,-0.35L17.65,6.35z"/>
+ android:fillColor="#53534D"
+ android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0" />
+ <group
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:fillColor="#E4E3DA"
+ 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="#E4E3DA"
+ 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"/>
+ </group>
</vector>
diff --git a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
index 0dea87c..17347f6 100644
--- a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
+++ b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
@@ -22,41 +22,34 @@
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="center"
android:clipToPadding="false"
- android:padding="@dimen/bubble_elevation">
+ android:paddingBottom="5dp">
<LinearLayout
+ android:id="@+id/size_compat_hint_popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@android:color/background_light"
- android:elevation="@dimen/bubble_elevation"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:clickable="true">
<TextView
- android:layout_width="180dp"
+ android:layout_width="188dp"
android:layout_height="wrap_content"
- android:paddingLeft="10dp"
- android:paddingRight="10dp"
- android:paddingTop="10dp"
+ android:lineSpacingExtra="4sp"
+ android:background="@drawable/size_compat_hint_bubble"
+ android:padding="16dp"
android:text="@string/restart_button_description"
android:textAlignment="viewStart"
- android:textColor="@android:color/primary_text_light"
- android:textSize="16sp"/>
+ android:textColor="#E4E3DA"
+ android:textSize="14sp"/>
- <Button
- android:id="@+id/got_it"
+ <ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:includeFontPadding="false"
android:layout_gravity="end"
- android:minHeight="36dp"
- android:background="?android:attr/selectableItemBackground"
- android:text="@string/got_it"
- android:textAllCaps="true"
- android:textColor="#3c78d8"
- android:textSize="16sp"
- android:textStyle="bold"/>
+ android:src="@drawable/size_compat_hint_point"
+ android:paddingHorizontal="@dimen/size_compat_hint_corner_radius"
+ android:contentDescription="@null"/>
</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml b/libs/WindowManager/Shell/res/layout/size_compat_ui.xml
index cd31531..47e76f0 100644
--- a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml
+++ b/libs/WindowManager/Shell/res/layout/size_compat_ui.xml
@@ -19,12 +19,21 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <ImageButton
- android:id="@+id/size_compat_restart_button"
- android:layout_width="@dimen/size_compat_button_size"
- android:layout_height="@dimen/size_compat_button_size"
- android:layout_gravity="center"
- android:src="@drawable/size_compat_restart_button"
- android:contentDescription="@string/restart_button_description"/>
+ <FrameLayout
+ android:layout_width="@dimen/size_compat_button_width"
+ android:layout_height="@dimen/size_compat_button_height"
+ android:clipToPadding="false"
+ android:paddingBottom="16dp">
+
+ <ImageButton
+ android:id="@+id/size_compat_restart_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/size_compat_restart_button"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/restart_button_description"/>
+
+ </FrameLayout>
</com.android.wm.shell.sizecompatui.SizeCompatRestartButton>
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 93c0352..b25a218 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -29,6 +29,7 @@
<color name="bubbles_light">#FFFFFF</color>
<color name="bubbles_dark">@color/GM2_grey_800</color>
<color name="bubbles_icon_tint">@color/GM2_grey_700</color>
+ <color name="size_compat_hint_bubble">#30312B</color>
<!-- GM2 colors -->
<color name="GM2_grey_200">#E8EAED</color>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f8576643..11bb4e9 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -194,8 +194,17 @@
<!-- Size of user education views on large screens (phone is just match parent). -->
<dimen name="bubbles_user_education_width_large_screen">400dp</dimen>
- <!-- The width/height of the size compat restart button. -->
- <dimen name="size_compat_button_size">48dp</dimen>
+ <!-- The width of the size compat restart button including padding. -->
+ <dimen name="size_compat_button_width">80dp</dimen>
+
+ <!-- The height of the size compat restart button including padding. -->
+ <dimen name="size_compat_button_height">64dp</dimen>
+
+ <!-- The radius of the corners of the size compat hint bubble. -->
+ <dimen name="size_compat_hint_corner_radius">28dp</dimen>
+
+ <!-- The width of the size compat hint point. -->
+ <dimen name="size_compat_hint_point_width">10dp</dimen>
<!-- The width of the brand image on staring surface. -->
<dimen name="starting_surface_brand_image_width">200dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index e512698..764854a 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -155,7 +155,4 @@
<!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
<string name="restart_button_description">Tap to restart this app and go full screen.</string>
-
- <!-- Generic "got it" acceptance of dialog or cling [CHAR LIMIT=NONE] -->
- <string name="got_it">Got it</string>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index b6d65be..f7af4e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1028,13 +1028,16 @@
// If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason.
// This means that the app or channel's ability to bubble has been revoked.
mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED);
- } else if (isActiveBubble && !shouldBubbleUp) {
- // If this entry is allowed to bubble, but cannot currently bubble up, dismiss it.
- // This happens when DND is enabled and configured to hide bubbles. Dismissing with
- // the reason DISMISS_NO_BUBBLE_UP will retain the underlying notification, so that
- // the bubble will be re-created if shouldBubbleUp returns true.
+ } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) {
+ // If this entry is allowed to bubble, but cannot currently bubble up or is
+ // suspended, dismiss it. This happens when DND is enabled and configured to hide
+ // bubbles, or focus mode is enabled and the app is designated as distracting.
+ // Dismissing with the reason DISMISS_NO_BUBBLE_UP will retain the underlying
+ // notification, so that the bubble will be re-created if shouldBubbleUp returns
+ // true.
mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP);
- } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) {
+ } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble
+ && !entry.getRanking().isSuspended()) {
entry.setFlagBubble(true);
onEntryUpdated(entry, true /* shouldBubbleUp */);
}
@@ -1134,7 +1137,8 @@
if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) {
continue;
}
- if (reason == DISMISS_NOTIF_CANCEL) {
+ if (reason == DISMISS_NOTIF_CANCEL
+ || reason == DISMISS_SHORTCUT_REMOVED) {
bubblesToBeRemovedFromRepository.add(bubble);
}
if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java
new file mode 100644
index 0000000..b77ac8a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import androidx.annotation.BinderThread;
+
+import java.util.function.Consumer;
+
+/**
+ * Manages the lifecycle of a single instance of a remote listener, including the clean up if the
+ * remote process dies. All calls on this class should happen on the main shell thread.
+ *
+ * @param <C> The controller (must be RemoteCallable)
+ * @param <L> The remote listener interface type
+ */
+public class SingleInstanceRemoteListener<C extends RemoteCallable, L extends IInterface> {
+ private static final String TAG = SingleInstanceRemoteListener.class.getSimpleName();
+
+ /**
+ * Simple callable interface that throws a remote exception.
+ */
+ public interface RemoteCall<L> {
+ void accept(L l) throws RemoteException;
+ }
+
+ private final C mCallableController;
+ private final Consumer<C> mOnRegisterCallback;
+ private final Consumer<C> mOnUnregisterCallback;
+
+ L mListener;
+
+ private final IBinder.DeathRecipient mListenerDeathRecipient =
+ new IBinder.DeathRecipient() {
+ @Override
+ @BinderThread
+ public void binderDied() {
+ final C callableController = mCallableController;
+ mCallableController.getRemoteCallExecutor().execute(() -> {
+ mListener = null;
+ mOnUnregisterCallback.accept(callableController);
+ });
+ }
+ };
+
+ /**
+ * @param onRegisterCallback Callback when register() is called (same thread)
+ * @param onUnregisterCallback Callback when unregister() is called (same thread as unregister()
+ * or the callableController.getRemoteCallbackExecutor() thread)
+ */
+ public SingleInstanceRemoteListener(C callableController,
+ Consumer<C> onRegisterCallback,
+ Consumer<C> onUnregisterCallback) {
+ mCallableController = callableController;
+ mOnRegisterCallback = onRegisterCallback;
+ mOnUnregisterCallback = onUnregisterCallback;
+ }
+
+ /**
+ * Registers this listener, storing a reference to it and calls the provided method in the
+ * constructor.
+ */
+ public void register(L listener) {
+ if (mListener != null) {
+ mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, 0 /* flags */);
+ }
+ if (listener != null) {
+ try {
+ listener.asBinder().linkToDeath(mListenerDeathRecipient, 0 /* flags */);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to link to death");
+ return;
+ }
+ }
+ mListener = listener;
+ mOnRegisterCallback.accept(mCallableController);
+ }
+
+ /**
+ * Unregisters this listener, removing all references to it and calls the provided method in the
+ * constructor.
+ */
+ public void unregister() {
+ if (mListener != null) {
+ mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, 0 /* flags */);
+ }
+ mListener = null;
+ mOnUnregisterCallback.accept(mCallableController);
+ }
+
+ /**
+ * Safely wraps a call to the remote listener.
+ */
+ public void call(RemoteCall<L> handler) {
+ if (mListener == null) {
+ Slog.e(TAG, "Failed remote call on null listener");
+ return;
+ }
+ try {
+ handler.accept(mListener);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed remote call", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java
index 6e4b815..2a7dd5a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java
@@ -25,6 +25,8 @@
import android.content.pm.ActivityInfo;
import android.view.DragEvent;
+import androidx.annotation.Nullable;
+
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.UiEvent;
@@ -61,9 +63,7 @@
mInstanceId = mIdSequence.newInstanceId();
}
mActivityInfo = item.getActivityInfo();
- mUiEventLogger.logWithInstanceId(getStartEnum(description),
- mActivityInfo.applicationInfo.uid,
- mActivityInfo.applicationInfo.packageName, mInstanceId);
+ log(getStartEnum(description), mActivityInfo);
return mInstanceId;
}
@@ -71,18 +71,21 @@
* Logs a successful drop.
*/
public void logDrop() {
- mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_DROPPED,
- mActivityInfo.applicationInfo.uid,
- mActivityInfo.applicationInfo.packageName, mInstanceId);
+ log(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_DROPPED, mActivityInfo);
}
/**
* Logs the end of a drag.
*/
public void logEnd() {
- mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_END,
- mActivityInfo.applicationInfo.uid,
- mActivityInfo.applicationInfo.packageName, mInstanceId);
+ log(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_END, mActivityInfo);
+ }
+
+ private void log(UiEventLogger.UiEventEnum event, @Nullable ActivityInfo activityInfo) {
+ mUiEventLogger.logWithInstanceId(event,
+ activityInfo == null ? 0 : activityInfo.applicationInfo.uid,
+ activityInfo == null ? null : activityInfo.applicationInfo.packageName,
+ mInstanceId);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 5fb3297..8a8d7c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.freeform;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
@@ -27,6 +28,7 @@
import android.util.Slog;
import android.util.SparseArray;
import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -83,6 +85,13 @@
Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
return;
}
+
+ // Clears windowing mode and window bounds to let the task inherits from its new parent.
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(taskInfo.token, null)
+ .setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ mSyncQueue.queue(wct);
+
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d",
taskInfo.taskId);
mTasks.remove(taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 8e5c5c5..51eea37 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -43,7 +43,6 @@
import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
import android.graphics.Rect;
-import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
@@ -69,6 +68,7 @@
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.onehanded.OneHandedController;
@@ -117,13 +117,28 @@
private final Rect mTmpInsetBounds = new Rect();
private boolean mIsInFixedRotation;
- private IPipAnimationListener mPinnedStackAnimationRecentsCallback;
+ private PipAnimationListener mPinnedStackAnimationRecentsCallback;
protected PhonePipMenuController mMenuController;
protected PipTaskOrganizer mPipTaskOrganizer;
protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener =
new PipControllerPinnedTaskListener();
+ private interface PipAnimationListener {
+ /**
+ * Notifies the listener that the Pip animation is started.
+ */
+ void onPipAnimationStarted();
+
+ /**
+ * Notifies the listener about PiP round corner radius changes.
+ * Listener can expect an immediate callback the first time they attach.
+ *
+ * @param cornerRadius the pixel value of the corner radius, zero means it's disabled.
+ */
+ void onPipCornerRadiusChanged(int cornerRadius);
+ }
+
/**
* Handler for display rotation changes.
*/
@@ -551,7 +566,7 @@
animationType == PipAnimationController.ANIM_TYPE_BOUNDS);
}
- private void setPinnedStackAnimationListener(IPipAnimationListener callback) {
+ private void setPinnedStackAnimationListener(PipAnimationListener callback) {
mPinnedStackAnimationRecentsCallback = callback;
onPipCornerRadiusChanged();
}
@@ -560,11 +575,7 @@
if (mPinnedStackAnimationRecentsCallback != null) {
final int cornerRadius =
mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius);
- try {
- mPinnedStackAnimationRecentsCallback.onPipCornerRadiusChanged(cornerRadius);
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to call onPipCornerRadiusChanged", e);
- }
+ mPinnedStackAnimationRecentsCallback.onPipCornerRadiusChanged(cornerRadius);
}
}
@@ -623,11 +634,7 @@
// Disable touches while the animation is running
mTouchHandler.setTouchEnabled(false);
if (mPinnedStackAnimationRecentsCallback != null) {
- try {
- mPinnedStackAnimationRecentsCallback.onPipAnimationStarted();
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to call onPinnedStackAnimationStarted()", e);
- }
+ mPinnedStackAnimationRecentsCallback.onPipAnimationStarted();
}
}
@@ -866,22 +873,25 @@
@BinderThread
private static class IPipImpl extends IPip.Stub {
private PipController mController;
- private IPipAnimationListener mListener;
- private final IBinder.DeathRecipient mListenerDeathRecipient =
- new IBinder.DeathRecipient() {
- @Override
- @BinderThread
- public void binderDied() {
- final PipController controller = mController;
- controller.getRemoteCallExecutor().execute(() -> {
- mListener = null;
- controller.setPinnedStackAnimationListener(null);
- });
- }
- };
+ private final SingleInstanceRemoteListener<PipController,
+ IPipAnimationListener> mListener;
+ private final PipAnimationListener mPipAnimationListener = new PipAnimationListener() {
+ @Override
+ public void onPipAnimationStarted() {
+ mListener.call(l -> l.onPipAnimationStarted());
+ }
+
+ @Override
+ public void onPipCornerRadiusChanged(int cornerRadius) {
+ mListener.call(l -> l.onPipCornerRadiusChanged(cornerRadius));
+ }
+ };
IPipImpl(PipController controller) {
mController = controller;
+ mListener = new SingleInstanceRemoteListener<>(mController,
+ c -> c.setPinnedStackAnimationListener(mPipAnimationListener),
+ c -> c.setPinnedStackAnimationListener(null));
}
/**
@@ -925,23 +935,11 @@
public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener",
(controller) -> {
- if (mListener != null) {
- // Reset the old death recipient
- mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- }
if (listener != null) {
- // Register the death recipient for the new listener to clear the listener
- try {
- listener.asBinder().linkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to link to death");
- return;
- }
+ mListener.register(listener);
+ } else {
+ mListener.unregister();
}
- mListener = listener;
- controller.setPinnedStackAnimationListener(listener);
});
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java
index 78af9df..ff6f913 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java
@@ -17,13 +17,10 @@
package com.android.wm.shell.sizecompatui;
import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
import android.view.View;
-import android.widget.Button;
import android.widget.FrameLayout;
+import android.widget.LinearLayout;
import androidx.annotation.Nullable;
@@ -58,10 +55,8 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- final Button gotItButton = findViewById(R.id.got_it);
- gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY),
- null /* content */, null /* mask */));
- gotItButton.setOnClickListener(this);
+ final LinearLayout hintPopup = findViewById(R.id.size_compat_hint_popup);
+ hintPopup.setOnClickListener(this);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
index 08a8402..d75fe517 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
@@ -17,10 +17,6 @@
package com.android.wm.shell.sizecompatui;
import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
@@ -63,11 +59,6 @@
protected void onFinishInflate() {
super.onFinishInflate();
final ImageButton restartButton = findViewById(R.id.size_compat_restart_button);
- final ColorStateList color = ColorStateList.valueOf(Color.LTGRAY);
- final GradientDrawable mask = new GradientDrawable();
- mask.setShape(GradientDrawable.OVAL);
- mask.setColor(color);
- restartButton.setBackground(new RippleDrawable(color, null /* content */, mask));
restartButton.setOnClickListener(this);
restartButton.setOnLongClickListener(this);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
index 7cf9559..bebb6d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
@@ -25,6 +25,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Binder;
@@ -54,6 +55,10 @@
private final int mTaskId;
private ShellTaskOrganizer.TaskListener mTaskListener;
private DisplayLayout mDisplayLayout;
+ private final int mButtonWidth;
+ private final int mButtonHeight;
+ private final int mPopupOffsetX;
+ private final int mPopupOffsetY;
@VisibleForTesting
final SizeCompatUIWindowManager mButtonWindowManager;
@@ -66,9 +71,7 @@
@VisibleForTesting
@Nullable
SizeCompatHintPopup mHint;
- final int mButtonSize;
- final int mPopupOffsetX;
- final int mPopupOffsetY;
+ @VisibleForTesting
boolean mShouldShowHint;
SizeCompatUILayout(SyncTransactionQueue syncQueue,
@@ -86,10 +89,13 @@
mShouldShowHint = !hasShownHint;
mButtonWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this);
- mButtonSize =
- mContext.getResources().getDimensionPixelSize(R.dimen.size_compat_button_size);
- mPopupOffsetX = mButtonSize / 4;
- mPopupOffsetY = mButtonSize;
+ final Resources resources = mContext.getResources();
+ mButtonWidth = resources.getDimensionPixelSize(R.dimen.size_compat_button_width);
+ mButtonHeight = resources.getDimensionPixelSize(R.dimen.size_compat_button_height);
+ mPopupOffsetX = (mButtonWidth / 2) - resources.getDimensionPixelSize(
+ R.dimen.size_compat_hint_corner_radius) - (resources.getDimensionPixelSize(
+ R.dimen.size_compat_hint_point_width) / 2);
+ mPopupOffsetY = mButtonHeight;
}
/** Creates the activity restart button window. */
@@ -222,7 +228,7 @@
WindowManager.LayoutParams getButtonWindowLayoutParams() {
final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
// Cannot be wrap_content as this determines the actual window size
- mButtonSize, mButtonSize,
+ mButtonWidth, mButtonHeight,
TYPE_APPLICATION_OVERLAY,
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT);
@@ -278,8 +284,8 @@
// Position of the button in the container coordinate.
final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
? stableBounds.left - taskBounds.left
- : stableBounds.right - taskBounds.left - mButtonSize;
- final int positionY = stableBounds.bottom - taskBounds.top - mButtonSize;
+ : stableBounds.right - taskBounds.left - mButtonWidth;
+ final int positionY = stableBounds.bottom - taskBounds.top - mButtonHeight;
updateSurfacePosition(leash, positionX, positionY);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 36f1406..14a6574 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -34,7 +34,6 @@
import android.content.pm.LauncherApps;
import android.graphics.Rect;
import android.os.Bundle;
-import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
@@ -61,6 +60,7 @@
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
@@ -433,46 +433,26 @@
@BinderThread
private static class ISplitScreenImpl extends ISplitScreen.Stub {
private SplitScreenController mController;
- private ISplitScreenListener mListener;
+ private final SingleInstanceRemoteListener<SplitScreenController,
+ ISplitScreenListener> mListener;
private final SplitScreen.SplitScreenListener mSplitScreenListener =
new SplitScreen.SplitScreenListener() {
@Override
public void onStagePositionChanged(int stage, int position) {
- try {
- if (mListener != null) {
- mListener.onStagePositionChanged(stage, position);
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "onStagePositionChanged", e);
- }
+ mListener.call(l -> l.onStagePositionChanged(stage, position));
}
@Override
public void onTaskStageChanged(int taskId, int stage, boolean visible) {
- try {
- if (mListener != null) {
- mListener.onTaskStageChanged(taskId, stage, visible);
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "onTaskStageChanged", e);
- }
- }
- };
- private final IBinder.DeathRecipient mListenerDeathRecipient =
- new IBinder.DeathRecipient() {
- @Override
- @BinderThread
- public void binderDied() {
- final SplitScreenController controller = mController;
- controller.getRemoteCallExecutor().execute(() -> {
- mListener = null;
- controller.unregisterSplitScreenListener(mSplitScreenListener);
- });
+ mListener.call(l -> l.onTaskStageChanged(taskId, stage, visible));
}
};
public ISplitScreenImpl(SplitScreenController controller) {
mController = controller;
+ mListener = new SingleInstanceRemoteListener<>(controller,
+ c -> c.registerSplitScreenListener(mSplitScreenListener),
+ c -> c.unregisterSplitScreenListener(mSplitScreenListener));
}
/**
@@ -485,36 +465,13 @@
@Override
public void registerSplitScreenListener(ISplitScreenListener listener) {
executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener",
- (controller) -> {
- if (mListener != null) {
- mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- }
- if (listener != null) {
- try {
- listener.asBinder().linkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to link to death");
- return;
- }
- }
- mListener = listener;
- controller.registerSplitScreenListener(mSplitScreenListener);
- });
+ (controller) -> mListener.register(listener));
}
@Override
public void unregisterSplitScreenListener(ISplitScreenListener listener) {
executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener",
- (controller) -> {
- if (mListener != null) {
- mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- }
- mListener = null;
- controller.unregisterSplitScreenListener(mSplitScreenListener);
- });
+ (controller) -> mListener.unregister());
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java
index 4e477ca1..003d8a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java
@@ -18,6 +18,8 @@
import static android.view.Choreographer.CALLBACK_COMMIT;
import static android.view.View.GONE;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_EXIT_ANIM;
+
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
@@ -42,6 +44,7 @@
import android.view.animation.PathInterpolator;
import android.window.SplashScreenView;
+import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.common.TransactionPool;
@@ -311,17 +314,19 @@
@Override
public void onAnimationStart(Animator animation) {
- // ignore
+ InteractionJankMonitor.getInstance().begin(mSplashScreenView, CUJ_SPLASHSCREEN_EXIT_ANIM);
}
@Override
public void onAnimationEnd(Animator animation) {
reset();
+ InteractionJankMonitor.getInstance().end(CUJ_SPLASHSCREEN_EXIT_ANIM);
}
@Override
public void onAnimationCancel(Animator animation) {
reset();
+ InteractionJankMonitor.getInstance().cancel(CUJ_SPLASHSCREEN_EXIT_ANIM);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
index e2a72bd..709e221 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
@@ -19,6 +19,7 @@
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
@@ -263,11 +264,12 @@
* A lightweight AdaptiveIconDrawable which support foreground to be Animatable, and keep this
* drawable masked by config_icon_mask.
*/
- private static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
+ public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
implements SplashScreenView.IconAnimateListener {
private Animatable mAnimatableIcon;
private Animator mIconAnimator;
private boolean mAnimationTriggered;
+ private AnimatorListenerAdapter mJankMonitoringListener;
AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) {
super(foregroundDrawable);
@@ -275,6 +277,11 @@
}
@Override
+ public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {
+ mJankMonitoringListener = listener;
+ }
+
+ @Override
public boolean prepareAnimate(long duration, Runnable startListener) {
mAnimatableIcon = (Animatable) mForegroundDrawable;
mIconAnimator = ValueAnimator.ofInt(0, 1);
@@ -286,6 +293,9 @@
startListener.run();
}
try {
+ if (mJankMonitoringListener != null) {
+ mJankMonitoringListener.onAnimationStart(animation);
+ }
mAnimatableIcon.start();
} catch (Exception ex) {
Log.e(TAG, "Error while running the splash screen animated icon", ex);
@@ -296,11 +306,17 @@
@Override
public void onAnimationEnd(Animator animation) {
mAnimatableIcon.stop();
+ if (mJankMonitoringListener != null) {
+ mJankMonitoringListener.onAnimationEnd(animation);
+ }
}
@Override
public void onAnimationCancel(Animator animation) {
mAnimatableIcon.stop();
+ if (mJankMonitoringListener != null) {
+ mJankMonitoringListener.onAnimationCancel(animation);
+ }
}
@Override
@@ -316,6 +332,7 @@
public void stopAnimation() {
if (mIconAnimator != null && mIconAnimator.isRunning()) {
mIconAnimator.end();
+ mJankMonitoringListener = null;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index a86e07a..e98a3e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -46,6 +46,7 @@
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.TransactionPool;
/**
@@ -237,24 +238,19 @@
@BinderThread
private static class IStartingWindowImpl extends IStartingWindow.Stub {
private StartingWindowController mController;
- private IStartingWindowListener mListener;
+ private SingleInstanceRemoteListener<StartingWindowController,
+ IStartingWindowListener> mListener;
private final TriConsumer<Integer, Integer, Integer> mStartingWindowListener =
- this::notifyIStartingWindowListener;
- private final IBinder.DeathRecipient mListenerDeathRecipient =
- new IBinder.DeathRecipient() {
- @Override
- @BinderThread
- public void binderDied() {
- final StartingWindowController controller = mController;
- controller.getRemoteCallExecutor().execute(() -> {
- mListener = null;
- controller.setStartingWindowListener(null);
- });
- }
+ (taskId, supportedType, startingWindowBackgroundColor) -> {
+ mListener.call(l -> l.onTaskLaunching(taskId, supportedType,
+ startingWindowBackgroundColor));
};
public IStartingWindowImpl(StartingWindowController controller) {
mController = controller;
+ mListener = new SingleInstanceRemoteListener<>(controller,
+ c -> c.setStartingWindowListener(mStartingWindowListener),
+ c -> c.setStartingWindowListener(null));
}
/**
@@ -268,36 +264,12 @@
public void setStartingWindowListener(IStartingWindowListener listener) {
executeRemoteCallWithTaskPermission(mController, "setStartingWindowListener",
(controller) -> {
- if (mListener != null) {
- // Reset the old death recipient
- mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- }
if (listener != null) {
- try {
- listener.asBinder().linkToDeath(mListenerDeathRecipient,
- 0 /* flags */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to link to death");
- return;
- }
+ mListener.register(listener);
+ } else {
+ mListener.unregister();
}
- mListener = listener;
- controller.setStartingWindowListener(mStartingWindowListener);
});
}
-
- private void notifyIStartingWindowListener(int taskId, int supportedType,
- int startingWindowBackgroundColor) {
- if (mListener == null) {
- return;
- }
-
- try {
- mListener.onTaskLaunching(taskId, supportedType, startingWindowBackgroundColor);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to notify task launching", e);
- }
- }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
index 802d25f..b34049d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
@@ -38,11 +38,11 @@
/**
* Registers a remote transition.
*/
- void registerRemote(@NonNull TransitionFilter filter,
- @NonNull RemoteTransition remoteTransition);
+ default void registerRemote(@NonNull TransitionFilter filter,
+ @NonNull RemoteTransition remoteTransition) {}
/**
* Unregisters a remote transition.
*/
- void unregisterRemote(@NonNull RemoteTransition remoteTransition);
+ default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index c369831..804e449 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -171,24 +171,6 @@
}
}
- /** Create an empty/non-registering transitions object for system-ui tests. */
- @VisibleForTesting
- public static ShellTransitions createEmptyForTesting() {
- return new ShellTransitions() {
- @Override
- public void registerRemote(@androidx.annotation.NonNull TransitionFilter filter,
- @androidx.annotation.NonNull RemoteTransition remoteTransition) {
- // Do nothing
- }
-
- @Override
- public void unregisterRemote(
- @androidx.annotation.NonNull RemoteTransition remoteTransition) {
- // Do nothing
- }
- };
- }
-
/** Register this transition handler with Core */
public void register(ShellTaskOrganizer taskOrganizer) {
if (mPlayerImpl == null) return;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
index 10fd7d7..3a14a33 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
@@ -24,7 +24,7 @@
import android.content.res.Configuration;
import android.testing.AndroidTestingRunner;
import android.view.LayoutInflater;
-import android.widget.Button;
+import android.widget.LinearLayout;
import androidx.test.filters.SmallTest;
@@ -77,8 +77,8 @@
public void testOnClick() {
doNothing().when(mLayout).dismissHint();
- final Button button = mHint.findViewById(R.id.got_it);
- button.performClick();
+ final LinearLayout hintPopup = mHint.findViewById(R.id.size_compat_hint_popup);
+ hintPopup.performClick();
verify(mLayout).dismissHint();
}
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index f4faa62..86e2661 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -329,7 +329,8 @@
android:layout_height="wrap_content"
android:paddingBottom="4dp"
android:clickable="false"
- android:focusable="false">
+ android:focusable="false"
+ android:visibility="gone">
<LinearLayout
android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 9bdd572..3f855c7 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -419,6 +419,9 @@
<style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog">
<item name="android:buttonCornerRadius">28dp</item>
+ <item name="android:buttonBarPositiveButtonStyle">@style/Widget.QSDialog.Button</item>
+ <item name="android:buttonBarNegativeButtonStyle">@style/Widget.QSDialog.Button.BorderButton</item>
+ <item name="android:buttonBarNeutralButtonStyle">@style/Widget.QSDialog.Button.BorderButton</item>
</style>
<style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
@@ -980,12 +983,15 @@
<style name="InternetDialog.Network">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">88dp</item>
+ <item name="android:layout_marginStart">@dimen/internet_dialog_network_layout_margin</item>
<item name="android:layout_marginEnd">@dimen/internet_dialog_network_layout_margin</item>
+ <item name="android:layout_gravity">center_vertical|start</item>
<item name="android:paddingStart">22dp</item>
<item name="android:paddingEnd">22dp</item>
<item name="android:orientation">horizontal</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
</style>
<style name="InternetDialog.NetworkTitle">
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index db729da..fcf1b2c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -20,11 +20,14 @@
import android.view.View;
import android.view.ViewRootImpl;
+import androidx.annotation.Nullable;
+
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.statusbar.phone.BiometricUnlockController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.NotificationPanelViewController;
import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
/**
* Interface to control Keyguard View. It should be implemented by KeyguardViewManagers, which
@@ -184,14 +187,10 @@
/**
* Registers the StatusBar to which this Keyguard View is mounted.
- * @param statusBar
- * @param notificationPanelViewController
- * @param biometricUnlockController
- * @param notificationContainer
- * @param bypassController
*/
void registerStatusBar(StatusBar statusBar,
NotificationPanelViewController notificationPanelViewController,
+ @Nullable PanelExpansionStateManager panelExpansionStateManager,
BiometricUnlockController biometricUnlockController,
View notificationContainer,
KeyguardBypassController bypassController);
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index c64f416..f4f99b7 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -30,6 +30,7 @@
import com.android.systemui.dagger.WMComponent;
import com.android.systemui.navigationbar.gestural.BackGestureTfClassifierProvider;
import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider;
+import com.android.wm.shell.transition.ShellTransitions;
import com.android.wm.shell.transition.Transitions;
import java.util.Optional;
@@ -133,7 +134,7 @@
.setShellCommandHandler(Optional.ofNullable(null))
.setAppPairs(Optional.ofNullable(null))
.setTaskViewFactory(Optional.ofNullable(null))
- .setTransitions(Transitions.createEmptyForTesting())
+ .setTransitions(new ShellTransitions() {})
.setDisplayAreaHelper(Optional.ofNullable(null))
.setStartingSurface(Optional.ofNullable(null))
.setTaskSurfaceHelper(Optional.ofNullable(null));
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index a4e2572..3fb8c1a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -18,7 +18,6 @@
import android.app.INotificationManager;
import android.content.Context;
-import android.view.LayoutInflater;
import androidx.annotation.Nullable;
@@ -27,7 +26,6 @@
import com.android.keyguard.dagger.KeyguardBouncerComponent;
import com.android.systemui.BootCompleteCache;
import com.android.systemui.BootCompleteCacheImpl;
-import com.android.systemui.R;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.appops.dagger.AppOpsModule;
import com.android.systemui.assist.AssistModule;
@@ -66,7 +64,6 @@
import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.phone.StatusBarWindowView;
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -212,17 +209,4 @@
groupManager, entryManager, notifPipeline, sysUiState, featureFlags, dumpManager,
sysuiMainExecutor));
}
-
- @Provides
- @SysUISingleton
- static StatusBarWindowView providesStatusBarWindowView(LayoutInflater layoutInflater) {
- StatusBarWindowView view =
- (StatusBarWindowView) layoutInflater.inflate(R.layout.super_status_bar,
- /* root= */ null);
- if (view == null) {
- throw new IllegalStateException(
- "R.layout.super_status_bar could not be properly inflated");
- }
- return view;
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 19ee50a..f438181 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -82,6 +82,8 @@
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
+import androidx.annotation.Nullable;
+
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.jank.InteractionJankMonitor.Configuration;
import com.android.internal.policy.IKeyguardDismissCallback;
@@ -117,6 +119,7 @@
import com.android.systemui.statusbar.phone.NotificationPanelViewController;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
@@ -2654,10 +2657,16 @@
*/
public KeyguardViewController registerStatusBar(StatusBar statusBar,
NotificationPanelViewController panelView,
+ @Nullable PanelExpansionStateManager panelExpansionStateManager,
BiometricUnlockController biometricUnlockController,
View notificationContainer, KeyguardBypassController bypassController) {
- mKeyguardViewControllerLazy.get().registerStatusBar(statusBar, panelView,
- biometricUnlockController, notificationContainer, bypassController);
+ mKeyguardViewControllerLazy.get().registerStatusBar(
+ statusBar,
+ panelView,
+ panelExpansionStateManager,
+ biometricUnlockController,
+ notificationContainer,
+ bypassController);
return mKeyguardViewControllerLazy.get();
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
index c1db8ed..9e00381 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
@@ -61,7 +61,7 @@
*
* @param name The name of this buffer
* @param maxLogs The maximum number of messages to keep in memory at any one time, including the
- * unused pool.
+ * unused pool. Must be >= [poolSize].
* @param poolSize The maximum amount that the size of the buffer is allowed to flex in response to
* sequential calls to [document] that aren't immediately followed by a matching call to [push].
*/
@@ -71,6 +71,13 @@
private val poolSize: Int,
private val logcatEchoTracker: LogcatEchoTracker
) {
+ init {
+ if (maxLogs < poolSize) {
+ throw IllegalArgumentException("maxLogs must be greater than or equal to poolSize, " +
+ "but maxLogs=$maxLogs < $poolSize=poolSize")
+ }
+ }
+
private val buffer: ArrayDeque<LogMessageImpl> = ArrayDeque()
var frozen = false
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 72601e9..46e2274 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -112,6 +112,17 @@
}
/**
+ * Provides a logging buffer for logs related to {@link com.android.systemui.qs.QSFragment}'s
+ * disable flag adjustments.
+ */
+ @Provides
+ @SysUISingleton
+ @QSFragmentDisableLog
+ public static LogBuffer provideQSFragmentDisableLogBuffer(LogBufferFactory factory) {
+ return factory.create("QSFragmentDisableFlagsLog", 10);
+ }
+
+ /**
* Provides a logging buffer for logs related to swiping away the status bar while in immersive
* mode. See {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
new file mode 100644
index 0000000..557a254
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for disable flag adjustments made in
+ * {@link com.android.systemui.qs.QSFragment}.
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface QSFragmentDisableLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index eeca239..dd876b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -15,6 +15,7 @@
package com.android.systemui.qs;
import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
+import static com.android.systemui.statusbar.DisableFlagsLogger.DisableState;
import static com.android.systemui.media.dagger.MediaModule.QS_PANEL;
import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL;
@@ -101,6 +102,7 @@
private final MediaHost mQsMediaHost;
private final MediaHost mQqsMediaHost;
private final QSFragmentComponent.Factory mQsComponentFactory;
+ private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
private final QSTileHost mHost;
private boolean mShowCollapsedOnKeyguard;
private boolean mLastKeyguardAndExpanded;
@@ -151,6 +153,7 @@
@Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
KeyguardBypassController keyguardBypassController,
QSFragmentComponent.Factory qsComponentFactory,
+ QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
FalsingManager falsingManager, DumpManager dumpManager) {
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
mCommandQueue = commandQueue;
@@ -158,6 +161,7 @@
mQsMediaHost = qsMediaHost;
mQqsMediaHost = qqsMediaHost;
mQsComponentFactory = qsComponentFactory;
+ mQsFragmentDisableFlagsLogger = qsFragmentDisableFlagsLogger;
commandQueue.observe(getLifecycle(), this);
mHost = qsTileHost;
mFalsingManager = falsingManager;
@@ -363,8 +367,14 @@
if (displayId != getContext().getDisplayId()) {
return;
}
+ int state2BeforeAdjustment = state2;
state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
+ mQsFragmentDisableFlagsLogger.logDisableFlagChange(
+ /* new= */ new DisableState(state1, state2BeforeAdjustment),
+ /* newAfterLocalModification= */ new DisableState(state1, state2)
+ );
+
final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
if (disabled == mQsDisabled) return;
mQsDisabled = disabled;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
new file mode 100644
index 0000000..17a815e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
@@ -0,0 +1,48 @@
+package com.android.systemui.qs
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.QSFragmentDisableLog
+import com.android.systemui.statusbar.DisableFlagsLogger
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment
+import javax.inject.Inject
+
+/** A helper class for logging disable flag changes made in [QSFragment]. */
+class QSFragmentDisableFlagsLogger @Inject constructor(
+ @QSFragmentDisableLog private val buffer: LogBuffer,
+ private val disableFlagsLogger: DisableFlagsLogger
+) {
+
+ /**
+ * Logs a string representing the new state received by [QSFragment] and any modifications that
+ * were made to the flags locally.
+ *
+ * @param new see [DisableFlagsLogger.getDisableFlagsString]
+ * @param newAfterLocalModification see [DisableFlagsLogger.getDisableFlagsString]
+ */
+ fun logDisableFlagChange(
+ new: DisableFlagsLogger.DisableState,
+ newAfterLocalModification: DisableFlagsLogger.DisableState
+ ) {
+ buffer.log(
+ TAG,
+ LogLevel.INFO,
+ {
+ int1 = new.disable1
+ int2 = new.disable2
+ long1 = newAfterLocalModification.disable1.toLong()
+ long2 = newAfterLocalModification.disable2.toLong()
+ },
+ {
+ disableFlagsLogger.getDisableFlagsString(
+ old = null,
+ new = DisableFlagsLogger.DisableState(int1, int2),
+ newAfterLocalModification =
+ DisableFlagsLogger.DisableState(long1.toInt(), long2.toInt())
+ )
+ }
+ )
+ }
+}
+
+private const val TAG = "QSFragmentDisableFlagsLog"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt b/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
index 6de8370..48546009 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
@@ -34,7 +34,11 @@
* Change the height of all tiles and repositions their siblings.
*/
private fun updateSquishiness() {
- // Start by updating the height of all tiles
+ // Update tile positions in the layout
+ val tileLayout = quickQSPanelController.tileLayout as TileLayout
+ tileLayout.setSquishinessFraction(squishiness)
+
+ // Adjust their heights as well
for (tile in qsTileHost.tiles) {
val tileView = quickQSPanelController.getTileView(tile)
(tileView as? HeightOverrideable)?.let {
@@ -42,10 +46,6 @@
}
}
- // Update tile positions in the layout
- val tileLayout = quickQSPanelController.tileLayout as TileLayout
- tileLayout.setSquishinessFraction(squishiness)
-
// Calculate how much we should move the footer
val tileHeightOffset = tileLayout.height - tileLayout.tilesHeight
val footerTopMargin = (qqsFooterActionsView.layoutParams as ViewGroup.MarginLayoutParams)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index ee5d5ff..58c0508 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -212,7 +212,7 @@
return mMaxCellHeight;
}
- private void layoutTileRecords(int numRecords) {
+ private void layoutTileRecords(int numRecords, boolean forLayout) {
final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
int row = 0;
int column = 0;
@@ -232,14 +232,18 @@
final int left = getColumnStart(isRtl ? mColumns - column - 1 : column);
final int right = left + mCellWidth;
final int bottom = top + record.tileView.getMeasuredHeight();
- record.tileView.layout(left, top, right, bottom);
+ if (forLayout) {
+ record.tileView.layout(left, top, right, bottom);
+ } else {
+ record.tileView.setLeftTopRightBottom(left, top, right, bottom);
+ }
mLastTileBottom = bottom;
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- layoutTileRecords(mRecords.size());
+ layoutTileRecords(mRecords.size(), true /* forLayout */);
}
protected int getRowTop(int row) {
@@ -280,6 +284,6 @@
return;
}
mSquishinessFraction = squishinessFraction;
- layoutTileRecords(mRecords.size());
+ layoutTileRecords(mRecords.size(), false /* forLayout */);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 69be3326..2bd5c8f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -242,12 +242,12 @@
}
private fun updateHeight() {
- val actualHeight = (if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
+ val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
heightOverride
} else {
measuredHeight
- } * squishinessFraction).toInt()
- bottom = top + actualHeight
+ }
+ bottom = top + (actualHeight * squishinessFraction).toInt()
scrollY = (actualHeight - height) / 2
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index 1dab263..f6dbb0b9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -19,6 +19,7 @@
import android.app.AlertDialog;
import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.net.Network;
import android.net.NetworkCapabilities;
@@ -122,6 +123,7 @@
private Switch mWiFiToggle;
private FrameLayout mDoneLayout;
private Drawable mBackgroundOn;
+ private Drawable mBackgroundOff = null;
private int mDefaultDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private boolean mCanConfigMobileData;
@@ -209,6 +211,14 @@
mInternetDialogTitle.setText(getDialogTitleText());
mInternetDialogTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ TypedArray typedArray = mContext.obtainStyledAttributes(
+ new int[]{android.R.attr.selectableItemBackground});
+ try {
+ mBackgroundOff = typedArray.getDrawable(0 /* index */);
+ } finally {
+ typedArray.recycle();
+ }
+
setOnClickListener();
mTurnWifiOnLayout.setBackground(null);
mWifiRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
@@ -364,7 +374,8 @@
mMobileSummaryText.setTextAppearance(isCarrierNetworkConnected
? R.style.TextAppearance_InternetDialog_Secondary_Active
: R.style.TextAppearance_InternetDialog_Secondary);
- mMobileNetworkLayout.setBackground(isCarrierNetworkConnected ? mBackgroundOn : null);
+ mMobileNetworkLayout.setBackground(
+ isCarrierNetworkConnected ? mBackgroundOn : mBackgroundOff);
mMobileDataToggle.setVisibility(mCanConfigMobileData ? View.VISIBLE : View.INVISIBLE);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt
index cf34db2..4272bb1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt
@@ -85,24 +85,30 @@
* is no difference. the new-after-modification state also won't be included if there's no
* difference from the new state.
*
- * @param old the disable state that had been previously sent.
+ * @param old the disable state that had been previously sent. Null if we don't need to log the
+ * previously sent state.
* @param new the new disable state that has just been sent.
* @param newAfterLocalModification the new disable states after a class has locally modified
* them. Null if the class does not locally modify.
*/
fun getDisableFlagsString(
- old: DisableState,
+ old: DisableState? = null,
new: DisableState,
newAfterLocalModification: DisableState? = null
): String {
val builder = StringBuilder("Received new disable state. ")
- builder.append("Old: ")
- builder.append(getFlagsString(old))
- builder.append(" | New: ")
- if (old != new) {
+
+ old?.let {
+ builder.append("Old: ")
+ builder.append(getFlagsString(old))
+ builder.append(" | ")
+ }
+
+ builder.append("New: ")
+ if (old != null && old != new) {
builder.append(getFlagsStringWithDiff(old, new))
} else {
- builder.append(getFlagsString(old))
+ builder.append(getFlagsString(new))
}
if (newAfterLocalModification != null && new != newAfterLocalModification) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 18a3d86..1ce7f03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -15,21 +15,16 @@
*/
package com.android.systemui.statusbar;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
-import android.app.RemoteInputHistoryItem;
import android.content.Context;
import android.content.Intent;
import android.content.pm.UserInfo;
-import android.net.Uri;
import android.os.Handler;
-import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -48,6 +43,9 @@
import android.widget.RemoteViews.InteractionHandler;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
@@ -55,6 +53,7 @@
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.dagger.StatusBarDependenciesModule;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -70,12 +69,10 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
-import java.util.stream.Stream;
import dagger.Lazy;
@@ -93,27 +90,7 @@
private static final boolean DEBUG = false;
private static final String TAG = "NotifRemoteInputManager";
- /**
- * How long to wait before auto-dismissing a notification that was kept for remote input, and
- * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
- * these given that they technically don't exist anymore. We wait a bit in case the app issues
- * an update.
- */
- private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
-
- /**
- * Notifications that are already removed but are kept around because we want to show the
- * remote input history. See {@link RemoteInputHistoryExtender} and
- * {@link SmartReplyHistoryExtender}.
- */
- protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
-
- /**
- * Notifications that are already removed but are kept around because the remote input is
- * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
- */
- protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
- new ArraySet<>();
+ private RemoteInputListener mRemoteInputListener;
// Dependencies:
private final NotificationLockscreenUserManager mLockscreenUserManager;
@@ -125,18 +102,17 @@
private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
protected final Context mContext;
+ protected final FeatureFlags mFeatureFlags;
private final UserManager mUserManager;
private final KeyguardManager mKeyguardManager;
+ private final RemoteInputNotificationRebuilder mRebuilder;
private final StatusBarStateController mStatusBarStateController;
private final RemoteInputUriController mRemoteInputUriController;
private final NotificationClickNotifier mClickNotifier;
protected RemoteInputController mRemoteInputController;
- protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
- mNotificationLifetimeFinishedCallback;
protected IStatusBarService mBarService;
protected Callback mCallback;
- protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>();
@@ -226,6 +202,7 @@
ViewGroup actionGroup = (ViewGroup) parent;
buttonIndex = actionGroup.indexOfChild(view);
}
+ // TODO(b/204183781): get this from the current pipeline
final int count = mEntryManager.getActiveNotificationsCount();
final int rank = entry.getRanking().getRank();
@@ -283,9 +260,11 @@
*/
public NotificationRemoteInputManager(
Context context,
+ FeatureFlags featureFlags,
NotificationLockscreenUserManager lockscreenUserManager,
SmartReplyController smartReplyController,
NotificationEntryManager notificationEntryManager,
+ RemoteInputNotificationRebuilder rebuilder,
Lazy<Optional<StatusBar>> statusBarOptionalLazy,
StatusBarStateController statusBarStateController,
@Main Handler mainHandler,
@@ -294,6 +273,7 @@
ActionClickLogger logger,
DumpManager dumpManager) {
mContext = context;
+ mFeatureFlags = featureFlags;
mLockscreenUserManager = lockscreenUserManager;
mSmartReplyController = smartReplyController;
mEntryManager = notificationEntryManager;
@@ -303,7 +283,11 @@
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- addLifetimeExtenders();
+ mRebuilder = rebuilder;
+ if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
+ mRemoteInputListener = createLegacyRemoteInputLifetimeExtender(mainHandler,
+ notificationEntryManager, smartReplyController);
+ }
mKeyguardManager = context.getSystemService(KeyguardManager.class);
mStatusBarStateController = statusBarStateController;
mRemoteInputUriController = remoteInputUriController;
@@ -335,10 +319,35 @@
});
}
+ /** Add a listener for various remote input events. Works with NEW pipeline only. */
+ public void setRemoteInputListener(@NonNull RemoteInputListener remoteInputListener) {
+ if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
+ if (mRemoteInputListener != null) {
+ throw new IllegalStateException("mRemoteInputListener is already set");
+ }
+ mRemoteInputListener = remoteInputListener;
+ if (mRemoteInputController != null) {
+ mRemoteInputListener.setRemoteInputController(mRemoteInputController);
+ }
+ }
+ }
+
+ @NonNull
+ @VisibleForTesting
+ protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
+ Handler mainHandler,
+ NotificationEntryManager notificationEntryManager,
+ SmartReplyController smartReplyController) {
+ return new LegacyRemoteInputLifetimeExtender();
+ }
+
/** Initializes this component with the provided dependencies. */
public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
mCallback = callback;
mRemoteInputController = new RemoteInputController(delegate, mRemoteInputUriController);
+ if (mRemoteInputListener != null) {
+ mRemoteInputListener.setRemoteInputController(mRemoteInputController);
+ }
// Register all stored callbacks from before the Controller was initialized.
for (RemoteInputController.Callback cb : mControllerCallbacks) {
mRemoteInputController.addCallback(cb);
@@ -347,19 +356,8 @@
mRemoteInputController.addCallback(new RemoteInputController.Callback() {
@Override
public void onRemoteInputSent(NotificationEntry entry) {
- if (FORCE_REMOTE_INPUT_HISTORY
- && isNotificationKeptForRemoteInputHistory(entry.getKey())) {
- mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
- } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
- // We're currently holding onto this notification, but from the apps point of
- // view it is already canceled, so we'll need to cancel it on the apps behalf
- // after sending - unless the app posts an update in the mean time, so wait a
- // bit.
- mMainHandler.postDelayed(() -> {
- if (mEntriesKeptForRemoteInputActive.remove(entry)) {
- mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
- }
- }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+ if (mRemoteInputListener != null) {
+ mRemoteInputListener.onRemoteInputSent(entry);
}
try {
mBarService.onNotificationDirectReplied(entry.getSbn().getKey());
@@ -381,12 +379,12 @@
}
}
});
- mSmartReplyController.setCallback((entry, reply) -> {
- StatusBarNotification newSbn =
- rebuildNotificationWithRemoteInputInserted(entry, reply, true /* showSpinner */,
- null /* mimeType */, null /* uri */);
- mEntryManager.updateNotification(newSbn, null /* ranking */);
- });
+ if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
+ mSmartReplyController.setCallback((entry, reply) -> {
+ StatusBarNotification newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply);
+ mEntryManager.updateNotification(newSbn, null /* ranking */);
+ });
+ }
}
public void addControllerCallback(RemoteInputController.Callback callback) {
@@ -574,51 +572,47 @@
if (v == null) {
return null;
}
- return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
- }
-
- /**
- * Adds all the notification lifetime extenders. Each extender represents a reason for the
- * NotificationRemoteInputManager to keep a notification lifetime extended.
- */
- protected void addLifetimeExtenders() {
- mLifetimeExtenders.add(new RemoteInputHistoryExtender());
- mLifetimeExtenders.add(new SmartReplyHistoryExtender());
- mLifetimeExtenders.add(new RemoteInputActiveExtender());
+ return v.findViewWithTag(RemoteInputView.VIEW_TAG);
}
public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
- return mLifetimeExtenders;
+ // OLD pipeline code ONLY; can assume implementation
+ return ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener).mLifetimeExtenders;
}
@VisibleForTesting
void onPerformRemoveNotification(NotificationEntry entry, final String key) {
- if (mKeysKeptForRemoteInputHistory.contains(key)) {
- mKeysKeptForRemoteInputHistory.remove(key);
- }
+ // OLD pipeline code ONLY; can assume implementation
+ ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener)
+ .mKeysKeptForRemoteInputHistory.remove(key);
+ cleanUpRemoteInputForUserRemoval(entry);
+ }
+
+ /**
+ * Disable remote input on the entry and remove the remote input view.
+ * This should be called when a user dismisses a notification that won't be lifetime extended.
+ */
+ public void cleanUpRemoteInputForUserRemoval(NotificationEntry entry) {
if (isRemoteInputActive(entry)) {
entry.mRemoteEditImeVisible = false;
mRemoteInputController.removeRemoteInput(entry, null);
}
}
+ /** Informs the remote input system that the panel has collapsed */
public void onPanelCollapsed() {
- for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
- NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
- if (mRemoteInputController != null) {
- mRemoteInputController.removeRemoteInput(entry, null);
- }
- if (mNotificationLifetimeFinishedCallback != null) {
- mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
- }
+ if (mRemoteInputListener != null) {
+ mRemoteInputListener.onPanelCollapsed();
}
- mEntriesKeptForRemoteInputActive.clear();
}
+ /** Returns whether the given notification is lifetime extended because of remote input */
public boolean isNotificationKeptForRemoteInputHistory(String key) {
- return mKeysKeptForRemoteInputHistory.contains(key);
+ return mRemoteInputListener != null
+ && mRemoteInputListener.isNotificationKeptForRemoteInputHistory(key);
}
+ /** Returns whether the notification should be lifetime extended for remote input history */
public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
if (!FORCE_REMOTE_INPUT_HISTORY) {
return false;
@@ -636,16 +630,12 @@
if (entry == null) {
return;
}
- final String key = entry.getKey();
- if (isNotificationKeptForRemoteInputHistory(key)) {
- mMainHandler.postDelayed(() -> {
- if (isNotificationKeptForRemoteInputHistory(key)) {
- mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
- }
- }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+ if (mRemoteInputListener != null) {
+ mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry);
}
}
+ /** Returns whether the notification should be lifetime extended for smart reply history */
public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
if (!FORCE_REMOTE_INPUT_HISTORY) {
return false;
@@ -661,64 +651,11 @@
}
}
- @VisibleForTesting
- StatusBarNotification rebuildNotificationForCanceledSmartReplies(
- NotificationEntry entry) {
- return rebuildNotificationWithRemoteInputInserted(entry, null /* remoteInputTest */,
- false /* showSpinner */, null /* mimeType */, null /* uri */);
- }
-
- @VisibleForTesting
- StatusBarNotification rebuildNotificationWithRemoteInputInserted(NotificationEntry entry,
- CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
- StatusBarNotification sbn = entry.getSbn();
-
- Notification.Builder b = Notification.Builder
- .recoverBuilder(mContext, sbn.getNotification().clone());
- if (remoteInputText != null || uri != null) {
- RemoteInputHistoryItem newItem = uri != null
- ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
- : new RemoteInputHistoryItem(remoteInputText);
- Parcelable[] oldHistoryItems = sbn.getNotification().extras
- .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
- RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
- ? Stream.concat(
- Stream.of(newItem),
- Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
- .toArray(RemoteInputHistoryItem[]::new)
- : new RemoteInputHistoryItem[] { newItem };
- b.setRemoteInputHistory(newHistoryItems);
- }
- b.setShowRemoteInputSpinner(showSpinner);
- b.setHideSmartReplies(true);
-
- Notification newNotification = b.build();
-
- // Undo any compatibility view inflation
- newNotification.contentView = sbn.getNotification().contentView;
- newNotification.bigContentView = sbn.getNotification().bigContentView;
- newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
-
- return new StatusBarNotification(
- sbn.getPackageName(),
- sbn.getOpPkg(),
- sbn.getId(),
- sbn.getTag(),
- sbn.getUid(),
- sbn.getInitialPid(),
- newNotification,
- sbn.getUser(),
- sbn.getOverrideGroupKey(),
- sbn.getPostTime());
- }
-
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.println("NotificationRemoteInputManager state:");
- pw.print(" mKeysKeptForRemoteInputHistory: ");
- pw.println(mKeysKeptForRemoteInputHistory);
- pw.print(" mEntriesKeptForRemoteInputActive: ");
- pw.println(mEntriesKeptForRemoteInputActive);
+ if (mRemoteInputListener instanceof Dumpable) {
+ ((Dumpable) mRemoteInputListener).dump(fd, pw, args);
+ }
}
public void bindRow(ExpandableNotificationRow row) {
@@ -734,11 +671,6 @@
return mInteractionHandler;
}
- @VisibleForTesting
- public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
- return mEntriesKeptForRemoteInputActive;
- }
-
public boolean isRemoteInputActive() {
return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive();
}
@@ -758,131 +690,6 @@
}
/**
- * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
- * so we implement multiple NotificationLifetimeExtenders
- */
- protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
- @Override
- public void setCallback(NotificationSafeToRemoveCallback callback) {
- if (mNotificationLifetimeFinishedCallback == null) {
- mNotificationLifetimeFinishedCallback = callback;
- }
- }
- }
-
- /**
- * Notification is kept alive as it was cancelled in response to a remote input interaction.
- * This allows us to show what you replied and allows you to continue typing into it.
- */
- protected class RemoteInputHistoryExtender extends RemoteInputExtender {
- @Override
- public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
- return shouldKeepForRemoteInputHistory(entry);
- }
-
- @Override
- public void setShouldManageLifetime(NotificationEntry entry,
- boolean shouldExtend) {
- if (shouldExtend) {
- CharSequence remoteInputText = entry.remoteInputText;
- if (TextUtils.isEmpty(remoteInputText)) {
- remoteInputText = entry.remoteInputTextWhenReset;
- }
- String remoteInputMimeType = entry.remoteInputMimeType;
- Uri remoteInputUri = entry.remoteInputUri;
- StatusBarNotification newSbn = rebuildNotificationWithRemoteInputInserted(entry,
- remoteInputText, false /* showSpinner */, remoteInputMimeType,
- remoteInputUri);
- entry.onRemoteInputInserted();
-
- if (newSbn == null) {
- return;
- }
-
- mEntryManager.updateNotification(newSbn, null);
-
- // Ensure the entry hasn't already been removed. This can happen if there is an
- // inflation exception while updating the remote history
- if (entry.isRemoved()) {
- return;
- }
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Keeping notification around after sending remote input "
- + entry.getKey());
- }
-
- mKeysKeptForRemoteInputHistory.add(entry.getKey());
- } else {
- mKeysKeptForRemoteInputHistory.remove(entry.getKey());
- }
- }
- }
-
- /**
- * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
- * {@link SmartReplyController} specific logic
- */
- protected class SmartReplyHistoryExtender extends RemoteInputExtender {
- @Override
- public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
- return shouldKeepForSmartReplyHistory(entry);
- }
-
- @Override
- public void setShouldManageLifetime(NotificationEntry entry,
- boolean shouldExtend) {
- if (shouldExtend) {
- StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
-
- if (newSbn == null) {
- return;
- }
-
- mEntryManager.updateNotification(newSbn, null);
-
- if (entry.isRemoved()) {
- return;
- }
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Keeping notification around after sending smart reply "
- + entry.getKey());
- }
-
- mKeysKeptForRemoteInputHistory.add(entry.getKey());
- } else {
- mKeysKeptForRemoteInputHistory.remove(entry.getKey());
- mSmartReplyController.stopSending(entry);
- }
- }
- }
-
- /**
- * Notification is kept alive because the user is still using the remote input
- */
- protected class RemoteInputActiveExtender extends RemoteInputExtender {
- @Override
- public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
- return isRemoteInputActive(entry);
- }
-
- @Override
- public void setShouldManageLifetime(NotificationEntry entry,
- boolean shouldExtend) {
- if (shouldExtend) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Keeping notification around while remote input active "
- + entry.getKey());
- }
- mEntriesKeptForRemoteInputActive.add(entry);
- } else {
- mEntriesKeptForRemoteInputActive.remove(entry);
- }
- }
- }
-
- /**
* Callback for various remote input related events, or for providing information that
* NotificationRemoteInputManager needs to know to decide what to do.
*/
@@ -975,4 +782,256 @@
*/
boolean showBouncerIfNecessary();
}
+
+ /** An interface for listening to remote input events that relate to notification lifetime */
+ public interface RemoteInputListener {
+ /** Called when remote input pending intent has been sent */
+ void onRemoteInputSent(@NonNull NotificationEntry entry);
+
+ /** Called when the notification shade becomes fully closed */
+ void onPanelCollapsed();
+
+ /** @return whether lifetime of a notification is being extended by the listener */
+ boolean isNotificationKeptForRemoteInputHistory(@NonNull String key);
+
+ /** Called on user interaction to end lifetime extension for history */
+ void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry);
+
+ /** Called when the RemoteInputController is attached to the manager */
+ void setRemoteInputController(@NonNull RemoteInputController remoteInputController);
+ }
+
+ @VisibleForTesting
+ protected class LegacyRemoteInputLifetimeExtender implements RemoteInputListener, Dumpable {
+
+ /**
+ * How long to wait before auto-dismissing a notification that was kept for remote input,
+ * and has now sent a remote input. We auto-dismiss, because the app may not see a reason to
+ * cancel these given that they technically don't exist anymore. We wait a bit in case the
+ * app issues an update.
+ */
+ private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
+
+ /**
+ * Notifications that are already removed but are kept around because we want to show the
+ * remote input history. See {@link RemoteInputHistoryExtender} and
+ * {@link SmartReplyHistoryExtender}.
+ */
+ protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
+
+ /**
+ * Notifications that are already removed but are kept around because the remote input is
+ * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
+ */
+ protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
+ new ArraySet<>();
+
+ protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
+ mNotificationLifetimeFinishedCallback;
+
+ protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders =
+ new ArrayList<>();
+ private RemoteInputController mRemoteInputController;
+
+ LegacyRemoteInputLifetimeExtender() {
+ addLifetimeExtenders();
+ }
+
+ /**
+ * Adds all the notification lifetime extenders. Each extender represents a reason for the
+ * NotificationRemoteInputManager to keep a notification lifetime extended.
+ */
+ protected void addLifetimeExtenders() {
+ mLifetimeExtenders.add(new RemoteInputHistoryExtender());
+ mLifetimeExtenders.add(new SmartReplyHistoryExtender());
+ mLifetimeExtenders.add(new RemoteInputActiveExtender());
+ }
+
+ @Override
+ public void setRemoteInputController(@NonNull RemoteInputController remoteInputController) {
+ mRemoteInputController= remoteInputController;
+ }
+
+ @Override
+ public void onRemoteInputSent(@NonNull NotificationEntry entry) {
+ if (FORCE_REMOTE_INPUT_HISTORY
+ && isNotificationKeptForRemoteInputHistory(entry.getKey())) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
+ } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
+ // We're currently holding onto this notification, but from the apps point of
+ // view it is already canceled, so we'll need to cancel it on the apps behalf
+ // after sending - unless the app posts an update in the mean time, so wait a
+ // bit.
+ mMainHandler.postDelayed(() -> {
+ if (mEntriesKeptForRemoteInputActive.remove(entry)) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
+ }
+ }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+ }
+ }
+
+ @Override
+ public void onPanelCollapsed() {
+ for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
+ NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
+ if (mRemoteInputController != null) {
+ mRemoteInputController.removeRemoteInput(entry, null);
+ }
+ if (mNotificationLifetimeFinishedCallback != null) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
+ }
+ }
+ mEntriesKeptForRemoteInputActive.clear();
+ }
+
+ @Override
+ public boolean isNotificationKeptForRemoteInputHistory(@NonNull String key) {
+ return mKeysKeptForRemoteInputHistory.contains(key);
+ }
+
+ @Override
+ public void releaseNotificationIfKeptForRemoteInputHistory(
+ @NonNull NotificationEntry entry) {
+ final String key = entry.getKey();
+ if (isNotificationKeptForRemoteInputHistory(key)) {
+ mMainHandler.postDelayed(() -> {
+ if (isNotificationKeptForRemoteInputHistory(key)) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
+ }
+ }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+ }
+ }
+
+ @VisibleForTesting
+ public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
+ return mEntriesKeptForRemoteInputActive;
+ }
+
+ @Override
+ public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
+ @NonNull String[] args) {
+ pw.println("LegacyRemoteInputLifetimeExtender:");
+ pw.print(" mKeysKeptForRemoteInputHistory: ");
+ pw.println(mKeysKeptForRemoteInputHistory);
+ pw.print(" mEntriesKeptForRemoteInputActive: ");
+ pw.println(mEntriesKeptForRemoteInputActive);
+ }
+
+ /**
+ * NotificationRemoteInputManager has multiple reasons to keep notification lifetime
+ * extended so we implement multiple NotificationLifetimeExtenders
+ */
+ protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
+ @Override
+ public void setCallback(NotificationSafeToRemoveCallback callback) {
+ if (mNotificationLifetimeFinishedCallback == null) {
+ mNotificationLifetimeFinishedCallback = callback;
+ }
+ }
+ }
+
+ /**
+ * Notification is kept alive as it was cancelled in response to a remote input interaction.
+ * This allows us to show what you replied and allows you to continue typing into it.
+ */
+ protected class RemoteInputHistoryExtender extends RemoteInputExtender {
+ @Override
+ public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
+ return shouldKeepForRemoteInputHistory(entry);
+ }
+
+ @Override
+ public void setShouldManageLifetime(NotificationEntry entry,
+ boolean shouldExtend) {
+ if (shouldExtend) {
+ StatusBarNotification newSbn = mRebuilder.rebuildForRemoteInputReply(entry);
+ entry.onRemoteInputInserted();
+
+ if (newSbn == null) {
+ return;
+ }
+
+ mEntryManager.updateNotification(newSbn, null);
+
+ // Ensure the entry hasn't already been removed. This can happen if there is an
+ // inflation exception while updating the remote history
+ if (entry.isRemoved()) {
+ return;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Keeping notification around after sending remote input "
+ + entry.getKey());
+ }
+
+ mKeysKeptForRemoteInputHistory.add(entry.getKey());
+ } else {
+ mKeysKeptForRemoteInputHistory.remove(entry.getKey());
+ }
+ }
+ }
+
+ /**
+ * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but
+ * with {@link SmartReplyController} specific logic
+ */
+ protected class SmartReplyHistoryExtender extends RemoteInputExtender {
+ @Override
+ public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
+ return shouldKeepForSmartReplyHistory(entry);
+ }
+
+ @Override
+ public void setShouldManageLifetime(NotificationEntry entry,
+ boolean shouldExtend) {
+ if (shouldExtend) {
+ StatusBarNotification newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry);
+
+ if (newSbn == null) {
+ return;
+ }
+
+ mEntryManager.updateNotification(newSbn, null);
+
+ if (entry.isRemoved()) {
+ return;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Keeping notification around after sending smart reply "
+ + entry.getKey());
+ }
+
+ mKeysKeptForRemoteInputHistory.add(entry.getKey());
+ } else {
+ mKeysKeptForRemoteInputHistory.remove(entry.getKey());
+ mSmartReplyController.stopSending(entry);
+ }
+ }
+ }
+
+ /**
+ * Notification is kept alive because the user is still using the remote input
+ */
+ protected class RemoteInputActiveExtender extends RemoteInputExtender {
+ @Override
+ public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
+ return isRemoteInputActive(entry);
+ }
+
+ @Override
+ public void setShouldManageLifetime(NotificationEntry entry,
+ boolean shouldExtend) {
+ if (shouldExtend) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Keeping notification around while remote input active "
+ + entry.getKey());
+ }
+ mEntriesKeptForRemoteInputActive.add(entry);
+ } else {
+ mEntriesKeptForRemoteInputActive.remove(entry);
+ }
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 5648741e..eb89be1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -38,8 +38,8 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.phone.PanelExpansionListener
import com.android.systemui.statusbar.phone.ScrimController
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.WallpaperController
import java.io.FileDescriptor
@@ -329,10 +329,12 @@
/**
* Update blurs when pulling down the shade
*/
- override fun onPanelExpansionChanged(rawExpansion: Float, tracking: Boolean) {
+ override fun onPanelExpansionChanged(
+ rawFraction: Float, expanded: Boolean, tracking: Boolean
+ ) {
val timestamp = SystemClock.elapsedRealtimeNanos()
val expansion = MathUtils.saturate(
- (rawExpansion - panelPullDownMinFraction) / (1f - panelPullDownMinFraction))
+ (rawFraction - panelPullDownMinFraction) / (1f - panelPullDownMinFraction))
if (shadeExpansion == expansion && prevTracking == tracking) {
prevTimestamp = timestamp
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
index 83701a0..cde3b0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
@@ -299,6 +299,9 @@
default void onRemoteInputSent(NotificationEntry entry) {}
}
+ /**
+ * This is a delegate which implements some view controller pieces of the remote input process
+ */
public interface Delegate {
/**
* Activate remote input if necessary.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
new file mode 100644
index 0000000..90abec1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.RemoteInputHistoryItem;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcelable;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+
+/**
+ * A helper class which will augment the notifications using arguments and other information
+ * accessible to the entry in order to provide intermediate remote input states.
+ */
+@SysUISingleton
+public class RemoteInputNotificationRebuilder {
+
+ private final Context mContext;
+
+ @Inject
+ RemoteInputNotificationRebuilder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * When a smart reply is sent off to the app, we insert the text into the remote input history,
+ * and show a spinner to indicate that the app has yet to respond.
+ */
+ @NonNull
+ public StatusBarNotification rebuildForSendingSmartReply(NotificationEntry entry,
+ CharSequence reply) {
+ return rebuildWithRemoteInputInserted(entry, reply,
+ true /* showSpinner */,
+ null /* mimeType */, null /* uri */);
+ }
+
+ /**
+ * When the app cancels a notification in response to a smart reply, we remove the spinner
+ * and leave the previously-added reply. This is the lifetime-extended appearance of the
+ * notification.
+ */
+ @NonNull
+ public StatusBarNotification rebuildForCanceledSmartReplies(
+ NotificationEntry entry) {
+ return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */,
+ false /* showSpinner */, null /* mimeType */, null /* uri */);
+ }
+
+ /**
+ * When the app cancels a notification in response to a remote input reply, we update the
+ * notification with the reply text and/or attachment. This is the lifetime-extended
+ * appearance of the notification.
+ */
+ @NonNull
+ public StatusBarNotification rebuildForRemoteInputReply(NotificationEntry entry) {
+ CharSequence remoteInputText = entry.remoteInputText;
+ if (TextUtils.isEmpty(remoteInputText)) {
+ remoteInputText = entry.remoteInputTextWhenReset;
+ }
+ String remoteInputMimeType = entry.remoteInputMimeType;
+ Uri remoteInputUri = entry.remoteInputUri;
+ StatusBarNotification newSbn = rebuildWithRemoteInputInserted(entry,
+ remoteInputText, false /* showSpinner */, remoteInputMimeType,
+ remoteInputUri);
+ return newSbn;
+ }
+
+ /** Inner method for generating the SBN */
+ @VisibleForTesting
+ @NonNull
+ StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
+ CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
+ StatusBarNotification sbn = entry.getSbn();
+
+ Notification.Builder b = Notification.Builder
+ .recoverBuilder(mContext, sbn.getNotification().clone());
+ if (remoteInputText != null || uri != null) {
+ RemoteInputHistoryItem newItem = uri != null
+ ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+ : new RemoteInputHistoryItem(remoteInputText);
+ Parcelable[] oldHistoryItems = sbn.getNotification().extras
+ .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
+ ? Stream.concat(
+ Stream.of(newItem),
+ Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+ .toArray(RemoteInputHistoryItem[]::new)
+ : new RemoteInputHistoryItem[] { newItem };
+ b.setRemoteInputHistory(newHistoryItems);
+ }
+ b.setShowRemoteInputSpinner(showSpinner);
+ b.setHideSmartReplies(true);
+
+ Notification newNotification = b.build();
+
+ // Undo any compatibility view inflation
+ newNotification.contentView = sbn.getNotification().contentView;
+ newNotification.bigContentView = sbn.getNotification().bigContentView;
+ newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
+
+ return new StatusBarNotification(
+ sbn.getPackageName(),
+ sbn.getOpPkg(),
+ sbn.getId(),
+ sbn.getTag(),
+ sbn.getUid(),
+ sbn.getInitialPid(),
+ newNotification,
+ sbn.getUser(),
+ sbn.getOverrideGroupKey(),
+ sbn.getPostTime());
+ }
+
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
index 7fc18b7..e288b1530 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
@@ -19,35 +19,44 @@
import android.os.RemoteException;
import android.util.ArraySet;
+import androidx.annotation.NonNull;
+
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.Dumpable;
+import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.dagger.StatusBarModule;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.Set;
/**
* Handles when smart replies are added to a notification
* and clicked upon.
*/
-public class SmartReplyController {
+public class SmartReplyController implements Dumpable {
private final IStatusBarService mBarService;
private final NotificationEntryManager mEntryManager;
private final NotificationClickNotifier mClickNotifier;
- private Set<String> mSendingKeys = new ArraySet<>();
+ private final Set<String> mSendingKeys = new ArraySet<>();
private Callback mCallback;
/**
* Injected constructor. See {@link StatusBarModule}.
*/
- public SmartReplyController(NotificationEntryManager entryManager,
+ public SmartReplyController(
+ DumpManager dumpManager,
+ NotificationEntryManager entryManager,
IStatusBarService statusBarService,
NotificationClickNotifier clickNotifier) {
mBarService = statusBarService;
mEntryManager = entryManager;
mClickNotifier = clickNotifier;
+ dumpManager.registerDumpable(this);
}
public void setCallback(Callback callback) {
@@ -75,6 +84,7 @@
public void smartActionClicked(
NotificationEntry entry, int actionIndex, Notification.Action action,
boolean generatedByAssistant) {
+ // TODO(b/204183781): get this from the current pipeline
final int count = mEntryManager.getActiveNotificationsCount();
final int rank = entry.getRanking().getRank();
NotificationVisibility.NotificationLocation location =
@@ -112,6 +122,14 @@
}
}
+ @Override
+ public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
+ pw.println("mSendingKeys: " + mSendingKeys.size());
+ for (String key : mSendingKeys) {
+ pw.println(" * " + key);
+ }
+ }
+
/**
* Callback for any class that needs to do something in response to a smart reply being sent.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 1c9174a..bb697c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -43,6 +43,7 @@
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.NotificationViewHierarchyManager;
+import com.android.systemui.statusbar.RemoteInputNotificationRebuilder;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -96,9 +97,11 @@
@Provides
static NotificationRemoteInputManager provideNotificationRemoteInputManager(
Context context,
+ FeatureFlags featureFlags,
NotificationLockscreenUserManager lockscreenUserManager,
SmartReplyController smartReplyController,
NotificationEntryManager notificationEntryManager,
+ RemoteInputNotificationRebuilder rebuilder,
Lazy<Optional<StatusBar>> statusBarOptionalLazy,
StatusBarStateController statusBarStateController,
Handler mainHandler,
@@ -108,9 +111,11 @@
DumpManager dumpManager) {
return new NotificationRemoteInputManager(
context,
+ featureFlags,
lockscreenUserManager,
smartReplyController,
notificationEntryManager,
+ rebuilder,
statusBarOptionalLazy,
statusBarStateController,
mainHandler,
@@ -166,10 +171,11 @@
@SysUISingleton
@Provides
static SmartReplyController provideSmartReplyController(
+ DumpManager dumpManager,
NotificationEntryManager entryManager,
IStatusBarService statusBarService,
NotificationClickNotifier clickNotifier) {
- return new SmartReplyController(entryManager, statusBarService, clickNotifier);
+ return new SmartReplyController(dumpManager, entryManager, statusBarService, clickNotifier);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 589446f..7e4db03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -27,7 +27,6 @@
import com.android.systemui.R
import com.android.systemui.statusbar.phone.StatusBarLocationPublisher
import com.android.systemui.statusbar.phone.StatusBarWindowController
-import com.android.systemui.statusbar.phone.StatusBarWindowView
import javax.inject.Inject
/**
@@ -35,7 +34,6 @@
*/
class SystemEventChipAnimationController @Inject constructor(
private val context: Context,
- private val statusBarWindowView: StatusBarWindowView,
private val statusBarWindowController: StatusBarWindowController,
private val locationPublisher: StatusBarLocationPublisher
) : SystemStatusChipAnimationCallback {
@@ -126,7 +124,7 @@
animationDotView = animationWindowView.findViewById(R.id.dot_view)
val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL
- statusBarWindowView.addView(animationWindowView, lp)
+ statusBarWindowController.addViewToWindow(animationWindowView, lp)
}
private fun start() = if (animationWindowView.isLayoutRtl) right() else left()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 60f44a0d..8bc41c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -689,8 +689,9 @@
for (NotificationEntryListener listener : mNotificationEntryListeners) {
listener.onPreEntryUpdated(entry);
}
+ final boolean fromSystem = ranking != null;
for (NotifCollectionListener listener : mNotifCollectionListeners) {
- listener.onEntryUpdated(entry);
+ listener.onEntryUpdated(entry, fromSystem);
}
if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index a2c9ffc..38b5ee8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -27,8 +27,8 @@
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.phone.PanelExpansionListener
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
import javax.inject.Inject
@@ -294,8 +294,8 @@
this.state = newState
}
- override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) {
- val collapsedEnough = expansion <= 0.9f
+ override fun onPanelExpansionChanged(fraction: Float, expanded: Boolean, tracking: Boolean) {
+ val collapsedEnough = fraction <= 0.9f
if (collapsedEnough != this.collapsedEnoughToHide) {
val couldShowPulsingHuns = canShowPulsingHuns
this.collapsedEnoughToHide = collapsedEnough
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index b36b7c9..f36f430 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -47,6 +47,7 @@
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.Notification;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.Trace;
import android.os.UserHandle;
@@ -62,6 +63,7 @@
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.LogBufferEulogizer;
import com.android.systemui.flags.FeatureFlags;
@@ -76,6 +78,7 @@
import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent;
import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent;
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
@@ -131,6 +134,7 @@
private final SystemClock mClock;
private final FeatureFlags mFeatureFlags;
private final NotifCollectionLogger mLogger;
+ private final Handler mMainHandler;
private final LogBufferEulogizer mEulogizer;
private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
@@ -154,6 +158,7 @@
SystemClock clock,
FeatureFlags featureFlags,
NotifCollectionLogger logger,
+ @Main Handler mainHandler,
LogBufferEulogizer logBufferEulogizer,
DumpManager dumpManager) {
Assert.isMainThread();
@@ -161,6 +166,7 @@
mClock = clock;
mFeatureFlags = featureFlags;
mLogger = logger;
+ mMainHandler = mainHandler;
mEulogizer = logBufferEulogizer;
dumpManager.registerDumpable(TAG, this);
@@ -442,7 +448,7 @@
mEventQueue.add(new BindEntryEvent(entry, sbn));
mLogger.logNotifUpdated(sbn.getKey());
- mEventQueue.add(new EntryUpdatedEvent(entry));
+ mEventQueue.add(new EntryUpdatedEvent(entry, true /* fromSystem */));
}
}
@@ -791,6 +797,51 @@
private static final String TAG = "NotifCollection";
+ /**
+ * Get an object which can be used to update a notification (internally to the pipeline)
+ * in response to a user action.
+ *
+ * @param name the name of the component that will update notifiations
+ * @return an updater
+ */
+ public InternalNotifUpdater getInternalNotifUpdater(String name) {
+ return (sbn, reason) -> mMainHandler.post(
+ () -> updateNotificationInternally(sbn, name, reason));
+ }
+
+ /**
+ * Provide an updated StatusBarNotification for an existing entry. If no entry exists for the
+ * given notification key, this method does nothing.
+ *
+ * @param sbn the updated notification
+ * @param name the component which is updating the notification
+ * @param reason the reason the notification is being updated
+ */
+ private void updateNotificationInternally(StatusBarNotification sbn, String name,
+ String reason) {
+ Assert.isMainThread();
+ checkForReentrantCall();
+
+ // Make sure we have the notification to update
+ NotificationEntry entry = mNotificationSet.get(sbn.getKey());
+ if (entry == null) {
+ mLogger.logNotifInternalUpdateFailed(sbn.getKey(), name, reason);
+ return;
+ }
+ mLogger.logNotifInternalUpdate(sbn.getKey(), name, reason);
+
+ // First do the pieces of postNotification which are not about assuming the notification
+ // was sent by the app
+ entry.setSbn(sbn);
+ mEventQueue.add(new BindEntryEvent(entry, sbn));
+
+ mLogger.logNotifUpdated(sbn.getKey());
+ mEventQueue.add(new EntryUpdatedEvent(entry, false /* fromSystem */));
+
+ // Skip the applyRanking step and go straight to dispatching the events
+ dispatchEventsAndRebuildList();
+ }
+
@IntDef(prefix = { "REASON_" }, value = {
REASON_NOT_CANCELED,
REASON_UNKNOWN,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
index 5777925..27ba4c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.collection;
+import android.os.Handler;
+
import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
@@ -30,6 +32,7 @@
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
@@ -223,6 +226,17 @@
}
/**
+ * Get an object which can be used to update a notification (internally to the pipeline)
+ * in response to a user action.
+ *
+ * @param name the name of the component that will update notifiations
+ * @return an updater
+ */
+ public InternalNotifUpdater getInternalNotifUpdater(String name) {
+ return mNotifCollection.getInternalNotifUpdater(name);
+ }
+
+ /**
* Returns a read-only view in to the current shade list, i.e. the list of notifications that
* are currently present in the shade. If this method is called during pipeline execution it
* will return the current state of the list, which will likely be only partially-generated.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 66290bb..39b1ec4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -48,6 +48,7 @@
conversationCoordinator: ConversationCoordinator,
preparationCoordinator: PreparationCoordinator,
mediaCoordinator: MediaCoordinator,
+ remoteInputCoordinator: RemoteInputCoordinator,
shadeEventCoordinator: ShadeEventCoordinator,
smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
viewConfigCoordinator: ViewConfigCoordinator,
@@ -72,6 +73,7 @@
mCoordinators.add(bubbleCoordinator)
mCoordinators.add(conversationCoordinator)
mCoordinators.add(mediaCoordinator)
+ mCoordinators.add(remoteInputCoordinator)
mCoordinators.add(shadeEventCoordinator)
mCoordinators.add(viewConfigCoordinator)
mCoordinators.add(visualStabilityCoordinator)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
new file mode 100644
index 0000000..3397815
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.os.Handler
+import android.service.notification.NotificationListenerService.REASON_CANCEL
+import android.service.notification.NotificationListenerService.REASON_CLICK
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputListener
+import com.android.systemui.statusbar.RemoteInputController
+import com.android.systemui.statusbar.RemoteInputNotificationRebuilder
+import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.SelfTrackingLifetimeExtender
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import javax.inject.Inject
+
+private const val TAG = "RemoteInputCoordinator"
+
+/**
+ * How long to wait before auto-dismissing a notification that was kept for active remote input, and
+ * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel
+ * these given that they technically don't exist anymore. We wait a bit in case the app issues
+ * an update, and to also give the other lifetime extenders a beat to decide they want it.
+ */
+private const val REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY: Long = 500
+
+/**
+ * How long to wait before releasing a lifetime extension when requested to do so due to a user
+ * interaction (such as tapping another action).
+ * We wait a bit in case the app issues an update in response to the action, but not too long or we
+ * risk appearing unresponsive to the user.
+ */
+private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200
+
+/** Whether this class should print spammy debug logs */
+private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) }
+
+@SysUISingleton
+class RemoteInputCoordinator @Inject constructor(
+ dumpManager: DumpManager,
+ private val mRebuilder: RemoteInputNotificationRebuilder,
+ private val mNotificationRemoteInputManager: NotificationRemoteInputManager,
+ @Main private val mMainHandler: Handler,
+ private val mSmartReplyController: SmartReplyController
+) : Coordinator, RemoteInputListener, Dumpable {
+
+ @VisibleForTesting val mRemoteInputHistoryExtender = RemoteInputHistoryExtender()
+ @VisibleForTesting val mSmartReplyHistoryExtender = SmartReplyHistoryExtender()
+ @VisibleForTesting val mRemoteInputActiveExtender = RemoteInputActiveExtender()
+ private val mRemoteInputLifetimeExtenders = listOf(
+ mRemoteInputHistoryExtender,
+ mSmartReplyHistoryExtender,
+ mRemoteInputActiveExtender
+ )
+
+ private lateinit var mNotifUpdater: InternalNotifUpdater
+
+ init {
+ dumpManager.registerDumpable(this)
+ }
+
+ fun getLifetimeExtenders(): List<NotifLifetimeExtender> = mRemoteInputLifetimeExtenders
+
+ override fun attach(pipeline: NotifPipeline) {
+ mNotificationRemoteInputManager.setRemoteInputListener(this)
+ mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
+ mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
+ pipeline.addCollectionListener(mCollectionListener)
+ }
+
+ val mCollectionListener = object : NotifCollectionListener {
+ override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
+ if (DEBUG) {
+ Log.d(TAG, "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
+ " fromSystem=$fromSystem)")
+ }
+ if (fromSystem) {
+ // Mark smart replies as sent whenever a notification is updated by the app,
+ // otherwise the smart replies are never marked as sent.
+ mSmartReplyController.stopSending(entry)
+ }
+ }
+
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
+ // We're removing the notification, the smart reply controller can forget about it.
+ // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it.
+ mSmartReplyController.stopSending(entry)
+
+ // When we know the entry will not be lifetime extended, clean up the remote input view
+ // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
+ if (reason == REASON_CANCEL || reason == REASON_CLICK) {
+ mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
+ }
+ }
+ }
+
+ override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+ mRemoteInputLifetimeExtenders.forEach { it.dump(fd, pw, args) }
+ }
+
+ override fun onRemoteInputSent(entry: NotificationEntry) {
+ if (DEBUG) Log.d(TAG, "onRemoteInputSent(entry=${entry.key})")
+ // These calls effectively ensure the freshness of the lifetime extensions.
+ // NOTE: This is some trickery! By removing the lifetime extensions when we know they should
+ // be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to
+ // fire again, thus ensuring that we add subsequent replies to the notification.
+ mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
+ mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+
+ // If we're extending for remote input being active, then from the apps point of
+ // view it is already canceled, so we'll need to cancel it on the apps behalf
+ // now that a reply has been sent. However, delay so that the app has time to posts an
+ // update in the mean time, and to give another lifetime extender time to pick it up.
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+ }
+
+ private fun onSmartReplySent(entry: NotificationEntry, reply: CharSequence) {
+ if (DEBUG) Log.d(TAG, "onSmartReplySent(entry=${entry.key})")
+ val newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply)
+ mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ "Adding smart reply spinner for sent")
+
+ // If we're extending for remote input being active, then from the apps point of
+ // view it is already canceled, so we'll need to cancel it on the apps behalf
+ // now that a reply has been sent. However, delay so that the app has time to posts an
+ // update in the mean time, and to give another lifetime extender time to pick it up.
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+ }
+
+ override fun onPanelCollapsed() {
+ mRemoteInputActiveExtender.endAllLifetimeExtensions()
+ }
+
+ override fun isNotificationKeptForRemoteInputHistory(key: String) =
+ mRemoteInputHistoryExtender.isExtending(key) ||
+ mSmartReplyHistoryExtender.isExtending(key)
+
+ override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
+ if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
+ mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ }
+
+ override fun setRemoteInputController(remoteInputController: RemoteInputController) {
+ mSmartReplyController.setCallback(this::onSmartReplySent)
+ }
+
+ @VisibleForTesting
+ inner class RemoteInputHistoryExtender :
+ SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
+
+ override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
+ mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
+
+ override fun onStartedLifetimeExtension(entry: NotificationEntry) {
+ val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+ entry.onRemoteInputInserted()
+ mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ "Extending lifetime of notification with remote input")
+ // TODO: Check if the entry was removed due perhaps to an inflation exception?
+ }
+ }
+
+ @VisibleForTesting
+ inner class SmartReplyHistoryExtender :
+ SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
+
+ override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
+ mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
+
+ override fun onStartedLifetimeExtension(entry: NotificationEntry) {
+ val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+ mSmartReplyController.stopSending(entry)
+ mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ "Extending lifetime of notification with smart reply")
+ // TODO: Check if the entry was removed due perhaps to an inflation exception?
+ }
+
+ override fun onCanceledLifetimeExtension(entry: NotificationEntry) {
+ // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it.
+ mSmartReplyController.stopSending(entry)
+ }
+ }
+
+ @VisibleForTesting
+ inner class RemoteInputActiveExtender :
+ SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
+
+ override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
+ mNotificationRemoteInputManager.isRemoteInputActive(entry)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/InternalNotifUpdater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/InternalNotifUpdater.java
new file mode 100644
index 0000000..5692fb2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/InternalNotifUpdater.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.notifcollection;
+
+import android.service.notification.StatusBarNotification;
+
+/**
+ * An object that allows Coordinators to update notifications internally to SystemUI.
+ * This is used when part of the UI involves updating the underlying appearance of a notification
+ * on behalf of an app, such as to add a spinner or remote input history.
+ */
+public interface InternalNotifUpdater {
+ /**
+ * Called when an already-existing notification needs to be updated to a new temporary
+ * appearance.
+ * This update is local to the SystemUI process.
+ * This has no effect if no notification with the given key exists in the pipeline.
+ *
+ * @param sbn a notification to update
+ * @param reason a debug reason for the update
+ */
+ void onInternalNotificationUpdate(StatusBarNotification sbn, String reason);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
index db0c174..68a346f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
@@ -56,6 +56,17 @@
/**
* Called whenever a notification with the same key as an existing notification is posted. By
* the time this listener is called, the entry's SBN and Ranking will already have been updated.
+ * This delegates to {@link #onEntryUpdated(NotificationEntry)} by default.
+ * @param fromSystem If true, this update came from the NotificationManagerService.
+ * If false, the notification update is an internal change within systemui.
+ */
+ default void onEntryUpdated(@NonNull NotificationEntry entry, boolean fromSystem) {
+ onEntryUpdated(entry);
+ }
+
+ /**
+ * Called whenever a notification with the same key as an existing notification is posted. By
+ * the time this listener is called, the entry's SBN and Ranking will already have been updated.
*/
default void onEntryUpdated(@NonNull NotificationEntry entry) {
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index f8a778d..1ebc66e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -121,6 +121,26 @@
})
}
+ fun logNotifInternalUpdate(key: String, name: String, reason: String) {
+ buffer.log(TAG, INFO, {
+ str1 = key
+ str2 = name
+ str3 = reason
+ }, {
+ "UPDATED INTERNALLY $str1 BY $str2 BECAUSE $str3"
+ })
+ }
+
+ fun logNotifInternalUpdateFailed(key: String, name: String, reason: String) {
+ buffer.log(TAG, INFO, {
+ str1 = key
+ str2 = name
+ str3 = reason
+ }, {
+ "FAILED INTERNAL UPDATE $str1 BY $str2 BECAUSE $str3"
+ })
+ }
+
fun logNoNotificationToRemoveWithKey(key: String) {
buffer.log(TAG, ERROR, {
str1 = key
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt
index 2810b89..179e953 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt
@@ -64,10 +64,11 @@
}
data class EntryUpdatedEvent(
- val entry: NotificationEntry
+ val entry: NotificationEntry,
+ val fromSystem: Boolean
) : NotifEvent() {
override fun dispatchToListener(listener: NotifCollectionListener) {
- listener.onEntryUpdated(entry)
+ listener.onEntryUpdated(entry, fromSystem)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
new file mode 100644
index 0000000..145c1e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
@@ -0,0 +1,113 @@
+package com.android.systemui.statusbar.notification.collection.notifcollection
+
+import android.os.Handler
+import android.util.ArrayMap
+import android.util.Log
+import com.android.systemui.Dumpable
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.FileDescriptor
+import java.io.PrintWriter
+
+/**
+ * A helpful class that implements the core contract of the lifetime extender internally,
+ * making it easier for coordinators to interact with them
+ */
+abstract class SelfTrackingLifetimeExtender(
+ private val tag: String,
+ private val name: String,
+ private val debug: Boolean,
+ private val mainHandler: Handler
+) : NotifLifetimeExtender, Dumpable {
+ private lateinit var mCallback: NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+ protected val mEntriesExtended = ArrayMap<String, NotificationEntry>()
+ private var mEnding = false
+
+ /**
+ * When debugging, warn if the call is happening during and "end lifetime extension" call.
+ *
+ * Note: this will warn a lot! The pipeline explicitly re-invokes all lifetime extenders
+ * whenever one ends, giving all of them a chance to re-up their lifetime extension.
+ */
+ private fun warnIfEnding() {
+ if (debug && mEnding) Log.w(tag, "reentrant code while ending a lifetime extension")
+ }
+
+ fun endAllLifetimeExtensions() {
+ // clear the map before iterating over a copy of the items, because the pipeline will
+ // always give us another chance to extend the lifetime again, and we don't want
+ // concurrent modification
+ val entries = mEntriesExtended.values.toList()
+ if (debug) Log.d(tag, "$name.endAllLifetimeExtensions() entries=$entries")
+ mEntriesExtended.clear()
+ warnIfEnding()
+ mEnding = true
+ entries.forEach { mCallback.onEndLifetimeExtension(this, it) }
+ mEnding = false
+ }
+
+ fun endLifetimeExtensionAfterDelay(key: String, delayMillis: Long) {
+ if (debug) {
+ Log.d(tag, "$name.endLifetimeExtensionAfterDelay" +
+ "(key=$key, delayMillis=$delayMillis)" +
+ " isExtending=${isExtending(key)}")
+ }
+ if (isExtending(key)) {
+ mainHandler.postDelayed({ endLifetimeExtension(key) }, delayMillis)
+ }
+ }
+
+ fun endLifetimeExtension(key: String) {
+ if (debug) {
+ Log.d(tag, "$name.endLifetimeExtension(key=$key)" +
+ " isExtending=${isExtending(key)}")
+ }
+ warnIfEnding()
+ mEnding = true
+ mEntriesExtended.remove(key)?.let { removedEntry ->
+ mCallback.onEndLifetimeExtension(this, removedEntry)
+ }
+ mEnding = false
+ }
+
+ fun isExtending(key: String) = mEntriesExtended.contains(key)
+
+ final override fun getName(): String = name
+
+ final override fun shouldExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
+ val shouldExtend = queryShouldExtendLifetime(entry)
+ if (debug) {
+ Log.d(tag, "$name.shouldExtendLifetime(key=${entry.key}, reason=$reason)" +
+ " isExtending=${isExtending(entry.key)}" +
+ " shouldExtend=$shouldExtend")
+ }
+ warnIfEnding()
+ if (shouldExtend && mEntriesExtended.put(entry.key, entry) == null) {
+ onStartedLifetimeExtension(entry)
+ }
+ return shouldExtend
+ }
+
+ final override fun cancelLifetimeExtension(entry: NotificationEntry) {
+ if (debug) {
+ Log.d(tag, "$name.cancelLifetimeExtension(key=${entry.key})" +
+ " isExtending=${isExtending(entry.key)}")
+ }
+ warnIfEnding()
+ mEntriesExtended.remove(entry.key)
+ onCanceledLifetimeExtension(entry)
+ }
+
+ abstract fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean
+ open fun onStartedLifetimeExtension(entry: NotificationEntry) {}
+ open fun onCanceledLifetimeExtension(entry: NotificationEntry) {}
+
+ final override fun setCallback(callback: NotifLifetimeExtender.OnEndLifetimeExtensionCallback) {
+ mCallback = callback
+ }
+
+ final override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+ pw.println("LifetimeExtender: $name:")
+ pw.println(" mEntriesExtended: ${mEntriesExtended.size}")
+ mEntriesExtended.forEach { pw.println(" * ${it.key}") }
+ }
+}
\ No newline at end of file
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 ccd4843..1bbd451 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
@@ -1760,7 +1760,9 @@
* Called when a notification is dropped on proper target window.
*/
public void dragAndDropSuccess() {
- mOnDragSuccessListener.onDragSuccess(getEntry());
+ if (mOnDragSuccessListener != null) {
+ mOnDragSuccessListener.onDragSuccess(getEntry());
+ }
}
private void doLongClickCallback() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
index 989f6b8..d5912e0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -253,8 +253,8 @@
state1 = adjustDisableFlags(state1);
mCollapsedStatusBarFragmentLogger.logDisableFlagChange(
- new DisableState(state1BeforeAdjustment, state2),
- new DisableState(state1, state2));
+ /* new= */ new DisableState(state1BeforeAdjustment, state2),
+ /* newAfterLocalModification= */ new DisableState(state1, state2));
final int old1 = mDisabled1;
final int diff1 = state1 ^ old1;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLogger.kt
index 3c2b555..4d472e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLogger.kt
@@ -28,22 +28,31 @@
private val disableFlagsLogger: DisableFlagsLogger,
) {
- /** Logs a string representing the old and new disable flag states to [buffer]. */
+ /**
+ * Logs a string representing the new state received by [CollapsedStatusBarFragment] and any
+ * modifications that were made to the flags locally.
+ *
+ * @param new see [DisableFlagsLogger.getDisableFlagsString]
+ * @param newAfterLocalModification see [DisableFlagsLogger.getDisableFlagsString]
+ */
fun logDisableFlagChange(
- oldState: DisableFlagsLogger.DisableState,
- newState: DisableFlagsLogger.DisableState) {
+ new: DisableFlagsLogger.DisableState,
+ newAfterLocalModification: DisableFlagsLogger.DisableState
+ ) {
buffer.log(
TAG,
LogLevel.INFO,
{
- int1 = oldState.disable1
- int2 = oldState.disable2
- long1 = newState.disable1.toLong()
- long2 = newState.disable2.toLong()
+ int1 = new.disable1
+ int2 = new.disable2
+ long1 = newAfterLocalModification.disable1.toLong()
+ long2 = newAfterLocalModification.disable2.toLong()
},
{
disableFlagsLogger.getDisableFlagsString(
- DisableFlagsLogger.DisableState(int1, int2),
+ old = null,
+ new = DisableFlagsLogger.DisableState(int1, int2),
+ newAfterLocalModification =
DisableFlagsLogger.DisableState(long1.toInt(), long2.toInt())
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 3fe393d..19e8025 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -169,6 +169,7 @@
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -686,6 +687,7 @@
SplitShadeHeaderController splitShadeHeaderController,
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
LockscreenGestureLogger lockscreenGestureLogger,
+ PanelExpansionStateManager panelExpansionStateManager,
NotificationRemoteInputManager remoteInputManager,
ControlsComponent controlsComponent) {
super(view,
@@ -699,6 +701,7 @@
flingAnimationUtilsBuilder.get(),
statusBarTouchableRegionManager,
lockscreenGestureLogger,
+ panelExpansionStateManager,
ambientState);
mView = view;
mVibratorHelper = vibratorHelper;
@@ -2205,10 +2208,6 @@
mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
}
- for (int i = 0; i < mExpansionListeners.size(); i++) {
- mExpansionListeners.get(i).onQsExpansionChanged(
- mQsMaxExpansionHeight != 0 ? mQsExpansionHeight / mQsMaxExpansionHeight : 0);
- }
if (DEBUG) {
mView.invalidate();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 36bd31b..09bcb69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -54,6 +54,7 @@
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.tuner.TunerService;
@@ -105,7 +106,8 @@
private boolean mExpandingBelowNotch;
private final DockManager mDockManager;
private final NotificationPanelViewController mNotificationPanelViewController;
- private final StatusBarWindowView mStatusBarWindowView;
+ private final PanelExpansionStateManager mPanelExpansionStateManager;
+ private final StatusBarWindowController mStatusBarWindowController;
// Used for determining view / touch intersection
private int[] mTempLocation = new int[2];
@@ -134,7 +136,8 @@
NotificationShadeDepthController depthController,
NotificationShadeWindowView notificationShadeWindowView,
NotificationPanelViewController notificationPanelViewController,
- StatusBarWindowView statusBarWindowView,
+ PanelExpansionStateManager panelExpansionStateManager,
+ StatusBarWindowController statusBarWindowController,
NotificationStackScrollLayoutController notificationStackScrollLayoutController,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
LockIconViewController lockIconViewController) {
@@ -157,8 +160,9 @@
mShadeController = shadeController;
mDockManager = dockManager;
mNotificationPanelViewController = notificationPanelViewController;
+ mPanelExpansionStateManager = panelExpansionStateManager;
mDepthController = depthController;
- mStatusBarWindowView = statusBarWindowView;
+ mStatusBarWindowController = statusBarWindowController;
mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mLockIconViewController = lockIconViewController;
@@ -442,7 +446,7 @@
setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper());
mDepthController.setRoot(mView);
- mNotificationPanelViewController.addExpansionListener(mDepthController);
+ mPanelExpansionStateManager.addListener(mDepthController);
}
public NotificationShadeWindowView getView() {
@@ -496,7 +500,7 @@
if (statusBarView != null) {
mBarTransitions = new PhoneStatusBarTransitions(
statusBarView,
- mStatusBarWindowView.findViewById(R.id.status_bar_container));
+ mStatusBarWindowController.getBackgroundView());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
index e5296af..b508ddf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -59,12 +59,12 @@
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.wm.shell.animation.FlingAnimationUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.ArrayList;
public abstract class PanelViewController {
public static final boolean DEBUG = PanelBar.DEBUG;
@@ -86,7 +86,6 @@
private boolean mVibrateOnOpening;
protected boolean mIsLaunchAnimationRunning;
private int mFixedDuration = NO_FIXED_DURATION;
- protected ArrayList<PanelExpansionListener> mExpansionListeners = new ArrayList<>();
protected float mOverExpansion;
/**
@@ -185,6 +184,7 @@
protected final SysuiStatusBarStateController mStatusBarStateController;
protected final AmbientState mAmbientState;
protected final LockscreenGestureLogger mLockscreenGestureLogger;
+ private final PanelExpansionStateManager mPanelExpansionStateManager;
private final TouchHandler mTouchHandler;
protected abstract void onExpandingFinished();
@@ -211,20 +211,25 @@
return mAmbientState;
}
- public PanelViewController(PanelView view,
- FalsingManager falsingManager, DozeLog dozeLog,
+ public PanelViewController(
+ PanelView view,
+ FalsingManager falsingManager,
+ DozeLog dozeLog,
KeyguardStateController keyguardStateController,
- SysuiStatusBarStateController statusBarStateController, VibratorHelper vibratorHelper,
+ SysuiStatusBarStateController statusBarStateController,
+ VibratorHelper vibratorHelper,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
LatencyTracker latencyTracker,
FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
StatusBarTouchableRegionManager statusBarTouchableRegionManager,
LockscreenGestureLogger lockscreenGestureLogger,
+ PanelExpansionStateManager panelExpansionStateManager,
AmbientState ambientState) {
mAmbientState = ambientState;
mView = view;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mLockscreenGestureLogger = lockscreenGestureLogger;
+ mPanelExpansionStateManager = panelExpansionStateManager;
mTouchHandler = createTouchHandler();
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
@@ -1088,9 +1093,8 @@
mBar.panelExpansionChanged(mExpandedFraction, isExpanded());
}
updateVisibility();
- for (int i = 0; i < mExpansionListeners.size(); i++) {
- mExpansionListeners.get(i).onPanelExpansionChanged(mExpandedFraction, mTracking);
- }
+ mPanelExpansionStateManager.onPanelExpansionChanged(
+ mExpandedFraction, isExpanded(), mTracking);
}
public boolean isExpanded() {
@@ -1102,10 +1106,6 @@
&& !mIsSpringBackAnimation;
}
- public void addExpansionListener(PanelExpansionListener panelExpansionListener) {
- mExpansionListeners.add(panelExpansionListener);
- }
-
protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 1921357..cef0613 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -53,6 +53,7 @@
import com.android.systemui.dock.DockManager;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.statusbar.notification.stack.ViewState;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.AlarmTimeout;
@@ -233,7 +234,8 @@
DelayedWakeLock.Builder delayedWakeLockBuilder, Handler handler,
KeyguardUpdateMonitor keyguardUpdateMonitor, DockManager dockManager,
ConfigurationController configurationController, @Main Executor mainExecutor,
- UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
+ UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
+ PanelExpansionStateManager panelExpansionStateManager) {
mScrimStateListener = lightBarController::setScrimState;
mDefaultScrimAlpha = BUSY_SCRIM_ALPHA;
@@ -269,6 +271,9 @@
ScrimController.this.onThemeChanged();
}
});
+ panelExpansionStateManager.addListener(
+ (fraction, expanded, tracking) -> setRawPanelExpansionFraction(fraction)
+ );
mColors = new GradientColors();
}
@@ -481,11 +486,12 @@
*
* The expansion fraction is tied to the scrim opacity.
*
- * See {@link PanelBar#panelExpansionChanged}.
+ * See {@link PanelExpansionListener#onPanelExpansionChanged}.
*
* @param rawPanelExpansionFraction From 0 to 1 where 0 means collapsed and 1 expanded.
*/
- public void setRawPanelExpansionFraction(
+ @VisibleForTesting
+ void setRawPanelExpansionFraction(
@FloatRange(from = 0.0, to = 1.0) float rawPanelExpansionFraction) {
if (isNaN(rawPanelExpansionFraction)) {
throw new IllegalArgumentException("rawPanelExpansionFraction should not be NaN");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 711e941..491e564 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -219,6 +219,7 @@
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -504,7 +505,6 @@
private final StatusBarNotificationActivityStarter.Builder
mStatusBarNotificationActivityStarterBuilder;
private final ShadeController mShadeController;
- private final StatusBarWindowView mStatusBarWindowView;
private final LightsOutNotifController mLightsOutNotifController;
private final InitController mInitController;
@@ -541,6 +541,7 @@
private final NotificationGutsManager mGutsManager;
private final NotificationLogger mNotificationLogger;
private final NotificationViewHierarchyManager mViewHierarchyManager;
+ private final PanelExpansionStateManager mPanelExpansionStateManager;
private final KeyguardViewMediator mKeyguardViewMediator;
protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
private final BrightnessSliderController.Factory mBrightnessSliderFactory;
@@ -726,6 +727,7 @@
NotificationLogger notificationLogger,
NotificationInterruptStateProvider notificationInterruptStateProvider,
NotificationViewHierarchyManager notificationViewHierarchyManager,
+ PanelExpansionStateManager panelExpansionStateManager,
KeyguardViewMediator keyguardViewMediator,
DisplayMetrics displayMetrics,
MetricsLogger metricsLogger,
@@ -767,7 +769,6 @@
StatusBarNotificationActivityStarter.Builder
statusBarNotificationActivityStarterBuilder,
ShadeController shadeController,
- StatusBarWindowView statusBarWindowView,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
ViewMediatorCallback viewMediatorCallback,
InitController initController,
@@ -832,6 +833,7 @@
mNotificationLogger = notificationLogger;
mNotificationInterruptStateProvider = notificationInterruptStateProvider;
mViewHierarchyManager = notificationViewHierarchyManager;
+ mPanelExpansionStateManager = panelExpansionStateManager;
mKeyguardViewMediator = keyguardViewMediator;
mDisplayMetrics = displayMetrics;
mMetricsLogger = metricsLogger;
@@ -872,7 +874,6 @@
mSplitScreenOptional = splitScreenOptional;
mStatusBarNotificationActivityStarterBuilder = statusBarNotificationActivityStarterBuilder;
mShadeController = shadeController;
- mStatusBarWindowView = statusBarWindowView;
mLightsOutNotifController = lightsOutNotifController;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mKeyguardViewMediatorCallback = viewMediatorCallback;
@@ -908,9 +909,7 @@
lockscreenShadeTransitionController.setStatusbar(this);
mExpansionChangedListeners = new ArrayList<>();
- addExpansionChangedListener(
- (expansion, expanded) -> mScrimController.setRawPanelExpansionFraction(expansion));
- addExpansionChangedListener(this::onPanelExpansionChanged);
+ mPanelExpansionStateManager.addListener(this::onPanelExpansionChanged);
mBubbleExpandListener =
(isExpanding, key) -> mContext.getMainExecutor().execute(() -> {
@@ -1158,16 +1157,14 @@
mNotificationLogger.setUpWithContainer(notifListContainer);
mNotificationIconAreaController.setupShelf(mNotificationShelfController);
- mNotificationPanelViewController.addExpansionListener(mWakeUpCoordinator);
- mNotificationPanelViewController.addExpansionListener(
- this::dispatchPanelExpansionForKeyguardDismiss);
+ mPanelExpansionStateManager.addListener(mWakeUpCoordinator);
mUserSwitcherController.init(mNotificationShadeWindowView);
// Allow plugins to reference DarkIconDispatcher and StatusBarStateController
mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class);
- FragmentHostManager.get(mStatusBarWindowView)
+ mStatusBarWindowController.getFragmentHostManager()
.addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
CollapsedStatusBarFragment statusBarFragment =
(CollapsedStatusBarFragment) fragment;
@@ -1425,15 +1422,15 @@
/**
- * When swiping up to dismiss the lock screen, the panel expansion goes from 1f to 0f. This
- * results in the clock/notifications/other content disappearing off the top of the screen.
+ * When swiping up to dismiss the lock screen, the panel expansion fraction goes from 1f to 0f.
+ * This results in the clock/notifications/other content disappearing off the top of the screen.
*
- * We also use the expansion amount to animate in the app/launcher surface from the bottom of
+ * We also use the expansion fraction to animate in the app/launcher surface from the bottom of
* the screen, 'pushing' off the notifications and other content. To do this, we dispatch the
- * expansion amount to the KeyguardViewMediator if we're in the process of dismissing the
+ * expansion fraction to the KeyguardViewMediator if we're in the process of dismissing the
* keyguard.
*/
- private void dispatchPanelExpansionForKeyguardDismiss(float expansion, boolean trackingTouch) {
+ private void dispatchPanelExpansionForKeyguardDismiss(float fraction, boolean trackingTouch) {
// Things that mean we're not dismissing the keyguard, and should ignore this expansion:
// - Keyguard isn't even visible.
// - Keyguard is visible, but can't be dismissed (swiping up will show PIN/password prompt).
@@ -1452,12 +1449,14 @@
|| mKeyguardViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehindOrWillBe()
|| mKeyguardUnlockAnimationController.isUnlockingWithSmartSpaceTransition()) {
mKeyguardStateController.notifyKeyguardDismissAmountChanged(
- 1f - expansion, trackingTouch);
+ 1f - fraction, trackingTouch);
}
}
- private void onPanelExpansionChanged(float frac, boolean expanded) {
- if (frac == 0 || frac == 1) {
+ private void onPanelExpansionChanged(float fraction, boolean expanded, boolean tracking) {
+ dispatchPanelExpansionForKeyguardDismiss(fraction, tracking);
+
+ if (fraction == 0 || fraction == 1) {
if (getNavigationBarView() != null) {
getNavigationBarView().onStatusBarPanelStateChanged();
}
@@ -1669,8 +1668,11 @@
});
mStatusBarKeyguardViewManager.registerStatusBar(
/* statusBar= */ this,
- mNotificationPanelViewController, mBiometricUnlockController,
- mStackScroller, mKeyguardBypassController);
+ mNotificationPanelViewController,
+ mPanelExpansionStateManager,
+ mBiometricUnlockController,
+ mStackScroller,
+ mKeyguardBypassController);
mKeyguardIndicationController
.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
@@ -1691,10 +1693,6 @@
return mNotificationShadeWindowView;
}
- public StatusBarWindowView getStatusBarWindow() {
- return mStatusBarWindowView;
- }
-
public NotificationShadeWindowViewController getNotificationShadeWindowViewController() {
return mNotificationShadeWindowViewController;
}
@@ -2421,7 +2419,7 @@
pw.print(" mDozing="); pw.println(mDozing);
pw.print(" mWallpaperSupported= "); pw.println(mWallpaperSupported);
- pw.println(" StatusBarWindowView: ");
+ pw.println(" ShadeWindowView: ");
if (mNotificationShadeWindowViewController != null) {
mNotificationShadeWindowViewController.dump(fd, pw, args);
dumpBarTransitions(pw, "PhoneStatusBarTransitions",
@@ -2658,26 +2656,12 @@
private ActivityLaunchAnimator.Controller wrapAnimationController(
ActivityLaunchAnimator.Controller animationController, boolean dismissShade) {
View rootView = animationController.getLaunchContainer().getRootView();
- if (rootView == mStatusBarWindowView) {
- // We are animating a view in the status bar. We have to make sure that the status bar
- // window matches the full screen during the animation and that we are expanding the
- // view below the other status bar text.
- animationController.setLaunchContainer(
- mStatusBarWindowController.getLaunchAnimationContainer());
- return new DelegateLaunchAnimatorController(animationController) {
- @Override
- public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
- getDelegate().onLaunchAnimationStart(isExpandingFullyAbove);
- mStatusBarWindowController.setLaunchAnimationRunning(true);
- }
-
- @Override
- public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) {
- getDelegate().onLaunchAnimationEnd(isExpandingFullyAbove);
- mStatusBarWindowController.setLaunchAnimationRunning(false);
- }
- };
+ Optional<ActivityLaunchAnimator.Controller> controllerFromStatusBar =
+ mStatusBarWindowController.wrapAnimationControllerIfInStatusBar(
+ rootView, animationController);
+ if (controllerFromStatusBar.isPresent()) {
+ return controllerFromStatusBar.get();
}
if (dismissShade && rootView == mNotificationShadeWindowView) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index cac66a3..30e668a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -63,6 +63,8 @@
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -266,6 +268,7 @@
@Override
public void registerStatusBar(StatusBar statusBar,
NotificationPanelViewController notificationPanelViewController,
+ PanelExpansionStateManager panelExpansionStateManager,
BiometricUnlockController biometricUnlockController,
View notificationContainer,
KeyguardBypassController bypassController) {
@@ -275,7 +278,9 @@
ViewGroup container = mStatusBar.getBouncerContainer();
mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
mNotificationPanelViewController = notificationPanelViewController;
- notificationPanelViewController.addExpansionListener(this);
+ if (panelExpansionStateManager != null) {
+ panelExpansionStateManager.addListener(this);
+ }
mBypassController = bypassController;
mNotificationContainer = notificationContainer;
mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create(
@@ -323,7 +328,7 @@
}
@Override
- public void onPanelExpansionChanged(float expansion, boolean tracking) {
+ public void onPanelExpansionChanged(float fraction, boolean expanded, boolean tracking) {
// We don't want to translate the bounce when:
// • Keyguard is occluded, because we're in a FLAG_SHOW_WHEN_LOCKED activity and need to
// conserve the original animation.
@@ -336,14 +341,14 @@
mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
} else if (mShowing) {
if (!isWakeAndUnlocking() && !mStatusBar.isInLaunchTransition()) {
- mBouncer.setExpansion(expansion);
+ mBouncer.setExpansion(fraction);
}
- if (expansion != KeyguardBouncer.EXPANSION_HIDDEN && tracking
+ if (fraction != KeyguardBouncer.EXPANSION_HIDDEN && tracking
&& !mKeyguardStateController.canDismissLockScreen()
&& !mBouncer.isShowing() && !mBouncer.isAnimatingAway()) {
mBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
}
- } else if (mPulsing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) {
+ } else if (mPulsing && fraction == KeyguardBouncer.EXPANSION_VISIBLE) {
// Panel expanded while pulsing but didn't translate the bouncer (because we are
// unlocked.) Let's simply wake-up to dismiss the lock screen.
mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mStatusBar.getBouncerContainer(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
index 9d2dbc1..f8895d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
@@ -36,14 +36,21 @@
import android.util.Log;
import android.view.Gravity;
import android.view.IWindowManager;
+import android.view.LayoutInflater;
import android.view.Surface;
+import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.R;
+import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.DelegateLaunchAnimatorController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.fragments.FragmentHostManager;
+
+import java.util.Optional;
import javax.inject.Inject;
@@ -63,7 +70,9 @@
private int mBarHeight = -1;
private final State mCurrentState = new State();
- private final ViewGroup mStatusBarView;
+ private final ViewGroup mStatusBarWindowView;
+ // The container in which we should run launch animations started from the status bar and
+ // expanding into the opening window.
private final ViewGroup mLaunchAnimationContainer;
private WindowManager.LayoutParams mLp;
private final WindowManager.LayoutParams mLpChanged;
@@ -73,15 +82,14 @@
Context context,
WindowManager windowManager,
IWindowManager iWindowManager,
- StatusBarWindowView statusBarWindowView,
StatusBarContentInsetsProvider contentInsetsProvider,
@Main Resources resources) {
mContext = context;
mWindowManager = windowManager;
mIWindowManager = iWindowManager;
mContentInsetsProvider = contentInsetsProvider;
- mStatusBarView = statusBarWindowView;
- mLaunchAnimationContainer = mStatusBarView.findViewById(
+ mStatusBarWindowView = createWindowView(mContext);
+ mLaunchAnimationContainer = mStatusBarWindowView.findViewById(
R.id.status_bar_launch_animation_container);
mLpChanged = new WindowManager.LayoutParams();
mResources = resources;
@@ -119,13 +127,63 @@
// hardware-accelerated.
mLp = getBarLayoutParams(mContext.getDisplay().getRotation());
- mWindowManager.addView(mStatusBarView, mLp);
+ mWindowManager.addView(mStatusBarWindowView, mLp);
mLpChanged.copyFrom(mLp);
mContentInsetsProvider.addCallback(this::calculateStatusBarLocationsForAllRotations);
calculateStatusBarLocationsForAllRotations();
}
+ /** Adds the given view to the status bar window view. */
+ public void addViewToWindow(View view, ViewGroup.LayoutParams layoutParams) {
+ mStatusBarWindowView.addView(view, layoutParams);
+ }
+
+ /** Returns the status bar window's background view. */
+ public View getBackgroundView() {
+ return mStatusBarWindowView.findViewById(R.id.status_bar_container);
+ }
+
+ /** Returns a fragment host manager for the status bar window view. */
+ public FragmentHostManager getFragmentHostManager() {
+ return FragmentHostManager.get(mStatusBarWindowView);
+ }
+
+ /**
+ * Provides an updated animation controller if we're animating a view in the status bar.
+ *
+ * This is needed because we have to make sure that the status bar window matches the full
+ * screen during the animation and that we are expanding the view below the other status bar
+ * text.
+ *
+ * @param rootView the root view of the animation
+ * @param animationController the default animation controller to use
+ * @return If the animation is on a view in the status bar, returns an Optional containing an
+ * updated animation controller that handles status-bar-related animation details. Returns an
+ * empty optional if the animation is *not* on a view in the status bar.
+ */
+ public Optional<ActivityLaunchAnimator.Controller> wrapAnimationControllerIfInStatusBar(
+ View rootView, ActivityLaunchAnimator.Controller animationController) {
+ if (rootView != mStatusBarWindowView) {
+ return Optional.empty();
+ }
+
+ animationController.setLaunchContainer(mLaunchAnimationContainer);
+ return Optional.of(new DelegateLaunchAnimatorController(animationController) {
+ @Override
+ public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
+ getDelegate().onLaunchAnimationStart(isExpandingFullyAbove);
+ setLaunchAnimationRunning(true);
+ }
+
+ @Override
+ public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) {
+ getDelegate().onLaunchAnimationEnd(isExpandingFullyAbove);
+ setLaunchAnimationRunning(false);
+ }
+ });
+ }
+
private WindowManager.LayoutParams getBarLayoutParams(int rotation) {
WindowManager.LayoutParams lp = getBarLayoutParamsForRotation(rotation);
lp.paramsForRotation = new WindowManager.LayoutParams[4];
@@ -214,21 +272,11 @@
}
/**
- * Return the container in which we should run launch animations started from the status bar and
- * expanding into the opening window.
- *
- * @see #setLaunchAnimationRunning
- */
- public ViewGroup getLaunchAnimationContainer() {
- return mLaunchAnimationContainer;
- }
-
- /**
* Set whether a launch animation is currently running. If true, this will ensure that the
* window matches its parent height so that the animation is not clipped by the normal status
* bar height.
*/
- public void setLaunchAnimationRunning(boolean isLaunchAnimationRunning) {
+ private void setLaunchAnimationRunning(boolean isLaunchAnimationRunning) {
if (isLaunchAnimationRunning == mCurrentState.mIsLaunchAnimationRunning) {
return;
}
@@ -246,7 +294,7 @@
applyForceStatusBarVisibleFlag(state);
applyHeight(state);
if (mLp != null && mLp.copyFrom(mLpChanged) != 0) {
- mWindowManager.updateViewLayout(mStatusBarView, mLp);
+ mWindowManager.updateViewLayout(mStatusBarWindowView, mLp);
}
}
@@ -266,4 +314,14 @@
mLpChanged.privateFlags &= ~PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
}
}
+
+ private ViewGroup createWindowView(Context context) {
+ ViewGroup view = (ViewGroup) LayoutInflater.from(context).inflate(
+ R.layout.super_status_bar, /* root= */ null);
+ if (view == null) {
+ throw new IllegalStateException(
+ "R.layout.super_status_bar could not be properly inflated");
+ }
+ return view;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index 959c673..adff2fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -99,9 +99,9 @@
import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController;
import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter;
import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
-import com.android.systemui.statusbar.phone.StatusBarWindowView;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -164,6 +164,7 @@
NotificationLogger notificationLogger,
NotificationInterruptStateProvider notificationInterruptStateProvider,
NotificationViewHierarchyManager notificationViewHierarchyManager,
+ PanelExpansionStateManager panelExpansionStateManager,
KeyguardViewMediator keyguardViewMediator,
DisplayMetrics displayMetrics,
MetricsLogger metricsLogger,
@@ -205,7 +206,6 @@
StatusBarNotificationActivityStarter.Builder
statusBarNotificationActivityStarterBuilder,
ShadeController shadeController,
- StatusBarWindowView statusBarWindowView,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
ViewMediatorCallback viewMediatorCallback,
InitController initController,
@@ -268,6 +268,7 @@
notificationLogger,
notificationInterruptStateProvider,
notificationViewHierarchyManager,
+ panelExpansionStateManager,
keyguardViewMediator,
displayMetrics,
metricsLogger,
@@ -308,7 +309,6 @@
lightsOutNotifController,
statusBarNotificationActivityStarterBuilder,
shadeController,
- statusBarWindowView,
statusBarKeyguardViewManager,
viewMediatorCallback,
initController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 3806d9a..31cc823 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -86,7 +86,7 @@
//
// TODO(b/183229367): Remove this function override when b/178406514 is fixed.
override fun onEntryAdded(entry: NotificationEntry) {
- onEntryUpdated(entry)
+ onEntryUpdated(entry, true)
}
override fun onEntryUpdated(entry: NotificationEntry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelExpansionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.java
similarity index 65%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelExpansionListener.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.java
index 655a25d..b9f806d20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelExpansionListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.java
@@ -11,28 +11,21 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
-package com.android.systemui.statusbar.phone;
+package com.android.systemui.statusbar.phone.panelstate;
-/**
- * Panel and QS expansion callbacks.
- */
+/** A listener interface to be notified of expansion events for the notification panel. */
public interface PanelExpansionListener {
/**
* Invoked whenever the notification panel expansion changes, at every animation frame.
* This is the main expansion that happens when the user is swiping up to dismiss the
- * lock screen.
+ * lock screen and swiping to pull down the notification shade.
*
- * @param expansion 0 when collapsed, 1 when expanded.
+ * @param fraction 0 when collapsed, 1 when fully expanded.
+ * @param expanded true if the panel should be considered expanded.
* @param tracking {@code true} when the user is actively dragging the panel.
*/
- void onPanelExpansionChanged(float expansion, boolean tracking);
-
- /**
- * Invoked whenever the QS expansion changes, at every animation frame.
- * @param expansion 0 when collapsed, 1 when expanded.
- */
- default void onQsExpansionChanged(float expansion) {};
+ void onPanelExpansionChanged(float fraction, boolean expanded, boolean tracking);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
new file mode 100644
index 0000000..71b7066
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.panelstate
+
+import androidx.annotation.FloatRange
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/**
+ * A class responsible for managing the notification panel's current state.
+ *
+ * TODO(b/200063118): Move [PanelBar.panelExpansionChanged] logic to this class and make this class
+ * the one source of truth for the state of panel expansion.
+ */
+@SysUISingleton
+class PanelExpansionStateManager @Inject constructor() {
+
+ private val listeners: MutableList<PanelExpansionListener> = mutableListOf()
+
+ @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
+ private var expanded: Boolean = false
+ private var tracking: Boolean = false
+
+ /**
+ * Adds a listener that will be notified when the panel expansion has changed.
+ *
+ * Listener will also be immediately notified with the current values.
+ */
+ fun addListener(listener: PanelExpansionListener) {
+ listeners.add(listener)
+ listener.onPanelExpansionChanged(fraction, expanded, tracking)
+ }
+
+ /** Called when the panel expansion has changed. Notifies all listeners of change. */
+ fun onPanelExpansionChanged(
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float,
+ expanded: Boolean,
+ tracking: Boolean
+ ) {
+ this.fraction = fraction
+ this.expanded = expanded
+ this.tracking = tracking
+ listeners.forEach { it.onPanelExpansionChanged(fraction, expanded, tracking) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
new file mode 100644
index 0000000..e2c6ff9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.statusbar.DisableFlagsLogger
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.mock
+import java.io.PrintWriter
+import java.io.StringWriter
+
+@SmallTest
+class QSFragmentDisableFlagsLoggerTest : SysuiTestCase() {
+
+ private val buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+ .create("buffer", 10)
+ private val disableFlagsLogger = DisableFlagsLogger(
+ listOf(DisableFlagsLogger.DisableFlag(0b001, 'A', 'a')),
+ listOf(DisableFlagsLogger.DisableFlag(0b001, 'B', 'b'))
+ )
+ private val logger = QSFragmentDisableFlagsLogger(buffer, disableFlagsLogger)
+
+ @Test
+ fun logDisableFlagChange_bufferHasStates() {
+ val state = DisableFlagsLogger.DisableState(0, 1)
+
+ logger.logDisableFlagChange(state, state)
+
+ val stringWriter = StringWriter()
+ buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+ val actualString = stringWriter.toString()
+ val expectedLogString = disableFlagsLogger.getDisableFlagsString(
+ old = null, new = state, newAfterLocalModification = state
+ )
+
+ assertThat(actualString).contains(expectedLogString)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index c4bab73..30664ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -183,6 +183,7 @@
mQQSMediaHost,
mBypassController,
mQsComponentFactory,
+ mock(QSFragmentDisableFlagsLogger.class),
mFalsingManager,
mock(DumpManager.class));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/DisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DisableFlagsLoggerTest.kt
index 096efad..38ad6b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/DisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DisableFlagsLoggerTest.kt
@@ -86,6 +86,23 @@
}
@Test
+ fun getDisableFlagsString_nullOld_onlyNewStateLogged() {
+ val result = disableFlagsLogger.getDisableFlagsString(
+ old = null,
+ new = DisableFlagsLogger.DisableState(
+ 0b001, // abC
+ 0b01, // mN
+ ),
+ )
+
+ assertThat(result).doesNotContain("Old")
+ assertThat(result).contains("New: abC.mN")
+ // We have no state to diff on, so we shouldn't see any diff in parentheses
+ assertThat(result).doesNotContain("(")
+ assertThat(result).doesNotContain(")")
+ }
+
+ @Test
fun getDisableFlagsString_nullLocalModification_localModNotLogged() {
val result = disableFlagsLogger.getDisableFlagsString(
DisableFlagsLogger.DisableState(0, 0),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index 5944e9c..4ed7224 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.systemui.statusbar;
@@ -10,26 +25,25 @@
import static org.mockito.Mockito.when;
import android.app.Notification;
-import android.app.RemoteInputHistoryItem;
import android.content.Context;
-import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
-import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputActiveExtender;
-import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputHistoryExtender;
-import com.android.systemui.statusbar.NotificationRemoteInputManager.SmartReplyHistoryExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputActiveExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputHistoryExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.SmartReplyHistoryExtender;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -76,13 +90,19 @@
private RemoteInputHistoryExtender mRemoteInputHistoryExtender;
private SmartReplyHistoryExtender mSmartReplyHistoryExtender;
private RemoteInputActiveExtender mRemoteInputActiveExtender;
+ private TestableNotificationRemoteInputManager.FakeLegacyRemoteInputLifetimeExtender
+ mLegacyRemoteInputLifetimeExtender;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext,
- mLockscreenUserManager, mSmartReplyController, mEntryManager,
+ mock(FeatureFlags.class),
+ mLockscreenUserManager,
+ mSmartReplyController,
+ mEntryManager,
+ mock(RemoteInputNotificationRebuilder.class),
() -> Optional.of(mock(StatusBar.class)),
mStateController,
Handler.createAsync(Looper.myLooper()),
@@ -120,6 +140,7 @@
public void testShouldExtendLifetime_remoteInputActive() {
when(mController.isRemoteInputActive(mEntry)).thenReturn(true);
+ assertTrue(mRemoteInputManager.isRemoteInputActive(mEntry));
assertTrue(mRemoteInputActiveExtender.shouldExtendLifetime(mEntry));
}
@@ -128,6 +149,7 @@
NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
when(mController.isSpinning(mEntry.getKey())).thenReturn(true);
+ assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
}
@@ -136,6 +158,7 @@
NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime();
+ assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
}
@@ -144,6 +167,7 @@
NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
when(mSmartReplyController.isSendingSmartReply(mEntry.getKey())).thenReturn(true);
+ assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry));
assertTrue(mSmartReplyHistoryExtender.shouldExtendLifetime(mEntry));
}
@@ -151,124 +175,24 @@
public void testNotificationWithRemoteInputActiveIsRemovedOnCollapse() {
mRemoteInputActiveExtender.setShouldManageLifetime(mEntry, true /* shouldManage */);
- assertEquals(mRemoteInputManager.getEntriesKeptForRemoteInputActive(),
+ assertEquals(mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive(),
Sets.newArraySet(mEntry));
mRemoteInputManager.onPanelCollapsed();
- assertTrue(mRemoteInputManager.getEntriesKeptForRemoteInputActive().isEmpty());
+ assertTrue(
+ mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive().isEmpty());
}
- @Test
- public void testRebuildWithRemoteInput_noExistingInput_image() {
- Uri uri = mock(Uri.class);
- String mimeType = "image/jpeg";
- String text = "image inserted";
- StatusBarNotification newSbn =
- mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
- mEntry, text, false, mimeType, uri);
- RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
- .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
- assertEquals(1, messages.length);
- assertEquals(text, messages[0].getText());
- assertEquals(mimeType, messages[0].getMimeType());
- assertEquals(uri, messages[0].getUri());
- }
-
- @Test
- public void testRebuildWithRemoteInput_noExistingInputNoSpinner() {
- StatusBarNotification newSbn =
- mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
- mEntry, "A Reply", false, null, null);
- RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
- .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
- assertEquals(1, messages.length);
- assertEquals("A Reply", messages[0].getText());
- assertFalse(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
- assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
- }
-
- @Test
- public void testRebuildWithRemoteInput_noExistingInputWithSpinner() {
- StatusBarNotification newSbn =
- mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
- mEntry, "A Reply", true, null, null);
- RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
- .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
- assertEquals(1, messages.length);
- assertEquals("A Reply", messages[0].getText());
- assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
- assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
- }
-
- @Test
- public void testRebuildWithRemoteInput_withExistingInput() {
- // Setup a notification entry with 1 remote input.
- StatusBarNotification newSbn =
- mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
- mEntry, "A Reply", false, null, null);
- NotificationEntry entry = new NotificationEntryBuilder()
- .setSbn(newSbn)
- .build();
-
- // Try rebuilding to add another reply.
- newSbn = mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
- entry, "Reply 2", true, null, null);
- RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
- .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
- assertEquals(2, messages.length);
- assertEquals("Reply 2", messages[0].getText());
- assertEquals("A Reply", messages[1].getText());
- }
-
- @Test
- public void testRebuildWithRemoteInput_withExistingInput_image() {
- // Setup a notification entry with 1 remote input.
- Uri uri = mock(Uri.class);
- String mimeType = "image/jpeg";
- String text = "image inserted";
- StatusBarNotification newSbn =
- mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
- mEntry, text, false, mimeType, uri);
- NotificationEntry entry = new NotificationEntryBuilder()
- .setSbn(newSbn)
- .build();
-
- // Try rebuilding to add another reply.
- newSbn = mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
- entry, "Reply 2", true, null, null);
- RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
- .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
- assertEquals(2, messages.length);
- assertEquals("Reply 2", messages[0].getText());
- assertEquals(text, messages[1].getText());
- assertEquals(mimeType, messages[1].getMimeType());
- assertEquals(uri, messages[1].getUri());
- }
-
- @Test
- public void testRebuildNotificationForCanceledSmartReplies() {
- // Try rebuilding to remove spinner and hide buttons.
- StatusBarNotification newSbn =
- mRemoteInputManager.rebuildNotificationForCanceledSmartReplies(mEntry);
- assertFalse(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
- assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
- }
-
-
private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
TestableNotificationRemoteInputManager(
Context context,
+ FeatureFlags featureFlags,
NotificationLockscreenUserManager lockscreenUserManager,
SmartReplyController smartReplyController,
NotificationEntryManager notificationEntryManager,
+ RemoteInputNotificationRebuilder rebuilder,
Lazy<Optional<StatusBar>> statusBarOptionalLazy,
StatusBarStateController statusBarStateController,
Handler mainHandler,
@@ -278,9 +202,11 @@
DumpManager dumpManager) {
super(
context,
+ featureFlags,
lockscreenUserManager,
smartReplyController,
notificationEntryManager,
+ rebuilder,
statusBarOptionalLazy,
statusBarStateController,
mainHandler,
@@ -297,14 +223,28 @@
mRemoteInputController = controller;
}
+ @NonNull
@Override
- protected void addLifetimeExtenders() {
- mRemoteInputActiveExtender = new RemoteInputActiveExtender();
- mRemoteInputHistoryExtender = new RemoteInputHistoryExtender();
- mSmartReplyHistoryExtender = new SmartReplyHistoryExtender();
- mLifetimeExtenders.add(mRemoteInputHistoryExtender);
- mLifetimeExtenders.add(mSmartReplyHistoryExtender);
- mLifetimeExtenders.add(mRemoteInputActiveExtender);
+ protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
+ Handler mainHandler,
+ NotificationEntryManager notificationEntryManager,
+ SmartReplyController smartReplyController) {
+ mLegacyRemoteInputLifetimeExtender = new FakeLegacyRemoteInputLifetimeExtender();
+ return mLegacyRemoteInputLifetimeExtender;
}
+
+ class FakeLegacyRemoteInputLifetimeExtender extends LegacyRemoteInputLifetimeExtender {
+
+ @Override
+ protected void addLifetimeExtenders() {
+ mRemoteInputActiveExtender = new RemoteInputActiveExtender();
+ mRemoteInputHistoryExtender = new RemoteInputHistoryExtender();
+ mSmartReplyHistoryExtender = new SmartReplyHistoryExtender();
+ mLifetimeExtenders.add(mRemoteInputHistoryExtender);
+ mLifetimeExtenders.add(mSmartReplyHistoryExtender);
+ mLifetimeExtenders.add(mRemoteInputActiveExtender);
+ }
+ }
+
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index dbd5168..0bce621 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -119,15 +119,17 @@
@Test
fun onPanelExpansionChanged_apliesBlur_ifShade() {
- notificationShadeDepthController.onPanelExpansionChanged(1f /* expansion */,
- false /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = false
+ )
verify(shadeAnimation).animateTo(eq(maxBlur), any())
}
@Test
fun onPanelExpansionChanged_animatesBlurIn_ifShade() {
- notificationShadeDepthController.onPanelExpansionChanged(0.01f /* expansion */,
- false /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 0.01f, expanded = false, tracking = false
+ )
verify(shadeAnimation).animateTo(eq(maxBlur), any())
}
@@ -135,8 +137,9 @@
fun onPanelExpansionChanged_animatesBlurOut_ifShade() {
onPanelExpansionChanged_animatesBlurIn_ifShade()
clearInvocations(shadeAnimation)
- notificationShadeDepthController.onPanelExpansionChanged(0f /* expansion */,
- false /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 0f, expanded = false, tracking = false
+ )
verify(shadeAnimation).animateTo(eq(0), any())
}
@@ -144,16 +147,19 @@
fun onPanelExpansionChanged_animatesBlurOut_ifFlick() {
onPanelExpansionChanged_apliesBlur_ifShade()
clearInvocations(shadeAnimation)
- notificationShadeDepthController.onPanelExpansionChanged(1f /* expansion */,
- true /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = true
+ )
verify(shadeAnimation, never()).animateTo(anyInt(), any())
- notificationShadeDepthController.onPanelExpansionChanged(0.9f /* expansion */,
- true /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 0.9f, expanded = true, tracking = true
+ )
verify(shadeAnimation, never()).animateTo(anyInt(), any())
- notificationShadeDepthController.onPanelExpansionChanged(0.8f /* expansion */,
- false /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 0.8f, expanded = true, tracking = false
+ )
verify(shadeAnimation).animateTo(eq(0), any())
}
@@ -161,24 +167,28 @@
fun onPanelExpansionChanged_animatesBlurIn_ifFlickCancelled() {
onPanelExpansionChanged_animatesBlurOut_ifFlick()
clearInvocations(shadeAnimation)
- notificationShadeDepthController.onPanelExpansionChanged(0.6f /* expansion */,
- true /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 0.6f, expanded = true, tracking = true
+ )
verify(shadeAnimation).animateTo(eq(maxBlur), any())
}
@Test
fun onPanelExpansionChanged_respectsMinPanelPullDownFraction() {
notificationShadeDepthController.panelPullDownMinFraction = 0.5f
- notificationShadeDepthController.onPanelExpansionChanged(0.5f /* expansion */,
- true /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 0.5f, expanded = true, tracking = true
+ )
assertThat(notificationShadeDepthController.shadeExpansion).isEqualTo(0f)
- notificationShadeDepthController.onPanelExpansionChanged(0.75f /* expansion */,
- true /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 0.75f, expanded = true, tracking = true
+ )
assertThat(notificationShadeDepthController.shadeExpansion).isEqualTo(0.5f)
- notificationShadeDepthController.onPanelExpansionChanged(1f /* expansion */,
- true /* tracking */)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = true
+ )
assertThat(notificationShadeDepthController.shadeExpansion).isEqualTo(1f)
}
@@ -196,7 +206,9 @@
fun setQsPanelExpansion_appliesBlur() {
statusBarState = StatusBarState.KEYGUARD
notificationShadeDepthController.qsPanelExpansion = 1f
- notificationShadeDepthController.onPanelExpansionChanged(1f, tracking = false)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = false
+ )
notificationShadeDepthController.updateBlurCallback.doFrame(0)
verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
}
@@ -205,7 +217,9 @@
fun setQsPanelExpansion_easing() {
statusBarState = StatusBarState.KEYGUARD
notificationShadeDepthController.qsPanelExpansion = 0.25f
- notificationShadeDepthController.onPanelExpansionChanged(1f, tracking = false)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = false
+ )
notificationShadeDepthController.updateBlurCallback.doFrame(0)
verify(wallpaperController).setNotificationShadeZoom(
eq(ShadeInterpolation.getNotificationScrimAlpha(0.25f)))
@@ -261,7 +275,9 @@
@Test
fun updateBlurCallback_setsBlur_whenExpanded() {
- notificationShadeDepthController.onPanelExpansionChanged(1f, false)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = false
+ )
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
notificationShadeDepthController.updateBlurCallback.doFrame(0)
verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
@@ -269,7 +285,9 @@
@Test
fun updateBlurCallback_ignoreShadeBlurUntilHidden_overridesZoom() {
- notificationShadeDepthController.onPanelExpansionChanged(1f, false)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = false
+ )
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
notificationShadeDepthController.blursDisabledForAppLaunch = true
notificationShadeDepthController.updateBlurCallback.doFrame(0)
@@ -300,7 +318,9 @@
// Brightness mirror is fully visible
`when`(brightnessSpring.ratio).thenReturn(1f)
// And shade is blurred
- notificationShadeDepthController.onPanelExpansionChanged(1f, false)
+ notificationShadeDepthController.onPanelExpansionChanged(
+ rawFraction = 1f, expanded = true, tracking = false
+ )
`when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
notificationShadeDepthController.updateBlurCallback.doFrame(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilderTest.java
new file mode 100644
index 0000000..ce11d6a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilderTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.mock;
+
+import android.app.Notification;
+import android.app.RemoteInputHistoryItem;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class RemoteInputNotificationRebuilderTest extends SysuiTestCase {
+ private static final String TEST_PACKAGE_NAME = "test";
+ private static final int TEST_UID = 0;
+ @Mock
+ private ExpandableNotificationRow mRow;
+
+ private RemoteInputNotificationRebuilder mRebuilder;
+ private NotificationEntry mEntry;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mRebuilder = new RemoteInputNotificationRebuilder(mContext);
+ mEntry = new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setNotification(new Notification())
+ .setUser(UserHandle.CURRENT)
+ .build();
+ mEntry.setRow(mRow);
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_noExistingInput_image() {
+ Uri uri = mock(Uri.class);
+ String mimeType = "image/jpeg";
+ String text = "image inserted";
+ StatusBarNotification newSbn =
+ mRebuilder.rebuildWithRemoteInputInserted(
+ mEntry, text, false, mimeType, uri);
+ RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+ .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ assertEquals(1, messages.length);
+ assertEquals(text, messages[0].getText());
+ assertEquals(mimeType, messages[0].getMimeType());
+ assertEquals(uri, messages[0].getUri());
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_noExistingInputNoSpinner() {
+ StatusBarNotification newSbn =
+ mRebuilder.rebuildWithRemoteInputInserted(
+ mEntry, "A Reply", false, null, null);
+ RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+ .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ assertEquals(1, messages.length);
+ assertEquals("A Reply", messages[0].getText());
+ assertFalse(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_noExistingInputWithSpinner() {
+ StatusBarNotification newSbn =
+ mRebuilder.rebuildWithRemoteInputInserted(
+ mEntry, "A Reply", true, null, null);
+ RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+ .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ assertEquals(1, messages.length);
+ assertEquals("A Reply", messages[0].getText());
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_withExistingInput() {
+ // Setup a notification entry with 1 remote input.
+ StatusBarNotification newSbn =
+ mRebuilder.rebuildWithRemoteInputInserted(
+ mEntry, "A Reply", false, null, null);
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setSbn(newSbn)
+ .build();
+
+ // Try rebuilding to add another reply.
+ newSbn = mRebuilder.rebuildWithRemoteInputInserted(
+ entry, "Reply 2", true, null, null);
+ RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+ .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ assertEquals(2, messages.length);
+ assertEquals("Reply 2", messages[0].getText());
+ assertEquals("A Reply", messages[1].getText());
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_withExistingInput_image() {
+ // Setup a notification entry with 1 remote input.
+ Uri uri = mock(Uri.class);
+ String mimeType = "image/jpeg";
+ String text = "image inserted";
+ StatusBarNotification newSbn =
+ mRebuilder.rebuildWithRemoteInputInserted(
+ mEntry, text, false, mimeType, uri);
+ NotificationEntry entry = new NotificationEntryBuilder()
+ .setSbn(newSbn)
+ .build();
+
+ // Try rebuilding to add another reply.
+ newSbn = mRebuilder.rebuildWithRemoteInputInserted(
+ entry, "Reply 2", true, null, null);
+ RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+ .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ assertEquals(2, messages.length);
+ assertEquals("Reply 2", messages[0].getText());
+ assertEquals(text, messages[1].getText());
+ assertEquals(mimeType, messages[1].getMimeType());
+ assertEquals(uri, messages[1].getUri());
+ }
+
+ @Test
+ public void testRebuildNotificationForCanceledSmartReplies() {
+ // Try rebuilding to remove spinner and hide buttons.
+ StatusBarNotification newSbn =
+ mRebuilder.rebuildForCanceledSmartReplies(mEntry);
+ assertFalse(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
index 837d71f..99c965a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
@@ -39,6 +39,7 @@
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.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -86,14 +87,20 @@
mDependency.injectTestDependency(NotificationEntryManager.class,
mNotificationEntryManager);
- mSmartReplyController = new SmartReplyController(mNotificationEntryManager,
- mIStatusBarService, mClickNotifier);
+ mSmartReplyController = new SmartReplyController(
+ mock(DumpManager.class),
+ mNotificationEntryManager,
+ mIStatusBarService,
+ mClickNotifier);
mDependency.injectTestDependency(SmartReplyController.class,
mSmartReplyController);
mRemoteInputManager = new NotificationRemoteInputManager(mContext,
+ mock(FeatureFlags.class),
mock(NotificationLockscreenUserManager.class), mSmartReplyController,
- mNotificationEntryManager, () -> Optional.of(mock(StatusBar.class)),
+ mNotificationEntryManager,
+ new RemoteInputNotificationRebuilder(mContext),
+ () -> Optional.of(mock(StatusBar.class)),
mStatusBarStateController,
Handler.createAsync(Looper.myLooper()),
mRemoteInputUriController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index ebeb591..f08a74a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -35,6 +35,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
@@ -50,6 +51,7 @@
import android.annotation.Nullable;
import android.app.Notification;
+import android.os.Handler;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
@@ -77,6 +79,7 @@
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
@@ -107,6 +110,7 @@
@Mock private FeatureFlags mFeatureFlags;
@Mock private NotifCollectionLogger mLogger;
@Mock private LogBufferEulogizer mEulogizer;
+ @Mock private Handler mMainHandler;
@Mock private GroupCoalescer mGroupCoalescer;
@Spy private RecordingCollectionListener mCollectionListener;
@@ -152,6 +156,7 @@
mClock,
mFeatureFlags,
mLogger,
+ mMainHandler,
mEulogizer,
mock(DumpManager.class));
mCollection.attach(mGroupCoalescer);
@@ -1322,6 +1327,78 @@
verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt());
}
+ private Runnable getInternalNotifUpdateRunnable(StatusBarNotification sbn) {
+ InternalNotifUpdater updater = mCollection.getInternalNotifUpdater("Test");
+ updater.onInternalNotificationUpdate(sbn, "reason");
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mMainHandler).post(runnableCaptor.capture());
+ return runnableCaptor.getValue();
+ }
+
+ @Test
+ public void testGetInternalNotifUpdaterPostsToMainHandler() {
+ InternalNotifUpdater updater = mCollection.getInternalNotifUpdater("Test");
+ updater.onInternalNotificationUpdate(mock(StatusBarNotification.class), "reason");
+ verify(mMainHandler).post(any());
+ }
+
+ @Test
+ public void testSecondPostCallsUpdateWithTrue() {
+ // GIVEN a pipeline with one notification
+ NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+ NotificationEntry entry = mCollectionListener.getEntry(notifEvent.key);
+
+ // KNOWING that it already called listener methods once
+ verify(mCollectionListener).onEntryAdded(eq(entry));
+ verify(mCollectionListener).onRankingApplied();
+
+ // WHEN we update the notification via the system
+ mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+
+ // THEN entry updated gets called, added does not, and ranking is called again
+ verify(mCollectionListener).onEntryUpdated(eq(entry));
+ verify(mCollectionListener).onEntryUpdated(eq(entry), eq(true));
+ verify(mCollectionListener).onEntryAdded((entry));
+ verify(mCollectionListener, times(2)).onRankingApplied();
+ }
+
+ @Test
+ public void testInternalNotifUpdaterCallsUpdate() {
+ // GIVEN a pipeline with one notification
+ NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+ NotificationEntry entry = mCollectionListener.getEntry(notifEvent.key);
+
+ // KNOWING that it will call listener methods once
+ verify(mCollectionListener).onEntryAdded(eq(entry));
+ verify(mCollectionListener).onRankingApplied();
+
+ // WHEN we update that notification internally
+ StatusBarNotification sbn = notifEvent.sbn;
+ getInternalNotifUpdateRunnable(sbn).run();
+
+ // THEN only entry updated gets called a second time
+ verify(mCollectionListener).onEntryAdded(eq(entry));
+ verify(mCollectionListener).onRankingApplied();
+ verify(mCollectionListener).onEntryUpdated(eq(entry));
+ verify(mCollectionListener).onEntryUpdated(eq(entry), eq(false));
+ }
+
+ @Test
+ public void testInternalNotifUpdaterIgnoresNew() {
+ // GIVEN a pipeline without any notifications
+ StatusBarNotification sbn = buildNotif(TEST_PACKAGE, 47, "myTag").build().getSbn();
+
+ // WHEN we internally update an unknown notification
+ getInternalNotifUpdateRunnable(sbn).run();
+
+ // THEN only entry updated gets called a second time
+ verify(mCollectionListener, never()).onEntryAdded(any());
+ verify(mCollectionListener, never()).onRankingUpdate(any());
+ verify(mCollectionListener, never()).onRankingApplied();
+ verify(mCollectionListener, never()).onEntryUpdated(any());
+ verify(mCollectionListener, never()).onEntryUpdated(any(), anyBoolean());
+ }
+
private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) {
return new NotificationEntryBuilder()
.setPkg(pkg)
@@ -1372,6 +1449,11 @@
}
@Override
+ public void onEntryUpdated(NotificationEntry entry, boolean fromSystem) {
+ onEntryUpdated(entry);
+ }
+
+ @Override
public void onEntryRemoved(NotificationEntry entry, int reason) {
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
new file mode 100644
index 0000000..0ce6ada
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.os.Handler
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputListener
+import com.android.systemui.statusbar.RemoteInputNotificationRebuilder
+import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class RemoteInputCoordinatorTest : SysuiTestCase() {
+ private lateinit var coordinator: RemoteInputCoordinator
+ private lateinit var listener: RemoteInputListener
+ private lateinit var collectionListener: NotifCollectionListener
+
+ private lateinit var entry1: NotificationEntry
+ private lateinit var entry2: NotificationEntry
+
+ @Mock private lateinit var lifetimeExtensionCallback: OnEndLifetimeExtensionCallback
+ @Mock private lateinit var rebuilder: RemoteInputNotificationRebuilder
+ @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
+ @Mock private lateinit var mainHandler: Handler
+ @Mock private lateinit var smartReplyController: SmartReplyController
+ @Mock private lateinit var pipeline: NotifPipeline
+ @Mock private lateinit var notifUpdater: InternalNotifUpdater
+ @Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var sbn: StatusBarNotification
+
+ @Before
+ fun setUp() {
+ initMocks(this)
+ coordinator = RemoteInputCoordinator(
+ dumpManager,
+ rebuilder,
+ remoteInputManager,
+ mainHandler,
+ smartReplyController
+ )
+ `when`(pipeline.addNotificationLifetimeExtender(any())).thenAnswer {
+ (it.arguments[0] as NotifLifetimeExtender).setCallback(lifetimeExtensionCallback)
+ }
+ `when`(pipeline.getInternalNotifUpdater(any())).thenReturn(notifUpdater)
+ coordinator.attach(pipeline)
+ listener = withArgCaptor {
+ verify(remoteInputManager).setRemoteInputListener(capture())
+ }
+ collectionListener = withArgCaptor {
+ verify(pipeline).addCollectionListener(capture())
+ }
+ entry1 = NotificationEntryBuilder().setId(1).build()
+ entry2 = NotificationEntryBuilder().setId(2).build()
+ `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
+ `when`(rebuilder.rebuildForRemoteInputReply(any())).thenReturn(sbn)
+ `when`(rebuilder.rebuildForSendingSmartReply(any(), any())).thenReturn(sbn)
+ }
+
+ val remoteInputActiveExtender get() = coordinator.mRemoteInputActiveExtender
+ val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
+ val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender
+
+ @Test
+ fun testRemoteInputActive() {
+ `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
+ assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+ assertThat(remoteInputHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+ assertThat(smartReplyHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+ assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoteInputHistory() {
+ `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry1)).thenReturn(true)
+ assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+ assertThat(remoteInputHistoryExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+ assertThat(smartReplyHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+ assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isTrue()
+ }
+
+ @Test
+ fun testSmartReplyHistory() {
+ `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true)
+ assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+ assertThat(remoteInputHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+ assertThat(smartReplyHistoryExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+ assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isTrue()
+ }
+
+ @Test
+ fun testNotificationWithRemoteInputActiveIsRemovedOnCollapse() {
+ `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+
+ // Nothing should happen on panel collapse before we start extending the lifetime
+ listener.onPanelCollapsed()
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+ verify(lifetimeExtensionCallback, never()).onEndLifetimeExtension(any(), any())
+
+ // Start extending lifetime & validate that the extension is ended
+ assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isTrue()
+ listener.onPanelCollapsed()
+ verify(lifetimeExtensionCallback).onEndLifetimeExtension(remoteInputActiveExtender, entry1)
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtenderTest.kt
new file mode 100644
index 0000000..37ad835
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtenderTest.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.notifcollection
+
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+import java.util.function.Consumer
+import java.util.function.Predicate
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class SelfTrackingLifetimeExtenderTest : SysuiTestCase() {
+ private lateinit var extender: TestableSelfTrackingLifetimeExtender
+
+ private lateinit var entry1: NotificationEntry
+ private lateinit var entry2: NotificationEntry
+
+ @Mock
+ private lateinit var callback: OnEndLifetimeExtensionCallback
+ @Mock
+ private lateinit var mainHandler: Handler
+ @Mock
+ private lateinit var shouldExtend: Predicate<NotificationEntry>
+ @Mock
+ private lateinit var onStarted: Consumer<NotificationEntry>
+ @Mock
+ private lateinit var onCanceled: Consumer<NotificationEntry>
+
+ @Before
+ fun setUp() {
+ initMocks(this)
+ extender = TestableSelfTrackingLifetimeExtender()
+ extender.setCallback(callback)
+ entry1 = NotificationEntryBuilder().setId(1).build()
+ entry2 = NotificationEntryBuilder().setId(2).build()
+ }
+
+ @Test
+ fun testName() {
+ assertThat(extender.name).isEqualTo("Testable")
+ }
+
+ @Test
+ fun testNoExtend() {
+ `when`(shouldExtend.test(entry1)).thenReturn(false)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isFalse()
+ assertThat(extender.isExtending(entry1.key)).isFalse()
+ verify(onStarted, never()).accept(entry1)
+ verify(onCanceled, never()).accept(entry1)
+ }
+
+ @Test
+ fun testExtendThenCancelForRepost() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted).accept(entry1)
+ verify(onCanceled, never()).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+ extender.cancelLifetimeExtension(entry1)
+ verify(onCanceled).accept(entry1)
+ }
+
+ @Test
+ fun testExtendThenCancel_thenEndDoesNothing() {
+ testExtendThenCancelForRepost()
+ assertThat(extender.isExtending(entry1.key)).isFalse()
+
+ extender.endLifetimeExtension(entry1.key)
+ extender.endLifetimeExtensionAfterDelay(entry1.key, 1000)
+ verify(callback, never()).onEndLifetimeExtension(any(), any())
+ verify(mainHandler, never()).postDelayed(any(), anyLong())
+ }
+
+ @Test
+ fun testExtendThenEnd() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+ extender.endLifetimeExtension(entry1.key)
+ verify(callback).onEndLifetimeExtension(extender, entry1)
+ verify(onCanceled, never()).accept(entry1)
+ }
+
+ @Test
+ fun testExtendThenEndAfterDelay() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+
+ // Call the method and capture the posted runnable
+ extender.endLifetimeExtensionAfterDelay(entry1.key, 1234)
+ val runnable = withArgCaptor<Runnable> {
+ verify(mainHandler).postDelayed(capture(), eq(1234.toLong()))
+ }
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+ verify(callback, never()).onEndLifetimeExtension(any(), any())
+
+ // now run the posted runnable and ensure it works as expected
+ runnable.run()
+ verify(callback).onEndLifetimeExtension(extender, entry1)
+ assertThat(extender.isExtending(entry1.key)).isFalse()
+ verify(onCanceled, never()).accept(entry1)
+ }
+
+ @Test
+ fun testExtendThenEndAll() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true)
+ `when`(shouldExtend.test(entry2)).thenReturn(true)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+ assertThat(extender.isExtending(entry2.key)).isFalse()
+ assertThat(extender.shouldExtendLifetime(entry2, 0)).isTrue()
+ verify(onStarted).accept(entry2)
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+ assertThat(extender.isExtending(entry2.key)).isTrue()
+ extender.endAllLifetimeExtensions()
+ verify(callback).onEndLifetimeExtension(extender, entry1)
+ verify(callback).onEndLifetimeExtension(extender, entry2)
+ verify(onCanceled, never()).accept(entry1)
+ verify(onCanceled, never()).accept(entry2)
+ }
+
+ @Test
+ fun testExtendWithinEndCanReExtend() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted, times(1)).accept(entry1)
+
+ `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ }
+ extender.endLifetimeExtension(entry1.key)
+ verify(onStarted, times(2)).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+ }
+
+ @Test
+ fun testExtendWithinEndCanNotReExtend() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true, false)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted, times(1)).accept(entry1)
+
+ `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isFalse()
+ }
+ extender.endLifetimeExtension(entry1.key)
+ verify(onStarted, times(1)).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isFalse()
+ }
+
+ @Test
+ fun testExtendWithinEndAllCanReExtend() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted, times(1)).accept(entry1)
+
+ `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ }
+ extender.endAllLifetimeExtensions()
+ verify(onStarted, times(2)).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isTrue()
+ }
+
+ @Test
+ fun testExtendWithinEndAllCanNotReExtend() {
+ `when`(shouldExtend.test(entry1)).thenReturn(true, false)
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+ verify(onStarted, times(1)).accept(entry1)
+
+ `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+ assertThat(extender.shouldExtendLifetime(entry1, 0)).isFalse()
+ }
+ extender.endAllLifetimeExtensions()
+ verify(onStarted, times(1)).accept(entry1)
+ assertThat(extender.isExtending(entry1.key)).isFalse()
+ }
+
+ inner class TestableSelfTrackingLifetimeExtender(debug: Boolean = false) :
+ SelfTrackingLifetimeExtender("Test", "Testable", debug, mainHandler) {
+
+ override fun queryShouldExtendLifetime(entry: NotificationEntry) =
+ shouldExtend.test(entry)
+
+ override fun onStartedLifetimeExtension(entry: NotificationEntry) {
+ onStarted.accept(entry)
+ }
+
+ override fun onCanceledLifetimeExtension(entry: NotificationEntry) {
+ onCanceled.accept(entry)
+ }
+ }
+}
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 42aecfd..c5d1e3a 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
@@ -255,6 +255,22 @@
}
/**
+ * Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
+ */
+ public ExpandableNotificationRow createShortcutBubble(String shortcutId)
+ throws Exception {
+ Notification n = createNotification(false /* isGroupSummary */,
+ null /* groupKey */, makeShortcutBubbleMetadata(shortcutId));
+ n.flags |= FLAG_BUBBLE;
+ ExpandableNotificationRow row = generateRow(n, PKG, UID, USER_HANDLE,
+ 0 /* extraInflationFlags */, IMPORTANCE_HIGH);
+ modifyRanking(row.getEntry())
+ .setCanBubble(true)
+ .build();
+ return row;
+ }
+
+ /**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble and is part
* of a group of notifications.
*/
@@ -506,6 +522,12 @@
.build();
}
+ private BubbleMetadata makeShortcutBubbleMetadata(String shortcutId) {
+ return new BubbleMetadata.Builder(shortcutId)
+ .setDesiredHeight(314)
+ .build();
+ }
+
private static class MockSmartReplyInflater implements SmartReplyStateInflater {
@Override
public InflatedSmartReplyState inflateSmartReplyState(NotificationEntry entry) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLoggerTest.kt
index f3136c7..bf8cc37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLoggerTest.kt
@@ -40,7 +40,7 @@
private val logger = CollapsedStatusBarFragmentLogger(buffer, disableFlagsLogger)
@Test
- fun logToBuffer_bufferHasStates() {
+ fun logDisableFlagChange_bufferHasStates() {
val state = DisableFlagsLogger.DisableState(0, 1)
logger.logDisableFlagChange(state, state)
@@ -48,7 +48,9 @@
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
val actualString = stringWriter.toString()
- val expectedLogString = disableFlagsLogger.getDisableFlagsString(state, state)
+ val expectedLogString = disableFlagsLogger.getDisableFlagsString(
+ old = null, new = state, newAfterLocalModification = state
+ )
assertThat(actualString).contains(expectedLogString)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
index ead3291..fabe5a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
@@ -121,6 +121,7 @@
import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -444,6 +445,7 @@
mSplitShadeHeaderController,
mUnlockedScreenOffAnimationController,
mLockscreenGestureLogger,
+ new PanelExpansionStateManager(),
mNotificationRemoteInputManager,
mControlsComponent);
mNotificationPanelViewController.initDependencies(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
index 6e9bb2d..4bf8821 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
@@ -52,6 +52,7 @@
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.tuner.TunerService;
@@ -90,7 +91,7 @@
@Mock private NotificationPanelViewController mNotificationPanelViewController;
@Mock private NotificationStackScrollLayout mNotificationStackScrollLayout;
@Mock private NotificationShadeDepthController mNotificationShadeDepthController;
- @Mock private StatusBarWindowView mStatusBarWindowView;
+ @Mock private StatusBarWindowController mStatusBarWindowController;
@Mock private NotificationShadeWindowController mNotificationShadeWindowController;
@Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -135,7 +136,8 @@
mNotificationShadeDepthController,
mView,
mNotificationPanelViewController,
- mStatusBarWindowView,
+ new PanelExpansionStateManager(),
+ mStatusBarWindowController,
mNotificationStackScrollLayoutController,
mStatusBarKeyguardViewManager,
mLockIconViewController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 6849dab..42f2206 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -55,6 +55,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dock.DockManager;
import com.android.systemui.scrim.ScrimView;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -112,6 +113,10 @@
private ConfigurationController mConfigurationController;
@Mock
private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+ // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
+ // event-dispatch-on-registration pattern caused some of these unit tests to fail.)
+ @Mock
+ private PanelExpansionStateManager mPanelExpansionStateManager;
private static class AnimatorListener implements Animator.AnimatorListener {
@@ -224,7 +229,8 @@
mDozeParameters, mAlarmManager, mKeyguardStateController, mDelayedWakeLockBuilder,
new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor,
mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()),
- mUnlockedScreenOffAnimationController);
+ mUnlockedScreenOffAnimationController,
+ mPanelExpansionStateManager);
mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
mScrimController.setAnimatorListener(mAnimatorListener);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 2d944aa..dcffd22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -51,6 +51,7 @@
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -137,9 +138,13 @@
mUnlockedScreenOffAnimationController,
mKeyguardMessageAreaFactory,
mShadeController);
- mStatusBarKeyguardViewManager.registerStatusBar(mStatusBar,
- mNotificationPanelView, mBiometrucUnlockController,
- mNotificationContainer, mBypassController);
+ mStatusBarKeyguardViewManager.registerStatusBar(
+ mStatusBar,
+ mNotificationPanelView,
+ new PanelExpansionStateManager(),
+ mBiometrucUnlockController,
+ mNotificationContainer,
+ mBypassController);
mStatusBarKeyguardViewManager.show(null);
}
@@ -179,8 +184,10 @@
public void onPanelExpansionChanged_neverHidesFullscreenBouncer() {
// TODO: StatusBar should not be here, mBouncer.isFullscreenBouncer() should do the same.
when(mStatusBar.isFullScreenUserSwitcherState()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */,
- true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_VISIBLE));
}
@@ -188,51 +195,67 @@
public void onPanelExpansionChanged_neverHidesScrimmedBouncer() {
when(mBouncer.isShowing()).thenReturn(true);
when(mBouncer.isScrimmed()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */,
- true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_VISIBLE));
}
@Test
public void onPanelExpansionChanged_neverShowsDuringHintAnimation() {
when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */,
- true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
}
@Test
public void onPanelExpansionChanged_propagatesToBouncer() {
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */,
- true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer).setExpansion(eq(0.5f));
}
@Test
public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */,
- true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer).show(eq(false), eq(false));
// But not when it's already visible
reset(mBouncer);
when(mBouncer.isShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */, true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer, never()).show(eq(false), eq(false));
// Or animating away
reset(mBouncer);
when(mBouncer.isAnimatingAway()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */, true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer, never()).show(eq(false), eq(false));
}
@Test
public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() {
mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animate */);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(0.5f /* expansion */,
- true /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ 0.5f,
+ /* expanded= */ false,
+ /* tracking= */ true);
verify(mBouncer, never()).setExpansion(eq(0.5f));
}
@@ -240,16 +263,20 @@
public void onPanelExpansionChanged_neverTranslatesBouncerWhenWakeAndUnlock() {
when(mBiometrucUnlockController.getMode())
.thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(KeyguardBouncer.EXPANSION_VISIBLE,
- false /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* expanded= */ true,
+ /* tracking= */ false);
verify(mBouncer, never()).setExpansion(anyFloat());
}
@Test
public void onPanelExpansionChanged_neverTranslatesBouncerWhenLaunchingApp() {
when(mStatusBar.isInLaunchTransition()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(KeyguardBouncer.EXPANSION_VISIBLE,
- false /* tracking */);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(
+ /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* expanded= */ true,
+ /* tracking= */ false);
verify(mBouncer, never()).setExpansion(anyFloat());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index f14b126..9fe1423 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -133,6 +133,7 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -245,7 +246,6 @@
@Mock private StatusBarComponent mStatusBarComponent;
@Mock private PluginManager mPluginManager;
@Mock private LegacySplitScreen mLegacySplitScreen;
- @Mock private StatusBarWindowView mStatusBarWindowView;
@Mock private LightsOutNotifController mLightsOutNotifController;
@Mock private ViewMediatorCallback mViewMediatorCallback;
@Mock private StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
@@ -385,6 +385,7 @@
notificationLogger,
mNotificationInterruptStateProvider,
mNotificationViewHierarchyManager,
+ new PanelExpansionStateManager(),
mKeyguardViewMediator,
new DisplayMetrics(),
mMetricsLogger,
@@ -424,7 +425,6 @@
mLightsOutNotifController,
mStatusBarNotificationActivityStarterBuilder,
mShadeController,
- mStatusBarWindowView,
mStatusBarKeyguardViewManager,
mViewMediatorCallback,
mInitController,
@@ -464,9 +464,13 @@
mock(DumpManager.class),
mActivityLaunchAnimator,
mDialogLaunchAnimator);
- when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class),
- any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
- any(ViewGroup.class), any(KeyguardBypassController.class)))
+ when(mKeyguardViewMediator.registerStatusBar(
+ any(StatusBar.class),
+ any(NotificationPanelViewController.class),
+ any(PanelExpansionStateManager.class),
+ any(BiometricUnlockController.class),
+ any(ViewGroup.class),
+ any(KeyguardBypassController.class)))
.thenReturn(mStatusBarKeyguardViewManager);
when(mKeyguardViewMediator.getViewMediatorCallback()).thenReturn(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
new file mode 100644
index 0000000..e09cde9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.panelstate
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class PanelExpansionStateManagerTest : SysuiTestCase() {
+
+ private lateinit var panelExpansionStateManager: PanelExpansionStateManager
+
+ @Before
+ fun setUp() {
+ panelExpansionStateManager = PanelExpansionStateManager()
+ }
+
+ @Test
+ fun onPanelExpansionChanged_listenersNotified() {
+ val listener = TestPanelExpansionListener()
+ panelExpansionStateManager.addListener(listener)
+ val fraction = 0.6f
+ val expanded = true
+ val tracking = true
+
+ panelExpansionStateManager.onPanelExpansionChanged(fraction, expanded, tracking)
+
+ assertThat(listener.fraction).isEqualTo(fraction)
+ assertThat(listener.expanded).isEqualTo(expanded)
+ assertThat(listener.tracking).isEqualTo(tracking)
+ }
+
+ @Test
+ fun addPanelExpansionListener_listenerNotifiedOfCurrentValues() {
+ val fraction = 0.6f
+ val expanded = true
+ val tracking = true
+ panelExpansionStateManager.onPanelExpansionChanged(fraction, expanded, tracking)
+ val listener = TestPanelExpansionListener()
+
+ panelExpansionStateManager.addListener(listener)
+
+ assertThat(listener.fraction).isEqualTo(fraction)
+ assertThat(listener.expanded).isEqualTo(expanded)
+ assertThat(listener.tracking).isEqualTo(tracking)
+ }
+
+ class TestPanelExpansionListener : PanelExpansionListener {
+ var fraction: Float = 0f
+ var expanded: Boolean = false
+ var tracking: Boolean = false
+
+ override fun onPanelExpansionChanged(
+ fraction: Float,
+ expanded: Boolean,
+ tracking: Boolean
+ ) {
+ this.fraction = fraction
+ this.expanded = expanded
+ this.tracking = tracking
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 1159e09..15a92dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -185,6 +185,8 @@
private ArgumentCaptor<NotificationEntryListener> mEntryListenerCaptor;
@Captor
private ArgumentCaptor<NotificationRemoveInterceptor> mRemoveInterceptorCaptor;
+ @Captor
+ private ArgumentCaptor<List<Bubble>> mBubbleListCaptor;
private BubblesManager mBubblesManager;
// TODO(178618782): Move tests on the controller directly to the shell
@@ -1165,6 +1167,22 @@
verify(mDataRepository, times(1)).loadBubbles(anyInt(), any());
}
+ /**
+ * Verifies that shortcut deletions triggers that bubble being removed from XML.
+ */
+ @Test
+ public void testDeleteShortcutsDeletesXml() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createShortcutBubble("shortcutId");
+ BubbleEntry shortcutBubbleEntry = BubblesManager.notifToBubbleEntry(row.getEntry());
+ mBubbleController.updateBubble(shortcutBubbleEntry);
+
+ mBubbleData.dismissBubbleWithKey(shortcutBubbleEntry.getKey(),
+ Bubbles.DISMISS_SHORTCUT_REMOVED);
+
+ verify(mDataRepository, atLeastOnce()).removeBubbles(anyInt(), mBubbleListCaptor.capture());
+ assertThat(mBubbleListCaptor.getValue().get(0).getKey()).isEqualTo(
+ shortcutBubbleEntry.getKey());
+ }
/**
* Verifies that the package manager for the user is used when loading info for the bubble.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
index 05c4822..43b181e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -93,6 +93,7 @@
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
+import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
import com.android.wm.shell.bubbles.BubbleEntry;
@@ -167,6 +168,9 @@
@Captor
private ArgumentCaptor<NotifCollectionListener> mNotifListenerCaptor;
+ @Captor
+ private ArgumentCaptor<List<Bubble>> mBubbleListCaptor;
+
private BubblesManager mBubblesManager;
private TestableBubbleController mBubbleController;
private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
@@ -1013,6 +1017,23 @@
verify(mDataRepository, times(1)).loadBubbles(anyInt(), any());
}
+ /**
+ * Verifies that shortcut deletions triggers that bubble being removed from XML.
+ */
+ @Test
+ public void testDeleteShortcutsDeletesXml() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createShortcutBubble("shortcutId");
+ BubbleEntry shortcutBubbleEntry = BubblesManager.notifToBubbleEntry(row.getEntry());
+ mBubbleController.updateBubble(shortcutBubbleEntry);
+
+ mBubbleData.dismissBubbleWithKey(shortcutBubbleEntry.getKey(),
+ Bubbles.DISMISS_SHORTCUT_REMOVED);
+
+ verify(mDataRepository, atLeastOnce()).removeBubbles(anyInt(), mBubbleListCaptor.capture());
+ assertThat(mBubbleListCaptor.getValue().get(0).getKey()).isEqualTo(
+ shortcutBubbleEntry.getKey());
+ }
+
@Test
public void testShowManageMenuChangesSysuiState() {
mBubbleController.updateBubble(mBubbleEntry);
diff --git a/packages/services/CameraExtensionsProxy/AndroidManifest.xml b/packages/services/CameraExtensionsProxy/AndroidManifest.xml
index ef1d581..79c9d13 100644
--- a/packages/services/CameraExtensionsProxy/AndroidManifest.xml
+++ b/packages/services/CameraExtensionsProxy/AndroidManifest.xml
@@ -2,6 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.cameraextensions">
+ <queries>
+ <intent>
+ <action android:name="androidx.camera.extensions.action.VENDOR_ACTION" />
+ </intent>
+ </queries>
+
<application
android:label="@string/app_name"
android:defaultToDeviceProtectedStorage="true"
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 4946ad4..1af8ad3 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -187,7 +187,7 @@
@NonNull IPredictionCallback callback) {
final AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
if (sessionInfo == null) return;
- final boolean serviceExists = resolveService(sessionId, false,
+ final boolean serviceExists = resolveService(sessionId, true,
sessionInfo.mUsesPeopleService,
s -> s.registerPredictionUpdates(sessionId, callback));
if (serviceExists) {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 2028766..7c1e2da 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -1451,8 +1451,12 @@
}
void restartBleScan() {
- mBluetoothAdapter.getBluetoothLeScanner().stopScan(mBleScanCallback);
- startBleScan();
+ if (mBluetoothAdapter.getBluetoothLeScanner() != null) {
+ mBluetoothAdapter.getBluetoothLeScanner().stopScan(mBleScanCallback);
+ startBleScan();
+ } else {
+ Slog.w(LOG_TAG, "BluetoothLeScanner is null (likely BLE isn't ON yet).");
+ }
}
private List<ScanFilter> getBleScanFilters() {
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 8a42ddf..61d784e 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -223,7 +223,7 @@
@Override // from AbstractMasterSystemService
protected ContentCapturePerUserService newServiceLocked(@UserIdInt int resolvedUserId,
boolean disabled) {
- return new ContentCapturePerUserService(this, mLock, disabled, resolvedUserId);
+ return new ContentCapturePerUserService(this, mLock, disabled, resolvedUserId, mHandler);
}
@Override // from SystemService
@@ -1072,26 +1072,18 @@
ParcelFileDescriptor sourceOut = servicePipe.second;
ParcelFileDescriptor sinkOut = servicePipe.first;
- mParentService.mPackagesWithShareRequests.add(mDataShareRequest.getPackageName());
-
- try {
- mClientAdapter.write(sourceIn);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to call write() the client operation", e);
- sendErrorSignal(mClientAdapter, serviceAdapter,
- ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
- logServiceEvent(
- CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL);
- return;
+ synchronized (mParentService.mLock) {
+ mParentService.mPackagesWithShareRequests.add(mDataShareRequest.getPackageName());
}
- try {
- serviceAdapter.start(sinkOut);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to call start() the service operation", e);
+
+ if (!setUpSharingPipeline(mClientAdapter, serviceAdapter, sourceIn, sinkOut)) {
sendErrorSignal(mClientAdapter, serviceAdapter,
ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
- logServiceEvent(
- CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL);
+ bestEffortCloseFileDescriptors(sourceIn, sinkIn, sourceOut, sinkOut);
+ synchronized (mParentService.mLock) {
+ mParentService.mPackagesWithShareRequests
+ .remove(mDataShareRequest.getPackageName());
+ }
return;
}
@@ -1184,6 +1176,32 @@
}
}
+ private boolean setUpSharingPipeline(
+ IDataShareWriteAdapter clientAdapter,
+ IDataShareReadAdapter serviceAdapter,
+ ParcelFileDescriptor sourceIn,
+ ParcelFileDescriptor sinkOut) {
+ try {
+ clientAdapter.write(sourceIn);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to call write() the client operation", e);
+ logServiceEvent(
+ CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL);
+ return false;
+ }
+
+ try {
+ serviceAdapter.start(sinkOut);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to call start() the service operation", e);
+ logServiceEvent(
+ CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL);
+ return false;
+ }
+
+ return true;
+ }
+
private void enforceDataSharingTtl(ParcelFileDescriptor sourceIn,
ParcelFileDescriptor sinkIn,
ParcelFileDescriptor sourceOut,
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 6bd1fa6..822a42b 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -46,6 +46,7 @@
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
import android.os.UserHandle;
import android.provider.Settings;
@@ -75,6 +76,7 @@
import com.android.server.infra.AbstractPerUserSystemService;
import java.io.PrintWriter;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -88,6 +90,10 @@
private static final String TAG = ContentCapturePerUserService.class.getSimpleName();
+ private static final int MAX_REBIND_COUNTS = 5;
+ // 5 minutes
+ private static final long REBIND_DURATION_MS = 5 * 60 * 1_000;
+
@GuardedBy("mLock")
private final SparseArray<ContentCaptureServerSession> mSessions = new SparseArray<>();
@@ -121,11 +127,18 @@
@GuardedBy("mLock")
private ContentCaptureServiceInfo mInfo;
+ private Instant mLastRebindTime;
+ private int mRebindCount;
+ private final Handler mHandler;
+
+ private final Runnable mReBindServiceRunnable = new RebindServiceRunnable();
+
// TODO(b/111276913): add mechanism to prune stale sessions, similar to Autofill's
ContentCapturePerUserService(@NonNull ContentCaptureManagerService master,
- @NonNull Object lock, boolean disabled, @UserIdInt int userId) {
+ @NonNull Object lock, boolean disabled, @UserIdInt int userId, Handler handler) {
super(master, lock, userId);
+ mHandler = handler;
updateRemoteServiceLocked(disabled);
}
@@ -190,9 +203,43 @@
Slog.w(TAG, "remote service died: " + service);
synchronized (mLock) {
mZombie = true;
- writeServiceEvent(
- FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_REMOTE_SERVICE_DIED,
- getServiceComponentName());
+ // Reset rebindCount if over 12 hours mLastRebindTime
+ if (mLastRebindTime != null && Instant.now().isAfter(
+ mLastRebindTime.plusMillis(12 * 60 * 60 * 1000))) {
+ if (mMaster.debug) {
+ Slog.i(TAG, "The current rebind count " + mRebindCount + " is reset.");
+ }
+ mRebindCount = 0;
+ }
+ if (mRebindCount >= MAX_REBIND_COUNTS) {
+ writeServiceEvent(
+ FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_REMOTE_SERVICE_DIED,
+ getServiceComponentName());
+ }
+ if (mRebindCount < MAX_REBIND_COUNTS) {
+ mHandler.removeCallbacks(mReBindServiceRunnable);
+ mHandler.postDelayed(mReBindServiceRunnable, REBIND_DURATION_MS);
+ }
+ }
+ }
+
+ private void updateRemoteServiceAndResurrectSessionsLocked() {
+ boolean disabled = !isEnabledLocked();
+ updateRemoteServiceLocked(disabled);
+ resurrectSessionsLocked();
+ }
+
+ private final class RebindServiceRunnable implements Runnable{
+
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ if (mZombie) {
+ mLastRebindTime = Instant.now();
+ mRebindCount++;
+ updateRemoteServiceAndResurrectSessionsLocked();
+ }
+ }
}
}
@@ -240,8 +287,8 @@
}
void onPackageUpdatedLocked() {
- updateRemoteServiceLocked(!isEnabledLocked());
- resurrectSessionsLocked();
+ mRebindCount = 0;
+ updateRemoteServiceAndResurrectSessionsLocked();
}
@GuardedBy("mLock")
@@ -555,6 +602,8 @@
mInfo.dump(prefix2, pw);
}
pw.print(prefix); pw.print("Zombie: "); pw.println(mZombie);
+ pw.print(prefix); pw.print("Rebind count: "); pw.println(mRebindCount);
+ pw.print(prefix); pw.print("Last rebind: "); pw.println(mLastRebindTime);
if (mRemoteService != null) {
pw.print(prefix); pw.println("remote service:");
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 9d2cff9..5ce72c2 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -108,8 +108,9 @@
/**
* When enabled this change id forces the packages it is applied to override the default
- * camera rotate & crop behavior. The default behavior along with all possible override
- * combinations is discussed in the table below.
+ * camera rotate & crop behavior and always return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE .
+ * The default behavior along with all possible override combinations is discussed in the table
+ * below.
*/
@ChangeId
@Overridable
@@ -121,9 +122,7 @@
* When enabled this change id forces the packages it is applied to ignore the current value of
* 'android:resizeableActivity' as well as target SDK equal to or below M and consider the
* activity as non-resizeable. In this case, the value of camera rotate & crop will only depend
- * on potential mismatches between the orientation of the camera and the fixed orientation of
- * the activity. You can check the table below for further details on the possible override
- * combinations.
+ * on the needed compensation considering the current display rotation.
*/
@ChangeId
@Overridable
@@ -132,67 +131,30 @@
public static final long OVERRIDE_CAMERA_RESIZABLE_AND_SDK_CHECK = 191513214L; // buganizer id
/**
- * This change id forces the packages it is applied to override the default camera rotate & crop
- * behavior. Enabling it will set the crop & rotate parameter to
- * {@link android.hardware.camera2.CaptureRequest#SCALER_ROTATE_AND_CROP_90} and disabling it
- * will reset the parameter to
- * {@link android.hardware.camera2.CaptureRequest#SCALER_ROTATE_AND_CROP_NONE} as long as camera
- * clients include {@link android.hardware.camera2.CaptureRequest#SCALER_ROTATE_AND_CROP_AUTO}
- * in their capture requests.
- *
- * This treatment only takes effect if OVERRIDE_CAMERA_ROTATE_AND_CROP_DEFAULTS is also enabled.
- * The table below includes further information about the possible override combinations.
- */
- @ChangeId
- @Overridable
- @Disabled
- @TestApi
- public static final long OVERRIDE_CAMERA_ROTATE_AND_CROP = 190069291L; //buganizer id
-
- /**
* Possible override combinations
*
- * |OVERRIDE | |OVERRIDE_
- * |CAMERA_ |OVERRIDE |CAMERA_
- * |ROTATE_ |CAMERA_ |RESIZEABLE_
- * |AND_CROP_ |ROTATE_ |AND_SDK_
- * |DEFAULTS |AND_CROP |CHECK
- * ______________________________________________
- * Default | | |
- * Behavior | D |D |D
- * ______________________________________________
- * Ignore | | |
- * SDK&Resize | D |D |E
- * ______________________________________________
- * Default | | |
- * Behavior | D |E |D
- * ______________________________________________
- * Ignore | | |
- * SDK&Resize | D |E |E
- * ______________________________________________
- * Rotate&Crop| | |
- * disabled | E |D |D
- * ______________________________________________
- * Rotate&Crop| | |
- * disabled | E |D |E
- * ______________________________________________
- * Rotate&Crop| | |
- * enabled | E |E |D
- * ______________________________________________
- * Rotate&Crop| | |
- * enabled | E |E |E
- * ______________________________________________
+ * |OVERRIDE |OVERRIDE_
+ * |CAMERA_ |CAMERA_
+ * |ROTATE_ |RESIZEABLE_
+ * |AND_CROP_ |AND_SDK_
+ * |DEFAULTS |CHECK
+ * _________________________________________________
+ * Default Behavior | D |D
+ * _________________________________________________
+ * Ignore SDK&Resize | D |E
+ * _________________________________________________
+ * SCALER_ROTATE_AND_CROP_NONE | E |D, E
+ * _________________________________________________
* Where:
- * E -> Override enabled
- * D -> Override disabled
- * Default behavior -> Rotate&crop will be enabled only in cases
- * where the fixed app orientation mismatches
- * with the orientation of the camera.
- * Additionally the app must either target M (or below)
- * or is declared as non-resizeable.
- * Ignore SDK&Resize -> Rotate&crop will be enabled only in cases
- * where the fixed app orientation mismatches
- * with the orientation of the camera.
+ * E -> Override enabled
+ * D -> Override disabled
+ * Default behavior -> Rotate&crop will be calculated depending on the required
+ * compensation necessary for the current display rotation.
+ * Additionally the app must either target M (or below)
+ * or is declared as non-resizeable.
+ * Ignore SDK&Resize -> The Rotate&crop value will depend on the required
+ * compensation for the current display rotation.
+ * SCALER_ROTATE_AND_CROP_NONE -> Always return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE
*/
// Flags arguments to NFC adapter to enable/disable NFC
@@ -543,14 +505,8 @@
if ((taskInfo != null) && (CompatChanges.isChangeEnabled(
OVERRIDE_CAMERA_ROTATE_AND_CROP_DEFAULTS, packageName,
UserHandle.getUserHandleForUid(taskInfo.userId)))) {
- if (CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_ROTATE_AND_CROP, packageName,
- UserHandle.getUserHandleForUid(taskInfo.userId))) {
- Slog.v(TAG, "OVERRIDE_CAMERA_ROTATE_AND_CROP enabled!");
+ Slog.v(TAG, "OVERRIDE_CAMERA_ROTATE_AND_CROP_DEFAULTS enabled!");
return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
- } else {
- Slog.v(TAG, "OVERRIDE_CAMERA_ROTATE_AND_CROP disabled!");
- return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
- }
}
boolean ignoreResizableAndSdkCheck = false;
if ((taskInfo != null) && (CompatChanges.isChangeEnabled(
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 0f4f58b..630bf0a 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -98,7 +98,7 @@
}
private final Object mLock = new Object();
- private final WorkSource mWorkSource = new WorkSource();
+ private final WorkSource mWorkSource;
private final PowerManager.WakeLock mWakeLock;
private final IBatteryStats mBatteryStatsService;
private final VibrationSettings mVibrationSettings;
@@ -119,9 +119,8 @@
mVibrationSettings = vibrationSettings;
mDeviceEffectAdapter = effectAdapter;
mCallbacks = callbacks;
+ mWorkSource = new WorkSource(mVibration.uid);
mWakeLock = wakeLock;
- mWorkSource.set(vib.uid);
- mWakeLock.setWorkSource(mWorkSource);
mBatteryStatsService = batteryStatsService;
CombinedVibration effect = vib.getEffect();
@@ -152,6 +151,7 @@
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+ mWakeLock.setWorkSource(mWorkSource);
mWakeLock.acquire();
try {
mVibration.token.linkToDeath(this, 0);
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index c715c39..d137436 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -190,7 +190,7 @@
@VisibleForTesting
boolean allDrawn() {
- return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.allDrawn();
+ return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.mIsDrawn;
}
boolean hasActiveTransitionInfo() {
@@ -224,8 +224,8 @@
final boolean mProcessRunning;
/** whether the process of the launching activity didn't have any active activity. */
final boolean mProcessSwitch;
- /** The activities that should be drawn. */
- final ArrayList<ActivityRecord> mPendingDrawActivities = new ArrayList<>(2);
+ /** Whether the last launched activity has reported drawn. */
+ boolean mIsDrawn;
/** The latest activity to have been launched. */
@NonNull ActivityRecord mLastLaunchedActivity;
@@ -318,10 +318,7 @@
mLastLaunchedActivity.mLaunchRootTask = null;
}
mLastLaunchedActivity = r;
- if (!r.noDisplay && !r.isReportedDrawn()) {
- if (DEBUG_METRICS) Slog.i(TAG, "Add pending draw " + r);
- mPendingDrawActivities.add(r);
- }
+ mIsDrawn = r.isReportedDrawn();
}
/** Returns {@code true} if the incoming activity can belong to this transition. */
@@ -332,29 +329,7 @@
/** @return {@code true} if the activity matches a launched activity in this transition. */
boolean contains(ActivityRecord r) {
- return r != null && (r == mLastLaunchedActivity || mPendingDrawActivities.contains(r));
- }
-
- /** Called when the activity is drawn or won't be drawn. */
- void removePendingDrawActivity(ActivityRecord r) {
- if (DEBUG_METRICS) Slog.i(TAG, "Remove pending draw " + r);
- mPendingDrawActivities.remove(r);
- }
-
- boolean allDrawn() {
- return mPendingDrawActivities.isEmpty();
- }
-
- /** Only keep the records which can be drawn. */
- void updatePendingDraw(boolean keepInitializing) {
- for (int i = mPendingDrawActivities.size() - 1; i >= 0; i--) {
- final ActivityRecord r = mPendingDrawActivities.get(i);
- if (!r.mVisibleRequested
- && !(keepInitializing && r.isState(ActivityRecord.State.INITIALIZING))) {
- if (DEBUG_METRICS) Slog.i(TAG, "Discard pending draw " + r);
- mPendingDrawActivities.remove(i);
- }
- }
+ return r == mLastLaunchedActivity;
}
/**
@@ -377,7 +352,7 @@
@Override
public String toString() {
return "TransitionInfo{" + Integer.toHexString(System.identityHashCode(this))
- + " a=" + mLastLaunchedActivity + " ua=" + mPendingDrawActivities + "}";
+ + " a=" + mLastLaunchedActivity + " d=" + mIsDrawn + "}";
}
}
@@ -683,8 +658,7 @@
// visible such as after the top task is finished.
for (int i = mTransitionInfoList.size() - 2; i >= 0; i--) {
final TransitionInfo prevInfo = mTransitionInfoList.get(i);
- prevInfo.updatePendingDraw(false /* keepInitializing */);
- if (prevInfo.allDrawn()) {
+ if (prevInfo.mIsDrawn || !prevInfo.mLastLaunchedActivity.mVisibleRequested) {
abort(prevInfo, "nothing will be drawn");
}
}
@@ -711,17 +685,16 @@
if (DEBUG_METRICS) Slog.i(TAG, "notifyWindowsDrawn " + r);
final TransitionInfo info = getActiveTransitionInfo(r);
- if (info == null || info.allDrawn()) {
- if (DEBUG_METRICS) Slog.i(TAG, "notifyWindowsDrawn no activity to be drawn");
+ if (info == null || info.mIsDrawn) {
+ if (DEBUG_METRICS) Slog.i(TAG, "notifyWindowsDrawn not pending drawn " + info);
return null;
}
// Always calculate the delay because the caller may need to know the individual drawn time.
info.mWindowsDrawnDelayMs = info.calculateDelay(timestampNs);
- info.removePendingDrawActivity(r);
- info.updatePendingDraw(false /* keepInitializing */);
+ info.mIsDrawn = true;
final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info);
- if (info.mLoggedTransitionStarting && info.allDrawn()) {
- done(false /* abort */, info, "notifyWindowsDrawn - all windows drawn", timestampNs);
+ if (info.mLoggedTransitionStarting) {
+ done(false /* abort */, info, "notifyWindowsDrawn", timestampNs);
}
if (r.mWmService.isRecentsAnimationTarget(r)) {
r.mWmService.getRecentsAnimationController().logRecentsAnimationStartTime(
@@ -770,12 +743,8 @@
info.mCurrentTransitionDelayMs = info.calculateDelay(timestampNs);
info.mReason = activityToReason.valueAt(index);
info.mLoggedTransitionStarting = true;
- // Do not remove activity in initializing state because the transition may be started
- // by starting window. The initializing activity may be requested to visible soon.
- info.updatePendingDraw(true /* keepInitializing */);
- if (info.allDrawn()) {
- done(false /* abort */, info, "notifyTransitionStarting - all windows drawn",
- timestampNs);
+ if (info.mIsDrawn) {
+ done(false /* abort */, info, "notifyTransitionStarting drawn", timestampNs);
}
}
}
@@ -828,12 +797,9 @@
return;
}
if (!r.mVisibleRequested || r.finishing) {
- info.removePendingDrawActivity(r);
- if (info.mLastLaunchedActivity == r) {
- // Check if the tracker can be cancelled because the last launched activity may be
- // no longer visible.
- scheduleCheckActivityToBeDrawn(r, 0 /* delay */);
- }
+ // Check if the tracker can be cancelled because the last launched activity may be
+ // no longer visible.
+ scheduleCheckActivityToBeDrawn(r, 0 /* delay */);
}
}
@@ -852,17 +818,12 @@
// If we have an active transition that's waiting on a certain activity that will be
// invisible now, we'll never get onWindowsDrawn, so abort the transition if necessary.
- // We have no active transitions.
+ // We have no active transitions. Or the notified activity whose visibility changed is
+ // no longer the launched activity, then we can still wait to get onWindowsDrawn.
if (info == null) {
return;
}
- // The notified activity whose visibility changed is no longer the launched activity.
- // We can still wait to get onWindowsDrawn.
- if (info.mLastLaunchedActivity != r) {
- return;
- }
-
// If the task of the launched activity contains any activity to be drawn, then the
// window drawn event should report later to complete the transition. Otherwise all
// activities in this task may be finished, invisible or drawn, so the transition event
@@ -945,7 +906,6 @@
}
logAppTransitionFinished(info, isHibernating != null ? isHibernating : false);
}
- info.mPendingDrawActivities.clear();
mTransitionInfoList.remove(info);
}
@@ -1122,7 +1082,7 @@
if (info == null) {
return null;
}
- if (!info.allDrawn() && info.mPendingFullyDrawn == null) {
+ if (!info.mIsDrawn && info.mPendingFullyDrawn == null) {
// There are still undrawn activities, postpone reporting fully drawn until all of its
// windows are drawn. So that is closer to an usable state.
info.mPendingFullyDrawn = () -> {
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index cddb1e7..badb1f5 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -119,8 +119,12 @@
if (adjacentTaskFragments != null && adjacentTaskFragments.contains(
childTaskFragment)) {
- // Everything behind two adjacent TaskFragments are occluded.
- mBehindFullyOccludedContainer = true;
+ if (!childTaskFragment.isTranslucent(starting)
+ && !childTaskFragment.getAdjacentTaskFragment().isTranslucent(
+ starting)) {
+ // Everything behind two adjacent TaskFragments are occluded.
+ mBehindFullyOccludedContainer = true;
+ }
continue;
}
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index f5e7967..662b3d5 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -148,7 +148,8 @@
/**
* A launch root task for activity launching with {@link FLAG_ACTIVITY_LAUNCH_ADJACENT} flag.
*/
- private Task mLaunchAdjacentFlagRootTask;
+ @VisibleForTesting
+ Task mLaunchAdjacentFlagRootTask;
/**
* A focusable root task that is purposely to be positioned at the top. Although the root
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 51547c7..43a4f97 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -567,17 +567,17 @@
case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT: {
final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer());
final Task task = wc != null ? wc.asTask() : null;
+ final boolean clearRoot = hop.getToTop();
if (task == null) {
throw new IllegalArgumentException("Cannot set non-task as launch root: " + wc);
} else if (!task.mCreatedByOrganizer) {
throw new UnsupportedOperationException(
"Cannot set non-organized task as adjacent flag root: " + wc);
- } else if (task.getAdjacentTaskFragment() == null) {
+ } else if (task.getAdjacentTaskFragment() == null && !clearRoot) {
throw new UnsupportedOperationException(
"Cannot set non-adjacent task as adjacent flag root: " + wc);
}
- final boolean clearRoot = hop.getToTop();
task.getDisplayArea().setLaunchAdjacentFlagRootTask(clearRoot ? null : task);
break;
}
@@ -1186,6 +1186,12 @@
sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
return;
}
+ if (!ownerActivity.isResizeable()) {
+ final IllegalArgumentException exception = new IllegalArgumentException("Not allowed"
+ + " to operate with non-resizable owner Activity");
+ sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+ return;
+ }
// The ownerActivity has to belong to the same app as the root Activity of the target Task.
final ActivityRecord rootActivity = ownerActivity.getTask().getRootActivity();
if (rootActivity.getUid() != ownerActivity.getUid()) {
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 8e2c1f0..761cea7 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -160,6 +160,64 @@
}
@Test
+ public void create_stateWithCancelStickyRequestFlag() {
+ String configString = "<device-state-config>\n"
+ + " <device-state>\n"
+ + " <identifier>1</identifier>\n"
+ + " <flags>\n"
+ + " <flag>FLAG_CANCEL_STICKY_REQUESTS</flag>\n"
+ + " </flags>\n"
+ + " <conditions/>\n"
+ + " </device-state>\n"
+ + " <device-state>\n"
+ + " <identifier>2</identifier>\n"
+ + " <conditions/>\n"
+ + " </device-state>\n"
+ + "</device-state-config>\n";
+ DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
+ DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext,
+ config);
+
+ DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
+ provider.setListener(listener);
+
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ final DeviceState[] expectedStates = new DeviceState[]{
+ new DeviceState(1, "", DeviceState.FLAG_CANCEL_STICKY_REQUESTS),
+ new DeviceState(2, "", 0 /* flags */) };
+ assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
+ }
+
+ @Test
+ public void create_stateWithInvalidFlag() {
+ String configString = "<device-state-config>\n"
+ + " <device-state>\n"
+ + " <identifier>1</identifier>\n"
+ + " <flags>\n"
+ + " <flag>INVALID_FLAG</flag>\n"
+ + " </flags>\n"
+ + " <conditions/>\n"
+ + " </device-state>\n"
+ + " <device-state>\n"
+ + " <identifier>2</identifier>\n"
+ + " <conditions/>\n"
+ + " </device-state>\n"
+ + "</device-state-config>\n";
+ DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
+ DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext,
+ config);
+
+ DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
+ provider.setListener(listener);
+
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ final DeviceState[] expectedStates = new DeviceState[]{
+ new DeviceState(1, "", 0 /* flags */),
+ new DeviceState(2, "", 0 /* flags */) };
+ assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
+ }
+
+ @Test
public void create_lidSwitch() {
String configString = "<device-state-config>\n"
+ " <device-state>\n"
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 0b91802..d4d8b868 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -477,7 +477,6 @@
@Test
public void testConsecutiveLaunch() {
- mTrampolineActivity.setState(ActivityRecord.State.INITIALIZING, "test");
onActivityLaunched(mTrampolineActivity);
mActivityMetricsLogger.notifyActivityLaunching(mTopActivity.intent,
mTrampolineActivity /* caller */, mTrampolineActivity.getUid());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index a482bda..1eed79f1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -543,6 +543,36 @@
}
@Test
+ public void testSetAdjacentLaunchRoot() {
+ DisplayContent dc = mWm.mRoot.getDisplayContent(Display.DEFAULT_DISPLAY);
+
+ final Task task1 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+ dc, WINDOWING_MODE_MULTI_WINDOW, null);
+ final RunningTaskInfo info1 = task1.getTaskInfo();
+ final Task task2 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+ dc, WINDOWING_MODE_MULTI_WINDOW, null);
+ final RunningTaskInfo info2 = task2.getTaskInfo();
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setAdjacentRoots(info1.token, info2.token);
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
+ assertEquals(task1.getAdjacentTaskFragment(), task2);
+ assertEquals(task2.getAdjacentTaskFragment(), task1);
+
+ wct = new WindowContainerTransaction();
+ wct.setLaunchAdjacentFlagRoot(info1.token);
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
+ assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, task1);
+
+ task1.setAdjacentTaskFragment(null);
+ task2.setAdjacentTaskFragment(null);
+ wct = new WindowContainerTransaction();
+ wct.clearLaunchAdjacentFlagRoot(info1.token);
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
+ assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, null);
+ }
+
+ @Test
public void testTileAddRemoveChild() {
final StubOrganizer listener = new StubOrganizer();
mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(listener);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index cfc145a..54d3af5 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -963,6 +963,21 @@
= "carrier_use_ims_first_for_emergency_bool";
/**
+ * When {@code true}, the determination of whether to place a call as an emergency call will be
+ * based on the known {@link android.telephony.emergency.EmergencyNumber}s for the SIM on which
+ * the call is being placed. In a dual SIM scenario, if Sim A has the emergency numbers
+ * 123, 456 and Sim B has the emergency numbers 789, and the user places a call on SIM A to 789,
+ * it will not be treated as an emergency call in this case.
+ * When {@code false}, the determination is based on the emergency numbers from all device SIMs,
+ * regardless of which SIM the call is being placed on. If Sim A has the emergency numbers
+ * 123, 456 and Sim B has the emergency numbers 789, and the user places a call on SIM A to 789,
+ * the call will be dialed as an emergency number, but with an unspecified routing.
+ * @hide
+ */
+ public static final String KEY_USE_ONLY_DIALED_SIM_ECC_LIST_BOOL =
+ "use_only_dialed_sim_ecc_list_bool";
+
+ /**
* When IMS instant lettering is available for a carrier (see
* {@link #KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL}), determines the list of characters
* which may not be contained in messages. Should be specified as a regular expression suitable
@@ -5265,6 +5280,7 @@
sDefaults.putBoolean(KEY_CARRIER_IMS_GBA_REQUIRED_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_USE_IMS_FIRST_FOR_EMERGENCY_BOOL, true);
+ sDefaults.putBoolean(KEY_USE_ONLY_DIALED_SIM_ECC_LIST_BOOL, false);
sDefaults.putString(KEY_CARRIER_NETWORK_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING, "");
sDefaults.putString(KEY_CARRIER_NETWORK_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING, "");
sDefaults.putString(KEY_CARRIER_QUALIFIED_NETWORKS_SERVICE_PACKAGE_OVERRIDE_STRING, "");