diff options
Diffstat (limited to 'packages/SystemUI/src')
221 files changed, 8060 insertions, 3480 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java index aa2fe3c7f8fc..57b3761c294f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.content.res.ColorStateList; @@ -31,6 +30,8 @@ import android.util.Log; import android.view.KeyEvent; import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; + import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -101,7 +102,8 @@ public class KeyguardHostView extends FrameLayout implements SecurityCallback { public static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardViewBase"; - private KeyguardSecurityContainer mSecurityContainer; + @VisibleForTesting + protected KeyguardSecurityContainer mSecurityContainer; public KeyguardHostView(Context context) { this(context, null); @@ -446,4 +448,11 @@ public class KeyguardHostView extends FrameLayout implements SecurityCallback { public SecurityMode getCurrentSecurityMode() { return mSecurityContainer.getCurrentSecurityMode(); } + + /** + * When bouncer was visible and is starting to become hidden. + */ + public void onStartingToHide() { + mSecurityContainer.onStartingToHide(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index 718bcf16c832..65bf7e6e5025 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -152,6 +152,11 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView mImm.hideSoftInputFromWindow(getWindowToken(), 0); } + @Override + public void onStartingToHide() { + mImm.hideSoftInputFromWindow(getWindowToken(), 0); + } + private void updateSwitchImeButton() { // If there's more than one IME, enable the IME switcher button final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 5c3d17ce0e2b..b99fb057ee65 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -19,6 +19,7 @@ import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL; import static android.view.ViewRootImpl.sNewInsetsMode; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; +import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; import static com.android.systemui.DejankUtils.whitelistIpcs; @@ -30,12 +31,14 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; +import android.graphics.Rect; import android.metrics.LogMaker; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; +import android.util.MathUtils; import android.util.Slog; import android.util.TypedValue; import android.view.LayoutInflater; @@ -44,6 +47,7 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.WindowManager; import android.widget.FrameLayout; @@ -52,6 +56,9 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -63,6 +70,8 @@ import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.InjectionInflationController; +import java.util.List; + public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSecurityView { private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardSecurityView"; @@ -89,6 +98,8 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe // How much to scale the default slop by, to avoid accidental drags. private static final float SLOP_SCALE = 4f; + private static final UiEventLogger sUiEventLogger = new UiEventLoggerImpl(); + private KeyguardSecurityModel mSecurityModel; private LockPatternUtils mLockPatternUtils; @@ -114,6 +125,47 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe private boolean mIsDragging; private float mStartTouchY = -1; + private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback = + new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { + + private final Rect mInitialBounds = new Rect(); + private final Rect mFinalBounds = new Rect(); + + @Override + public void onPrepare(WindowInsetsAnimation animation) { + mSecurityViewFlipper.getBoundsOnScreen(mInitialBounds); + } + + @Override + public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, + WindowInsetsAnimation.Bounds bounds) { + mSecurityViewFlipper.getBoundsOnScreen(mFinalBounds); + return bounds; + } + + @Override + public WindowInsets onProgress(WindowInsets windowInsets, + List<WindowInsetsAnimation> list) { + int translationY = 0; + for (WindowInsetsAnimation animation : list) { + if ((animation.getTypeMask() & WindowInsets.Type.ime()) == 0) { + continue; + } + final int paddingBottom = (int) MathUtils.lerp( + mInitialBounds.bottom - mFinalBounds.bottom, 0, + animation.getInterpolatedFraction()); + translationY += paddingBottom; + } + mSecurityViewFlipper.setTranslationY(translationY); + return windowInsets; + } + + @Override + public void onEnd(WindowInsetsAnimation animation) { + mSecurityViewFlipper.setTranslationY(0); + } + }; + // Used to notify the container when something interesting happens. public interface SecurityCallback { public boolean dismiss(boolean authenticated, int targetUserId, @@ -131,6 +183,44 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe public void onCancelClicked(); } + @VisibleForTesting + public enum BouncerUiEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Default UiEvent used for variable initialization.") + UNKNOWN(0), + + @UiEvent(doc = "Bouncer is dismissed using extended security access.") + BOUNCER_DISMISS_EXTENDED_ACCESS(413), + + @UiEvent(doc = "Bouncer is dismissed using biometric.") + BOUNCER_DISMISS_BIOMETRIC(414), + + @UiEvent(doc = "Bouncer is dismissed without security access.") + BOUNCER_DISMISS_NONE_SECURITY(415), + + @UiEvent(doc = "Bouncer is dismissed using password security.") + BOUNCER_DISMISS_PASSWORD(416), + + @UiEvent(doc = "Bouncer is dismissed using sim security access.") + BOUNCER_DISMISS_SIM(417), + + @UiEvent(doc = "Bouncer is successfully unlocked using password.") + BOUNCER_PASSWORD_SUCCESS(418), + + @UiEvent(doc = "An attempt to unlock bouncer using password has failed.") + BOUNCER_PASSWORD_FAILURE(419); + + private final int mId; + + BouncerUiEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + public KeyguardSecurityContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -162,6 +252,7 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe if (mCurrentSecuritySelection != SecurityMode.None) { getSecurityView(mCurrentSecuritySelection).onResume(reason); } + mSecurityViewFlipper.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback); updateBiometricRetry(); } @@ -175,6 +266,14 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe if (mCurrentSecuritySelection != SecurityMode.None) { getSecurityView(mCurrentSecuritySelection).onPause(); } + mSecurityViewFlipper.setWindowInsetsAnimationCallback(null); + } + + @Override + public void onStartingToHide() { + if (mCurrentSecuritySelection != SecurityMode.None) { + getSecurityView(mCurrentSecuritySelection).onStartingToHide(); + } } @Override @@ -333,7 +432,9 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe } } - protected void onFinishInflate() { + @Override + public void onFinishInflate() { + super.onFinishInflate(); mSecurityViewFlipper = findViewById(R.id.view_flipper); mSecurityViewFlipper.setLockPatternUtils(mLockPatternUtils); } @@ -516,17 +617,21 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe boolean finish = false; boolean strongAuth = false; int eventSubtype = -1; + BouncerUiEvent uiEvent = BouncerUiEvent.UNKNOWN; if (mUpdateMonitor.getUserHasTrust(targetUserId)) { finish = true; eventSubtype = BOUNCER_DISMISS_EXTENDED_ACCESS; + uiEvent = BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS; } else if (mUpdateMonitor.getUserUnlockedWithBiometric(targetUserId)) { finish = true; eventSubtype = BOUNCER_DISMISS_BIOMETRIC; + uiEvent = BouncerUiEvent.BOUNCER_DISMISS_BIOMETRIC; } else if (SecurityMode.None == mCurrentSecuritySelection) { SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); if (SecurityMode.None == securityMode) { finish = true; // no security required eventSubtype = BOUNCER_DISMISS_NONE_SECURITY; + uiEvent = BouncerUiEvent.BOUNCER_DISMISS_NONE_SECURITY; } else { showSecurityScreen(securityMode); // switch to the alternate security view } @@ -538,6 +643,7 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe strongAuth = true; finish = true; eventSubtype = BOUNCER_DISMISS_PASSWORD; + uiEvent = BouncerUiEvent.BOUNCER_DISMISS_PASSWORD; break; case SimPin: @@ -548,6 +654,7 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe KeyguardUpdateMonitor.getCurrentUser())) { finish = true; eventSubtype = BOUNCER_DISMISS_SIM; + uiEvent = BouncerUiEvent.BOUNCER_DISMISS_SIM; } else { showSecurityScreen(securityMode); } @@ -572,6 +679,9 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe mMetricsLogger.write(new LogMaker(MetricsEvent.BOUNCER) .setType(MetricsEvent.TYPE_DISMISS).setSubtype(eventSubtype)); } + if (uiEvent != BouncerUiEvent.UNKNOWN) { + sUiEventLogger.log(uiEvent); + } if (finish) { mSecurityCallback.finish(strongAuth, targetUserId); } @@ -677,6 +787,8 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe } mMetricsLogger.write(new LogMaker(MetricsEvent.BOUNCER) .setType(success ? MetricsEvent.TYPE_SUCCESS : MetricsEvent.TYPE_FAILURE)); + sUiEventLogger.log(success ? BouncerUiEvent.BOUNCER_PASSWORD_SUCCESS + : BouncerUiEvent.BOUNCER_PASSWORD_FAILURE); } public void reset() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java index 20b1e0d2c822..43cef3acf147 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -159,4 +159,9 @@ public interface KeyguardSecurityView { default boolean disallowInterceptTouch(MotionEvent event) { return false; } + + /** + * When bouncer was visible but is being dragged down or dismissed. + */ + default void onStartingToHide() {}; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 367058fa58dd..a96ef91850df 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -1301,6 +1301,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private FingerprintManager mFpm; private FaceManager mFaceManager; private boolean mFingerprintLockedOut; + private TelephonyManager mTelephonyManager; /** * When we receive a @@ -1728,10 +1729,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } updateAirplaneModeState(); - TelephonyManager telephony = + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (telephony != null) { - telephony.listen(mPhoneStateListener, LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE); + if (mTelephonyManager != null) { + mTelephonyManager.listen(mPhoneStateListener, + LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE); + // Set initial sim states values. + for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) { + int state = mTelephonyManager.getSimState(slot); + int[] subIds = mSubscriptionManager.getSubscriptionIds(slot); + if (subIds != null) { + for (int subId : subIds) { + mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state) + .sendToTarget(); + } + } + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java index 409ae3f3c7d6..c92174a0d8af 100644 --- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java +++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java @@ -164,7 +164,9 @@ public class PasswordTextView extends View { currentDrawPosition = getPaddingLeft(); } } else { - currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2; + float maxRight = getWidth() - getPaddingRight() - totalDrawingWidth; + float center = getWidth() / 2f - totalDrawingWidth / 2f; + currentDrawPosition = center > 0 ? center : maxRight; } int length = mTextChars.size(); Rect bounds = getCharBounds(); diff --git a/packages/SystemUI/src/com/android/systemui/CornerHandleView.java b/packages/SystemUI/src/com/android/systemui/CornerHandleView.java index 85ce313670e3..cf7ee3a5753f 100644 --- a/packages/SystemUI/src/com/android/systemui/CornerHandleView.java +++ b/packages/SystemUI/src/com/android/systemui/CornerHandleView.java @@ -168,14 +168,14 @@ public class CornerHandleView extends View { // Attempt to get the bottom corner radius, otherwise fall back on the generic or top // values. If none are available, use the FALLBACK_RADIUS_DP. int radius = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.rounded_corner_radius_bottom); + com.android.systemui.R.dimen.config_rounded_mask_size_bottom); if (radius == 0) { radius = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.rounded_corner_radius); + com.android.systemui.R.dimen.config_rounded_mask_size); } if (radius == 0) { radius = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.rounded_corner_radius_top); + com.android.systemui.R.dimen.config_rounded_mask_size_top); } if (radius == 0) { radius = (int) convertDpToPixel(FALLBACK_RADIUS_DP, mContext); diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index b6152dae33d6..0af026eb3509 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -75,7 +75,6 @@ import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment; import com.android.systemui.statusbar.notification.NotificationFilter; import com.android.systemui.statusbar.notification.VisualStabilityManager; -import com.android.systemui.statusbar.notification.interruption.NotificationAlertingManager; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ChannelEditorDialogController; import com.android.systemui.statusbar.notification.row.NotificationBlockingHelperManager; @@ -293,8 +292,6 @@ public class Dependency { @Inject Lazy<RemoteInputQuickSettingsDisabler> mRemoteInputQuickSettingsDisabler; @Inject Lazy<BubbleController> mBubbleController; @Inject Lazy<NotificationEntryManager> mNotificationEntryManager; - @Inject - Lazy<NotificationAlertingManager> mNotificationAlertingManager; @Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager; @Inject Lazy<AutoHideController> mAutoHideController; @Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener; @@ -493,7 +490,6 @@ public class Dependency { mRemoteInputQuickSettingsDisabler::get); mProviders.put(BubbleController.class, mBubbleController::get); mProviders.put(NotificationEntryManager.class, mNotificationEntryManager::get); - mProviders.put(NotificationAlertingManager.class, mNotificationAlertingManager::get); mProviders.put(ForegroundServiceNotificationListener.class, mForegroundServiceNotificationListener::get); mProviders.put(ClockManager.class, mClockManager::get); diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java index 5442299881c0..71ec33e16e0e 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java @@ -16,9 +16,6 @@ package com.android.systemui; -import android.app.ActivityManager; -import android.content.Context; -import android.content.res.Configuration; import android.graphics.Rect; import android.os.Handler; import android.os.HandlerThread; @@ -27,17 +24,12 @@ import android.os.Trace; import android.service.wallpaper.WallpaperService; import android.util.Log; import android.util.Size; -import android.view.DisplayInfo; import android.view.SurfaceHolder; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.glwallpaper.EglHelper; import com.android.systemui.glwallpaper.GLWallpaperRenderer; import com.android.systemui.glwallpaper.ImageWallpaperRenderer; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.phone.DozeParameters; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -53,16 +45,12 @@ public class ImageWallpaper extends WallpaperService { // We delayed destroy render context that subsequent render requests have chance to cancel it. // This is to avoid destroying then recreating render context in a very short time. private static final int DELAY_FINISH_RENDERING = 1000; - private static final int INTERVAL_WAIT_FOR_RENDERING = 100; - private static final int PATIENCE_WAIT_FOR_RENDERING = 10; - private static final boolean DEBUG = true; - private final DozeParameters mDozeParameters; + private static final boolean DEBUG = false; private HandlerThread mWorker; @Inject - public ImageWallpaper(DozeParameters dozeParameters) { + public ImageWallpaper() { super(); - mDozeParameters = dozeParameters; } @Override @@ -74,7 +62,7 @@ public class ImageWallpaper extends WallpaperService { @Override public Engine onCreateEngine() { - return new GLEngine(this, mDozeParameters); + return new GLEngine(); } @Override @@ -84,7 +72,7 @@ public class ImageWallpaper extends WallpaperService { mWorker = null; } - class GLEngine extends Engine implements GLWallpaperRenderer.SurfaceProxy, StateListener { + class GLEngine extends Engine { // Surface is rejected if size below a threshold on some devices (ie. 8px on elfin) // set min to 64 px (CTS covers this), please refer to ag/4867989 for detail. @VisibleForTesting @@ -94,40 +82,15 @@ public class ImageWallpaper extends WallpaperService { private GLWallpaperRenderer mRenderer; private EglHelper mEglHelper; - private StatusBarStateController mController; private final Runnable mFinishRenderingTask = this::finishRendering; - private boolean mShouldStopTransition; - private final DisplayInfo mDisplayInfo = new DisplayInfo(); - private final Object mMonitor = new Object(); - @VisibleForTesting - boolean mIsHighEndGfx; - private boolean mDisplayNeedsBlanking; - private boolean mNeedTransition; private boolean mNeedRedraw; - // This variable can only be accessed in synchronized block. - private boolean mWaitingForRendering; - GLEngine(Context context, DozeParameters dozeParameters) { - init(dozeParameters); + GLEngine() { } @VisibleForTesting - GLEngine(DozeParameters dozeParameters, Handler handler) { + GLEngine(Handler handler) { super(SystemClock::elapsedRealtime, handler); - init(dozeParameters); - } - - private void init(DozeParameters dozeParameters) { - mIsHighEndGfx = ActivityManager.isHighEndGfx(); - mDisplayNeedsBlanking = dozeParameters.getDisplayNeedsBlanking(); - mNeedTransition = false; - - // We will preserve EGL context when we are in lock screen or aod - // to avoid janking in following transition, we need to release when back to home. - mController = Dependency.get(StatusBarStateController.class); - if (mController != null) { - mController.addCallback(this /* StateListener */); - } } @Override @@ -135,9 +98,8 @@ public class ImageWallpaper extends WallpaperService { mEglHelper = getEglHelperInstance(); // Deferred init renderer because we need to get wallpaper by display context. mRenderer = getRendererInstance(); - getDisplayContext().getDisplay().getDisplayInfo(mDisplayInfo); setFixedSizeAllowed(true); - setOffsetNotificationsEnabled(mNeedTransition); + setOffsetNotificationsEnabled(false); updateSurfaceSize(); } @@ -146,7 +108,7 @@ public class ImageWallpaper extends WallpaperService { } ImageWallpaperRenderer getRendererInstance() { - return new ImageWallpaperRenderer(getDisplayContext(), this /* SurfaceProxy */); + return new ImageWallpaperRenderer(getDisplayContext()); } private void updateSurfaceSize() { @@ -157,79 +119,13 @@ public class ImageWallpaper extends WallpaperService { holder.setFixedSize(width, height); } - /** - * Check if necessary to stop transition with current wallpaper on this device. <br/> - * This should only be invoked after {@link #onSurfaceCreated(SurfaceHolder)}} - * is invoked since it needs display context and surface frame size. - * @return true if need to stop transition. - */ - @VisibleForTesting - boolean checkIfShouldStopTransition() { - int orientation = getDisplayContext().getResources().getConfiguration().orientation; - Rect frame = getSurfaceHolder().getSurfaceFrame(); - Rect display = new Rect(); - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - display.set(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); - } else { - display.set(0, 0, mDisplayInfo.logicalHeight, mDisplayInfo.logicalWidth); - } - return mNeedTransition - && (frame.width() < display.width() || frame.height() < display.height()); - } - - @Override - public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, - float yOffsetStep, int xPixelOffset, int yPixelOffset) { - if (mWorker == null) return; - mWorker.getThreadHandler().post(() -> mRenderer.updateOffsets(xOffset, yOffset)); - } - - @Override - public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) { - if (mWorker == null || !mNeedTransition) return; - final long duration = mShouldStopTransition ? 0 : animationDuration; - if (DEBUG) { - Log.d(TAG, "onAmbientModeChanged: inAmbient=" + inAmbientMode - + ", duration=" + duration - + ", mShouldStopTransition=" + mShouldStopTransition); - } - mWorker.getThreadHandler().post( - () -> mRenderer.updateAmbientMode(inAmbientMode, duration)); - if (inAmbientMode && animationDuration == 0) { - // This means that we are transiting from home to aod, to avoid - // race condition between window visibility and transition, - // we don't return until the transition is finished. See b/136643341. - waitForBackgroundRendering(); - } - } - @Override public boolean shouldZoomOutWallpaper() { return true; } - private void waitForBackgroundRendering() { - synchronized (mMonitor) { - try { - mWaitingForRendering = true; - for (int patience = 1; mWaitingForRendering; patience++) { - mMonitor.wait(INTERVAL_WAIT_FOR_RENDERING); - mWaitingForRendering &= patience < PATIENCE_WAIT_FOR_RENDERING; - } - } catch (InterruptedException ex) { - } finally { - mWaitingForRendering = false; - } - } - } - @Override public void onDestroy() { - if (mController != null) { - mController.removeCallback(this /* StateListener */); - } - mController = null; - mWorker.getThreadHandler().post(() -> { mRenderer.finish(); mRenderer = null; @@ -240,7 +136,6 @@ public class ImageWallpaper extends WallpaperService { @Override public void onSurfaceCreated(SurfaceHolder holder) { - mShouldStopTransition = checkIfShouldStopTransition(); if (mWorker == null) return; mWorker.getThreadHandler().post(() -> { mEglHelper.init(holder, needSupportWideColorGamut()); @@ -251,32 +146,13 @@ public class ImageWallpaper extends WallpaperService { @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (mWorker == null) return; - mWorker.getThreadHandler().post(() -> { - mRenderer.onSurfaceChanged(width, height); - mNeedRedraw = true; - }); + mWorker.getThreadHandler().post(() -> mRenderer.onSurfaceChanged(width, height)); } @Override public void onSurfaceRedrawNeeded(SurfaceHolder holder) { if (mWorker == null) return; - if (DEBUG) { - Log.d(TAG, "onSurfaceRedrawNeeded: mNeedRedraw=" + mNeedRedraw); - } - - mWorker.getThreadHandler().post(() -> { - if (mNeedRedraw) { - drawFrame(); - mNeedRedraw = false; - } - }); - } - - @Override - public void onVisibilityChanged(boolean visible) { - if (DEBUG) { - Log.d(TAG, "wallpaper visibility changes: " + visible); - } + mWorker.getThreadHandler().post(this::drawFrame); } private void drawFrame() { @@ -285,15 +161,6 @@ public class ImageWallpaper extends WallpaperService { postRender(); } - @Override - public void onStatePostChange() { - // When back to home, we try to release EGL, which is preserved in lock screen or aod. - if (mWorker != null && mController.getState() == StatusBarState.SHADE) { - mWorker.getThreadHandler().post(this::scheduleFinishRendering); - } - } - - @Override public void preRender() { // This method should only be invoked from worker thread. Trace.beginSection("ImageWallpaper#preRender"); @@ -330,7 +197,6 @@ public class ImageWallpaper extends WallpaperService { } } - @Override public void requestRender() { // This method should only be invoked from worker thread. Trace.beginSection("ImageWallpaper#requestRender"); @@ -355,27 +221,13 @@ public class ImageWallpaper extends WallpaperService { } } - @Override public void postRender() { // This method should only be invoked from worker thread. Trace.beginSection("ImageWallpaper#postRender"); - notifyWaitingThread(); scheduleFinishRendering(); Trace.endSection(); } - private void notifyWaitingThread() { - synchronized (mMonitor) { - if (mWaitingForRendering) { - try { - mWaitingForRendering = false; - mMonitor.notify(); - } catch (IllegalMonitorStateException ex) { - } - } - } - } - private void cancelFinishRenderingTask() { if (mWorker == null) return; mWorker.getThreadHandler().removeCallbacks(mFinishRenderingTask); @@ -391,18 +243,11 @@ public class ImageWallpaper extends WallpaperService { Trace.beginSection("ImageWallpaper#finishRendering"); if (mEglHelper != null) { mEglHelper.destroyEglSurface(); - if (!needPreserveEglContext()) { - mEglHelper.destroyEglContext(); - } + mEglHelper.destroyEglContext(); } Trace.endSection(); } - private boolean needPreserveEglContext() { - return mNeedTransition && mController != null - && mController.getState() == StatusBarState.KEYGUARD; - } - private boolean needSupportWideColorGamut() { return mRenderer.isWcgContent(); } @@ -411,16 +256,6 @@ public class ImageWallpaper extends WallpaperService { protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { super.dump(prefix, fd, out, args); out.print(prefix); out.print("Engine="); out.println(this); - out.print(prefix); out.print("isHighEndGfx="); out.println(mIsHighEndGfx); - out.print(prefix); out.print("displayNeedsBlanking="); - out.println(mDisplayNeedsBlanking); - out.print(prefix); out.print("displayInfo="); out.print(mDisplayInfo); - out.print(prefix); out.print("mNeedTransition="); out.println(mNeedTransition); - out.print(prefix); out.print("mShouldStopTransition="); - out.println(mShouldStopTransition); - out.print(prefix); out.print("StatusBarState="); - out.println(mController != null ? mController.getState() : "null"); - out.print(prefix); out.print("valid surface="); out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null ? getSurfaceHolder().getSurface().isValid() diff --git a/packages/SystemUI/src/com/android/systemui/Interpolators.java b/packages/SystemUI/src/com/android/systemui/Interpolators.java index 6923079dd5c4..2eba9521b9e7 100644 --- a/packages/SystemUI/src/com/android/systemui/Interpolators.java +++ b/packages/SystemUI/src/com/android/systemui/Interpolators.java @@ -54,6 +54,11 @@ public class Interpolators { public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f, 1); public static final Interpolator BOUNCE = new BounceInterpolator(); + /** + * For state transitions on the control panel that lives in GlobalActions. + */ + public static final Interpolator CONTROL_STATE = new PathInterpolator(0.4f, 0f, 0.2f, + 1.0f); /** * Interpolator to be used when animating a move based on a click. Pair with enough duration. diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index 6aa2326c388a..87990cd3bffa 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -21,11 +21,25 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import com.android.systemui.settings.CurrentUserContextTracker; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; import java.util.Set; +/** + * A helper class to store simple preferences for SystemUI. Its main use case is things such as + * feature education, e.g. "has the user seen this tooltip". + * + * As of this writing, feature education settings are *intentionally exempted* from backup and + * restore because there is not a great way to know which subset of features the user _should_ see + * again if, for instance, they are coming from multiple OSes back or switching OEMs. + * + * NOTE: Clients of this class should take care to pass in the correct user context when querying + * settings, otherwise you will always read/write for user 0 which is almost never what you want. + * See {@link CurrentUserContextTracker} for a simple way to get the current context + */ public final class Prefs { private Prefs() {} // no instantation @@ -109,6 +123,8 @@ public final class Prefs { String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding"; String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding"; String CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT = "ControlsStructureSwipeTooltipCount"; + /** Tracks whether the user has seen the onboarding screen for priority conversations */ + String HAS_SEEN_PRIORITY_ONBOARDING = "HasSeenPriorityOnboarding"; } public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) { diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 8df3dd2ad845..7861211e802d 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -54,8 +54,10 @@ import android.graphics.Region; import android.graphics.drawable.VectorDrawable; import android.hardware.display.DisplayManager; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.HandlerThread; import android.os.SystemProperties; +import android.os.UserHandle; import android.provider.Settings.Secure; import android.util.DisplayMetrics; import android.util.Log; @@ -298,13 +300,15 @@ public class ScreenDecorations extends SystemUI implements Tunable { updateColorInversion(value); } }; + + mColorInversionSetting.setListening(true); + mColorInversionSetting.onChange(false); } - mColorInversionSetting.setListening(true); - mColorInversionSetting.onChange(false); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); - mBroadcastDispatcher.registerReceiverWithHandler(mIntentReceiver, filter, mHandler); + mBroadcastDispatcher.registerReceiver(mUserSwitchIntentReceiver, filter, + new HandlerExecutor(mHandler), UserHandle.ALL); mIsRegistered = true; } else { mMainHandler.post(() -> mTunerService.removeTunable(this)); @@ -313,7 +317,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { mColorInversionSetting.setListening(false); } - mBroadcastDispatcher.unregisterReceiver(mIntentReceiver); + mBroadcastDispatcher.unregisterReceiver(mUserSwitchIntentReceiver); mIsRegistered = false; } } @@ -503,17 +507,16 @@ public class ScreenDecorations extends SystemUI implements Tunable { } } - private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action.equals(Intent.ACTION_USER_SWITCHED)) { - int newUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - ActivityManager.getCurrentUser()); - // update color inversion setting to the new user - mColorInversionSetting.setUserId(newUserId); - updateColorInversion(mColorInversionSetting.getValue()); + int newUserId = ActivityManager.getCurrentUser(); + if (DEBUG) { + Log.d(TAG, "UserSwitched newUserId=" + newUserId); } + // update color inversion setting to the new user + mColorInversionSetting.setUserId(newUserId); + updateColorInversion(mColorInversionSetting.getValue()); } }; @@ -945,7 +948,12 @@ public class ScreenDecorations extends SystemUI implements Tunable { int dw = flipped ? lh : lw; int dh = flipped ? lw : lh; - mBoundingPath.set(DisplayCutout.pathFromResources(getResources(), dw, dh)); + Path path = DisplayCutout.pathFromResources(getResources(), dw, dh); + if (path != null) { + mBoundingPath.set(path); + } else { + mBoundingPath.reset(); + } Matrix m = new Matrix(); transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m); mBoundingPath.transform(m); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index cbdae4e6fe63..c84701c9512e 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -139,7 +139,7 @@ public class SystemUIApplication extends Application implements */ public void startServicesIfNeeded() { - String[] names = getResources().getStringArray(R.array.config_systemUIServiceComponents); + String[] names = SystemUIFactory.getInstance().getSystemUIServiceComponents(getResources()); startServicesIfNeeded(/* metricsPrefix= */ "StartServices", names); } @@ -150,8 +150,8 @@ public class SystemUIApplication extends Application implements * <p>This method must only be called from the main thread.</p> */ void startSecondaryUserServicesIfNeeded() { - String[] names = - getResources().getStringArray(R.array.config_systemUIServiceComponentsPerUser); + String[] names = SystemUIFactory.getInstance().getSystemUIServiceComponentsPerUser( + getResources()); startServicesIfNeeded(/* metricsPrefix= */ "StartSecondaryServices", names); } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index fb40774a1f5c..be82a2d5325b 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -18,6 +18,7 @@ package com.android.systemui; import android.annotation.NonNull; import android.content.Context; +import android.content.res.Resources; import android.os.Handler; import android.os.Looper; import android.util.Log; @@ -120,6 +121,16 @@ public class SystemUIFactory { return mRootComponent; } + /** Returns the list of system UI components that should be started. */ + public String[] getSystemUIServiceComponents(Resources resources) { + return resources.getStringArray(R.array.config_systemUIServiceComponents); + } + + /** Returns the list of system UI components that should be started per user. */ + public String[] getSystemUIServiceComponentsPerUser(Resources resources) { + return resources.getStringArray(R.array.config_systemUIServiceComponentsPerUser); + } + /** * Creates an instance of ScreenshotNotificationSmartActionsProvider. * This method is overridden in vendor specific implementation of Sys UI. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 7262f8caac89..73dfd32d03a2 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -16,6 +16,10 @@ package com.android.systemui.accessibility; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; + +import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; + import android.accessibilityservice.AccessibilityService; import android.app.PendingIntent; import android.app.RemoteAction; @@ -23,6 +27,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; import android.graphics.drawable.Icon; import android.hardware.input.InputManager; import android.os.Handler; @@ -30,6 +35,7 @@ import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.SystemClock; +import android.os.UserHandle; import android.util.Log; import android.view.Display; import android.view.IWindowManager; @@ -41,12 +47,15 @@ import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import com.android.internal.R; +import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; import com.android.internal.util.ScreenshotHelper; import com.android.systemui.Dependency; import com.android.systemui.SystemUI; import com.android.systemui.recents.Recents; import com.android.systemui.statusbar.phone.StatusBar; +import java.util.Locale; + import javax.inject.Inject; import javax.inject.Singleton; @@ -56,7 +65,6 @@ import javax.inject.Singleton; @Singleton public class SystemActions extends SystemUI { private static final String TAG = "SystemActions"; - // TODO(b/147916452): add implementation on launcher side to register this action. /** * Action ID to go back. @@ -94,12 +102,6 @@ public class SystemActions extends SystemUI { AccessibilityService.GLOBAL_ACTION_POWER_DIALOG; // = 6 /** - * Action ID to toggle docking the current app's window - */ - private static final int SYSTEM_ACTION_ID_TOGGLE_SPLIT_SCREEN = - AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN; // = 7 - - /** * Action ID to lock the screen */ private static final int SYSTEM_ACTION_ID_LOCK_SCREEN = @@ -112,13 +114,22 @@ public class SystemActions extends SystemUI { AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT; // = 9 /** - * Action ID to show accessibility menu + * Action ID to trigger the accessibility button */ - private static final int SYSTEM_ACTION_ID_ACCESSIBILITY_MENU = 10; + public static final int SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON = + AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_BUTTON; // 11 + + /** + * Action ID to show accessibility button's menu of services + */ + public static final int SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON_CHOOSER = + AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_BUTTON_CHOOSER; // 12 private Recents mRecents; private StatusBar mStatusBar; private SystemActionsBroadcastReceiver mReceiver; + private Locale mLocale; + private AccessibilityManager mA11yManager; @Inject public SystemActions(Context context) { @@ -126,96 +137,139 @@ public class SystemActions extends SystemUI { mRecents = Dependency.get(Recents.class); mStatusBar = Dependency.get(StatusBar.class); mReceiver = new SystemActionsBroadcastReceiver(); + mLocale = mContext.getResources().getConfiguration().getLocales().get(0); + mA11yManager = (AccessibilityManager) mContext.getSystemService( + Context.ACCESSIBILITY_SERVICE); } @Override public void start() { mContext.registerReceiverForAllUsers(mReceiver, mReceiver.createIntentFilter(), null, null); + registerActions(); + } - // TODO(b/148087487): update the icon used below to a valid one - RemoteAction actionBack = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_back_label), - mContext.getString(R.string.accessibility_system_action_back_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_BACK)); - RemoteAction actionHome = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_home_label), - mContext.getString(R.string.accessibility_system_action_home_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_HOME)); - - RemoteAction actionRecents = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_recents_label), - mContext.getString(R.string.accessibility_system_action_recents_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_RECENTS)); - - RemoteAction actionNotifications = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_notifications_label), - mContext.getString(R.string.accessibility_system_action_notifications_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_NOTIFICATIONS)); - - RemoteAction actionQuickSettings = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_quick_settings_label), - mContext.getString(R.string.accessibility_system_action_quick_settings_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_QUICK_SETTINGS)); - - RemoteAction actionPowerDialog = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_power_dialog_label), - mContext.getString(R.string.accessibility_system_action_power_dialog_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_POWER_DIALOG)); - - RemoteAction actionToggleSplitScreen = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_toggle_split_screen_label), - mContext.getString(R.string.accessibility_system_action_toggle_split_screen_label), - mReceiver.createPendingIntent( - mContext, - SystemActionsBroadcastReceiver.INTENT_ACTION_TOGGLE_SPLIT_SCREEN)); + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + final Locale locale = mContext.getResources().getConfiguration().getLocales().get(0); + if (!locale.equals(mLocale)) { + mLocale = locale; + registerActions(); + } + } - RemoteAction actionLockScreen = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_lock_screen_label), - mContext.getString(R.string.accessibility_system_action_lock_screen_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_LOCK_SCREEN)); + private void registerActions() { + RemoteAction actionBack = createRemoteAction( + R.string.accessibility_system_action_back_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_BACK); + + RemoteAction actionHome = createRemoteAction( + R.string.accessibility_system_action_home_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_HOME); + + RemoteAction actionRecents = createRemoteAction( + R.string.accessibility_system_action_recents_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_RECENTS); + + RemoteAction actionNotifications = createRemoteAction( + R.string.accessibility_system_action_notifications_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_NOTIFICATIONS); + + RemoteAction actionQuickSettings = createRemoteAction( + R.string.accessibility_system_action_quick_settings_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_QUICK_SETTINGS); + + RemoteAction actionPowerDialog = createRemoteAction( + R.string.accessibility_system_action_power_dialog_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_POWER_DIALOG); + + RemoteAction actionLockScreen = createRemoteAction( + R.string.accessibility_system_action_lock_screen_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_LOCK_SCREEN); + + RemoteAction actionTakeScreenshot = createRemoteAction( + R.string.accessibility_system_action_screenshot_label, + SystemActionsBroadcastReceiver.INTENT_ACTION_TAKE_SCREENSHOT); + + mA11yManager.registerSystemAction(actionBack, SYSTEM_ACTION_ID_BACK); + mA11yManager.registerSystemAction(actionHome, SYSTEM_ACTION_ID_HOME); + mA11yManager.registerSystemAction(actionRecents, SYSTEM_ACTION_ID_RECENTS); + mA11yManager.registerSystemAction(actionNotifications, SYSTEM_ACTION_ID_NOTIFICATIONS); + mA11yManager.registerSystemAction(actionQuickSettings, SYSTEM_ACTION_ID_QUICK_SETTINGS); + mA11yManager.registerSystemAction(actionPowerDialog, SYSTEM_ACTION_ID_POWER_DIALOG); + mA11yManager.registerSystemAction(actionLockScreen, SYSTEM_ACTION_ID_LOCK_SCREEN); + mA11yManager.registerSystemAction(actionTakeScreenshot, SYSTEM_ACTION_ID_TAKE_SCREENSHOT); + } - RemoteAction actionTakeScreenshot = new RemoteAction( - Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_screenshot_label), - mContext.getString(R.string.accessibility_system_action_screenshot_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_TAKE_SCREENSHOT)); + /** + * Register a system action. + * @param actionId the action ID to register. + */ + public void register(int actionId) { + int labelId; + String intent; + switch (actionId) { + case SYSTEM_ACTION_ID_BACK: + labelId = R.string.accessibility_system_action_back_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_BACK; + break; + case SYSTEM_ACTION_ID_HOME: + labelId = R.string.accessibility_system_action_home_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_HOME; + break; + case SYSTEM_ACTION_ID_RECENTS: + labelId = R.string.accessibility_system_action_recents_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_RECENTS; + break; + case SYSTEM_ACTION_ID_NOTIFICATIONS: + labelId = R.string.accessibility_system_action_notifications_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_NOTIFICATIONS; + break; + case SYSTEM_ACTION_ID_QUICK_SETTINGS: + labelId = R.string.accessibility_system_action_quick_settings_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_QUICK_SETTINGS; + break; + case SYSTEM_ACTION_ID_POWER_DIALOG: + labelId = R.string.accessibility_system_action_power_dialog_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_POWER_DIALOG; + break; + case SYSTEM_ACTION_ID_LOCK_SCREEN: + labelId = R.string.accessibility_system_action_lock_screen_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_LOCK_SCREEN; + break; + case SYSTEM_ACTION_ID_TAKE_SCREENSHOT: + labelId = R.string.accessibility_system_action_screenshot_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_TAKE_SCREENSHOT; + break; + case SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON: + labelId = R.string.accessibility_system_action_accessibility_button_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_ACCESSIBILITY_BUTTON; + break; + case SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON_CHOOSER: + labelId = R.string.accessibility_system_action_accessibility_button_chooser_label; + intent = SystemActionsBroadcastReceiver.INTENT_ACTION_ACCESSIBILITY_BUTTON_CHOOSER; + break; + default: + return; + } + mA11yManager.registerSystemAction(createRemoteAction(labelId, intent), actionId); + } - RemoteAction actionAccessibilityMenu = new RemoteAction( + private RemoteAction createRemoteAction(int labelId, String intent) { + // TODO(b/148087487): update the icon used below to a valid one + return new RemoteAction( Icon.createWithResource(mContext, R.drawable.ic_info), - mContext.getString(R.string.accessibility_system_action_accessibility_menu_label), - mContext.getString(R.string.accessibility_system_action_accessibility_menu_label), - mReceiver.createPendingIntent( - mContext, SystemActionsBroadcastReceiver.INTENT_ACTION_ACCESSIBILITY_MENU)); - - AccessibilityManager am = (AccessibilityManager) mContext.getSystemService( - Context.ACCESSIBILITY_SERVICE); + mContext.getString(labelId), + mContext.getString(labelId), + mReceiver.createPendingIntent(mContext, intent)); + } - am.registerSystemAction(actionBack, SYSTEM_ACTION_ID_BACK); - am.registerSystemAction(actionHome, SYSTEM_ACTION_ID_HOME); - am.registerSystemAction(actionRecents, SYSTEM_ACTION_ID_RECENTS); - am.registerSystemAction(actionNotifications, SYSTEM_ACTION_ID_NOTIFICATIONS); - am.registerSystemAction(actionQuickSettings, SYSTEM_ACTION_ID_QUICK_SETTINGS); - am.registerSystemAction(actionPowerDialog, SYSTEM_ACTION_ID_POWER_DIALOG); - am.registerSystemAction(actionToggleSplitScreen, SYSTEM_ACTION_ID_TOGGLE_SPLIT_SCREEN); - am.registerSystemAction(actionLockScreen, SYSTEM_ACTION_ID_LOCK_SCREEN); - am.registerSystemAction(actionTakeScreenshot, SYSTEM_ACTION_ID_TAKE_SCREENSHOT); - am.registerSystemAction(actionAccessibilityMenu, SYSTEM_ACTION_ID_ACCESSIBILITY_MENU); + /** + * Unregister a system action. + * @param actionId the action ID to unregister. + */ + public void unregister(int actionId) { + mA11yManager.unregisterSystemAction(actionId); } private void handleBack() { @@ -264,10 +318,6 @@ public class SystemActions extends SystemUI { } } - private void handleToggleSplitScreen() { - mStatusBar.toggleSplitScreen(); - } - private void handleLockScreen() { IWindowManager windowManager = WindowManagerGlobal.getWindowManagerService(); @@ -282,15 +332,23 @@ public class SystemActions extends SystemUI { private void handleTakeScreenshot() { ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext); - screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, - true, true, new Handler(Looper.getMainLooper()), null); + screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, true, true, + SCREENSHOT_GLOBAL_ACTIONS, new Handler(Looper.getMainLooper()), null); } - private void handleAccessibilityMenu() { + private void handleAccessibilityButton() { AccessibilityManager.getInstance(mContext).notifyAccessibilityButtonClicked( Display.DEFAULT_DISPLAY); } + private void handleAccessibilityButtonChooser() { + final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + final String chooserClassName = AccessibilityButtonChooserActivity.class.getName(); + intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName); + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + } + private class SystemActionsBroadcastReceiver extends BroadcastReceiver { private static final String INTENT_ACTION_BACK = "SYSTEM_ACTION_BACK"; private static final String INTENT_ACTION_HOME = "SYSTEM_ACTION_HOME"; @@ -298,12 +356,12 @@ public class SystemActions extends SystemUI { private static final String INTENT_ACTION_NOTIFICATIONS = "SYSTEM_ACTION_NOTIFICATIONS"; private static final String INTENT_ACTION_QUICK_SETTINGS = "SYSTEM_ACTION_QUICK_SETTINGS"; private static final String INTENT_ACTION_POWER_DIALOG = "SYSTEM_ACTION_POWER_DIALOG"; - private static final String INTENT_ACTION_TOGGLE_SPLIT_SCREEN = - "SYSTEM_ACTION_TOGGLE_SPLIT_SCREEN"; private static final String INTENT_ACTION_LOCK_SCREEN = "SYSTEM_ACTION_LOCK_SCREEN"; private static final String INTENT_ACTION_TAKE_SCREENSHOT = "SYSTEM_ACTION_TAKE_SCREENSHOT"; - private static final String INTENT_ACTION_ACCESSIBILITY_MENU = - "SYSTEM_ACTION_ACCESSIBILITY_MENU"; + private static final String INTENT_ACTION_ACCESSIBILITY_BUTTON = + "SYSTEM_ACTION_ACCESSIBILITY_BUTTON"; + private static final String INTENT_ACTION_ACCESSIBILITY_BUTTON_CHOOSER = + "SYSTEM_ACTION_ACCESSIBILITY_BUTTON_MENU"; private PendingIntent createPendingIntent(Context context, String intentAction) { switch (intentAction) { @@ -313,10 +371,10 @@ public class SystemActions extends SystemUI { case INTENT_ACTION_NOTIFICATIONS: case INTENT_ACTION_QUICK_SETTINGS: case INTENT_ACTION_POWER_DIALOG: - case INTENT_ACTION_TOGGLE_SPLIT_SCREEN: case INTENT_ACTION_LOCK_SCREEN: case INTENT_ACTION_TAKE_SCREENSHOT: - case INTENT_ACTION_ACCESSIBILITY_MENU: { + case INTENT_ACTION_ACCESSIBILITY_BUTTON: + case INTENT_ACTION_ACCESSIBILITY_BUTTON_CHOOSER: { Intent intent = new Intent(intentAction); return PendingIntent.getBroadcast(context, 0, intent, 0); } @@ -334,10 +392,10 @@ public class SystemActions extends SystemUI { intentFilter.addAction(INTENT_ACTION_NOTIFICATIONS); intentFilter.addAction(INTENT_ACTION_QUICK_SETTINGS); intentFilter.addAction(INTENT_ACTION_POWER_DIALOG); - intentFilter.addAction(INTENT_ACTION_TOGGLE_SPLIT_SCREEN); intentFilter.addAction(INTENT_ACTION_LOCK_SCREEN); intentFilter.addAction(INTENT_ACTION_TAKE_SCREENSHOT); - intentFilter.addAction(INTENT_ACTION_ACCESSIBILITY_MENU); + intentFilter.addAction(INTENT_ACTION_ACCESSIBILITY_BUTTON); + intentFilter.addAction(INTENT_ACTION_ACCESSIBILITY_BUTTON_CHOOSER); return intentFilter; } @@ -369,10 +427,6 @@ public class SystemActions extends SystemUI { handlePowerDialog(); break; } - case INTENT_ACTION_TOGGLE_SPLIT_SCREEN: { - handleToggleSplitScreen(); - break; - } case INTENT_ACTION_LOCK_SCREEN: { handleLockScreen(); break; @@ -381,8 +435,12 @@ public class SystemActions extends SystemUI { handleTakeScreenshot(); break; } - case INTENT_ACTION_ACCESSIBILITY_MENU: { - handleAccessibilityMenu(); + case INTENT_ACTION_ACCESSIBILITY_BUTTON: { + handleAccessibilityButton(); + break; + } + case INTENT_ACTION_ACCESSIBILITY_BUTTON_CHOOSER: { + handleAccessibilityButtonChooser(); break; } default: diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java index 251229f42da3..33e6ca49ddd5 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java @@ -84,8 +84,8 @@ public class DisplayUtils { public static int getCornerRadiusBottom(Context context) { int radius = 0; - int resourceId = context.getResources().getIdentifier("rounded_corner_radius_bottom", - "dimen", "android"); + int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_bottom", + "dimen", "com.android.systemui"); if (resourceId > 0) { radius = context.getResources().getDimensionPixelSize(resourceId); } @@ -103,8 +103,8 @@ public class DisplayUtils { public static int getCornerRadiusTop(Context context) { int radius = 0; - int resourceId = context.getResources().getIdentifier("rounded_corner_radius_top", - "dimen", "android"); + int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_top", + "dimen", "com.android.systemui"); if (resourceId > 0) { radius = context.getResources().getDimensionPixelSize(resourceId); } @@ -118,8 +118,8 @@ public class DisplayUtils { private static int getCornerRadiusDefault(Context context) { int radius = 0; - int resourceId = context.getResources().getIdentifier("rounded_corner_radius", "dimen", - "android"); + int resourceId = context.getResources().getIdentifier("config_rounded_mask_size", + "dimen", "com.android.systemui"); if (resourceId > 0) { radius = context.getResources().getDimensionPixelSize(resourceId); } diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/PathSpecCornerPathRenderer.java b/packages/SystemUI/src/com/android/systemui/assist/ui/PathSpecCornerPathRenderer.java index 2bad7fc9583a..523378e97c94 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/ui/PathSpecCornerPathRenderer.java +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/PathSpecCornerPathRenderer.java @@ -45,8 +45,8 @@ public final class PathSpecCornerPathRenderer extends CornerPathRenderer { mWidth = DisplayUtils.getWidth(context); mHeight = DisplayUtils.getHeight(context); - mBottomCornerRadius = DisplayUtils.getCornerRadiusBottom(context); - mTopCornerRadius = DisplayUtils.getCornerRadiusTop(context); + mBottomCornerRadius = DisplayUtils.getCornerRadiusBottom(context); + mTopCornerRadius = DisplayUtils.getCornerRadiusTop(context); String pathData = context.getResources().getString(R.string.config_rounded_mask); Path path = PathParser.createPathFromPathData(pathData); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java index d8a11d36a335..95bbea15a88c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java @@ -17,12 +17,13 @@ package com.android.systemui.biometrics; import android.content.Context; +import android.os.UserHandle; import android.text.InputType; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; +import android.widget.ImeAwareEditText; import android.widget.TextView; import com.android.internal.widget.LockPatternChecker; @@ -38,7 +39,7 @@ public class AuthCredentialPasswordView extends AuthCredentialView private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView"; private final InputMethodManager mImm; - private EditText mPasswordField; + private ImeAwareEditText mPasswordField; public AuthCredentialPasswordView(Context context, AttributeSet attrs) { @@ -68,16 +69,14 @@ public class AuthCredentialPasswordView extends AuthCredentialView protected void onAttachedToWindow() { super.onAttachedToWindow(); + mPasswordField.setTextOperationUser(UserHandle.of(mUserId)); if (mCredentialType == Utils.CREDENTIAL_PIN) { mPasswordField.setInputType( InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); } - // Wait a bit to focus the field so the focusable flag on the window is already set then. - postDelayed(() -> { - mPasswordField.requestFocus(); - mImm.showSoftInput(mPasswordField, InputMethodManager.SHOW_IMPLICIT); - }, 100); + mPasswordField.requestFocus(); + mPasswordField.scheduleShowSoftInput(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index 8bf259182544..084b791a8dcd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -220,15 +220,18 @@ public abstract class AuthCredentialView extends LinearLayout { setTextOrHide(mDescriptionView, getDescription(mBiometricPromptBundle)); announceForAccessibility(title); - final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId); - final Drawable image; - if (isManagedProfile) { - image = getResources().getDrawable(R.drawable.auth_dialog_enterprise, - mContext.getTheme()); - } else { - image = getResources().getDrawable(R.drawable.auth_dialog_lock, mContext.getTheme()); + if (mIconView != null) { + final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId); + final Drawable image; + if (isManagedProfile) { + image = getResources().getDrawable(R.drawable.auth_dialog_enterprise, + mContext.getTheme()); + } else { + image = getResources().getDrawable(R.drawable.auth_dialog_lock, + mContext.getTheme()); + } + mIconView.setImageDrawable(image); } - mIconView.setImageDrawable(image); // Only animate this if we're transitioning from a biometric view. if (mShouldAnimateContents) { @@ -286,6 +289,7 @@ public abstract class AuthCredentialView extends LinearLayout { if (matched) { mClearErrorRunnable.run(); + mLockPatternUtils.userPresent(mEffectiveUserId); mCallback.onCredentialMatched(attestation); } else { if (timeoutMs > 0) { diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index 74b94e76dfc1..4269605cef12 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -25,7 +25,6 @@ import android.os.Looper import android.os.Message import android.os.UserHandle import android.text.TextUtils -import android.util.Log import android.util.SparseArray import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable @@ -34,6 +33,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import java.io.FileDescriptor import java.io.PrintWriter +import java.lang.IllegalStateException import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Singleton @@ -60,7 +60,7 @@ private const val DEBUG = true * * Use only for IntentFilters with actions and optionally categories. It does not support, * permissions, schemes, data types, data authorities or priority different than 0. - * Cannot be used for getting sticky broadcasts. + * Cannot be used for getting sticky broadcasts (either as return of registering or as re-delivery). */ @Singleton open class BroadcastDispatcher @Inject constructor ( @@ -189,8 +189,8 @@ open class BroadcastDispatcher @Inject constructor ( data.user.identifier } if (userId < UserHandle.USER_ALL) { - if (DEBUG) Log.w(TAG, "Register receiver for invalid user: $userId") - return + throw IllegalStateException( + "Attempting to register receiver for invalid user {$userId}") } val uBR = receiversByUser.get(userId, createUBRForUser(userId)) receiversByUser.put(userId, uBR) diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 71f2bc09b983..0dbee663c1c9 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -78,6 +78,7 @@ class Bubble implements BubbleViewProvider { private BubbleViewInfoTask mInflationTask; private boolean mInflateSynchronously; + private boolean mPendingIntentCanceled; /** * Presentational info about the flyout. @@ -90,6 +91,7 @@ class Bubble implements BubbleViewProvider { } private FlyoutMessage mFlyoutMessage; + private Drawable mBadgedAppIcon; private Bitmap mBadgedImage; private int mDotColor; private Path mDotPath; @@ -133,6 +135,10 @@ class Bubble implements BubbleViewProvider { return mBadgedImage; } + public Drawable getBadgedAppIcon() { + return mBadgedAppIcon; + } + @Override public int getDotColor() { return mDotColor; @@ -177,6 +183,14 @@ class Bubble implements BubbleViewProvider { mIconView = null; } + void setPendingIntentCanceled() { + mPendingIntentCanceled = true; + } + + boolean getPendingIntentCanceled() { + return mPendingIntentCanceled; + } + /** * Sets whether to perform inflation on the same thread as the caller. This method should only * be used in tests, not in production. @@ -239,6 +253,7 @@ class Bubble implements BubbleViewProvider { mAppName = info.appName; mFlyoutMessage = info.flyoutMessage; + mBadgedAppIcon = info.badgedAppIcon; mBadgedImage = info.badgedBubbleImage; mDotColor = info.dotColor; mDotPath = info.dotPath; @@ -289,6 +304,13 @@ class Bubble implements BubbleViewProvider { } /** + * @return if the bubble was ever expanded + */ + boolean getWasAccessed() { + return mLastAccessed != 0L; + } + + /** * @return the display id of the virtual display on which bubble contents is drawn. */ @Override diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 669a86b8a742..013f22203fbc 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -17,6 +17,8 @@ package com.android.systemui.bubbles; import static android.app.Notification.FLAG_BUBBLE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; @@ -30,7 +32,6 @@ import static android.view.View.VISIBLE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_EXPERIMENTS; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -43,6 +44,9 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.UserIdInt; import android.app.ActivityManager.RunningTaskInfo; +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; @@ -50,6 +54,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.service.notification.NotificationListenerService; @@ -83,6 +88,7 @@ import com.android.systemui.shared.system.WindowManagerWrapper; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationRemoveInterceptor; +import com.android.systemui.statusbar.notification.NotificationChannelHelper; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotifCollection; @@ -103,7 +109,6 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; /** @@ -119,7 +124,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Retention(SOURCE) @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, - DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT}) + DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, + DISMISS_OVERFLOW_MAX_REACHED}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @@ -133,6 +139,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi static final int DISMISS_USER_CHANGED = 8; static final int DISMISS_GROUP_CANCELLED = 9; static final int DISMISS_INVALID_INTENT = 10; + static final int DISMISS_OVERFLOW_MAX_REACHED = 11; private final Context mContext; private final NotificationEntryManager mNotificationEntryManager; @@ -157,26 +164,22 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // Used when ranking updates occur and we check if things should bubble / unbubble private NotificationListenerService.Ranking mTmpRanking; - // Saves notification keys of user created "fake" bubbles so that we can allow notifications - // like these to bubble by default. Doesn't persist across reboots, not a long-term solution. - private final HashSet<String> mUserCreatedBubbles; - // If we're auto-bubbling bubbles via a whitelist, we need to track which notifs from that app - // have been "demoted" back to a notification so that we don't auto-bubbles those again. - // Doesn't persist across reboots, not a long-term solution. - private final HashSet<String> mUserBlockedBubbles; - // Bubbles get added to the status bar view private final NotificationShadeWindowController mNotificationShadeWindowController; private final ZenModeController mZenModeController; private StatusBarStateListener mStatusBarStateListener; + private INotificationManager mINotificationManager; // Callback that updates BubbleOverflowActivity on data change. - @Nullable private Runnable mOverflowCallback = null; + @Nullable private BubbleData.Listener mOverflowListener = null; private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private IStatusBarService mBarService; private SysUiState mSysUiState; + // Used to post to main UI thread + private Handler mHandler = new Handler(); + // Used for determining view rect for touch interaction private Rect mTempRect = new Rect(); @@ -293,11 +296,13 @@ public class BubbleController implements ConfigurationController.ConfigurationLi FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState) { + SysUiState sysUiState, + INotificationManager notificationManager) { this(context, notificationShadeWindowController, statusBarStateController, shadeController, data, null /* synchronizer */, configurationController, interruptionStateProvider, zenModeController, notifUserManager, groupManager, entryManager, - notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState); + notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState, + notificationManager); } /** @@ -319,7 +324,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState) { + SysUiState sysUiState, + INotificationManager notificationManager) { dumpManager.registerDumpable(TAG, this); mContext = context; mShadeController = shadeController; @@ -327,6 +333,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mNotifUserManager = notifUserManager; mZenModeController = zenModeController; mFloatingContentCoordinator = floatingContentCoordinator; + mINotificationManager = notificationManager; mZenModeController.addCallback(new ZenModeController.Callback() { @Override public void onZenChanged(int zen) { @@ -402,9 +409,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } }); - mUserCreatedBubbles = new HashSet<>(); - mUserBlockedBubbles = new HashSet<>(); - mBubbleIconFactory = new BubbleIconFactory(context); } @@ -464,11 +468,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi (entry != null && entry.isRowDismissed() && !isAppCancel) || isClearAll || isUserDimiss || isSummaryCancel; - if (userRemovedNotif || isUserCreatedBubble(key) - || isSummaryOfUserCreatedBubble(entry)) { + if (userRemovedNotif) { return handleDismissalInterception(entry); } - return false; } }); @@ -579,8 +581,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mInflateSynchronously = inflateSynchronously; } - void setOverflowCallback(Runnable updateOverflow) { - mOverflowCallback = updateOverflow; + void setOverflowListener(BubbleData.Listener listener) { + mOverflowListener = listener; } /** @@ -608,6 +610,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); } + + mStackView.setUnbubbleConversationCallback(notificationEntry -> + onUserChangedBubble(notificationEntry, false /* shouldBubble */)); } } @@ -664,8 +669,11 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mStackView.onThemeChanged(); } mBubbleIconFactory = new BubbleIconFactory(mContext); + // Reload each bubble for (Bubble b: mBubbleData.getBubbles()) { - // Reload each bubble + b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory); + } + for (Bubble b: mBubbleData.getOverflowBubbles()) { b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory); } } @@ -736,18 +744,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi */ public boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry) { String key = entry.getKey(); - boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key) - && !mBubbleData.getBubbleWithKey(key).showInShade(); + boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key) + && !mBubbleData.getAnyBubbleWithkey(key).showInShade()); String groupKey = entry.getSbn().getGroupKey(); boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey); boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey)); - - return (isSummary && isSuppressedSummary) || isBubbleAndSuppressed; + return (isSummary && isSuppressedSummary) || isSuppressedBubble; } void promoteBubbleFromOverflow(Bubble bubble) { bubble.setInflateSynchronously(mInflateSynchronously); + setIsBubble(bubble, /* isBubble */ true); mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory); } @@ -757,11 +765,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi * @param notificationKey the notification key for the bubble to be selected */ public void expandStackAndSelectBubble(String notificationKey) { - Bubble bubble = mBubbleData.getBubbleWithKey(notificationKey); - if (bubble != null) { + Bubble bubble = mBubbleData.getBubbleInStackWithKey(notificationKey); + if (bubble == null) { + bubble = mBubbleData.getOverflowBubbleWithKey(notificationKey); + if (bubble != null) { + mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory); + } + } else if (bubble.getEntry().isBubble()){ mBubbleData.setSelectedBubble(bubble); - mBubbleData.setExpanded(true); } + mBubbleData.setExpanded(true); } /** @@ -799,7 +812,21 @@ public class BubbleController implements ConfigurationController.ConfigurationLi Bubble bubble = mBubbleData.getOrCreateBubble(notif); bubble.setInflateSynchronously(mInflateSynchronously); bubble.inflate( - b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), + b -> { + mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade); + if (bubble.getBubbleIntent() == null) { + return; + } + bubble.getBubbleIntent().registerCancelListener(pendingIntent -> { + if (bubble.getWasAccessed()) { + bubble.setPendingIntentCanceled(); + return; + } + mHandler.post( + () -> removeBubble(bubble.getEntry(), + BubbleController.DISMISS_INVALID_INTENT)); + }); + }, mContext, mStackView, mBubbleIconFactory); } @@ -809,59 +836,44 @@ public class BubbleController implements ConfigurationController.ConfigurationLi * This method will collapse the shade, create the bubble without a flyout or dot, and suppress * the notification from appearing in the shade. * - * @param entry the notification to show as a bubble. + * @param entry the notification to change bubble state for. + * @param shouldBubble whether the notification should show as a bubble or not. */ - public void onUserCreatedBubbleFromNotification(NotificationEntry entry) { - if (DEBUG_EXPERIMENTS || DEBUG_BUBBLE_CONTROLLER) { - Log.d(TAG, "onUserCreatedBubble: " + entry.getKey()); + public void onUserChangedBubble(NotificationEntry entry, boolean shouldBubble) { + NotificationChannel channel = entry.getChannel(); + final String appPkg = entry.getSbn().getPackageName(); + final int appUid = entry.getSbn().getUid(); + if (channel == null || appPkg == null) { + return; } - mShadeController.collapsePanel(true); - entry.setFlagBubble(true); - updateBubble(entry, true /* suppressFlyout */, false /* showInShade */); - mUserCreatedBubbles.add(entry.getKey()); - mUserBlockedBubbles.remove(entry.getKey()); - } - /** - * Called when a user has indicated that an active notification appearing as a bubble should - * no longer be shown as a bubble. - * - * @param entry the notification to no longer show as a bubble. - */ - public void onUserDemotedBubbleFromNotification(NotificationEntry entry) { - if (DEBUG_EXPERIMENTS || DEBUG_BUBBLE_CONTROLLER) { - Log.d(TAG, "onUserDemotedBubble: " + entry.getKey()); - } - entry.setFlagBubble(false); - removeBubble(entry, DISMISS_BLOCKED); - mUserCreatedBubbles.remove(entry.getKey()); - if (BubbleExperimentConfig.isPackageWhitelistedToAutoBubble( - mContext, entry.getSbn().getPackageName())) { - // This package is whitelist but user demoted the bubble, let's save it so we don't - // auto-bubble for the whitelist again. - mUserBlockedBubbles.add(entry.getKey()); + // Update the state in NotificationManagerService + try { + int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); + } catch (RemoteException e) { } - } - /** - * Whether this bubble was explicitly created by the user via a SysUI affordance. - */ - boolean isUserCreatedBubble(String key) { - return mUserCreatedBubbles.contains(key); - } + // Change the settings + channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, + mINotificationManager, entry, channel); + channel.setAllowBubbles(shouldBubble); + try { + int currentPref = mINotificationManager.getBubblePreferenceForPackage(appPkg, appUid); + if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { + mINotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); + } + mINotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); + } catch (RemoteException e) { + Log.e(TAG, e.getMessage()); + } - boolean isSummaryOfUserCreatedBubble(NotificationEntry entry) { - if (isSummaryOfBubbles(entry)) { - List<Bubble> bubbleChildren = - mBubbleData.getBubblesInGroup(entry.getSbn().getGroupKey()); - for (int i = 0; i < bubbleChildren.size(); i++) { - // Check if any are user-created (i.e. experimental bubbles) - if (isUserCreatedBubble(bubbleChildren.get(i).getKey())) { - return true; - } + if (shouldBubble) { + mShadeController.collapsePanel(true); + if (entry.getRow() != null) { + entry.getRow().updateBubbleButton(); } } - return false; } /** @@ -871,43 +883,25 @@ public class BubbleController implements ConfigurationController.ConfigurationLi */ @MainThread void removeBubble(NotificationEntry entry, int reason) { - if (mBubbleData.hasBubbleWithKey(entry.getKey())) { + if (mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { mBubbleData.notificationEntryRemoved(entry, reason); } } private void onEntryAdded(NotificationEntry entry) { - boolean previouslyUserCreated = mUserCreatedBubbles.contains(entry.getKey()); - boolean userBlocked = mUserBlockedBubbles.contains(entry.getKey()); - boolean wasAdjusted = BubbleExperimentConfig.adjustForExperiments( - mContext, entry, previouslyUserCreated, userBlocked); - if (mNotificationInterruptStateProvider.shouldBubbleUp(entry) - && (canLaunchInActivityView(mContext, entry) || wasAdjusted)) { - if (wasAdjusted && !previouslyUserCreated) { - // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated - mUserCreatedBubbles.add(entry.getKey()); - } + && canLaunchInActivityView(mContext, entry)) { updateBubble(entry); } } private void onEntryUpdated(NotificationEntry entry) { - boolean previouslyUserCreated = mUserCreatedBubbles.contains(entry.getKey()); - boolean userBlocked = mUserBlockedBubbles.contains(entry.getKey()); - boolean wasAdjusted = BubbleExperimentConfig.adjustForExperiments( - mContext, entry, previouslyUserCreated, userBlocked); - boolean shouldBubble = mNotificationInterruptStateProvider.shouldBubbleUp(entry) - && (canLaunchInActivityView(mContext, entry) || wasAdjusted); - if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.getKey())) { + && canLaunchInActivityView(mContext, entry); + if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { // It was previously a bubble but no longer a bubble -- lets remove it removeBubble(entry, DISMISS_NO_LONGER_BUBBLE); } else if (shouldBubble) { - if (wasAdjusted && !previouslyUserCreated) { - // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated - mUserCreatedBubbles.add(entry.getKey()); - } updateBubble(entry); } } @@ -943,7 +937,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi String key = orderedKeys[i]; NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(key); rankingMap.getRanking(key, mTmpRanking); - boolean isActiveBubble = mBubbleData.hasBubbleWithKey(key); + boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); if (isActiveBubble && !mTmpRanking.canBubble()) { mBubbleData.notificationEntryRemoved(entry, BubbleController.DISMISS_BLOCKED); } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { @@ -953,14 +947,27 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } + private void setIsBubble(Bubble b, boolean isBubble) { + if (isBubble) { + b.getEntry().getSbn().getNotification().flags |= FLAG_BUBBLE; + } else { + b.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE; + } + try { + mBarService.onNotificationBubbleChanged(b.getKey(), isBubble, 0); + } catch (RemoteException e) { + // Bad things have happened + } + } + @SuppressWarnings("FieldCanBeLocal") private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { @Override public void applyUpdate(BubbleData.Update update) { // Update bubbles in overflow. - if (mOverflowCallback != null) { - mOverflowCallback.run(); + if (mOverflowListener != null) { + mOverflowListener.applyUpdate(update); } // Collapsing? Do this first before remaining steps. @@ -975,35 +982,37 @@ public class BubbleController implements ConfigurationController.ConfigurationLi final Bubble bubble = removed.first; @DismissReason final int reason = removed.second; mStackView.removeBubble(bubble); + // If the bubble is removed for user switching, leave the notification in place. - if (reason != DISMISS_USER_CHANGED) { - if (!mBubbleData.hasBubbleWithKey(bubble.getKey()) - && !bubble.showInShade()) { + if (reason == DISMISS_USER_CHANGED) { + continue; + } + if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey()) + && (!bubble.showInShade() + || reason == DISMISS_NOTIF_CANCEL + || reason == DISMISS_GROUP_CANCELLED + || reason == DISMISS_OVERFLOW_MAX_REACHED)) { // The bubble is now gone & the notification is hidden from the shade, so // time to actually remove it for (NotifCallback cb : mCallbacks) { cb.removeNotification(bubble.getEntry(), REASON_CANCEL); } } else { - // Update the flag for SysUI - bubble.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE; - - // Make sure NoMan knows it's not a bubble anymore so anyone querying it - // will get right result back - try { - mBarService.onNotificationBubbleChanged(bubble.getKey(), - false /* isBubble */); - } catch (RemoteException e) { - // Bad things have happened + if (bubble.getEntry().isBubble() && bubble.showInShade()) { + setIsBubble(bubble, /* isBubble */ false); + } + if (bubble.getEntry().getRow() != null) { + bubble.getEntry().getRow().updateBubbleButton(); } } - final String groupKey = bubble.getEntry().getSbn().getGroupKey(); - if (mBubbleData.getBubblesInGroup(groupKey).isEmpty()) { - // Time to potentially remove the summary - for (NotifCallback cb : mCallbacks) { - cb.maybeCancelSummary(bubble.getEntry()); - } + } + final String groupKey = bubble.getEntry().getSbn().getGroupKey(); + if (mBubbleData.getBubblesInGroup(groupKey).isEmpty()) { + // Time to potentially remove the summary + for (NotifCallback cb : mCallbacks) { + cb.maybeCancelSummary(bubble.getEntry()); } } } @@ -1050,9 +1059,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi Log.d(TAG, BubbleDebugConfig.formatBubblesString(mStackView.getBubblesOnScreen(), mStackView.getExpandedBubble())); } - Log.d(TAG, "\n[BubbleData] overflow:"); - Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getOverflowBubbles(), - null)); } } }; @@ -1071,21 +1077,19 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (entry == null) { return false; } - - final boolean interceptBubbleDismissal = mBubbleData.hasBubbleWithKey(entry.getKey()) - && entry.isBubble(); - final boolean interceptSummaryDismissal = isSummaryOfBubbles(entry); - - if (interceptSummaryDismissal) { + if (isSummaryOfBubbles(entry)) { handleSummaryDismissalInterception(entry); - } else if (interceptBubbleDismissal) { - Bubble bubble = mBubbleData.getBubbleWithKey(entry.getKey()); + } else { + Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey()); + if (bubble == null || !entry.isBubble()) { + bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey()); + } + if (bubble == null) { + return false; + } bubble.setSuppressNotification(true); bubble.setShowDot(false /* show */); - } else { - return false; } - // Update the shade for (NotifCallback cb : mCallbacks) { cb.invalidateNotifications("BubbleController.handleDismissalInterception"); @@ -1110,15 +1114,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi private void handleSummaryDismissalInterception(NotificationEntry summary) { // current children in the row: - final List<NotificationEntry> children = summary.getChildren(); + final List<NotificationEntry> children = summary.getAttachedNotifChildren(); if (children != null) { for (int i = 0; i < children.size(); i++) { NotificationEntry child = children.get(i); - if (mBubbleData.hasBubbleWithKey(child.getKey())) { + if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) { // Suppress the bubbled child // As far as group manager is concerned, once a child is no longer shown // in the shade, it is essentially removed. - Bubble bubbleChild = mBubbleData.getBubbleWithKey(child.getKey()); + Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey()); mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry()); bubbleChild.setSuppressNotification(true); bubbleChild.setShowDot(false /* show */); @@ -1237,7 +1241,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, - boolean clearedTask) { + boolean clearedTask, boolean wasVisible) { for (Bubble b : mBubbleData.getBubbles()) { if (b.getDisplayId() == task.displayId) { expandStackAndSelectBubble(b.getKey()); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index 4c149ddd3939..f2b1c031dcd5 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -15,6 +15,7 @@ */ package com.android.systemui.bubbles; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; @@ -73,6 +74,8 @@ public class BubbleData { @Nullable Bubble selectedBubble; @Nullable Bubble addedBubble; @Nullable Bubble updatedBubble; + @Nullable Bubble addedOverflowBubble; + @Nullable Bubble removedOverflowBubble; // Pair with Bubble and @DismissReason Integer final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); @@ -91,10 +94,12 @@ public class BubbleData { || addedBubble != null || updatedBubble != null || !removedBubbles.isEmpty() + || addedOverflowBubble != null + || removedOverflowBubble != null || orderChanged; } - void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { + void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { removedBubbles.add(new Pair<>(bubbleToRemove, reason)); } } @@ -120,9 +125,10 @@ public class BubbleData { /** Bubbles that are being loaded but haven't been added to the stack just yet. */ private final List<Bubble> mPendingBubbles; private Bubble mSelectedBubble; + private boolean mShowingOverflow; private boolean mExpanded; private final int mMaxBubbles; - private final int mMaxOverflowBubbles; + private int mMaxOverflowBubbles; // State tracked during an operation -- keeps track of what listener events to dispatch. private Update mStateChange; @@ -174,8 +180,16 @@ public class BubbleData { return mExpanded; } - public boolean hasBubbleWithKey(String key) { - return getBubbleWithKey(key) != null; + public boolean hasAnyBubbleWithKey(String key) { + return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key); + } + + public boolean hasBubbleInStackWithKey(String key) { + return getBubbleInStackWithKey(key) != null; + } + + public boolean hasOverflowBubbleWithKey(String key) { + return getOverflowBubbleWithKey(key) != null; } @Nullable @@ -205,6 +219,8 @@ public class BubbleData { Log.d(TAG, "promoteBubbleFromOverflow: " + bubble); } moveOverflowBubbleToPending(bubble); + // Preserve new order for next repack, which sorts by last updated time. + bubble.markUpdatedAt(mTimeSource.currentTimeMillis()); bubble.inflate( b -> { notificationEntryUpdated(bubble, /* suppressFlyout */ @@ -215,10 +231,13 @@ public class BubbleData { dispatchPendingChanges(); } + void setShowingOverflow(boolean showingOverflow) { + mShowingOverflow = showingOverflow; + } + private void moveOverflowBubbleToPending(Bubble b) { - // Preserve new order for next repack, which sorts by last updated time. - b.markUpdatedAt(mTimeSource.currentTimeMillis()); mOverflowBubbles.remove(b); + mStateChange.removedOverflowBubble = b; mPendingBubbles.add(b); } @@ -228,15 +247,16 @@ public class BubbleData { * for that. */ Bubble getOrCreateBubble(NotificationEntry entry) { - Bubble bubble = getBubbleWithKey(entry.getKey()); - if (bubble == null) { - for (int i = 0; i < mOverflowBubbles.size(); i++) { - Bubble b = mOverflowBubbles.get(i); - if (b.getKey().equals(entry.getKey())) { - moveOverflowBubbleToPending(b); - b.setEntry(entry); - return b; - } + String key = entry.getKey(); + Bubble bubble = getBubbleInStackWithKey(entry.getKey()); + if (bubble != null) { + bubble.setEntry(entry); + } else { + bubble = getOverflowBubbleWithKey(key); + if (bubble != null) { + moveOverflowBubbleToPending(bubble); + bubble.setEntry(entry); + return bubble; } // Check for it in pending for (int i = 0; i < mPendingBubbles.size(); i++) { @@ -248,8 +268,6 @@ public class BubbleData { } bubble = new Bubble(entry, mSuppressionListener); mPendingBubbles.add(bubble); - } else { - bubble.setEntry(entry); } return bubble; } @@ -264,7 +282,7 @@ public class BubbleData { Log.d(TAG, "notificationEntryUpdated: " + bubble); } mPendingBubbles.remove(bubble); // No longer pending once we're here - Bubble prevBubble = getBubbleWithKey(bubble.getKey()); + Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); suppressFlyout |= !bubble.getEntry().getRanking().visuallyInterruptive(); if (prevBubble == null) { @@ -417,6 +435,20 @@ public class BubbleData { } int indexToRemove = indexForKey(key); if (indexToRemove == -1) { + if (hasOverflowBubbleWithKey(key) + && (reason == BubbleController.DISMISS_NOTIF_CANCEL + || reason == BubbleController.DISMISS_GROUP_CANCELLED + || reason == BubbleController.DISMISS_NO_LONGER_BUBBLE + || reason == BubbleController.DISMISS_BLOCKED)) { + + Bubble b = getOverflowBubbleWithKey(key); + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "Cancel overflow bubble: " + b); + } + mOverflowBubbles.remove(b); + mStateChange.bubbleRemoved(b, reason); + mStateChange.removedOverflowBubble = b; + } return; } Bubble bubbleToRemove = mBubbles.get(indexToRemove); @@ -448,21 +480,26 @@ public class BubbleData { } void overflowBubble(@DismissReason int reason, Bubble bubble) { - if (reason == BubbleController.DISMISS_AGED - || reason == BubbleController.DISMISS_USER_GESTURE) { + if (bubble.getPendingIntentCanceled() + || !(reason == BubbleController.DISMISS_AGED + || reason == BubbleController.DISMISS_USER_GESTURE)) { + return; + } + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "Overflowing: " + bubble); + } + mOverflowBubbles.add(0, bubble); + mStateChange.addedOverflowBubble = bubble; + bubble.stopInflation(); + if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { + // Remove oldest bubble. + Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1); if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "Overflowing: " + bubble); - } - mOverflowBubbles.add(0, bubble); - bubble.stopInflation(); - if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { - // Remove oldest bubble. - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "Overflow full. Remove: " + mOverflowBubbles.get( - mOverflowBubbles.size() - 1)); - } - mOverflowBubbles.remove(mOverflowBubbles.size() - 1); + Log.d(TAG, "Overflow full. Remove: " + oldest); } + mOverflowBubbles.remove(oldest); + mStateChange.removedOverflowBubble = oldest; + mStateChange.bubbleRemoved(oldest, BubbleController.DISMISS_OVERFLOW_MAX_REACHED); } } @@ -513,9 +550,11 @@ public class BubbleData { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setSelectedBubbleInternal: " + bubble); } - if (Objects.equals(bubble, mSelectedBubble)) { + if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) { return; } + // Otherwise, if we are showing the overflow menu, return to the previously selected bubble. + if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) { Log.e(TAG, "Cannot select bubble which doesn't exist!" + " (" + bubble + ") bubbles=" + mBubbles); @@ -559,6 +598,10 @@ public class BubbleData { mStateChange.orderChanged |= repackAll(); // Save the state which should be returned to when expanded (with no other changes) + if (mShowingOverflow) { + // Show previously selected bubble instead of overflow menu on next expansion. + setSelectedBubbleInternal(mSelectedBubble); + } if (mBubbles.indexOf(mSelectedBubble) > 0) { // Move the selected bubble to the top while collapsed. if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) { @@ -739,7 +782,7 @@ public class BubbleData { /** * The set of bubbles in row. */ - @VisibleForTesting(visibility = PRIVATE) + @VisibleForTesting(visibility = PACKAGE) public List<Bubble> getBubbles() { return Collections.unmodifiableList(mBubbles); } @@ -753,7 +796,17 @@ public class BubbleData { @VisibleForTesting(visibility = PRIVATE) @Nullable - Bubble getBubbleWithKey(String key) { + Bubble getAnyBubbleWithkey(String key) { + Bubble b = getBubbleInStackWithKey(key); + if (b == null) { + b = getOverflowBubbleWithKey(key); + } + return b; + } + + @VisibleForTesting(visibility = PRIVATE) + @Nullable + Bubble getBubbleInStackWithKey(String key) { for (int i = 0; i < mBubbles.size(); i++) { Bubble bubble = mBubbles.get(i); if (bubble.getKey().equals(key)) { @@ -795,6 +848,15 @@ public class BubbleData { } /** + * Set maximum number of bubbles allowed in overflow. + * This method should only be used in tests, not in production. + */ + @VisibleForTesting + void setMaxOverflowBubbles(int maxOverflowBubbles) { + mMaxOverflowBubbles = maxOverflowBubbles; + } + + /** * Description of current bubble data state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 93fb6972fad5..bb2365559f74 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -43,7 +43,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; import android.os.RemoteException; -import android.service.notification.StatusBarNotification; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -55,14 +54,13 @@ import com.android.internal.policy.ScreenDecorationsUtils; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.recents.TriangleShape; -import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.AlphaOptimizedButton; import com.android.systemui.statusbar.notification.collection.NotificationEntry; /** * Container for the expanded bubble view, handles rendering the caret and settings icon. */ -public class BubbleExpandedView extends LinearLayout implements View.OnClickListener { +public class BubbleExpandedView extends LinearLayout { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; private enum ActivityViewStatus { @@ -100,9 +98,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList private int mPointerWidth; private int mPointerHeight; private ShapeDrawable mPointerDrawable; - private Rect mTempRect = new Rect(); - private int[] mTempLoc = new int[2]; - private int mExpandedViewTouchSlop; @Nullable private Bubble mBubble; @@ -193,7 +188,7 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList + " mActivityViewStatus=" + mActivityViewStatus + " bubble=" + getBubbleKey()); } - if (mBubble != null && !mBubbleController.isUserCreatedBubble(mBubble.getKey())) { + if (mBubble != null) { // Must post because this is called from a binder thread. post(() -> mBubbleController.removeBubble(mBubble.getEntry(), BubbleController.DISMISS_TASK_FINISHED)); @@ -224,7 +219,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); - mExpandedViewTouchSlop = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_slop); } @Override @@ -239,7 +233,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); - mPointerDrawable = new ShapeDrawable(TriangleShape.create( mPointerWidth, mPointerHeight, true /* pointUp */)); mPointerView.setBackground(mPointerDrawable); @@ -248,7 +241,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( R.dimen.bubble_manage_button_height); mSettingsIcon = findViewById(R.id.settings_button); - mSettingsIcon.setOnClickListener(this); mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */, true /* singleTaskInstance */); @@ -289,6 +281,19 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList return mBubble != null ? mBubble.getEntry() : null; } + void setManageClickListener(OnClickListener manageClickListener) { + findViewById(R.id.settings_button).setOnClickListener(manageClickListener); + } + + /** + * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which + * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful + * if a view has been added or removed from on top of the ActivityView, such as the manage menu. + */ + void updateObscuredTouchableRegion() { + mActivityView.onLocationChanged(); + } + void applyThemeAttrs() { final TypedArray ta = mContext.obtainStyledAttributes( new int[] { @@ -473,51 +478,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } /** - * Whether the provided x, y values (in raw coordinates) are in a touchable area of the - * expanded view. - * - * The touchable areas are the ActivityView (plus some slop around it) and the manage button. - */ - boolean intersectingTouchableContent(int rawX, int rawY) { - mTempRect.setEmpty(); - if (mActivityView != null) { - mTempLoc = mActivityView.getLocationOnScreen(); - mTempRect.set(mTempLoc[0] - mExpandedViewTouchSlop, - mTempLoc[1] - mExpandedViewTouchSlop, - mTempLoc[0] + mActivityView.getWidth() + mExpandedViewTouchSlop, - mTempLoc[1] + mActivityView.getHeight() + mExpandedViewTouchSlop); - } - if (mTempRect.contains(rawX, rawY)) { - return true; - } - mTempLoc = mSettingsIcon.getLocationOnScreen(); - mTempRect.set(mTempLoc[0], - mTempLoc[1], - mTempLoc[0] + mSettingsIcon.getWidth(), - mTempLoc[1] + mSettingsIcon.getHeight()); - if (mTempRect.contains(rawX, rawY)) { - return true; - } - return false; - } - - @Override - public void onClick(View view) { - if (mBubble == null) { - return; - } - int id = view.getId(); - if (id == R.id.settings_button) { - Intent intent = mBubble.getSettingsIntent(); - mStackView.collapseStack(() -> { - mContext.startActivityAsUser(intent, mBubble.getEntry().getSbn().getUser()); - logBubbleClickEvent(mBubble, - SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); - }); - } - } - - /** * Update appearance of the expanded view being displayed. */ public void updateView() { @@ -547,10 +507,8 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList * Position of the manage button displayed in the expanded view. Used for placing user * education about the manage button. */ - public Rect getManageButtonLocationOnScreen() { - mTempLoc = mSettingsIcon.getLocationOnScreen(); - return new Rect(mTempLoc[0], mTempLoc[1], mTempLoc[0] + mSettingsIcon.getWidth(), - mTempLoc[1] + mSettingsIcon.getHeight()); + public void getManageButtonBoundsOnScreen(Rect rect) { + mSettingsIcon.getBoundsOnScreen(rect); } /** @@ -611,26 +569,4 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } return INVALID_DISPLAY; } - - /** - * Logs bubble UI click event. - * - * @param bubble the bubble notification entry that user is interacting with. - * @param action the user interaction enum. - */ - private void logBubbleClickEvent(Bubble bubble, int action) { - StatusBarNotification notification = bubble.getEntry().getSbn(); - SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, - notification.getPackageName(), - notification.getNotification().getChannelId(), - notification.getId(), - mStackView.getBubbleIndex(mStackView.getExpandedBubble()), - mStackView.getBubbleCount(), - action, - mStackView.getNormalizedXPosition(), - mStackView.getNormalizedYPosition(), - bubble.showInShade(), - bubble.isOngoing(), - false /* isAppForeground (unused) */); - } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java index 41dbb489c2f6..a888bd57c699 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java @@ -57,16 +57,13 @@ import java.util.List; public class BubbleExperimentConfig { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; - private static final String SHORTCUT_DUMMY_INTENT = "bubble_experiment_shortcut_intent"; - private static PendingIntent sDummyShortcutIntent; - private static final int BUBBLE_HEIGHT = 10000; private static final String ALLOW_ANY_NOTIF_TO_BUBBLE = "allow_any_notif_to_bubble"; private static final boolean ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT = false; private static final String ALLOW_MESSAGE_NOTIFS_TO_BUBBLE = "allow_message_notifs_to_bubble"; - private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT = true; + private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT = false; private static final String ALLOW_SHORTCUTS_TO_BUBBLE = "allow_shortcuts_to_bubble"; private static final boolean ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT = false; @@ -74,7 +71,7 @@ public class BubbleExperimentConfig { private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps"; private static final String ALLOW_BUBBLE_OVERFLOW = "allow_bubble_overflow"; - private static final boolean ALLOW_BUBBLE_OVERFLOW_DEFAULT = false; + private static final boolean ALLOW_BUBBLE_OVERFLOW_DEFAULT = true; /** * When true, if a notification has the information necessary to bubble (i.e. valid diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java index 37841f24a3cf..de54c353fc85 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java @@ -21,6 +21,7 @@ import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import android.app.Activity; +import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; @@ -60,6 +61,16 @@ public class BubbleOverflowActivity extends Activity { private RecyclerView mRecyclerView; private List<Bubble> mOverflowBubbles = new ArrayList<>(); + private class NoScrollGridLayoutManager extends GridLayoutManager { + NoScrollGridLayoutManager(Context context, int columns) { + super(context, columns); + } + @Override + public boolean canScrollVertically() { + return false; + } + } + @Inject public BubbleOverflowActivity(BubbleController controller) { mBubbleController = controller; @@ -78,7 +89,7 @@ public class BubbleOverflowActivity extends Activity { Resources res = getResources(); final int columns = res.getInteger(R.integer.bubbles_overflow_columns); mRecyclerView.setLayoutManager( - new GridLayoutManager(getApplicationContext(), columns)); + new NoScrollGridLayoutManager(getApplicationContext(), columns)); DisplayMetrics displayMetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); @@ -95,11 +106,12 @@ public class BubbleOverflowActivity extends Activity { mAdapter = new BubbleOverflowAdapter(mOverflowBubbles, mBubbleController::promoteBubbleFromOverflow, viewWidth, viewHeight); mRecyclerView.setAdapter(mAdapter); - onDataChanged(mBubbleController.getOverflowBubbles()); - mBubbleController.setOverflowCallback(() -> { - onDataChanged(mBubbleController.getOverflowBubbles()); - }); - onThemeChanged(); + + mOverflowBubbles.addAll(mBubbleController.getOverflowBubbles()); + mAdapter.notifyDataSetChanged(); + setEmptyStateVisibility(); + + mBubbleController.setOverflowListener(mDataListener); } /** @@ -126,6 +138,14 @@ public class BubbleOverflowActivity extends Activity { } } + void setEmptyStateVisibility() { + if (mOverflowBubbles.isEmpty()) { + mEmptyState.setVisibility(View.VISIBLE); + } else { + mEmptyState.setVisibility(View.GONE); + } + } + void setBackgroundColor() { final TypedArray ta = getApplicationContext().obtainStyledAttributes( new int[]{android.R.attr.colorBackgroundFloating}); @@ -134,22 +154,40 @@ public class BubbleOverflowActivity extends Activity { findViewById(android.R.id.content).setBackgroundColor(bgColor); } - void onDataChanged(List<Bubble> bubbles) { - mOverflowBubbles.clear(); - mOverflowBubbles.addAll(bubbles); - mAdapter.notifyDataSetChanged(); + private final BubbleData.Listener mDataListener = new BubbleData.Listener() { - if (mOverflowBubbles.isEmpty()) { - mEmptyState.setVisibility(View.VISIBLE); - } else { - mEmptyState.setVisibility(View.GONE); - } + @Override + public void applyUpdate(BubbleData.Update update) { - if (DEBUG_OVERFLOW) { - Log.d(TAG, "Updated overflow bubbles:\n" + BubbleDebugConfig.formatBubblesString( - mOverflowBubbles, /*selected*/ null)); + Bubble toRemove = update.removedOverflowBubble; + if (toRemove != null) { + if (DEBUG_OVERFLOW) { + Log.d(TAG, "remove: " + toRemove); + } + toRemove.cleanupViews(); + int i = mOverflowBubbles.indexOf(toRemove); + mOverflowBubbles.remove(toRemove); + mAdapter.notifyItemRemoved(i); + } + + Bubble toAdd = update.addedOverflowBubble; + if (toAdd != null) { + if (DEBUG_OVERFLOW) { + Log.d(TAG, "add: " + toAdd); + } + mOverflowBubbles.add(0, toAdd); + mAdapter.notifyItemInserted(0); + } + + setEmptyStateVisibility(); + + if (DEBUG_OVERFLOW) { + Log.d(TAG, BubbleDebugConfig.formatBubblesString( + mBubbleController.getOverflowBubbles(), + null)); + } } - } + }; @Override public void onStart() { @@ -215,6 +253,7 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V Bubble b = mBubbles.get(index); vh.iconView.setRenderedBubble(b); + vh.iconView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); vh.iconView.setOnClickListener(view -> { mBubbles.remove(b); notifyDataSetChanged(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 71dbbbc9da50..366d4a7345af 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -33,12 +33,14 @@ import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.Notification; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; +import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PointF; @@ -47,6 +49,7 @@ import android.graphics.RectF; import android.graphics.Region; import android.os.Bundle; import android.os.Vibrator; +import android.service.notification.StatusBarNotification; import android.util.Log; import android.view.Choreographer; import android.view.DisplayCutout; @@ -55,6 +58,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; @@ -62,6 +66,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.MainThread; @@ -83,6 +88,7 @@ import com.android.systemui.bubbles.animation.StackAnimationController; import com.android.systemui.model.SysUiState; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.phone.NotificationShadeWindowController; import com.android.systemui.util.DismissCircleView; import com.android.systemui.util.FloatingContentCoordinator; @@ -97,6 +103,7 @@ import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * Renders bubbles in a stack and handles animating expanded and collapsed states. @@ -224,7 +231,7 @@ public class BubbleStackView extends FrameLayout { private int mPointerHeight; private int mStatusBarHeight; private int mImeOffset; - private BubbleViewProvider mExpandedBubble; + @Nullable private BubbleViewProvider mExpandedBubble; private boolean mIsExpanded; /** Whether the stack is currently on the left side of the screen, or animating there. */ @@ -244,6 +251,10 @@ public class BubbleStackView extends FrameLayout { } private BubbleController.BubbleExpandListener mExpandListener; + + /** Callback to run when we want to unbubble the given notification's conversation. */ + private Consumer<NotificationEntry> mUnbubbleConversationCallback; + private SysUiState mSysUiState; private boolean mViewUpdatedRequested = false; @@ -255,9 +266,7 @@ public class BubbleStackView extends FrameLayout { private LayoutInflater mInflater; - // Used for determining view / touch intersection - int[] mTempLoc = new int[2]; - RectF mTempRect = new RectF(); + private Rect mTempRect = new Rect(); private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); @@ -374,8 +383,9 @@ public class BubbleStackView extends FrameLayout { @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { mExpandedAnimationController.dismissDraggedOutBubble( - mExpandedAnimationController.getDraggedOutBubble(), - BubbleStackView.this::dismissMagnetizedObject); + mExpandedAnimationController.getDraggedOutBubble() /* bubble */, + mDismissTargetContainer.getHeight() /* translationYBy */, + BubbleStackView.this::dismissMagnetizedObject /* after */); hideDismissTarget(); } }; @@ -405,7 +415,8 @@ public class BubbleStackView extends FrameLayout { @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - mStackAnimationController.implodeStack( + mStackAnimationController.animateStackDismissal( + mDismissTargetContainer.getHeight() /* translationYBy */, () -> { resetDesaturationAndDarken(); dismissMagnetizedObject(); @@ -442,8 +453,8 @@ public class BubbleStackView extends FrameLayout { // that means overflow was previously expanded. Set the selected bubble // internally without going through BubbleData (which would ignore it since it's // already selected). + mBubbleData.setShowingOverflow(true); setSelectedBubble(clickedBubble); - } } else { // Otherwise, we either tapped the stack (which means we're collapsed @@ -469,6 +480,11 @@ public class BubbleStackView extends FrameLayout { return true; } + // If the manage menu is visible, just hide it. + if (mShowingManage) { + showManageMenu(false /* show */); + } + if (mBubbleData.isExpanded()) { maybeShowManageEducation(false /* show */); @@ -625,6 +641,13 @@ public class BubbleStackView extends FrameLayout { private BubbleManageEducationView mManageEducationView; private boolean mAnimatingManageEducationAway; + private ViewGroup mManageMenu; + private ImageView mManageSettingsIcon; + private TextView mManageSettingsText; + private boolean mShowingManage = false; + private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + @SuppressLint("ClickableViewAccessibility") public BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, @@ -687,6 +710,8 @@ public class BubbleStackView extends FrameLayout { mExpandedViewContainer.setClipChildren(false); addView(mExpandedViewContainer); + setUpManageMenu(); + setUpFlyout(); mFlyoutTransitionSpring.setSpring(new SpringForce() .setStiffness(SpringForce.STIFFNESS_LOW) @@ -836,7 +861,9 @@ public class BubbleStackView extends FrameLayout { // ActivityViews, etc.) were touched. Collapse the stack if it's expanded. setOnTouchListener((view, ev) -> { if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if (mBubbleData.isExpanded()) { + if (mShowingManage) { + showManageMenu(false /* show */); + } else if (mBubbleData.isExpanded()) { mBubbleData.setExpanded(false); } } @@ -845,6 +872,66 @@ public class BubbleStackView extends FrameLayout { }); } + private void setUpManageMenu() { + if (mManageMenu != null) { + removeView(mManageMenu); + } + + mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate( + R.layout.bubble_manage_menu, this, false); + mManageMenu.setVisibility(View.INVISIBLE); + + PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] {android.R.attr.dialogCornerRadius}); + final int menuCornerRadius = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + + mManageMenu.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), menuCornerRadius); + } + }); + mManageMenu.setClipToOutline(true); + + mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + dismissBubbleIfExists(mBubbleData.getSelectedBubble()); + }); + + mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + final Bubble bubble = mBubbleData.getSelectedBubble(); + if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + mUnbubbleConversationCallback.accept(bubble.getEntry()); + } + }); + + mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + final Bubble bubble = mBubbleData.getSelectedBubble(); + if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + final Intent intent = bubble.getSettingsIntent(); + collapseStack(() -> { + mContext.startActivityAsUser( + intent, bubble.getEntry().getSbn().getUser()); + logBubbleClickEvent( + bubble, + SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); + }); + } + }); + + mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon); + mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name); + addView(mManageMenu); + } + private void setUpUserEducation() { if (mUserEducationView != null) { removeView(mUserEducationView); @@ -932,6 +1019,7 @@ public class BubbleStackView extends FrameLayout { setUpFlyout(); setUpOverflow(); setUpUserEducation(); + setUpManageMenu(); } /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ @@ -954,8 +1042,13 @@ public class BubbleStackView extends FrameLayout { mVerticalPosPercentBeforeRotation = (mStackAnimationController.getStackPosition().y - allowablePos.top) / (allowablePos.bottom - allowablePos.top); + mVerticalPosPercentBeforeRotation = + Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation)); addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); + + mManageMenu.setVisibility(View.INVISIBLE); + mShowingManage = false; } @Override @@ -1013,6 +1106,8 @@ public class BubbleStackView extends FrameLayout { // R constants are not final so we cannot use switch-case here. if (action == AccessibilityNodeInfo.ACTION_DISMISS) { mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION); + announceForAccessibility( + getResources().getString(R.string.accessibility_bubble_dismissed)); return true; } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { mBubbleData.setExpanded(false); @@ -1043,32 +1138,29 @@ public class BubbleStackView extends FrameLayout { if (mBubbleData.getBubbles().isEmpty()) { return; } - Bubble topBubble = mBubbleData.getBubbles().get(0); - String appName = topBubble.getAppName(); - Notification notification = topBubble.getEntry().getSbn().getNotification(); - CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE); - String titleStr = getResources().getString(R.string.notification_bubble_title); - if (titleCharSeq != null) { - titleStr = titleCharSeq.toString(); - } - int moreCount = mBubbleContainer.getChildCount() - 1; - // Example: Title from app name. - String singleDescription = getResources().getString( - R.string.bubble_content_description_single, titleStr, appName); + for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { + final Bubble bubble = mBubbleData.getBubbles().get(i); + final String appName = bubble.getAppName(); + final Notification notification = bubble.getEntry().getSbn().getNotification(); + final CharSequence titleCharSeq = + notification.extras.getCharSequence(Notification.EXTRA_TITLE); - // Example: Title from app name and 4 more. - String stackDescription = getResources().getString( - R.string.bubble_content_description_stack, titleStr, appName, moreCount); + String titleStr = getResources().getString(R.string.notification_bubble_title); + if (titleCharSeq != null) { + titleStr = titleCharSeq.toString(); + } - if (mIsExpanded) { - // TODO(b/129522932) - update content description for each bubble in expanded view. - } else { - // Collapsed stack. - if (moreCount > 0) { - mBubbleContainer.setContentDescription(stackDescription); - } else { - mBubbleContainer.setContentDescription(singleDescription); + if (bubble.getIconView() != null) { + if (mIsExpanded || i > 0) { + bubble.getIconView().setContentDescription(getResources().getString( + R.string.bubble_content_description_single, titleStr, appName)); + } else { + final int moreCount = mBubbleContainer.getChildCount() - 1; + bubble.getIconView().setContentDescription(getResources().getString( + R.string.bubble_content_description_stack, + titleStr, appName, moreCount)); + } } } } @@ -1096,6 +1188,12 @@ public class BubbleStackView extends FrameLayout { mExpandListener = listener; } + /** Sets the function to call to un-bubble the given conversation. */ + public void setUnbubbleConversationCallback( + Consumer<NotificationEntry> unbubbleConversationCallback) { + mUnbubbleConversationCallback = unbubbleConversationCallback; + } + /** * Whether the stack of bubbles is expanded or not. */ @@ -1232,8 +1330,12 @@ public class BubbleStackView extends FrameLayout { if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) { return; } + if (bubbleToSelect == null || bubbleToSelect.getKey() != BubbleOverflow.KEY) { + mBubbleData.setShowingOverflow(false); + } final BubbleViewProvider previouslySelected = mExpandedBubble; mExpandedBubble = bubbleToSelect; + updatePointerPosition(); if (mIsExpanded) { // Make the container of the expanded view transparent before removing the expanded view @@ -1243,7 +1345,6 @@ public class BubbleStackView extends FrameLayout { mSurfaceSynchronizer.syncSurfaceAndRun(() -> { previouslySelected.setContentVisibility(false); updateExpandedBubble(); - updatePointerPosition(); requestUpdate(); logBubbleEvent(previouslySelected, @@ -1354,15 +1455,14 @@ public class BubbleStackView extends FrameLayout { mManageEducationView.setAlpha(0); mManageEducationView.setVisibility(VISIBLE); mManageEducationView.post(() -> { - final Rect position = - mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen(); + mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); final int viewHeight = mManageEducationView.getManageViewHeight(); final int inset = getResources().getDimensionPixelSize( R.dimen.bubbles_manage_education_top_inset); mManageEducationView.bringToFront(); - mManageEducationView.setManageViewPosition(position.left, - position.top - viewHeight + inset); - mManageEducationView.setPointerPosition(position.centerX() - position.left); + mManageEducationView.setManageViewPosition(mTempRect.left, + mTempRect.top - viewHeight + inset); + mManageEducationView.setPointerPosition(mTempRect.centerX() - mTempRect.left); mManageEducationView.animate() .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION) .setInterpolator(FAST_OUT_SLOW_IN).alpha(1); @@ -1436,6 +1536,9 @@ public class BubbleStackView extends FrameLayout { } private void animateCollapse() { + // Hide the menu if it's visible. + showManageMenu(false); + mIsExpanded = false; final BubbleViewProvider previouslySelected = mExpandedBubble; beforeExpandedViewAnimation(); @@ -1563,9 +1666,9 @@ public class BubbleStackView extends FrameLayout { */ @Override public void subtractObscuredTouchableRegion(Region touchableRegion, View view) { - // If the notification shade is expanded, we shouldn't let the ActivityView steal any touch - // events from any location. - if (mNotificationShadeWindowController.getPanelExpanded()) { + // If the notification shade is expanded, or the manage menu is open, we shouldn't let the + // ActivityView steal any touch events from any location. + if (mNotificationShadeWindowController.getPanelExpanded() || mShowingManage) { touchableRegion.setEmpty(); } } @@ -1651,17 +1754,19 @@ public class BubbleStackView extends FrameLayout { private void dismissMagnetizedObject() { if (mIsExpanded) { final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); - final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView); - - if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) { - mBubbleData.notificationEntryRemoved( - draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE); - } + dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView)); } else { mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE); } } + private void dismissBubbleIfExists(@Nullable Bubble bubble) { + if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + mBubbleData.notificationEntryRemoved( + bubble.getEntry(), BubbleController.DISMISS_USER_GESTURE); + } + } + /** Prepares and starts the desaturate/darken animation on the bubble stack. */ private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { mDesaturateAndDarkenTargetView = targetView; @@ -1905,6 +2010,63 @@ public class BubbleStackView extends FrameLayout { invalidate(); } + private void showManageMenu(boolean show) { + mShowingManage = show; + + // This should not happen, since the manage menu is only visible when there's an expanded + // bubble. If we end up in this state, just hide the menu immediately. + if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + mManageMenu.setVisibility(View.INVISIBLE); + return; + } + + // If available, update the manage menu's settings option with the expanded bubble's app + // name and icon. + if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) { + final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); + mManageSettingsIcon.setImageDrawable(bubble.getBadgedAppIcon()); + mManageSettingsText.setText(getResources().getString( + R.string.bubbles_app_settings, bubble.getAppName())); + } + + mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); + + // When the menu is open, it should be at these coordinates. This will make the menu's + // bottom left corner match up with the button's bottom left corner. + final float targetX = mTempRect.left; + final float targetY = mTempRect.bottom - mManageMenu.getHeight(); + + if (show) { + mManageMenu.setScaleX(0.5f); + mManageMenu.setScaleY(0.5f); + mManageMenu.setTranslationX(targetX - mManageMenu.getWidth() / 4); + mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4); + mManageMenu.setAlpha(0f); + + PhysicsAnimator.getInstance(mManageMenu) + .spring(DynamicAnimation.ALPHA, 1f) + .spring(DynamicAnimation.SCALE_X, 1f) + .spring(DynamicAnimation.SCALE_Y, 1f) + .spring(DynamicAnimation.TRANSLATION_X, targetX) + .spring(DynamicAnimation.TRANSLATION_Y, targetY) + .start(); + + mManageMenu.setVisibility(View.VISIBLE); + } else { + PhysicsAnimator.getInstance(mManageMenu) + .spring(DynamicAnimation.ALPHA, 0f) + .spring(DynamicAnimation.SCALE_X, 0.5f) + .spring(DynamicAnimation.SCALE_Y, 0.5f) + .spring(DynamicAnimation.TRANSLATION_X, targetX - mManageMenu.getWidth() / 4) + .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4) + .withEndActions(() -> mManageMenu.setVisibility(View.INVISIBLE)) + .start(); + } + + // Update the AV's obscured touchable region for the new menu visibility state. + mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + } + private void updateExpandedBubble() { if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "updateExpandedBubble()"); @@ -1914,6 +2076,7 @@ public class BubbleStackView extends FrameLayout { && mExpandedBubble.getExpandedView() != null) { BubbleExpandedView bev = mExpandedBubble.getExpandedView(); mExpandedViewContainer.addView(bev); + bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); bev.populateExpandedView(); mExpandedViewContainer.setVisibility(VISIBLE); mExpandedViewContainer.setAlpha(1.0f); @@ -2076,10 +2239,32 @@ public class BubbleStackView extends FrameLayout { View child = mBubbleContainer.getChildAt(i); if (child instanceof BadgedImageView) { String key = ((BadgedImageView) child).getKey(); - Bubble bubble = mBubbleData.getBubbleWithKey(key); + Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); bubbles.add(bubble); } } return bubbles; } + + /** + * Logs bubble UI click event. + * + * @param bubble the bubble notification entry that user is interacting with. + * @param action the user interaction enum. + */ + private void logBubbleClickEvent(Bubble bubble, int action) { + StatusBarNotification notification = bubble.getEntry().getSbn(); + SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, + notification.getPackageName(), + notification.getNotification().getChannelId(), + notification.getId(), + getBubbleIndex(getExpandedBubble()), + getBubbleCount(), + action, + getNormalizedXPosition(), + getNormalizedYPosition(), + bubble.showInShade(), + bubble.isOngoing(), + false /* isAppForeground (unused) */); + } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java index 501e5024d940..8a57a735f6cb 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java @@ -116,6 +116,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask ShortcutInfo shortcutInfo; String appName; Bitmap badgedBubbleImage; + Drawable badgedAppIcon; int dotColor; Path dotPath; Bubble.FlyoutMessage flyoutMessage; @@ -139,22 +140,11 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask StatusBarNotification sbn = b.getEntry().getSbn(); String packageName = sbn.getPackageName(); - // Real shortcut info for this bubble String bubbleShortcutId = b.getEntry().getBubbleMetadata().getShortcutId(); if (bubbleShortcutId != null) { - info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c, packageName, - sbn.getUser(), bubbleShortcutId); - } else { - // Check for experimental shortcut - String shortcutId = sbn.getNotification().getShortcutId(); - if (BubbleExperimentConfig.useShortcutInfoToBubble(c) && shortcutId != null) { - info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c, - packageName, - sbn.getUser(), shortcutId); - } + info.shortcutInfo = b.getEntry().getRanking().getShortcutInfo(); } - // App name & app icon PackageManager pm = c.getPackageManager(); ApplicationInfo appInfo; @@ -187,6 +177,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask } BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon); + info.badgedAppIcon = badgedIcon; info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable, badgeBitmapInfo).icon; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java index ef84c73b3145..ca3e2e27fa9a 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java @@ -20,15 +20,17 @@ import android.graphics.Bitmap; import android.graphics.Path; import android.view.View; +import androidx.annotation.Nullable; + /** * Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow. */ interface BubbleViewProvider { - BubbleExpandedView getExpandedView(); + @Nullable BubbleExpandedView getExpandedView(); void setContentVisibility(boolean visible); - View getIconView(); + @Nullable View getIconView(); void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java index d974adc34ee0..35406c71a080 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java @@ -128,17 +128,31 @@ public class ExpandedAnimationController */ private boolean mBubbleDraggedOutEnough = false; + /** End action to run when the lead bubble's expansion animation completes. */ + @Nullable private Runnable mLeadBubbleEndAction; + /** - * Animates expanding the bubbles into a row along the top of the screen. + * Animates expanding the bubbles into a row along the top of the screen, optionally running an + * end action when the entire animation completes, and an end action when the lead bubble's + * animation ends. */ - public void expandFromStack(@Nullable Runnable after) { + public void expandFromStack( + @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) { mAnimatingCollapse = false; mAnimatingExpand = true; mAfterExpand = after; + mLeadBubbleEndAction = leadBubbleEndAction; startOrUpdatePathAnimation(true /* expanding */); } + /** + * Animates expanding the bubbles into a row along the top of the screen. + */ + public void expandFromStack(@Nullable Runnable after) { + expandFromStack(after, null /* leadBubbleEndAction */); + } + /** Animate collapsing the bubbles back to their stacked position. */ public void collapseBackToStack(PointF collapsePoint, Runnable after) { mAnimatingExpand = false; @@ -237,11 +251,17 @@ public class ExpandedAnimationController ? (index * 10) : ((mLayout.getChildCount() - index) * 10); + final boolean isLeadBubble = + (firstBubbleLeads && index == 0) + || (!firstBubbleLeads && index == mLayout.getChildCount() - 1); + animation .followAnimatedTargetAlongPath( path, EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */, - Interpolators.LINEAR /* targetAnimInterpolator */) + Interpolators.LINEAR /* targetAnimInterpolator */, + isLeadBubble ? mLeadBubbleEndAction : null /* endAction */, + () -> mLeadBubbleEndAction = null /* endAction */) .withStartDelay(startDelay) .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS); }).startAll(after); @@ -329,7 +349,7 @@ public class ExpandedAnimationController } /** Plays a dismiss animation on the dragged out bubble. */ - public void dismissDraggedOutBubble(View bubble, Runnable after) { + public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) { if (bubble == null) { return; } @@ -337,6 +357,7 @@ public class ExpandedAnimationController .withStiffness(SpringForce.STIFFNESS_HIGH) .scaleX(1.1f) .scaleY(1.1f) + .translationY(bubble.getTranslationY() + translationYBy) .alpha(0f, after) .start(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java index b1bbafc1ed8f..a7d1be1a766a 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java @@ -758,21 +758,34 @@ public class PhysicsAnimationLayout extends FrameLayout { * or {@link #position}, ultimately animating the view's position to the final point on the * given path. * - * Any provided end listeners will be called when the physics-based animations kicked off by - * the moving target have completed - not when the target animation completes. + * @param pathAnimEndActions End actions to run after the animator that moves the target + * along the path ends. The views following the target may still + * be moving. */ public PhysicsPropertyAnimator followAnimatedTargetAlongPath( Path path, int targetAnimDuration, TimeInterpolator targetAnimInterpolator, - Runnable... endActions) { + Runnable... pathAnimEndActions) { mPathAnimator = ObjectAnimator.ofFloat( this, mCurrentPointOnPathXProperty, mCurrentPointOnPathYProperty, path); + + if (pathAnimEndActions != null) { + mPathAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + for (Runnable action : pathAnimEndActions) { + if (action != null) { + action.run(); + } + } + } + }); + } + mPathAnimator.setDuration(targetAnimDuration); mPathAnimator.setInterpolator(targetAnimInterpolator); - mPositionEndActions = endActions; - // Remove translation related values since we're going to ignore them and follow the // path instead. clearTranslationValues(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index 00de8b4a51b8..5f3a2bd9eb8b 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -647,17 +647,18 @@ public class StackAnimationController extends } /** - * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out. + * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down. */ - public void implodeStack(Runnable after) { - // Pop and fade the bubbles sequentially. - animationForChildAtIndex(0) - .scaleX(0.5f) - .scaleY(0.5f) - .alpha(0f) - .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) - .withStiffness(SpringForce.STIFFNESS_HIGH) - .start(after); + public void animateStackDismissal(float translationYBy, Runnable after) { + animationsForChildrenFromIndex(0, (index, animation) -> + animation + .scaleX(0.5f) + .scaleY(0.5f) + .alpha(0f) + .translationY( + mLayout.getChildAt(index).getTranslationY() + translationYBy) + .withStiffness(SpringForce.STIFFNESS_HIGH)) + .startAll(after); } /** @@ -710,8 +711,6 @@ public class StackAnimationController extends if (property.equals(DynamicAnimation.TRANSLATION_X) || property.equals(DynamicAnimation.TRANSLATION_Y)) { return index + 1; - } else if (isStackStuckToTarget()) { - return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used). } else { return NONE; } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java index e84e932c9e61..72d646e0554d 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java @@ -16,6 +16,7 @@ package com.android.systemui.bubbles.dagger; +import android.app.INotificationManager; import android.content.Context; import com.android.systemui.bubbles.BubbleController; @@ -64,14 +65,15 @@ public interface BubbleModule { FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState) { + SysUiState sysUiState, + INotificationManager notifManager) { return new BubbleController( context, notificationShadeWindowController, statusBarStateController, shadeController, data, - /* synchronizer */null, + null /* synchronizer */, configurationController, interruptionStateProvider, zenModeController, @@ -82,6 +84,7 @@ public interface BubbleModule { featureFlags, dumpManager, floatingContentCoordinator, - sysUiState); + sysUiState, + notifManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt index 7e8fec716b1f..e84f439c1fe2 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -31,6 +31,7 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Singleton @@ -284,13 +285,19 @@ open class ControlsBindingControllerImpl @Inject constructor( val requestLimit: Long ) : IControlsSubscriber.Stub() { val loadedControls = ArrayList<Control>() - private var isTerminated = false + private var isTerminated = AtomicBoolean(false) private var _loadCancelInternal: (() -> Unit)? = null private lateinit var subscription: IControlsSubscription + /** + * Potentially cancel a subscriber. The subscriber may also have terminated, in which case + * the request is ignored. + */ fun loadCancel() = Runnable { - Log.d(TAG, "Cancel load requested") - _loadCancelInternal?.invoke() + _loadCancelInternal?.let { + Log.d(TAG, "Canceling loadSubscribtion") + it.invoke() + } } override fun onSubscribe(token: IBinder, subs: IControlsSubscription) { @@ -301,7 +308,7 @@ open class ControlsBindingControllerImpl @Inject constructor( override fun onNext(token: IBinder, c: Control) { backgroundExecutor.execute { - if (isTerminated) return@execute + if (isTerminated.get()) return@execute loadedControls.add(c) @@ -325,13 +332,15 @@ open class ControlsBindingControllerImpl @Inject constructor( } private fun maybeTerminateAndRun(postTerminateFn: Runnable) { - if (isTerminated) return + if (isTerminated.get()) return - isTerminated = true _loadCancelInternal = {} currentProvider?.cancelLoadTimeout() - backgroundExecutor.execute(postTerminateFn) + backgroundExecutor.execute { + isTerminated.compareAndSet(false, true) + postTerminateFn.run() + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 7cab847d52f7..bc97c10756fd 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -23,6 +23,7 @@ import android.service.controls.actions.ControlAction import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.UserAwareController import com.android.systemui.controls.management.ControlsFavoritingActivity +import com.android.systemui.controls.ui.ControlWithState import com.android.systemui.controls.ui.ControlsUiController import java.util.function.Consumer @@ -51,19 +52,17 @@ interface ControlsController : UserAwareController { * Load all available [Control] for a given service. * * @param componentName the [ComponentName] of the [ControlsProviderService] to load from - * @param dataCallback a callback in which to retrieve the result. + * @param dataCallback a callback in which to retrieve the result + * @param cancelWrapper a callback to receive a [Runnable] that can be run to cancel the + * request */ fun loadForComponent( componentName: ComponentName, - dataCallback: Consumer<LoadData> + dataCallback: Consumer<LoadData>, + cancelWrapper: Consumer<Runnable> ) /** - * Cancels a pending load call - */ - fun cancelLoad() - - /** * Request to subscribe for favorited controls per structure * * @param structureInfo structure to limit the subscription to @@ -111,6 +110,13 @@ interface ControlsController : UserAwareController { @ControlAction.ResponseResult response: Int ) + /** + * When a control should be highlighted, dimming down what's around it. + * + * @param cws focused control, or {@code null} if nothing should be highlighted. + */ + fun onFocusChanged(cws: ControlWithState?) + // FAVORITE MANAGEMENT /** diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 6d34009169d5..8e88756b16fd 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -41,6 +41,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.ui.ControlWithState import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager @@ -76,8 +77,6 @@ class ControlsControllerImpl @Inject constructor ( private var userChanging: Boolean = true - private var loadCanceller: Runnable? = null - private var seedingInProgress = false private val seedingCallbacks = mutableListOf<Consumer<Boolean>>() @@ -275,28 +274,29 @@ class ControlsControllerImpl @Inject constructor ( override fun loadForComponent( componentName: ComponentName, - dataCallback: Consumer<ControlsController.LoadData> + dataCallback: Consumer<ControlsController.LoadData>, + cancelWrapper: Consumer<Runnable> ) { if (!confirmAvailability()) { if (userChanging) { // Try again later, userChanging should not last forever. If so, we have bigger // problems. This will return a runnable that allows to cancel the delayed version, // it will not be able to cancel the load if - loadCanceller = executor.executeDelayed( - { loadForComponent(componentName, dataCallback) }, - USER_CHANGE_RETRY_DELAY, - TimeUnit.MILLISECONDS + executor.executeDelayed( + { loadForComponent(componentName, dataCallback, cancelWrapper) }, + USER_CHANGE_RETRY_DELAY, + TimeUnit.MILLISECONDS ) - } else { - dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true)) } - return + + dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true)) } - loadCanceller = bindingController.bindAndLoad( + + cancelWrapper.accept( + bindingController.bindAndLoad( componentName, object : ControlsBindingController.LoadCallback { override fun accept(controls: List<Control>) { - loadCanceller = null executor.execute { val favoritesForComponentKeys = Favorites .getControlsForComponent(componentName).map { it.controlId } @@ -332,7 +332,6 @@ class ControlsControllerImpl @Inject constructor ( } override fun error(message: String) { - loadCanceller = null executor.execute { val controls = Favorites.getStructuresForComponent(componentName) .flatMap { st -> @@ -347,6 +346,7 @@ class ControlsControllerImpl @Inject constructor ( } } } + ) ) } @@ -431,12 +431,6 @@ class ControlsControllerImpl @Inject constructor ( seedingCallbacks.clear() } - override fun cancelLoad() { - loadCanceller?.let { - executor.execute(it) - } - } - private fun createRemovedStatus( componentName: ComponentName, controlInfo: ControlInfo, @@ -504,6 +498,10 @@ class ControlsControllerImpl @Inject constructor ( } } + override fun onFocusChanged(cws: ControlWithState?) { + uiController.onFocusChanged(cws) + } + override fun refreshStatus(componentName: ComponentName, control: Control) { if (!confirmAvailability()) { Log.d(TAG, "Controls not available") diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index 3bed55912332..5765be57b5b0 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -30,6 +30,8 @@ import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsRequestDialog import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.ControlsUiControllerImpl +import com.android.systemui.controls.ui.ControlActionCoordinator +import com.android.systemui.controls.ui.ControlActionCoordinatorImpl import dagger.Binds import dagger.BindsOptionalOf import dagger.Module @@ -55,6 +57,11 @@ abstract class ControlsModule { @Binds abstract fun provideUiController(controller: ControlsUiControllerImpl): ControlsUiController + @Binds + abstract fun provideControlActionCoordinator( + coordinator: ControlActionCoordinatorImpl + ): ControlActionCoordinator + @BindsOptionalOf abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper @@ -85,4 +92,4 @@ abstract class ControlsModule { abstract fun provideControlsRequestDialog( activity: ControlsRequestDialog ): Activity -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt index 8b3454a2bc7c..0bc6579739db 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt @@ -102,7 +102,9 @@ class AppAdapter( fun bindData(data: ControlsServiceInfo) { icon.setImageDrawable(data.loadIcon()) title.text = data.loadLabel() - favorites.text = favRenderer.renderFavoritesForComponent(data.componentName) + val text = favRenderer.renderFavoritesForComponent(data.componentName) + favorites.text = text + favorites.visibility = if (text == null) View.GONE else View.VISIBLE } } } @@ -112,12 +114,12 @@ class FavoritesRenderer( private val favoriteFunction: (ComponentName) -> Int ) { - fun renderFavoritesForComponent(component: ComponentName): String { + fun renderFavoritesForComponent(component: ComponentName): String? { val qty = favoriteFunction(component) if (qty != 0) { return resources.getQuantityString(R.plurals.controls_number_of_favorites, qty, qty) } else { - return "" + return null } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt new file mode 100644 index 000000000000..cad166d7cd9e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.controls.management + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.annotation.IdRes +import android.content.Intent + +import android.transition.Transition +import android.transition.TransitionValues +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.Window + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent + +import com.android.systemui.Interpolators +import com.android.systemui.R + +import com.android.systemui.controls.ui.ControlsUiController + +object ControlsAnimations { + + private const val ALPHA_EXIT_DURATION = 167L + private const val ALPHA_ENTER_DELAY = ALPHA_EXIT_DURATION + private const val ALPHA_ENTER_DURATION = 350L - ALPHA_ENTER_DELAY + + private const val Y_TRANSLATION_EXIT_DURATION = 183L + private const val Y_TRANSLATION_ENTER_DELAY = Y_TRANSLATION_EXIT_DURATION - ALPHA_ENTER_DELAY + private const val Y_TRANSLATION_ENTER_DURATION = 400L - Y_TRANSLATION_EXIT_DURATION + private var translationY: Float = -1f + + /** + * Setup an activity to handle enter/exit animations. [view] should be the root of the content. + * Fade and translate together. + */ + fun observerForAnimations(view: ViewGroup, window: Window, intent: Intent): LifecycleObserver { + return object : LifecycleObserver { + var showAnimation = intent.getBooleanExtra(ControlsUiController.EXTRA_ANIMATE, false) + + init { + // Must flag the parent group to move it all together, and set the initial + // transitionAlpha to 0.0f. This property is reserved for fade animations. + view.setTransitionGroup(true) + view.transitionAlpha = 0.0f + + if (translationY == -1f) { + translationY = view.context.resources.getDimensionPixelSize( + R.dimen.global_actions_controls_y_translation).toFloat() + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun setup() { + with(window) { + allowEnterTransitionOverlap = true + enterTransition = enterWindowTransition(view.getId()) + exitTransition = exitWindowTransition(view.getId()) + reenterTransition = enterWindowTransition(view.getId()) + returnTransition = exitWindowTransition(view.getId()) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun enterAnimation() { + if (showAnimation) { + ControlsAnimations.enterAnimation(view).start() + showAnimation = false + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun resetAnimation() { + view.translationY = 0f + } + } + } + + fun enterAnimation(view: View): Animator { + Log.d(ControlsUiController.TAG, "Enter animation for $view") + + view.transitionAlpha = 0.0f + view.alpha = 1.0f + + view.translationY = translationY + + val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f, 1.0f).apply { + interpolator = Interpolators.DECELERATE_QUINT + startDelay = ALPHA_ENTER_DELAY + duration = ALPHA_ENTER_DURATION + } + + val yAnimator = ObjectAnimator.ofFloat(view, "translationY", 0.0f).apply { + interpolator = Interpolators.DECELERATE_QUINT + startDelay = Y_TRANSLATION_ENTER_DURATION + duration = Y_TRANSLATION_ENTER_DURATION + } + + return AnimatorSet().apply { + playTogether(alphaAnimator, yAnimator) + } + } + + /** + * Properly handle animations originating from dialogs. Activity transitions require + * transitioning between two activities, so expose this method for dialogs to animate + * on exit. + */ + @JvmStatic + fun exitAnimation(view: View, onEnd: Runnable? = null): Animator { + Log.d(ControlsUiController.TAG, "Exit animation for $view") + + val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f).apply { + interpolator = Interpolators.ACCELERATE + duration = ALPHA_EXIT_DURATION + } + + view.translationY = 0.0f + val yAnimator = ObjectAnimator.ofFloat(view, "translationY", -translationY).apply { + interpolator = Interpolators.ACCELERATE + duration = Y_TRANSLATION_EXIT_DURATION + } + + return AnimatorSet().apply { + playTogether(alphaAnimator, yAnimator) + onEnd?.let { + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + it.run() + } + }) + } + } + } + + fun enterWindowTransition(@IdRes id: Int) = + WindowTransition({ view: View -> enterAnimation(view) }).apply { + addTarget(id) + } + + fun exitWindowTransition(@IdRes id: Int) = + WindowTransition({ view: View -> exitAnimation(view) }).apply { + addTarget(id) + } +} + +/** + * In order to animate, at least one property must be marked on each view that should move. + * Setting "item" is just a flag to indicate that it should move by the animator. + */ +class WindowTransition( + val animator: (view: View) -> Animator +) : Transition() { + override fun captureStartValues(tv: TransitionValues) { + tv.values["item"] = 0.0f + } + + override fun captureEndValues(tv: TransitionValues) { + tv.values["item"] = 1.0f + } + + override fun createAnimator( + sceneRoot: ViewGroup, + startValues: TransitionValues?, + endValues: TransitionValues? + ): Animator? = animator(startValues!!.view) +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt index ee1ce7ab3d83..4e9c550297c5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -16,11 +16,11 @@ package com.android.systemui.controls.management -import android.app.Activity import android.content.ComponentName import android.content.Intent import android.os.Bundle import android.view.View +import android.view.ViewGroup import android.view.ViewStub import android.widget.Button import android.widget.TextView @@ -31,7 +31,9 @@ import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.util.LifecycleActivity import javax.inject.Inject /** @@ -39,8 +41,9 @@ import javax.inject.Inject */ class ControlsEditingActivity @Inject constructor( private val controller: ControlsControllerImpl, - broadcastDispatcher: BroadcastDispatcher -) : Activity() { + broadcastDispatcher: BroadcastDispatcher, + private val globalActionsComponent: GlobalActionsComponent +) : LifecycleActivity() { companion object { private const val TAG = "ControlsEditingActivity" @@ -66,10 +69,6 @@ class ControlsEditingActivity @Inject constructor( } } - override fun onBackPressed() { - finish() - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -84,14 +83,48 @@ class ControlsEditingActivity @Inject constructor( bindViews() bindButtons() + } + override fun onStart() { + super.onStart() setUpList() currentUserTracker.startTracking() } + override fun onStop() { + super.onStop() + currentUserTracker.stopTracking() + } + + override fun onBackPressed() { + globalActionsComponent.handleShowGlobalActionsMenu() + animateExitAndFinish() + } + + private fun animateExitAndFinish() { + val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) + ControlsAnimations.exitAnimation( + rootView, + object : Runnable { + override fun run() { + finish() + } + } + ).start() + } + private fun bindViews() { setContentView(R.layout.controls_management) + + getLifecycle().addObserver( + ControlsAnimations.observerForAnimations( + requireViewById<ViewGroup>(R.id.controls_management_root), + window, + intent + ) + ) + requireViewById<ViewStub>(R.id.stub).apply { layoutResource = R.layout.controls_management_editing inflate() @@ -103,27 +136,14 @@ class ControlsEditingActivity @Inject constructor( } private fun bindButtons() { - requireViewById<Button>(R.id.other_apps).apply { - visibility = View.VISIBLE - setText(R.string.controls_menu_add) - setOnClickListener { - saveFavorites() - val intent = Intent(this@ControlsEditingActivity, - ControlsFavoritingActivity::class.java).apply { - putExtras(this@ControlsEditingActivity.intent) - putExtra(ControlsFavoritingActivity.EXTRA_SINGLE_STRUCTURE, true) - } - startActivity(intent) - finish() - } - } - + val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) saveButton = requireViewById<Button>(R.id.done).apply { isEnabled = false setText(R.string.save) setOnClickListener { saveFavorites() - finishAffinity() + animateExitAndFinish() + globalActionsComponent.handleShowGlobalActionsMenu() } } } @@ -151,26 +171,38 @@ class ControlsEditingActivity @Inject constructor( val controls = controller.getFavoritesForStructure(component, structure) model = FavoritesModel(component, controls, favoritesModelCallback) val elevation = resources.getFloat(R.dimen.control_card_elevation) - val adapter = ControlAdapter(elevation) - val recycler = requireViewById<RecyclerView>(R.id.list) + val recyclerView = requireViewById<RecyclerView>(R.id.list) + recyclerView.alpha = 0.0f + val adapter = ControlAdapter(elevation).apply { + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + var hasAnimated = false + override fun onChanged() { + if (!hasAnimated) { + hasAnimated = true + ControlsAnimations.enterAnimation(recyclerView).start() + } + } + }) + } + val margin = resources .getDimensionPixelSize(R.dimen.controls_card_margin) val itemDecorator = MarginItemDecorator(margin, margin) - recycler.apply { + recyclerView.apply { this.adapter = adapter - layoutManager = GridLayoutManager(recycler.context, 2).apply { + layoutManager = GridLayoutManager(recyclerView.context, 2).apply { spanSizeLookup = adapter.spanSizeLookup } addItemDecoration(itemDecorator) } adapter.changeModel(model) model.attachAdapter(adapter) - ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recycler) + ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recyclerView) } override fun onDestroy() { currentUserTracker.stopTracking() super.onDestroy() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt index 6f34deeb8547..e3175aafb1b1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -16,11 +16,12 @@ package com.android.systemui.controls.management -import android.app.Activity +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.ActivityOptions import android.content.ComponentName import android.content.Intent import android.content.res.Configuration -import android.graphics.drawable.Drawable import android.os.Bundle import android.text.TextUtils import android.view.Gravity @@ -29,8 +30,8 @@ import android.view.ViewGroup import android.view.ViewStub import android.widget.Button import android.widget.FrameLayout -import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.viewpager2.widget.ViewPager2 import com.android.systemui.Prefs import com.android.systemui.R @@ -40,7 +41,9 @@ import com.android.systemui.controls.TooltipManager import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.util.LifecycleActivity import java.text.Collator import java.util.concurrent.Executor import java.util.function.Consumer @@ -50,8 +53,9 @@ class ControlsFavoritingActivity @Inject constructor( @Main private val executor: Executor, private val controller: ControlsControllerImpl, private val listingController: ControlsListingController, - broadcastDispatcher: BroadcastDispatcher -) : Activity() { + broadcastDispatcher: BroadcastDispatcher, + private val globalActionsComponent: GlobalActionsComponent +) : LifecycleActivity() { companion object { private const val TAG = "ControlsFavoritingActivity" @@ -62,6 +66,7 @@ class ControlsFavoritingActivity @Inject constructor( // If provided, show this structure page first const val EXTRA_STRUCTURE = "extra_structure" const val EXTRA_SINGLE_STRUCTURE = "extra_single_structure" + internal const val EXTRA_FROM_PROVIDER_SELECTOR = "extra_from_provider_selector" private const val TOOLTIP_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT private const val TOOLTIP_MAX_SHOWN = 2 } @@ -69,18 +74,20 @@ class ControlsFavoritingActivity @Inject constructor( private var component: ComponentName? = null private var appName: CharSequence? = null private var structureExtra: CharSequence? = null + private var fromProviderSelector = false private lateinit var structurePager: ViewPager2 private lateinit var statusText: TextView private lateinit var titleView: TextView - private lateinit var iconView: ImageView - private lateinit var iconFrame: View private lateinit var pageIndicator: ManagementPageIndicator private var mTooltipManager: TooltipManager? = null private lateinit var doneButton: View + private lateinit var otherAppsButton: View private var listOfStructures = emptyList<StructureContainer>() private lateinit var comparator: Comparator<StructureContainer> + private var cancelLoadRunnable: Runnable? = null + private var isPagerLoaded = false private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { private val startingUser = controller.currentUserId @@ -94,42 +101,32 @@ class ControlsFavoritingActivity @Inject constructor( } private val listingCallback = object : ControlsListingController.ControlsListingCallback { - private var icon: Drawable? = null override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { - val newIcon = serviceInfos.firstOrNull { it.componentName == component }?.loadIcon() - if (icon == newIcon) return - icon = newIcon - executor.execute { - if (icon != null) { - iconView.setImageDrawable(icon) - } - iconFrame.visibility = if (icon != null) View.VISIBLE else View.GONE + if (serviceInfos.size > 1) { + otherAppsButton.visibility = View.VISIBLE } } } override fun onBackPressed() { - finish() + if (!fromProviderSelector) { + globalActionsComponent.handleShowGlobalActionsMenu() + } + animateExitAndFinish() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val collator = Collator.getInstance(resources.configuration.locales[0]) comparator = compareBy(collator) { it.structureName } appName = intent.getCharSequenceExtra(EXTRA_APP) structureExtra = intent.getCharSequenceExtra(EXTRA_STRUCTURE) ?: "" component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME) + fromProviderSelector = intent.getBooleanExtra(EXTRA_FROM_PROVIDER_SELECTOR, false) bindViews() - - setUpPager() - - loadControls() - - listingController.addCallback(listingCallback) - - currentUserTracker.startTracking() } private val controlsModelCallback = object : ControlsModel.ControlsModelCallback { @@ -174,12 +171,33 @@ class ControlsFavoritingActivity @Inject constructor( pageIndicator.setLocation(0f) pageIndicator.visibility = if (listOfStructures.size > 1) View.VISIBLE else View.GONE + + ControlsAnimations.enterAnimation(pageIndicator).apply { + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + // Position the tooltip if necessary after animations are complete + // so we can get the position on screen. The tooltip is not + // rooted in the layout root. + if (pageIndicator.visibility == View.VISIBLE && + mTooltipManager != null) { + val p = IntArray(2) + pageIndicator.getLocationOnScreen(p) + val x = p[0] + pageIndicator.width / 2 + val y = p[1] + pageIndicator.height + mTooltipManager?.show(R.string.controls_structure_tooltip, x, y) + } + } + }) + }.start() + ControlsAnimations.enterAnimation(structurePager).start() } - }) + }, Consumer { runnable -> cancelLoadRunnable = runnable }) } } private fun setUpPager() { + structurePager.alpha = 0.0f + pageIndicator.alpha = 0.0f structurePager.apply { adapter = StructureAdapter(emptyList()) registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { @@ -203,6 +221,15 @@ class ControlsFavoritingActivity @Inject constructor( private fun bindViews() { setContentView(R.layout.controls_management) + + getLifecycle().addObserver( + ControlsAnimations.observerForAnimations( + requireViewById<ViewGroup>(R.id.controls_management_root), + window, + intent + ) + ) + requireViewById<ViewStub>(R.id.stub).apply { layoutResource = R.layout.controls_management_favorites inflate() @@ -223,27 +250,6 @@ class ControlsFavoritingActivity @Inject constructor( } pageIndicator = requireViewById<ManagementPageIndicator>( R.id.structure_page_indicator).apply { - addOnLayoutChangeListener(object : View.OnLayoutChangeListener { - override fun onLayoutChange( - v: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - if (v.visibility == View.VISIBLE && mTooltipManager != null) { - val p = IntArray(2) - v.getLocationOnScreen(p) - val x = p[0] + (right - left) / 2 - val y = p[1] + bottom - top - mTooltipManager?.show(R.string.controls_structure_tooltip, x, y) - } - } - }) visibilityListener = { if (it != View.VISIBLE) { mTooltipManager?.hide(true) @@ -259,8 +265,6 @@ class ControlsFavoritingActivity @Inject constructor( } requireViewById<TextView>(R.id.subtitle).text = resources.getText(R.string.controls_favorite_subtitle) - iconView = requireViewById(com.android.internal.R.id.icon) - iconFrame = requireViewById(R.id.icon_frame) structurePager = requireViewById<ViewPager2>(R.id.structure_pager) structurePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { @@ -271,14 +275,35 @@ class ControlsFavoritingActivity @Inject constructor( bindButtons() } + private fun animateExitAndFinish() { + val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) + ControlsAnimations.exitAnimation( + rootView, + object : Runnable { + override fun run() { + finish() + } + } + ).start() + } + private fun bindButtons() { - requireViewById<Button>(R.id.other_apps).apply { - visibility = View.VISIBLE + otherAppsButton = requireViewById<Button>(R.id.other_apps).apply { setOnClickListener { - val i = Intent() - i.setComponent( - ComponentName(context, ControlsProviderSelectorActivity::class.java)) - context.startActivity(i) + val i = Intent().apply { + component = ComponentName(context, ControlsProviderSelectorActivity::class.java) + } + if (doneButton.isEnabled) { + // The user has made changes + Toast.makeText( + applicationContext, + R.string.controls_favorite_toast_no_changes, + Toast.LENGTH_SHORT + ).show() + } + startActivity(i, ActivityOptions + .makeSceneTransitionAnimation(this@ControlsFavoritingActivity).toBundle()) + animateExitAndFinish() } } @@ -292,7 +317,8 @@ class ControlsFavoritingActivity @Inject constructor( StructureInfo(component!!, it.structureName, favoritesForStorage) ) } - finishAffinity() + animateExitAndFinish() + globalActionsComponent.handleShowGlobalActionsMenu() } } } @@ -302,15 +328,39 @@ class ControlsFavoritingActivity @Inject constructor( mTooltipManager?.hide(false) } + override fun onStart() { + super.onStart() + + listingController.addCallback(listingCallback) + currentUserTracker.startTracking() + } + + override fun onResume() { + super.onResume() + + // only do once, to make sure that any user changes do not get replaces if resume is called + // more than once + if (!isPagerLoaded) { + setUpPager() + loadControls() + isPagerLoaded = true + } + } + + override fun onStop() { + super.onStop() + + listingController.removeCallback(listingCallback) + currentUserTracker.stopTracking() + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) mTooltipManager?.hide(false) } override fun onDestroy() { - currentUserTracker.stopTracking() - listingController.removeCallback(listingCallback) - controller.cancelLoad() + cancelLoadRunnable?.run() super.onDestroy() } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt index 3be59009f531..08147746a4c8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt @@ -16,20 +16,25 @@ package com.android.systemui.controls.management +import android.app.ActivityOptions import android.content.ComponentName import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.ViewStub import android.widget.Button import android.widget.TextView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.util.LifecycleActivity import java.util.concurrent.Executor @@ -43,6 +48,7 @@ class ControlsProviderSelectorActivity @Inject constructor( @Background private val backExecutor: Executor, private val listingController: ControlsListingController, private val controlsController: ControlsController, + private val globalActionsComponent: GlobalActionsComponent, broadcastDispatcher: BroadcastDispatcher ) : LifecycleActivity() { @@ -66,12 +72,47 @@ class ControlsProviderSelectorActivity @Inject constructor( super.onCreate(savedInstanceState) setContentView(R.layout.controls_management) + + getLifecycle().addObserver( + ControlsAnimations.observerForAnimations( + requireViewById<ViewGroup>(R.id.controls_management_root), + window, + intent + ) + ) + requireViewById<ViewStub>(R.id.stub).apply { layoutResource = R.layout.controls_management_apps inflate() } recyclerView = requireViewById(R.id.list) + recyclerView.layoutManager = LinearLayoutManager(applicationContext) + + requireViewById<TextView>(R.id.title).apply { + text = resources.getText(R.string.controls_providers_title) + } + + requireViewById<Button>(R.id.other_apps).apply { + visibility = View.VISIBLE + setText(com.android.internal.R.string.cancel) + setOnClickListener { + onBackPressed() + } + } + requireViewById<View>(R.id.done).visibility = View.GONE + } + + override fun onBackPressed() { + globalActionsComponent.handleShowGlobalActionsMenu() + animateExitAndFinish() + } + + override fun onStart() { + super.onStart() + currentUserTracker.startTracking() + + recyclerView.alpha = 0.0f recyclerView.adapter = AppAdapter( backExecutor, executor, @@ -80,17 +121,22 @@ class ControlsProviderSelectorActivity @Inject constructor( LayoutInflater.from(this), ::launchFavoritingActivity, FavoritesRenderer(resources, controlsController::countFavoritesForComponent), - resources) - recyclerView.layoutManager = LinearLayoutManager(applicationContext) - - requireViewById<TextView>(R.id.title).text = - resources.getText(R.string.controls_providers_title) - - requireViewById<Button>(R.id.done).setOnClickListener { - this@ControlsProviderSelectorActivity.finishAffinity() + resources).apply { + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + var hasAnimated = false + override fun onChanged() { + if (!hasAnimated) { + hasAnimated = true + ControlsAnimations.enterAnimation(recyclerView).start() + } + } + }) } + } - currentUserTracker.startTracking() + override fun onStop() { + super.onStop() + currentUserTracker.stopTracking() } /** @@ -98,16 +144,16 @@ class ControlsProviderSelectorActivity @Inject constructor( * @param component a component name for a [ControlsProviderService] */ fun launchFavoritingActivity(component: ComponentName?) { - backExecutor.execute { + executor.execute { component?.let { val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java) .apply { putExtra(ControlsFavoritingActivity.EXTRA_APP, listingController.getAppLabel(it)) putExtra(Intent.EXTRA_COMPONENT_NAME, it) - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + putExtra(ControlsFavoritingActivity.EXTRA_FROM_PROVIDER_SELECTOR, true) } - startActivity(intent) + startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } } @@ -116,4 +162,16 @@ class ControlsProviderSelectorActivity @Inject constructor( currentUserTracker.stopTracking() super.onDestroy() } + + private fun animateExitAndFinish() { + val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) + ControlsAnimations.exitAnimation( + rootView, + object : Runnable { + override fun run() { + finish() + } + } + ).start() + } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt index a7a41033bb5d..1f07e37d2ad0 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt @@ -47,16 +47,31 @@ object ChallengeDialogs { * [ControlAction#RESPONSE_CHALLENGE_PIN] responses, decided by the useAlphaNumeric * parameter. */ - fun createPinDialog(cvh: ControlViewHolder, useAlphaNumeric: Boolean): Dialog? { + fun createPinDialog( + cvh: ControlViewHolder, + useAlphaNumeric: Boolean, + useRetryStrings: Boolean + ): Dialog? { val lastAction = cvh.lastAction if (lastAction == null) { Log.e(ControlsUiController.TAG, "PIN Dialog attempted but no last action is set. Will not show") return null } + val res = cvh.context.resources + val (title, instructions) = if (useRetryStrings) { + Pair( + res.getString(R.string.controls_pin_wrong), + R.string.controls_pin_instructions_retry + ) + } else { + Pair( + res.getString(R.string.controls_pin_verify, cvh.title.getText()), + R.string.controls_pin_instructions + ) + } val builder = AlertDialog.Builder(cvh.context, STYLE).apply { - val res = cvh.context.resources - setTitle(res.getString(R.string.controls_pin_verify, cvh.title.getText())) + setTitle(title) setView(R.layout.controls_dialog_pin) setPositiveButton( android.R.string.ok, @@ -81,6 +96,7 @@ object ChallengeDialogs { } setOnShowListener(DialogInterface.OnShowListener { _ -> val editText = requireViewById<EditText>(R.id.controls_pin_input) + editText.setHint(instructions) val useAlphaCheckBox = requireViewById<CheckBox>(R.id.controls_pin_use_alpha) useAlphaCheckBox.setChecked(useAlphaNumeric) setInputType(editText, useAlphaCheckBox.isChecked()) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt index ad86eeb089c8..70092d31fe1e 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt @@ -16,68 +16,52 @@ package com.android.systemui.controls.ui -import android.app.Dialog -import android.app.PendingIntent -import android.content.Intent import android.service.controls.Control -import android.service.controls.actions.BooleanAction -import android.service.controls.actions.CommandAction -import android.util.Log -import android.view.HapticFeedbackConstants -import com.android.systemui.R - -object ControlActionCoordinator { - public const val MIN_LEVEL = 0 - public const val MAX_LEVEL = 10000 - - private var dialog: Dialog? = null +/** + * All control interactions should be routed through this coordinator. It handles dispatching of + * actions, haptic support, and all detail panels + */ +interface ControlActionCoordinator { - fun closeDialog() { - dialog?.dismiss() - dialog = null - } + /** + * Close any dialogs which may have been open + */ + fun closeDialogs() - fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) { - cvh.action(BooleanAction(templateId, !isChecked)) + /** + * Create a [BooleanAction], and inform the service of a request to change the device state + * + * @param cvh [ControlViewHolder] for the control + * @param templateId id of the control's template, as given by the service + * @param isChecked new requested state of the control + */ + fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) - val nextLevel = if (isChecked) MIN_LEVEL else MAX_LEVEL - cvh.clipLayer.setLevel(nextLevel) - } + /** + * For non-toggle controls, touching may create a dialog or invoke a [CommandAction]. + * + * @param cvh [ControlViewHolder] for the control + * @param templateId id of the control's template, as given by the service + * @param control the control as sent by the service + */ + fun touch(cvh: ControlViewHolder, templateId: String, control: Control) - fun touch(cvh: ControlViewHolder, templateId: String, control: Control) { - if (cvh.usePanel()) { - showDialog(cvh, control.getAppIntent().getIntent()) - } else { - cvh.action(CommandAction(templateId)) - } - } + /** + * When a ToggleRange control is interacting with, a drag event is sent. + * + * @param isEdge did the drag event reach a control edge + */ + fun drag(isEdge: Boolean) /** - * Allow apps to specify whether they would like to appear in a detail panel or within - * the full activity by setting the {@link Control#EXTRA_USE_PANEL} flag. In order for - * activities to determine how they are being launched, they should inspect the - * {@link Control#EXTRA_USE_PANEL} flag for a value of true. + * All long presses will be shown in a 3/4 height bottomsheet panel, in order for the user to + * retain context with their favorited controls in the power menu. */ - fun longPress(cvh: ControlViewHolder) { - // Long press snould only be called when there is valid control state, otherwise ignore - cvh.cws.control?.let { - try { - it.getAppIntent().send() - cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - cvh.context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) - } catch (e: PendingIntent.CanceledException) { - Log.e(ControlsUiController.TAG, "Error sending pending intent", e) - cvh.setTransientStatus( - cvh.context.resources.getString(R.string.controls_error_failed)) - } - } - } + fun longPress(cvh: ControlViewHolder) - private fun showDialog(cvh: ControlViewHolder, intent: Intent) { - dialog = DetailDialog(cvh, intent).also { - it.setOnDismissListener { _ -> dialog = null } - it.show() - } - } + /** + * Event to inform the UI that the user has has focused on a single control. + */ + fun setFocusedElement(cvh: ControlViewHolder?) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt new file mode 100644 index 000000000000..9e6f58851caf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.controls.ui + +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.os.Vibrator +import android.os.VibrationEffect +import android.service.controls.Control +import android.service.controls.actions.BooleanAction +import android.service.controls.actions.CommandAction +import android.util.Log +import android.view.HapticFeedbackConstants +import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.globalactions.GlobalActionsComponent +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.concurrency.DelayableExecutor + +import dagger.Lazy + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ControlActionCoordinatorImpl @Inject constructor( + private val context: Context, + private val bgExecutor: DelayableExecutor, + private val controlsController: Lazy<ControlsController>, + private val activityStarter: ActivityStarter, + private val keyguardStateController: KeyguardStateController, + private val globalActionsComponent: GlobalActionsComponent +) : ControlActionCoordinator { + private var dialog: Dialog? = null + private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private var lastAction: (() -> Unit)? = null + + override fun closeDialogs() { + dialog?.dismiss() + dialog = null + } + + override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) { + bouncerOrRun { + val effect = if (isChecked) Vibrations.toggleOnEffect else Vibrations.toggleOffEffect + vibrate(effect) + cvh.action(BooleanAction(templateId, !isChecked)) + } + } + + override fun touch(cvh: ControlViewHolder, templateId: String, control: Control) { + vibrate(Vibrations.toggleOnEffect) + + bouncerOrRun { + if (cvh.usePanel()) { + showDialog(cvh, control.getAppIntent().getIntent()) + } else { + cvh.action(CommandAction(templateId)) + } + } + } + + override fun drag(isEdge: Boolean) { + bouncerOrRun { + if (isEdge) { + vibrate(Vibrations.rangeEdgeEffect) + } else { + vibrate(Vibrations.rangeMiddleEffect) + } + } + } + + override fun longPress(cvh: ControlViewHolder) { + bouncerOrRun { + // Long press snould only be called when there is valid control state, otherwise ignore + cvh.cws.control?.let { + cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + showDialog(cvh, it.getAppIntent().getIntent()) + } + } + } + + override fun setFocusedElement(cvh: ControlViewHolder?) { + controlsController.get().onFocusChanged(cvh?.cws) + } + + private fun bouncerOrRun(f: () -> Unit) { + if (!keyguardStateController.isUnlocked()) { + context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) + activityStarter.dismissKeyguardThenExecute({ + Log.d(ControlsUiController.TAG, "Device unlocked, invoking controls action") + globalActionsComponent.handleShowGlobalActionsMenu() + f() + true + }, null, true) + } else { + f() + } + } + + private fun vibrate(effect: VibrationEffect) { + bgExecutor.execute { vibrator.vibrate(effect) } + } + + private fun showDialog(cvh: ControlViewHolder, intent: Intent) { + dialog = DetailDialog(cvh, intent).also { + it.setOnDismissListener { _ -> dialog = null } + it.show() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index 0eb6cb100933..a9b540eddb2c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -16,6 +16,10 @@ package com.android.systemui.controls.ui +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.app.Dialog import android.content.Context import android.graphics.drawable.ClipDrawable import android.graphics.drawable.GradientDrawable @@ -28,15 +32,16 @@ import android.service.controls.templates.StatelessTemplate import android.service.controls.templates.TemperatureControlTemplate import android.service.controls.templates.ToggleRangeTemplate import android.service.controls.templates.ToggleTemplate +import android.util.MathUtils import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView - +import com.android.internal.graphics.ColorUtils +import com.android.systemui.Interpolators +import com.android.systemui.R import com.android.systemui.controls.controller.ControlsController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.R - import kotlin.reflect.KClass /** @@ -49,19 +54,29 @@ class ControlViewHolder( val controlsController: ControlsController, val uiExecutor: DelayableExecutor, val bgExecutor: DelayableExecutor, - val usePanels: Boolean + val controlActionCoordinator: ControlActionCoordinator ) { companion object { + const val STATE_ANIMATION_DURATION = 700L private const val UPDATE_DELAY_IN_MILLIS = 3000L - private const val ALPHA_ENABLED = (255.0 * 0.2).toInt() - private const val ALPHA_DISABLED = 255 + private const val ALPHA_ENABLED = 255 + private const val ALPHA_DISABLED = 0 private val FORCE_PANEL_DEVICES = setOf( DeviceTypes.TYPE_THERMOSTAT, DeviceTypes.TYPE_CAMERA ) + + const val MIN_LEVEL = 0 + const val MAX_LEVEL = 10000 } + private val toggleBackgroundIntensity: Float = layout.context.resources + .getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1) + private val dimmedAlpha: Float = layout.context.resources + .getFraction(R.fraction.controls_dimmed_alpha, 1, 1) + private var stateAnimator: ValueAnimator? = null + private val baseLayer: GradientDrawable val icon: ImageView = layout.requireViewById(R.id.icon) val status: TextView = layout.requireViewById(R.id.status) val title: TextView = layout.requireViewById(R.id.title) @@ -72,13 +87,22 @@ class ControlViewHolder( var cancelUpdate: Runnable? = null var behavior: Behavior? = null var lastAction: ControlAction? = null + private var lastChallengeDialog: Dialog? = null + val deviceType: Int get() = cws.control?.let { it.getDeviceType() } ?: cws.ci.deviceType + var dimmed: Boolean = false + set(value) { + field = value + bindData(cws) + } init { val ld = layout.getBackground() as LayerDrawable ld.mutate() clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) as ClipDrawable + clipLayer.alpha = ALPHA_DISABLED + baseLayer = ld.findDrawableByLayerId(R.id.background) as GradientDrawable // needed for marquee to start status.setSelected(true) } @@ -101,7 +125,7 @@ class ControlViewHolder( cws.control?.let { layout.setClickable(true) layout.setOnLongClickListener(View.OnLongClickListener() { - ControlActionCoordinator.longPress(this@ControlViewHolder) + controlActionCoordinator.longPress(this@ControlViewHolder) true }) } @@ -123,7 +147,37 @@ class ControlViewHolder( } fun actionResponse(@ControlAction.ResponseResult response: Int) { - // TODO: b/150931809 - handle response codes + // OK responses signal normal behavior, and the app will provide control updates + val failedAttempt = lastChallengeDialog != null + when (response) { + ControlAction.RESPONSE_OK -> + lastChallengeDialog = null + ControlAction.RESPONSE_UNKNOWN -> { + lastChallengeDialog = null + setTransientStatus(context.resources.getString(R.string.controls_error_failed)) + } + ControlAction.RESPONSE_FAIL -> { + lastChallengeDialog = null + setTransientStatus(context.resources.getString(R.string.controls_error_failed)) + } + ControlAction.RESPONSE_CHALLENGE_PIN -> { + lastChallengeDialog = ChallengeDialogs.createPinDialog(this, false, failedAttempt) + lastChallengeDialog?.show() + } + ControlAction.RESPONSE_CHALLENGE_PASSPHRASE -> { + lastChallengeDialog = ChallengeDialogs.createPinDialog(this, true, failedAttempt) + lastChallengeDialog?.show() + } + ControlAction.RESPONSE_CHALLENGE_ACK -> { + lastChallengeDialog = ChallengeDialogs.createConfirmationDialog(this) + lastChallengeDialog?.show() + } + } + } + + fun dismiss() { + lastChallengeDialog?.dismiss() + lastChallengeDialog = null } fun setTransientStatus(tempStatus: String) { @@ -141,8 +195,7 @@ class ControlViewHolder( controlsController.action(cws.componentName, cws.ci, action) } - fun usePanel(): Boolean = - usePanels && deviceType in ControlViewHolder.FORCE_PANEL_DEVICES + fun usePanel(): Boolean = deviceType in ControlViewHolder.FORCE_PANEL_DEVICES private fun findBehavior( status: Int, @@ -150,7 +203,9 @@ class ControlViewHolder( deviceType: Int ): KClass<out Behavior> { return when { - status == Control.STATUS_UNKNOWN -> UnknownBehavior::class + status == Control.STATUS_UNKNOWN -> StatusBehavior::class + status == Control.STATUS_ERROR -> StatusBehavior::class + status == Control.STATUS_NOT_FOUND -> StatusBehavior::class deviceType == DeviceTypes.TYPE_CAMERA -> TouchBehavior::class template is ToggleTemplate -> ToggleBehavior::class template is StatelessTemplate -> TouchBehavior::class @@ -160,30 +215,77 @@ class ControlViewHolder( } } - internal fun applyRenderInfo(enabled: Boolean, offset: Int = 0) { + internal fun applyRenderInfo(enabled: Boolean, offset: Int = 0, animated: Boolean = true) { setEnabled(enabled) val ri = RenderInfo.lookup(context, cws.componentName, deviceType, enabled, offset) - val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme()) - val (bg, alpha) = if (enabled) { - Pair(ri.enabledBackground, ALPHA_ENABLED) + val fg = context.resources.getColorStateList(ri.foreground, context.theme) + val bg = context.resources.getColor(R.color.control_default_background, context.theme) + val dimAlpha = if (dimmed) dimmedAlpha else 1f + var (newClipColor, newAlpha) = if (enabled) { + // allow color overrides for the enabled state only + val color = cws.control?.getCustomColor()?.let { + val state = intArrayOf(android.R.attr.state_enabled) + it.getColorForState(state, it.getDefaultColor()) + } ?: context.resources.getColor(ri.enabledBackground, context.theme) + listOf(color, ALPHA_ENABLED) } else { - Pair(R.color.control_default_background, ALPHA_DISABLED) + listOf( + context.resources.getColor(R.color.control_default_background, context.theme), + ALPHA_DISABLED + ) } status.setTextColor(fg) - icon.setImageDrawable(ri.icon) + cws.control?.getCustomIcon()?.let { + // do not tint custom icons, assume the intended icon color is correct + icon.imageTintList = null + icon.setImageIcon(it) + } ?: run { + icon.setImageDrawable(ri.icon) - // do not color app icons - if (deviceType != DeviceTypes.TYPE_ROUTINE) { - icon.setImageTintList(fg) + // do not color app icons + if (deviceType != DeviceTypes.TYPE_ROUTINE) { + icon.imageTintList = fg + } } (clipLayer.getDrawable() as GradientDrawable).apply { - setColor(context.getResources().getColor(bg, context.getTheme())) - setAlpha(alpha) + val newBaseColor = if (behavior is ToggleRangeBehavior) { + ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity) + } else { + bg + } + stateAnimator?.cancel() + if (animated) { + val oldColor = color?.defaultColor ?: newClipColor + val oldBaseColor = baseLayer.color?.defaultColor ?: newBaseColor + val oldAlpha = layout.alpha + stateAnimator = ValueAnimator.ofInt(clipLayer.alpha, newAlpha).apply { + addUpdateListener { + alpha = it.animatedValue as Int + setColor(ColorUtils.blendARGB(oldColor, newClipColor, it.animatedFraction)) + baseLayer.setColor(ColorUtils.blendARGB(oldBaseColor, + newBaseColor, it.animatedFraction)) + layout.alpha = MathUtils.lerp(oldAlpha, dimAlpha, it.animatedFraction) + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + stateAnimator = null + } + }) + duration = STATE_ANIMATION_DURATION + interpolator = Interpolators.CONTROL_STATE + start() + } + } else { + alpha = newAlpha + setColor(newClipColor) + baseLayer.setColor(newBaseColor) + layout.alpha = dimAlpha + } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 0f105376847f..aed7cd316bc7 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -26,9 +26,10 @@ interface ControlsUiController { companion object { public const val TAG = "ControlsUiController" + public const val EXTRA_ANIMATE = "extra_animate" } - fun show(parent: ViewGroup) + fun show(parent: ViewGroup, dismissGlobalActions: Runnable) fun hide() fun onRefreshState(componentName: ComponentName, controls: List<Control>) fun onActionResponse( @@ -36,4 +37,5 @@ interface ControlsUiController { controlId: String, @ControlAction.ResponseResult response: Int ) + fun onFocusChanged(controlWithState: ControlWithState?) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index fab6fc7357dd..7108966072b9 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -20,7 +20,6 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.app.AlertDialog -import android.app.Dialog import android.content.ComponentName import android.content.Context import android.content.DialogInterface @@ -30,9 +29,7 @@ import android.content.res.Configuration import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.os.Process -import android.provider.Settings import android.service.controls.Control -import android.service.controls.actions.ControlAction import android.util.Log import android.util.TypedValue import android.view.ContextThemeWrapper @@ -61,6 +58,8 @@ import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy import java.text.Collator @@ -77,15 +76,17 @@ class ControlsUiControllerImpl @Inject constructor ( @Main val uiExecutor: DelayableExecutor, @Background val bgExecutor: DelayableExecutor, val controlsListingController: Lazy<ControlsListingController>, - @Main val sharedPreferences: SharedPreferences + @Main val sharedPreferences: SharedPreferences, + val controlActionCoordinator: ControlActionCoordinator, + private val activityStarter: ActivityStarter, + private val keyguardStateController: KeyguardStateController ) : ControlsUiController { companion object { private const val PREF_COMPONENT = "controls_component" private const val PREF_STRUCTURE = "controls_structure" - private const val USE_PANELS = "systemui.controls_use_panel" - private const val FADE_IN_MILLIS = 225L + private const val FADE_IN_MILLIS = 200L private val EMPTY_COMPONENT = ComponentName("", "") private val EMPTY_STRUCTURE = StructureInfo( @@ -102,8 +103,8 @@ class ControlsUiControllerImpl @Inject constructor ( private lateinit var parent: ViewGroup private lateinit var lastItems: List<SelectionItem> private var popup: ListPopupWindow? = null - private var activeDialog: Dialog? = null private var hidden = true + private lateinit var dismissGlobalActions: Runnable override val available: Boolean get() = controlsController.get().available @@ -134,9 +135,10 @@ class ControlsUiControllerImpl @Inject constructor ( } } - override fun show(parent: ViewGroup) { + override fun show(parent: ViewGroup, dismissGlobalActions: Runnable) { Log.d(ControlsUiController.TAG, "show()") this.parent = parent + this.dismissGlobalActions = dismissGlobalActions hidden = false allStructures = controlsController.get().getFavorites() @@ -164,12 +166,18 @@ class ControlsUiControllerImpl @Inject constructor ( private fun reload(parent: ViewGroup) { if (hidden) return + controlsListingController.get().removeCallback(listingCallback) + controlsController.get().unsubscribe() + val fadeAnim = ObjectAnimator.ofFloat(parent, "alpha", 1.0f, 0.0f) fadeAnim.setInterpolator(AccelerateInterpolator(1.0f)) fadeAnim.setDuration(FADE_IN_MILLIS) fadeAnim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - show(parent) + controlViewsById.clear() + controlsById.clear() + + show(parent, dismissGlobalActions) val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f) showAnim.setInterpolator(DecelerateInterpolator(1.0f)) showAnim.setDuration(FADE_IN_MILLIS) @@ -209,6 +217,20 @@ class ControlsUiControllerImpl @Inject constructor ( } } + override fun onFocusChanged(focusedControl: ControlWithState?) { + controlViewsById.forEach { key: ControlKey, viewHolder: ControlViewHolder -> + val state = controlsById.get(key) ?: return@forEach + val shouldBeDimmed = focusedControl != null && state != focusedControl + if (viewHolder.dimmed == shouldBeDimmed) { + return@forEach + } + + uiExecutor.execute { + viewHolder.dimmed = shouldBeDimmed + } + } + } + private fun startFavoritingActivity(context: Context, si: StructureInfo) { startTargetedActivity(context, si, ControlsFavoritingActivity::class.java) } @@ -242,9 +264,18 @@ class ControlsUiControllerImpl @Inject constructor ( } private fun startActivity(context: Context, intent: Intent) { - val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) - context.sendBroadcast(closeDialog) - context.startActivity(intent) + // Force animations when transitioning from a dialog to an activity + intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true) + dismissGlobalActions.run() + + if (!keyguardStateController.isUnlocked()) { + activityStarter.dismissKeyguardThenExecute({ + context.startActivity(intent) + true + }, null, true) + } else { + context.startActivity(intent) + } } private fun showControlsView(items: List<SelectionItem>) { @@ -424,28 +455,27 @@ class ControlsUiControllerImpl @Inject constructor ( val maxColumns = findMaxColumns() - // use flag only temporarily for testing - val usePanels = Settings.Secure.getInt(context.contentResolver, USE_PANELS, 0) == 1 - val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup var lastRow: ViewGroup = createRow(inflater, listView) selectedStructure.controls.forEach { - if (lastRow.getChildCount() == maxColumns) { - lastRow = createRow(inflater, listView) - } - val baseLayout = inflater.inflate( - R.layout.controls_base_item, lastRow, false) as ViewGroup - lastRow.addView(baseLayout) - val cvh = ControlViewHolder( - baseLayout, - controlsController.get(), - uiExecutor, - bgExecutor, - usePanels - ) val key = ControlKey(selectedStructure.componentName, it.controlId) - cvh.bindData(controlsById.getValue(key)) - controlViewsById.put(key, cvh) + controlsById.get(key)?.let { + if (lastRow.getChildCount() == maxColumns) { + lastRow = createRow(inflater, listView) + } + val baseLayout = inflater.inflate( + R.layout.controls_base_item, lastRow, false) as ViewGroup + lastRow.addView(baseLayout) + val cvh = ControlViewHolder( + baseLayout, + controlsController.get(), + uiExecutor, + bgExecutor, + controlActionCoordinator + ) + cvh.bindData(it) + controlViewsById.put(key, cvh) + } } // add spacers if necessary to keep control size consistent @@ -511,7 +541,6 @@ class ControlsUiControllerImpl @Inject constructor ( if (newSelection != selectedStructure) { selectedStructure = newSelection updatePreferences(selectedStructure) - controlsListingController.get().removeCallback(listingCallback) reload(parent) } } @@ -519,22 +548,24 @@ class ControlsUiControllerImpl @Inject constructor ( override fun hide() { Log.d(ControlsUiController.TAG, "hide()") hidden = true - popup?.dismiss() - activeDialog?.dismiss() - ControlActionCoordinator.closeDialog() + popup?.dismissImmediate() + controlViewsById.forEach { + it.value.dismiss() + } + controlActionCoordinator.closeDialogs() controlsController.get().unsubscribe() parent.removeAllViews() controlsById.clear() controlViewsById.clear() + controlsListingController.get().removeCallback(listingCallback) RenderInfo.clearCache() } override fun onRefreshState(componentName: ComponentName, controls: List<Control>) { - Log.d(ControlsUiController.TAG, "onRefreshState()") controls.forEach { c -> controlsById.get(ControlKey(componentName, c.getControlId()))?.let { Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId()) @@ -552,23 +583,7 @@ class ControlsUiControllerImpl @Inject constructor ( override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) { val key = ControlKey(componentName, controlId) uiExecutor.execute { - controlViewsById.get(key)?.let { cvh -> - when (response) { - ControlAction.RESPONSE_CHALLENGE_PIN -> { - activeDialog = ChallengeDialogs.createPinDialog(cvh, false) - activeDialog?.show() - } - ControlAction.RESPONSE_CHALLENGE_PASSPHRASE -> { - activeDialog = ChallengeDialogs.createPinDialog(cvh, true) - activeDialog?.show() - } - ControlAction.RESPONSE_CHALLENGE_ACK -> { - activeDialog = ChallengeDialogs.createConfirmationDialog(cvh) - activeDialog?.show() - } - else -> cvh.actionResponse(response) - } - } + controlViewsById.get(key)?.actionResponse(response) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt index 15c41a2005a6..65ed9678c63e 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt @@ -24,9 +24,9 @@ import android.provider.Settings import android.view.View import android.view.ViewGroup import android.view.WindowInsets +import android.view.WindowInsets.Type import android.view.WindowManager import android.widget.ImageView -import android.widget.TextView import com.android.systemui.R @@ -45,7 +45,7 @@ class DetailDialog( private const val PANEL_TOP_OFFSET = "systemui.controls_panel_top_offset" } - lateinit var activityView: ActivityView + var activityView = ActivityView(context, null, 0, false) val stateCallback: ActivityView.StateCallback = object : ActivityView.StateCallback() { override fun onActivityViewReady(view: ActivityView) { @@ -67,10 +67,8 @@ class DetailDialog( init { window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY) - setContentView(R.layout.controls_detail_dialog) - activityView = ActivityView(context, null, 0, false) requireViewById<ViewGroup>(R.id.controls_activity_view).apply { addView(activityView) } @@ -79,14 +77,6 @@ class DetailDialog( setOnClickListener { _: View -> dismiss() } } - requireViewById<TextView>(R.id.title).apply { - setText(cvh.title.text) - } - - requireViewById<TextView>(R.id.subtitle).apply { - setText(cvh.subtitle.text) - } - requireViewById<ImageView>(R.id.control_detail_open_in_app).apply { setOnClickListener { v: View -> dismiss() @@ -97,15 +87,15 @@ class DetailDialog( // consume all insets to achieve slide under effect window.getDecorView().setOnApplyWindowInsetsListener { - v: View, insets: WindowInsets -> + _: View, insets: WindowInsets -> activityView.apply { val l = getPaddingLeft() val t = getPaddingTop() val r = getPaddingRight() - setPadding(l, t, r, insets.getSystemWindowInsets().bottom) + setPadding(l, t, r, insets.getInsets(Type.systemBars()).bottom) } - insets.consumeSystemWindowInsets() + WindowInsets.CONSUMED } requireViewById<ViewGroup>(R.id.control_detail_root).apply { @@ -118,6 +108,9 @@ class DetailDialog( val lp = getLayoutParams() as ViewGroup.MarginLayoutParams lp.topMargin = offsetInPx setLayoutParams(lp) + + setOnClickListener { dismiss() } + (getParent() as View).setOnClickListener { dismiss() } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/UnknownBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt index c3572491f9f1..49c44088ce3d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/UnknownBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt @@ -16,7 +16,11 @@ package com.android.systemui.controls.ui -class UnknownBehavior : Behavior { +import android.service.controls.Control + +import com.android.systemui.R + +class StatusBehavior : Behavior { lateinit var cvh: ControlViewHolder override fun initialize(cvh: ControlViewHolder) { @@ -24,7 +28,13 @@ class UnknownBehavior : Behavior { } override fun bind(cws: ControlWithState) { - cvh.status.setText(cvh.context.getString(com.android.internal.R.string.loading)) + val status = cws.control?.status ?: Control.STATUS_UNKNOWN + val msg = when (status) { + Control.STATUS_ERROR -> R.string.controls_error_generic + Control.STATUS_NOT_FOUND -> R.string.controls_error_removed + else -> com.android.internal.R.string.loading + } + cvh.status.setText(cvh.context.getString(msg)) cvh.applyRenderInfo(false) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt index 6340db1d756d..b4d0e6349605 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt @@ -22,8 +22,8 @@ import android.service.controls.Control import android.service.controls.templates.TemperatureControlTemplate import com.android.systemui.R -import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL -import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL +import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL +import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL class TemperatureControlBehavior : Behavior { lateinit var clipLayer: Drawable @@ -35,7 +35,7 @@ class TemperatureControlBehavior : Behavior { this.cvh = cvh cvh.layout.setOnClickListener { _ -> - ControlActionCoordinator.touch(cvh, template.getTemplateId(), control) + cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt index a3368ef77a56..3e1669898a7f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt @@ -18,13 +18,11 @@ package com.android.systemui.controls.ui import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable -import android.view.View import android.service.controls.Control import android.service.controls.templates.ToggleTemplate - +import android.view.View import com.android.systemui.R -import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL -import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL +import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL class ToggleBehavior : Behavior { lateinit var clipLayer: Drawable @@ -34,10 +32,10 @@ class ToggleBehavior : Behavior { override fun initialize(cvh: ControlViewHolder) { this.cvh = cvh - cvh.applyRenderInfo(false) + cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */) cvh.layout.setOnClickListener(View.OnClickListener() { - ControlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked()) + cvh.controlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked()) }) } @@ -49,9 +47,9 @@ class ToggleBehavior : Behavior { val ld = cvh.layout.getBackground() as LayerDrawable clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) + clipLayer.level = MAX_LEVEL val checked = template.isChecked() - clipLayer.setLevel(if (checked) MAX_LEVEL else MIN_LEVEL) cvh.applyRenderInfo(checked) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt index 5a956653285c..bfc06450b360 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt @@ -16,11 +16,20 @@ package com.android.systemui.controls.ui -import android.os.Bundle +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator import android.content.Context import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable +import android.os.Bundle +import android.service.controls.Control +import android.service.controls.actions.FloatAction +import android.service.controls.templates.RangeTemplate +import android.service.controls.templates.ToggleRangeTemplate import android.util.Log +import android.util.MathUtils +import android.util.TypedValue import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent @@ -29,19 +38,14 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.widget.TextView -import android.service.controls.Control -import android.service.controls.actions.FloatAction -import android.service.controls.templates.RangeTemplate -import android.service.controls.templates.ToggleRangeTemplate -import android.util.TypedValue - +import com.android.systemui.Interpolators import com.android.systemui.R -import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL -import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL - +import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL +import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL import java.util.IllegalFormatException class ToggleRangeBehavior : Behavior { + private var rangeAnimator: ValueAnimator? = null lateinit var clipLayer: Drawable lateinit var template: ToggleRangeTemplate lateinit var control: Control @@ -61,20 +65,21 @@ class ToggleRangeBehavior : Behavior { status = cvh.status context = status.getContext() - cvh.applyRenderInfo(false) + cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */) val gestureListener = ToggleRangeGestureListener(cvh.layout) val gestureDetector = GestureDetector(context, gestureListener) cvh.layout.setOnTouchListener { v: View, e: MotionEvent -> if (gestureDetector.onTouchEvent(e)) { - return@setOnTouchListener true + // Don't return true to let the state list change to "pressed" + return@setOnTouchListener false } if (e.getAction() == MotionEvent.ACTION_UP && gestureListener.isDragging) { v.getParent().requestDisallowInterceptTouchEvent(false) gestureListener.isDragging = false endUpdateRange() - return@setOnTouchListener true + return@setOnTouchListener false } return@setOnTouchListener false @@ -87,17 +92,18 @@ class ToggleRangeBehavior : Behavior { currentStatusText = control.getStatusText() status.setText(currentStatusText) + // ControlViewHolder sets a long click listener, but we want to handle touch in + // here instead, otherwise we'll have state conflicts. + cvh.layout.setOnLongClickListener(null) + val ld = cvh.layout.getBackground() as LayerDrawable clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) - clipLayer.setLevel(MIN_LEVEL) template = control.getControlTemplate() as ToggleRangeTemplate rangeTemplate = template.getRange() val checked = template.isChecked() - val currentRatio = rangeTemplate.getCurrentValue() / - (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue()) - updateRange(currentRatio, checked, /* isDragging */ false) + updateRange(rangeToLevelValue(rangeTemplate.currentValue), checked, /* isDragging */ false) cvh.applyRenderInfo(checked) @@ -135,7 +141,7 @@ class ToggleRangeBehavior : Behavior { ): Boolean { val handled = when (action) { AccessibilityNodeInfo.ACTION_CLICK -> { - ControlActionCoordinator.toggle(cvh, template.getTemplateId(), + cvh.controlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked()) true } @@ -146,9 +152,8 @@ class ToggleRangeBehavior : Behavior { } else { val value = arguments.getFloat( AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE) - val ratioDiff = (value - rangeTemplate.getCurrentValue()) / - (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue()) - updateRange(ratioDiff, template.isChecked(), /* isDragging */ false) + val level = rangeToLevelValue(value - rangeTemplate.getCurrentValue()) + updateRange(level, template.isChecked(), /* isDragging */ false) endUpdateRange() true } @@ -170,15 +175,37 @@ class ToggleRangeBehavior : Behavior { fun beginUpdateRange() { status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources() .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat()) + cvh.controlActionCoordinator.setFocusedElement(cvh) } - fun updateRange(ratioDiff: Float, checked: Boolean, isDragging: Boolean) { - val changeAmount = if (checked) (MAX_LEVEL * ratioDiff).toInt() else MIN_LEVEL - val newLevel = Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, clipLayer.getLevel() + changeAmount)) - clipLayer.setLevel(newLevel) + fun updateRange(level: Int, checked: Boolean, isDragging: Boolean) { + val newLevel = if (checked) Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level)) else MIN_LEVEL + + if (newLevel == clipLayer.level) return + + rangeAnimator?.cancel() + if (isDragging) { + clipLayer.level = newLevel + val isEdge = newLevel == MIN_LEVEL || newLevel == MAX_LEVEL + cvh.controlActionCoordinator.drag(isEdge) + } else { + rangeAnimator = ValueAnimator.ofInt(cvh.clipLayer.level, newLevel).apply { + addUpdateListener { + cvh.clipLayer.level = it.animatedValue as Int + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + rangeAnimator = null + } + }) + duration = ControlViewHolder.STATE_ANIMATION_DURATION + interpolator = Interpolators.CONTROL_STATE + start() + } + } if (checked) { - val newValue = levelToRangeValue(clipLayer.getLevel()) + val newValue = levelToRangeValue(newLevel) currentRangeValue = format(rangeTemplate.getFormatString().toString(), DEFAULT_FORMAT, newValue) val text = if (isDragging) { @@ -206,9 +233,13 @@ class ToggleRangeBehavior : Behavior { } private fun levelToRangeValue(i: Int): Float { - val ratio = i.toFloat() / MAX_LEVEL - return rangeTemplate.getMinValue() + - (ratio * (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue())) + return MathUtils.constrainedMap(rangeTemplate.minValue, rangeTemplate.maxValue, + MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), i.toFloat()) + } + + private fun rangeToLevelValue(i: Float): Int { + return MathUtils.constrainedMap(MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), + rangeTemplate.minValue, rangeTemplate.maxValue, i).toInt() } fun endUpdateRange() { @@ -217,6 +248,7 @@ class ToggleRangeBehavior : Behavior { status.setText("$currentStatusText $currentRangeValue") cvh.action(FloatAction(rangeTemplate.getTemplateId(), findNearestStep(levelToRangeValue(clipLayer.getLevel())))) + cvh.controlActionCoordinator.setFocusedElement(null) } fun findNearestStep(value: Float): Float { @@ -247,7 +279,10 @@ class ToggleRangeBehavior : Behavior { } override fun onLongPress(e: MotionEvent) { - ControlActionCoordinator.longPress(this@ToggleRangeBehavior.cvh) + if (isDragging) { + return + } + cvh.controlActionCoordinator.longPress(this@ToggleRangeBehavior.cvh) } override fun onScroll( @@ -256,20 +291,25 @@ class ToggleRangeBehavior : Behavior { xDiff: Float, yDiff: Float ): Boolean { + if (!template.isChecked) { + return false + } if (!isDragging) { v.getParent().requestDisallowInterceptTouchEvent(true) this@ToggleRangeBehavior.beginUpdateRange() isDragging = true } - this@ToggleRangeBehavior.updateRange(-xDiff / v.getWidth(), - /* checked */ true, /* isDragging */ true) + val ratioDiff = -xDiff / v.width + val changeAmount = ((MAX_LEVEL - MIN_LEVEL) * ratioDiff).toInt() + this@ToggleRangeBehavior.updateRange(clipLayer.level + changeAmount, + checked = true, isDragging = true) return true } override fun onSingleTapUp(e: MotionEvent): Boolean { val th = this@ToggleRangeBehavior - ControlActionCoordinator.toggle(th.cvh, th.template.getTemplateId(), + cvh.controlActionCoordinator.toggle(th.cvh, th.template.getTemplateId(), th.template.isChecked()) return true } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt index b02c9c8972fc..7ae3df751419 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt @@ -23,7 +23,7 @@ import android.service.controls.Control import android.service.controls.templates.ControlTemplate import com.android.systemui.R -import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL +import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL /** * Supports touch events, but has no notion of state as the {@link ToggleBehavior} does. Must be @@ -37,10 +37,10 @@ class TouchBehavior : Behavior { override fun initialize(cvh: ControlViewHolder) { this.cvh = cvh - cvh.applyRenderInfo(false) + cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */) cvh.layout.setOnClickListener(View.OnClickListener() { - ControlActionCoordinator.touch(cvh, template.getTemplateId(), control) + cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control) }) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/Vibrations.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/Vibrations.kt new file mode 100644 index 000000000000..a97113cc598b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/Vibrations.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.controls.ui + +import android.os.VibrationEffect +import android.os.VibrationEffect.Composition.PRIMITIVE_TICK + +object Vibrations { + private const val TOGGLE_TICK_COUNT = 12 + + val toggleOnEffect = initToggleOnEffect() + val toggleOffEffect = initToggleOffEffect() + val rangeEdgeEffect = initRangeEdgeEffect() + val rangeMiddleEffect = initRangeMiddleEffect() + + private fun initToggleOnEffect(): VibrationEffect { + val composition = VibrationEffect.startComposition() + var i = 0 + while (i++ < TOGGLE_TICK_COUNT) { + composition.addPrimitive(PRIMITIVE_TICK, 0.05f, 0) + } + composition.addPrimitive(PRIMITIVE_TICK, 0.5f, 100) + return composition.compose() + } + + private fun initToggleOffEffect(): VibrationEffect { + val composition = VibrationEffect.startComposition() + composition.addPrimitive(PRIMITIVE_TICK, 0.5f, 0) + composition.addPrimitive(PRIMITIVE_TICK, 0.05f, 100) + var i = 0 + while (i++ < TOGGLE_TICK_COUNT) { + composition?.addPrimitive(PRIMITIVE_TICK, 0.05f, 0) + } + return composition.compose() + } + + private fun initRangeEdgeEffect(): VibrationEffect { + val composition = VibrationEffect.startComposition() + composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) + return composition.compose() + } + + private fun initRangeMiddleEffect(): VibrationEffect { + val composition = VibrationEffect.startComposition() + composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.1f) + return composition.compose() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java index 3a4b273e1c98..23bcb29923d8 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java @@ -33,6 +33,8 @@ import android.view.LayoutInflater; import android.view.WindowManager; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.util.NotificationMessagingUtil; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.ViewMediatorCallback; @@ -218,4 +220,11 @@ public class DependencyProvider { public Choreographer providesChoreographer() { return Choreographer.getInstance(); } + + /** Provides an instance of {@link com.android.internal.logging.UiEventLogger} */ + @Singleton + @Provides + static UiEventLogger provideUiEventLogger() { + return new UiEventLoggerImpl(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java index a7c40435b3ca..2b27436c85dd 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java @@ -19,8 +19,10 @@ package com.android.systemui.dagger; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.app.AlarmManager; import android.app.IActivityManager; +import android.app.IActivityTaskManager; import android.app.IWallpaperManager; import android.app.KeyguardManager; import android.app.NotificationManager; @@ -35,6 +37,7 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.hardware.SensorPrivacyManager; +import android.hardware.display.DisplayManager; import android.media.AudioManager; import android.net.ConnectivityManager; import android.net.NetworkScoreManager; @@ -111,6 +114,12 @@ public class SystemServicesModule { } @Provides + @Singleton + static DevicePolicyManager provideDevicePolicyManager(Context context) { + return context.getSystemService(DevicePolicyManager.class); + } + + @Provides @DisplayId static int provideDisplayId(Context context) { return context.getDisplayId(); @@ -118,8 +127,8 @@ public class SystemServicesModule { @Provides @Singleton - static DevicePolicyManager provideDevicePolicyManager(Context context) { - return context.getSystemService(DevicePolicyManager.class); + static DisplayManager provideDisplayManager(Context context) { + return context.getSystemService(DisplayManager.class); } @Singleton @@ -128,6 +137,12 @@ public class SystemServicesModule { return ActivityManager.getService(); } + @Singleton + @Provides + static IActivityTaskManager provideIActivityTaskManager() { + return ActivityTaskManager.getService(); + } + @Provides @Singleton static IBatteryStats provideIBatteryStats() { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 2e9ce1200c85..90cd13fd1330 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -29,6 +29,7 @@ import com.android.systemui.log.dagger.LogModule; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.Recents; +import com.android.systemui.settings.dagger.SettingsModule; import com.android.systemui.stackdivider.Divider; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; @@ -61,6 +62,7 @@ import dagger.Provides; ConcurrencyModule.class, LogModule.class, PeopleHubModule.class, + SettingsModule.class }, subcomponents = {StatusBarComponent.class, NotificationRowComponent.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java index 3f88f252bfe7..554457b3564a 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java @@ -75,6 +75,12 @@ public class DozeDockHandler implements DozeMachine.Part { public void onEvent(int dockState) { if (DEBUG) Log.d(TAG, "dock event = " + dockState); + // Only act upon state changes, otherwise we might overwrite other transitions, + // like proximity sensor initialization. + if (mDockState == dockState) { + return; + } + mDockState = dockState; if (isPulsing()) { return; diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index 18bfd899a4e7..490890f263aa 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -159,6 +159,15 @@ public class DozeMachine { mDozeHost = dozeHost; } + /** + * Clean ourselves up. + */ + public void destroy() { + for (Part part : mParts) { + part.destroy(); + } + } + /** Initializes the set of {@link Part}s. Must be called exactly once after construction. */ public void setParts(Part[] parts) { Preconditions.checkState(mParts == null); @@ -411,6 +420,9 @@ public class DozeMachine { /** Dump current state. For debugging only. */ default void dump(PrintWriter pw) {} + + /** Give the Part a chance to clean itself up. */ + default void destroy() {} } /** A wrapper interface for {@link android.service.dreams.DreamService} */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 700a8611c8bd..10776c91df84 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -164,6 +164,17 @@ public class DozeSensors { } /** + * Unregister any sensors. + */ + public void destroy() { + // Unregisters everything, which is enough to allow gc. + for (TriggerSensor triggerSensor : mSensors) { + triggerSensor.setListening(false); + } + mProximitySensor.pause(); + } + + /** * Temporarily disable some sensors to avoid turning on the device while the user is * turning it off. */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index 7cbbdd783e74..529b016aaca6 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -65,6 +65,7 @@ public class DozeService extends DreamService mPluginManager.removePluginListener(this); } super.onDestroy(); + mDozeMachine.destroy(); mDozeMachine = null; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index b3299916356c..3510e07d2cea 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -34,6 +34,9 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -45,6 +48,7 @@ import com.android.systemui.util.sensors.ProximitySensor; import com.android.systemui.util.wakelock.WakeLock; import java.io.PrintWriter; +import java.util.Optional; import java.util.function.Consumer; /** @@ -58,6 +62,8 @@ public class DozeTriggers implements DozeMachine.Part { /** adb shell am broadcast -a com.android.systemui.doze.pulse com.android.systemui */ private static final String PULSE_ACTION = "com.android.systemui.doze.pulse"; + private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); + /** * Last value sent by the wake-display sensor. * Assuming that the screen should start on. @@ -88,6 +94,62 @@ public class DozeTriggers implements DozeMachine.Part { private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); + @VisibleForTesting + public enum DozingUpdateUiEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Dozing updated due to notification.") + DOZING_UPDATE_NOTIFICATION(433), + + @UiEvent(doc = "Dozing updated due to sigmotion.") + DOZING_UPDATE_SIGMOTION(434), + + @UiEvent(doc = "Dozing updated because sensor was picked up.") + DOZING_UPDATE_SENSOR_PICKUP(435), + + @UiEvent(doc = "Dozing updated because sensor was double tapped.") + DOZING_UPDATE_SENSOR_DOUBLE_TAP(436), + + @UiEvent(doc = "Dozing updated because sensor was long squeezed.") + DOZING_UPDATE_SENSOR_LONG_SQUEEZE(437), + + @UiEvent(doc = "Dozing updated due to docking.") + DOZING_UPDATE_DOCKING(438), + + @UiEvent(doc = "Dozing updated because sensor woke up.") + DOZING_UPDATE_SENSOR_WAKEUP(439), + + @UiEvent(doc = "Dozing updated because sensor woke up the lockscreen.") + DOZING_UPDATE_SENSOR_WAKE_LOCKSCREEN(440), + + @UiEvent(doc = "Dozing updated because sensor was tapped.") + DOZING_UPDATE_SENSOR_TAP(441); + + private final int mId; + + DozingUpdateUiEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + + static DozingUpdateUiEvent fromReason(int reason) { + switch (reason) { + case 1: return DOZING_UPDATE_NOTIFICATION; + case 2: return DOZING_UPDATE_SIGMOTION; + case 3: return DOZING_UPDATE_SENSOR_PICKUP; + case 4: return DOZING_UPDATE_SENSOR_DOUBLE_TAP; + case 5: return DOZING_UPDATE_SENSOR_LONG_SQUEEZE; + case 6: return DOZING_UPDATE_DOCKING; + case 7: return DOZING_UPDATE_SENSOR_WAKEUP; + case 8: return DOZING_UPDATE_SENSOR_WAKE_LOCKSCREEN; + case 9: return DOZING_UPDATE_SENSOR_TAP; + default: return null; + } + } + } + public DozeTriggers(Context context, DozeMachine machine, DozeHost dozeHost, AlarmManager alarmManager, AmbientDisplayConfiguration config, DozeParameters dozeParameters, AsyncSensorManager sensorManager, Handler handler, @@ -111,6 +173,11 @@ public class DozeTriggers implements DozeMachine.Part { mBroadcastDispatcher = broadcastDispatcher; } + @Override + public void destroy() { + mDozeSensors.destroy(); + } + private void onNotification(Runnable onPulseSuppressedListener) { if (DozeMachine.DEBUG) { Log.d(TAG, "requestNotificationPulse"); @@ -219,6 +286,8 @@ public class DozeTriggers implements DozeMachine.Part { mMetricsLogger.write(new LogMaker(MetricsEvent.DOZING) .setType(MetricsEvent.TYPE_UPDATE) .setSubtype(reason)); + Optional.ofNullable(DozingUpdateUiEvent.fromReason(reason)) + .ifPresent(UI_EVENT_LOGGER::log); if (mDozeParameters.getDisplayNeedsBlanking()) { // Let's prepare the display to wake-up by drawing black. // This will cover the hardware wake-up sequence, where the display @@ -396,6 +465,8 @@ public class DozeTriggers implements DozeMachine.Part { // Logs request pulse reason on AOD screen. mMetricsLogger.write(new LogMaker(MetricsEvent.DOZING) .setType(MetricsEvent.TYPE_UPDATE).setSubtype(reason)); + Optional.ofNullable(DozingUpdateUiEvent.fromReason(reason)) + .ifPresent(UI_EVENT_LOGGER::log); } private boolean canPulse() { diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java index e949007a158b..b29c5b07c765 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java @@ -24,6 +24,7 @@ import com.android.systemui.plugins.GlobalActions; import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue.Callbacks; +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.ExtensionController; import com.android.systemui.statusbar.policy.ExtensionController.Extension; @@ -43,15 +44,18 @@ public class GlobalActionsComponent extends SystemUI implements Callbacks, Globa private GlobalActions mPlugin; private Extension<GlobalActions> mExtension; private IStatusBarService mBarService; + private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Inject public GlobalActionsComponent(Context context, CommandQueue commandQueue, ExtensionController extensionController, - Provider<GlobalActions> globalActionsProvider) { + Provider<GlobalActions> globalActionsProvider, + StatusBarKeyguardViewManager statusBarKeyguardViewManager) { super(context); mCommandQueue = commandQueue; mExtensionController = extensionController; mGlobalActionsProvider = globalActionsProvider; + mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; } @Override @@ -81,6 +85,7 @@ public class GlobalActionsComponent extends SystemUI implements Callbacks, Globa @Override public void handleShowGlobalActionsMenu() { + mStatusBarKeyguardViewManager.setGlobalActionsVisible(true); mExtension.get().showGlobalActions(this); } @@ -95,6 +100,7 @@ public class GlobalActionsComponent extends SystemUI implements Callbacks, Globa @Override public void onGlobalActionsHidden() { try { + mStatusBarKeyguardViewManager.setGlobalActionsVisible(false); mBarService.onGlobalActionsHidden(); } catch (RemoteException e) { } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 4db5374cd566..8123158408dd 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -16,6 +16,8 @@ package com.android.systemui.globalactions; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; +import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; @@ -43,7 +45,6 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Color; -import android.graphics.Insets; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.ConnectivityManager; @@ -75,9 +76,13 @@ import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; +import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.ListPopupWindow; +import android.widget.ListView; import android.widget.TextView; import androidx.annotation.NonNull; @@ -107,6 +112,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.controls.ControlsServiceInfo; import com.android.systemui.controls.controller.ControlsController; +import com.android.systemui.controls.management.ControlsAnimations; import com.android.systemui.controls.management.ControlsListingController; import com.android.systemui.controls.ui.ControlsUiController; import com.android.systemui.dagger.qualifiers.Background; @@ -123,7 +129,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.EmergencyDialerConstants; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.leak.RotationUtils; -import com.android.systemui.volume.SystemUIInterpolators.LogAccelerateInterpolator; import java.util.ArrayList; import java.util.List; @@ -151,19 +156,20 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, /* Valid settings for global actions keys. * see config.xml config_globalActionList */ - private static final String GLOBAL_ACTION_KEY_POWER = "power"; - private static final String GLOBAL_ACTION_KEY_AIRPLANE = "airplane"; - private static final String GLOBAL_ACTION_KEY_BUGREPORT = "bugreport"; - private static final String GLOBAL_ACTION_KEY_SILENT = "silent"; - private static final String GLOBAL_ACTION_KEY_USERS = "users"; - private static final String GLOBAL_ACTION_KEY_SETTINGS = "settings"; - private static final String GLOBAL_ACTION_KEY_LOCKDOWN = "lockdown"; - private static final String GLOBAL_ACTION_KEY_VOICEASSIST = "voiceassist"; - private static final String GLOBAL_ACTION_KEY_ASSIST = "assist"; - private static final String GLOBAL_ACTION_KEY_RESTART = "restart"; - private static final String GLOBAL_ACTION_KEY_LOGOUT = "logout"; - private static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency"; - private static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot"; + @VisibleForTesting + protected static final String GLOBAL_ACTION_KEY_POWER = "power"; + protected static final String GLOBAL_ACTION_KEY_AIRPLANE = "airplane"; + protected static final String GLOBAL_ACTION_KEY_BUGREPORT = "bugreport"; + protected static final String GLOBAL_ACTION_KEY_SILENT = "silent"; + protected static final String GLOBAL_ACTION_KEY_USERS = "users"; + protected static final String GLOBAL_ACTION_KEY_SETTINGS = "settings"; + protected static final String GLOBAL_ACTION_KEY_LOCKDOWN = "lockdown"; + protected static final String GLOBAL_ACTION_KEY_VOICEASSIST = "voiceassist"; + protected static final String GLOBAL_ACTION_KEY_ASSIST = "assist"; + protected static final String GLOBAL_ACTION_KEY_RESTART = "restart"; + protected static final String GLOBAL_ACTION_KEY_LOGOUT = "logout"; + protected static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency"; + protected static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot"; private static final String PREFS_CONTROLS_SEEDING_COMPLETED = "ControlsSeedingCompleted"; private static final String PREFS_CONTROLS_FILE = "controls_prefs"; @@ -191,13 +197,18 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, // Used for RingerModeTracker private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); - private ArrayList<Action> mItems; + @VisibleForTesting + protected final ArrayList<Action> mItems = new ArrayList<>(); + @VisibleForTesting + protected final ArrayList<Action> mOverflowItems = new ArrayList<>(); + private ActionsDialog mDialog; private Action mSilentModeAction; private ToggleAction mAirplaneModeOn; private MyAdapter mAdapter; + private MyOverflowAdapter mOverflowAdapter; private boolean mKeyguardShowing = false; private boolean mDeviceProvisioned = false; @@ -223,6 +234,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private SharedPreferences mControlsPreferences; private final RingerModeTracker mRingerModeTracker; private int mDialogPressDelay = DIALOG_PRESS_DELAY; // ms + private Handler mMainHandler; + private boolean mShowLockScreenCardsAndControls = false; @VisibleForTesting public enum GlobalActionsEvent implements UiEventLogger.UiEventEnum { @@ -277,7 +290,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, @Background Executor backgroundExecutor, ControlsListingController controlsListingController, ControlsController controlsController, UiEventLogger uiEventLogger, - RingerModeTracker ringerModeTracker) { + RingerModeTracker ringerModeTracker, @Main Handler handler) { mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme); mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; @@ -306,6 +319,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mBlurUtils = blurUtils; mRingerModeTracker = ringerModeTracker; mControlsController = controlsController; + mMainHandler = handler; // receive broadcasts IntentFilter filter = new IntentFilter(); @@ -341,10 +355,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, keyguardStateController.addCallback(new KeyguardStateController.Callback() { @Override public void onUnlockedChanged() { - if (mDialog != null && mDialog.mPanelController != null) { + if (mDialog != null) { boolean unlocked = keyguardStateController.isUnlocked() || keyguardStateController.canDismissLockScreen(); - mDialog.mPanelController.onDeviceLockStateChanged(unlocked); + if (mDialog.mPanelController != null) { + mDialog.mPanelController.onDeviceLockStateChanged(unlocked); + } + if (!mDialog.isShowingControls() && shouldShowControls()) { + mDialog.showControls(mControlsUiController); + } } } }); @@ -359,6 +378,17 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mControlsPreferences = userContext.getSharedPreferences(PREFS_CONTROLS_FILE, Context.MODE_PRIVATE); + // Listen for changes to show controls on the power menu while locked + onPowerMenuLockScreenSettingsChanged(); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.POWER_MENU_LOCKED_SHOW_CONTENT), + false /* notifyForDescendants */, + new ContentObserver(mMainHandler) { + @Override + public void onChange(boolean selfChange) { + onPowerMenuLockScreenSettingsChanged(); + } + }); } private void seedFavorites() { @@ -387,6 +417,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, if (preferredComponent == null) { Log.i(TAG, "Controls seeding: No preferred component has been set, will not seed"); mControlsPreferences.edit().putBoolean(PREFS_CONTROLS_SEEDING_COMPLETED, true).apply(); + return; } mControlsController.seedFavoritesForComponent( @@ -403,16 +434,19 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, * * @param keyguardShowing True if keyguard is showing */ - public void showDialog(boolean keyguardShowing, boolean isDeviceProvisioned, + public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned, GlobalActionsPanelPlugin panelPlugin) { mKeyguardShowing = keyguardShowing; mDeviceProvisioned = isDeviceProvisioned; mPanelPlugin = panelPlugin; - if (mDialog != null) { + if (mDialog != null && mDialog.isShowing()) { + // In order to force global actions to hide on the same affordance press, we must + // register a call to onGlobalActionsShown() first to prevent the default actions + // menu from showing. This will be followed by a subsequent call to + // onGlobalActionsHidden() on dismiss() + mWindowManagerFuncs.onGlobalActionsShown(); mDialog.dismiss(); mDialog = null; - // Show delayed, so that the dismiss of the previous dialog completes - mHandler.sendEmptyMessage(MESSAGE_SHOW); } else { handleShow(); } @@ -444,27 +478,59 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, prepareDialog(); seedFavorites(); - // If we only have 1 item and it's a simple press action, just do this action. - if (mAdapter.getCount() == 1 - && mAdapter.getItem(0) instanceof SinglePressAction - && !(mAdapter.getItem(0) instanceof LongPressAction)) { - ((SinglePressAction) mAdapter.getItem(0)).onPress(); + WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes(); + attrs.setTitle("ActionsDialog"); + attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + mDialog.getWindow().setAttributes(attrs); + mDialog.show(); + mWindowManagerFuncs.onGlobalActionsShown(); + } + + @VisibleForTesting + protected boolean shouldShowAction(Action action) { + if (mKeyguardShowing && !action.showDuringKeyguard()) { + return false; + } + if (!mDeviceProvisioned && !action.showBeforeProvisioning()) { + return false; + } + return true; + } + + /** + * Returns the maximum number of power menu items to show based on which GlobalActions + * layout is being used. + */ + @VisibleForTesting + protected int getMaxShownPowerItems() { + if (shouldUseControlsLayout()) { + return mResources.getInteger(com.android.systemui.R.integer.power_menu_max_columns); } else { - WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes(); - attrs.setTitle("ActionsDialog"); - attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - mDialog.getWindow().setAttributes(attrs); - mDialog.show(); - mWindowManagerFuncs.onGlobalActionsShown(); + return Integer.MAX_VALUE; } } /** - * Create the global actions dialog. - * - * @return A new dialog. + * Add a power menu action item for to either the main or overflow items lists, depending on + * whether controls are enabled and whether the max number of shown items has been reached. */ - private ActionsDialog createDialog() { + private void addActionItem(Action action) { + if (shouldShowAction(action)) { + if (mItems.size() < getMaxShownPowerItems()) { + mItems.add(action); + } else { + mOverflowItems.add(action); + } + } + } + + @VisibleForTesting + protected String[] getDefaultActions() { + return mResources.getStringArray(R.array.config_globalActionsList); + } + + @VisibleForTesting + protected void createActionItems() { // Simple toggle style if there's no vibrator, otherwise use a tri-state if (!mHasVibrator) { mSilentModeAction = new SilentModeToggleAction(); @@ -474,8 +540,14 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mAirplaneModeOn = new AirplaneModeAction(); onAirplaneModeChanged(); - mItems = new ArrayList<Action>(); - String[] defaultActions = mResources.getStringArray(R.array.config_globalActionsList); + mItems.clear(); + mOverflowItems.clear(); + String[] defaultActions = getDefaultActions(); + + // make sure emergency affordance action is first, if needed + if (mEmergencyAffordanceManager.needsEmergencyAffordance()) { + addActionItem(new EmergencyAffordanceAction()); + } ArraySet<String> addedKeys = new ArraySet<String>(); for (int i = 0; i < defaultActions.length; i++) { @@ -485,46 +557,46 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, continue; } if (GLOBAL_ACTION_KEY_POWER.equals(actionKey)) { - mItems.add(new PowerAction()); + addActionItem(new PowerAction()); } else if (GLOBAL_ACTION_KEY_AIRPLANE.equals(actionKey)) { - mItems.add(mAirplaneModeOn); + addActionItem(mAirplaneModeOn); } else if (GLOBAL_ACTION_KEY_BUGREPORT.equals(actionKey)) { if (Settings.Global.getInt(mContentResolver, Settings.Global.BUGREPORT_IN_POWER_MENU, 0) != 0 && isCurrentUserOwner()) { - mItems.add(new BugReportAction()); + addActionItem(new BugReportAction()); } } else if (GLOBAL_ACTION_KEY_SILENT.equals(actionKey)) { if (mShowSilentToggle) { - mItems.add(mSilentModeAction); + addActionItem(mSilentModeAction); } } else if (GLOBAL_ACTION_KEY_USERS.equals(actionKey)) { if (SystemProperties.getBoolean("fw.power_user_switcher", false)) { - addUsersToMenu(mItems); + addUsersToMenu(); } } else if (GLOBAL_ACTION_KEY_SETTINGS.equals(actionKey)) { - mItems.add(getSettingsAction()); + addActionItem(getSettingsAction()); } else if (GLOBAL_ACTION_KEY_LOCKDOWN.equals(actionKey)) { if (Settings.Secure.getIntForUser(mContentResolver, Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0, getCurrentUser().id) != 0 && shouldDisplayLockdown()) { - mItems.add(getLockdownAction()); + addActionItem(getLockdownAction()); } } else if (GLOBAL_ACTION_KEY_VOICEASSIST.equals(actionKey)) { - mItems.add(getVoiceAssistAction()); + addActionItem(getVoiceAssistAction()); } else if (GLOBAL_ACTION_KEY_ASSIST.equals(actionKey)) { - mItems.add(getAssistAction()); + addActionItem(getAssistAction()); } else if (GLOBAL_ACTION_KEY_RESTART.equals(actionKey)) { - mItems.add(new RestartAction()); + addActionItem(new RestartAction()); } else if (GLOBAL_ACTION_KEY_SCREENSHOT.equals(actionKey)) { - mItems.add(new ScreenshotAction()); + addActionItem(new ScreenshotAction()); } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) { if (mDevicePolicyManager.isLogoutEnabled() && getCurrentUser().id != UserHandle.USER_SYSTEM) { - mItems.add(new LogoutAction()); + addActionItem(new LogoutAction()); } } else if (GLOBAL_ACTION_KEY_EMERGENCY.equals(actionKey)) { if (!mEmergencyAffordanceManager.needsEmergencyAffordance()) { - mItems.add(new EmergencyDialerAction()); + addActionItem(new EmergencyDialerAction()); } } else { Log.e(TAG, "Invalid global action key " + actionKey); @@ -532,21 +604,31 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, // Add here so we don't add more than one. addedKeys.add(actionKey); } + } - if (mEmergencyAffordanceManager.needsEmergencyAffordance()) { - mItems.add(new EmergencyAffordanceAction()); - } + private void onRotate() { + // re-allocate actions between main and overflow lists + this.createActionItems(); + } - mAdapter = new MyAdapter(); + /** + * Create the global actions dialog. + * + * @return A new dialog. + */ + private ActionsDialog createDialog() { + createActionItems(); - mDepthController.setShowingHomeControls(shouldShowControls()); - ActionsDialog dialog = new ActionsDialog(mContext, mAdapter, getWalletPanelViewController(), - mDepthController, mSysuiColorExtractor, mStatusBarService, - mNotificationShadeWindowController, - shouldShowControls() ? mControlsUiController : null, mBlurUtils); + mAdapter = new MyAdapter(); + mOverflowAdapter = new MyOverflowAdapter(); + + mDepthController.setShowingHomeControls(shouldUseControlsLayout()); + ActionsDialog dialog = new ActionsDialog(mContext, mAdapter, mOverflowAdapter, + getWalletPanelViewController(), mDepthController, mSysuiColorExtractor, + mStatusBarService, mNotificationShadeWindowController, + shouldShowControls() ? mControlsUiController : null, mBlurUtils, + shouldUseControlsLayout(), this::onRotate, mKeyguardShowing); dialog.setCanceledOnTouchOutside(false); // Handled by the custom class. - dialog.setKeyguardShowing(mKeyguardShowing); - dialog.setOnDismissListener(this); dialog.setOnShowListener(this); @@ -644,7 +726,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, @Override public boolean shouldBeSeparated() { - return !shouldShowControls(); + return !shouldUseControlsLayout(); } @Override @@ -652,7 +734,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { View v = super.create(context, convertView, parent, inflater); int textColor; - if (shouldShowControls()) { + if (shouldUseControlsLayout()) { v.setBackgroundTintList(ColorStateList.valueOf(v.getResources().getColor( com.android.systemui.R.color.global_actions_emergency_background))); textColor = v.getResources().getColor( @@ -769,7 +851,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mHandler.postDelayed(new Runnable() { @Override public void run() { - mScreenshotHelper.takeScreenshot(1, true, true, mHandler, null); + mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true, + SCREENSHOT_GLOBAL_ACTIONS, mHandler, null); mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU); mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS); } @@ -1020,7 +1103,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return currentUser == null || currentUser.isPrimary(); } - private void addUsersToMenu(ArrayList<Action> items) { + private void addUsersToMenu() { if (mUserManager.isUserSwitcherEnabled()) { List<UserInfo> users = mUserManager.getUsers(); UserInfo currentUser = getCurrentUser(); @@ -1050,7 +1133,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return false; } }; - items.add(switchToUser); + addActionItem(switchToUser); } } } @@ -1092,18 +1175,14 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } private int getActionLayoutId() { - if (shouldShowControls()) { + if (shouldUseControlsLayout()) { return com.android.systemui.R.layout.global_actions_grid_item_v2; } return com.android.systemui.R.layout.global_actions_grid_item; } /** - * The adapter used for the list within the global actions dialog, taking into account whether - * the keyguard is showing via - * {@link com.android.systemui.globalactions.GlobalActionsDialog#mKeyguardShowing} - * and whether the device is provisioned via - * {@link com.android.systemui.globalactions.GlobalActionsDialog#mDeviceProvisioned}. + * The adapter used for power menu items shown in the global actions dialog. */ public class MyAdapter extends MultiListAdapter { private int countItems(boolean separated) { @@ -1111,23 +1190,13 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, for (int i = 0; i < mItems.size(); i++) { final Action action = mItems.get(i); - if (shouldBeShown(action) && action.shouldBeSeparated() == separated) { + if (action.shouldBeSeparated() == separated) { count++; } } return count; } - private boolean shouldBeShown(Action action) { - if (mKeyguardShowing && !action.showDuringKeyguard()) { - return false; - } - if (!mDeviceProvisioned && !action.showBeforeProvisioning()) { - return false; - } - return true; - } - @Override public int countSeparatedItems() { return countItems(true); @@ -1158,7 +1227,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, int filteredPos = 0; for (int i = 0; i < mItems.size(); i++) { final Action action = mItems.get(i); - if (!shouldBeShown(action)) { + if (!shouldShowAction(action)) { continue; } if (filteredPos == position) { @@ -1223,6 +1292,79 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } + /** + * The adapter used for items in the overflow menu. + */ + public class MyOverflowAdapter extends BaseAdapter { + @Override + public int getCount() { + return mOverflowItems.size(); + } + + @Override + public Action getItem(int position) { + return mOverflowItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Action action = getItem(position); + if (action == null) { + Log.w(TAG, "No overflow action found at position: " + position); + return null; + } + int viewLayoutResource = com.android.systemui.R.layout.controls_more_item; + View view = convertView != null ? convertView + : LayoutInflater.from(mContext).inflate(viewLayoutResource, parent, false); + TextView textView = (TextView) view; + textView.setOnClickListener(v -> onClickItem(position)); + if (action.getMessageResId() != 0) { + textView.setText(action.getMessageResId()); + } else { + textView.setText(action.getMessage()); + } + + if (action instanceof LongPressAction) { + textView.setOnLongClickListener(v -> onLongClickItem(position)); + } else { + textView.setOnLongClickListener(null); + } + return textView; + } + + private boolean onLongClickItem(int position) { + final Action action = getItem(position); + if (action instanceof LongPressAction) { + if (mDialog != null) { + mDialog.hidePowerOverflowMenu(); + mDialog.dismiss(); + } else { + Log.w(TAG, "Action long-clicked while mDialog is null."); + } + return ((LongPressAction) action).onLongPress(); + } + return false; + } + + private void onClickItem(int position) { + Action item = getItem(position); + if (!(item instanceof SilentModeTriStateAction)) { + if (mDialog != null) { + mDialog.hidePowerOverflowMenu(); + mDialog.dismiss(); + } else { + Log.w(TAG, "Action clicked while mDialog is null."); + } + item.onPress(); + } + } + } + // note: the scheme below made more sense when we were planning on having // 8 different things in the global actions dialog. seems overkill with // only 3 items now, but may as well keep this flexible approach so it will @@ -1248,8 +1390,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, boolean showDuringKeyguard(); /** - * @return whether this action should appear in the dialog before the device is - * provisioned.onlongpress + * @return whether this action should appear in the dialog before the + * device is provisioned.f */ boolean showBeforeProvisioning(); @@ -1258,6 +1400,18 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, default boolean shouldBeSeparated() { return false; } + + /** + * Return the id of the message associated with this action, or 0 if it doesn't have one. + * @return + */ + int getMessageResId(); + + /** + * Return the message associated with this action, or null if it doesn't have one. + * @return + */ + CharSequence getMessage(); } /** @@ -1309,6 +1463,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } + + public int getMessageResId() { + return mMessageResId; + } + + public CharSequence getMessage() { + return mMessage; + } + public View create( Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { View v = inflater.inflate(getActionLayoutId(), parent, false /* attach */); @@ -1396,6 +1559,23 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return context.getString(mMessageResId); } + private boolean isOn() { + return mState == ToggleState.On || mState == ToggleState.TurningOn; + } + + @Override + public CharSequence getMessage() { + return null; + } + @Override + public int getMessageResId() { + return isOn() ? mEnabledStatusMessageResId : mDisabledStatusMessageResId; + } + + private int getIconResId() { + return isOn() ? mEnabledIconResId : mDisabledIconResid; + } + public View create(Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { willCreate(); @@ -1405,17 +1585,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, ImageView icon = (ImageView) v.findViewById(R.id.icon); TextView messageView = (TextView) v.findViewById(R.id.message); final boolean enabled = isEnabled(); - boolean on = ((mState == ToggleState.On) || (mState == ToggleState.TurningOn)); if (messageView != null) { - messageView.setText(on ? mEnabledStatusMessageResId : mDisabledStatusMessageResId); + messageView.setText(getMessageResId()); messageView.setEnabled(enabled); messageView.setSelected(true); // necessary for marquee to work } if (icon != null) { - icon.setImageDrawable(context.getDrawable( - (on ? mEnabledIconResId : mDisabledIconResid))); + icon.setImageDrawable(context.getDrawable(getIconResId())); icon.setEnabled(enabled); } @@ -1553,6 +1731,16 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return null; } + @Override + public int getMessageResId() { + return 0; + } + + @Override + public CharSequence getMessage() { + return null; + } + public View create(Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { View v = inflater.inflate(R.layout.global_actions_silent_mode, parent, false); @@ -1627,7 +1815,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } }; - private ContentObserver mAirplaneModeObserver = new ContentObserver(new Handler()) { + private ContentObserver mAirplaneModeObserver = new ContentObserver(mMainHandler) { @Override public void onChange(boolean selfChange) { onAirplaneModeChanged(); @@ -1636,7 +1824,6 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private static final int MESSAGE_DISMISS = 0; private static final int MESSAGE_REFRESH = 1; - private static final int MESSAGE_SHOW = 2; private static final int DIALOG_DISMISS_DELAY = 300; // ms private static final int DIALOG_PRESS_DELAY = 850; // ms @@ -1650,7 +1837,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, case MESSAGE_DISMISS: if (mDialog != null) { if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) { - mDialog.dismissImmediately(); + mDialog.completeDismiss(); } else { mDialog.dismiss(); } @@ -1661,9 +1848,6 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, refreshSilentMode(); mAdapter.notifyDataSetChanged(); break; - case MESSAGE_SHOW: - handleShow(); - break; } } }; @@ -1708,6 +1892,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private final Context mContext; private final MyAdapter mAdapter; + private final MyOverflowAdapter mOverflowAdapter; private final IStatusBarService mStatusBarService; private final IBinder mToken = new Binder(); private MultiListLayout mGlobalActionsLayout; @@ -1722,25 +1907,34 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private final NotificationShadeWindowController mNotificationShadeWindowController; private final NotificationShadeDepthController mDepthController; private final BlurUtils mBlurUtils; + private final boolean mUseControlsLayout; + private ListPopupWindow mOverflowPopup; + private final Runnable mOnRotateCallback; private ControlsUiController mControlsUiController; private ViewGroup mControlsView; + private ViewGroup mContainer; - ActionsDialog(Context context, MyAdapter adapter, + ActionsDialog(Context context, MyAdapter adapter, MyOverflowAdapter overflowAdapter, GlobalActionsPanelPlugin.PanelViewController plugin, NotificationShadeDepthController depthController, SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, - ControlsUiController controlsUiController, BlurUtils blurUtils) { + ControlsUiController controlsUiController, BlurUtils blurUtils, + boolean useControlsLayout, Runnable onRotateCallback, boolean keyguardShowing) { super(context, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions); mContext = context; mAdapter = adapter; + mOverflowAdapter = overflowAdapter; mDepthController = depthController; mColorExtractor = sysuiColorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; mControlsUiController = controlsUiController; mBlurUtils = blurUtils; + mUseControlsLayout = useControlsLayout; + mOnRotateCallback = onRotateCallback; + mKeyguardShowing = keyguardShowing; // Window initialization Window window = getWindow(); @@ -1767,6 +1961,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, initializeLayout(); } + private boolean isShowingControls() { + return mControlsUiController != null; + } + + private void showControls(ControlsUiController controller) { + mControlsUiController = controller; + mControlsUiController.show(mControlsView, this::dismissForControlsActivity); + } + private boolean shouldUsePanel() { return mPanelController != null && mPanelController.getPanelContent() != null; } @@ -1817,12 +2020,50 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } + private ListPopupWindow createPowerOverflowPopup() { + ListPopupWindow popup = new ListPopupWindow(new ContextThemeWrapper( + mContext, com.android.systemui.R.style.Control_ListPopupWindow)); + popup.setWindowLayoutType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); + View overflowButton = + findViewById(com.android.systemui.R.id.global_actions_overflow_button); + popup.setAnchorView(overflowButton); + int parentWidth = mGlobalActionsLayout.getWidth(); + // arbitrarily set the menu width to half of parent + // TODO: Logic for menu sizing based on contents. + int halfParentWidth = Math.round(parentWidth * 0.5f); + popup.setContentWidth(halfParentWidth); + popup.setAdapter(mOverflowAdapter); + popup.setModal(true); + return popup; + } + + private void showPowerOverflowMenu() { + mOverflowPopup.show(); + + // Width is fixed to slightly more than half of the GlobalActionsLayout container. + // TODO: Resize the width of this dialog based on the sizes of the items in it. + int width = Math.round(mGlobalActionsLayout.getWidth() * 0.6f); + + ListView listView = mOverflowPopup.getListView(); + listView.setDividerHeight(mContext.getResources() + .getDimensionPixelSize(com.android.systemui.R.dimen.control_list_divider)); + listView.setDivider(mContext.getResources().getDrawable( + com.android.systemui.R.drawable.controls_list_divider)); + mOverflowPopup.setWidth(width); + mOverflowPopup.setHorizontalOffset(-width + mOverflowPopup.getAnchorView().getWidth()); + mOverflowPopup.setVerticalOffset(mOverflowPopup.getAnchorView().getHeight()); + mOverflowPopup.show(); + } + + private void hidePowerOverflowMenu() { + mOverflowPopup.dismiss(); + } + private void initializeLayout() { setContentView(getGlobalActionsLayoutId(mContext)); fixNavBarClipping(); mControlsView = findViewById(com.android.systemui.R.id.global_actions_controls); mGlobalActionsLayout = findViewById(com.android.systemui.R.id.global_actions_view); - mGlobalActionsLayout.setOutsideTouchListener(view -> dismiss()); mGlobalActionsLayout.setListViewAccessibilityDelegate(new View.AccessibilityDelegate() { @Override public boolean dispatchPopulateAccessibilityEvent( @@ -1834,14 +2075,31 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, }); mGlobalActionsLayout.setRotationListener(this::onRotate); mGlobalActionsLayout.setAdapter(mAdapter); + mContainer = findViewById(com.android.systemui.R.id.global_actions_container); + // Some legacy dialog layouts don't have the outer container + if (mContainer == null) { + mContainer = mGlobalActionsLayout; + } - View globalActionsParent = (View) mGlobalActionsLayout.getParent(); - globalActionsParent.setOnClickListener(v -> dismiss()); - - // add fall-through dismiss handling to root view - View rootView = findViewById(com.android.systemui.R.id.global_actions_grid_root); - if (rootView != null) { - rootView.setOnClickListener(v -> dismiss()); + mOverflowPopup = createPowerOverflowPopup(); + + View overflowButton = findViewById( + com.android.systemui.R.id.global_actions_overflow_button); + if (overflowButton != null) { + if (mOverflowAdapter.getCount() > 0) { + overflowButton.setOnClickListener((view) -> showPowerOverflowMenu()); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); + params.setMarginEnd(0); + mGlobalActionsLayout.setLayoutParams(params); + } else { + overflowButton.setVisibility(View.GONE); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); + params.setMarginEnd(mContext.getResources().getDimensionPixelSize( + com.android.systemui.R.dimen.global_actions_side_margin)); + mGlobalActionsLayout.setLayoutParams(params); + } } if (shouldUsePanel()) { @@ -1849,7 +2107,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } if (mBackgroundDrawable == null) { mBackgroundDrawable = new ScrimDrawable(); - if (mControlsUiController != null) { + if (mUseControlsLayout) { mScrimAlpha = 1.0f; } else { mScrimAlpha = mBlurUtils.supportsBlursOnWindows() @@ -1869,7 +2127,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } private int getGlobalActionsLayoutId(Context context) { - if (mControlsUiController != null) { + if (mUseControlsLayout) { return com.android.systemui.R.layout.global_actions_grid_v2; } @@ -1914,9 +2172,9 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, if (!(mBackgroundDrawable instanceof ScrimDrawable)) { return; } - boolean hasControls = mControlsUiController != null; ((ScrimDrawable) mBackgroundDrawable).setColor( - !hasControls && colors.supportsDarkText() ? Color.WHITE : Color.BLACK, animate); + !mUseControlsLayout && colors.supportsDarkText() + ? Color.WHITE : Color.BLACK, animate); View decorView = getWindow().getDecorView(); if (colors.supportsDarkText()) { decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | @@ -1939,10 +2197,10 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mHadTopUi = mNotificationShadeWindowController.getForceHasTopUi(); mNotificationShadeWindowController.setForceHasTopUi(true); mBackgroundDrawable.setAlpha(0); - mGlobalActionsLayout.setTranslationX(mGlobalActionsLayout.getAnimationOffsetX()); - mGlobalActionsLayout.setTranslationY(mGlobalActionsLayout.getAnimationOffsetY()); - mGlobalActionsLayout.setAlpha(0); - mGlobalActionsLayout.animate() + mContainer.setTranslationX(mGlobalActionsLayout.getAnimationOffsetX()); + mContainer.setTranslationY(mGlobalActionsLayout.getAnimationOffsetY()); + mContainer.setAlpha(0); + mContainer.animate() .alpha(1) .translationX(0) .translationY(0) @@ -1958,55 +2216,64 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, .start(); ViewGroup root = (ViewGroup) mGlobalActionsLayout.getRootView(); root.setOnApplyWindowInsetsListener((v, windowInsets) -> { - if (mControlsUiController != null) { - Insets insets = windowInsets.getInsets(WindowInsets.Type.all()); - root.setPadding(insets.left, insets.top, insets.right, insets.bottom); + if (mUseControlsLayout) { + root.setPadding(windowInsets.getStableInsetLeft(), + windowInsets.getStableInsetTop(), + windowInsets.getStableInsetRight(), + windowInsets.getStableInsetBottom()); } return WindowInsets.CONSUMED; }); if (mControlsUiController != null) { - mControlsUiController.show(mControlsView); + mControlsUiController.show(mControlsView, this::dismissForControlsActivity); } } @Override public void dismiss() { + dismissWithAnimation(() -> { + mContainer.setTranslationX(0); + mContainer.setTranslationY(0); + mContainer.setAlpha(1); + mContainer.animate() + .alpha(0) + .translationX(mGlobalActionsLayout.getAnimationOffsetX()) + .translationY(mGlobalActionsLayout.getAnimationOffsetY()) + .setDuration(450) + .withEndAction(this::completeDismiss) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setUpdateListener(animation -> { + float animatedValue = 1f - animation.getAnimatedFraction(); + int alpha = (int) (animatedValue * mScrimAlpha * 255); + mBackgroundDrawable.setAlpha(alpha); + mDepthController.updateGlobalDialogVisibility(animatedValue, + mGlobalActionsLayout); + }) + .start(); + }); + } + + private void dismissForControlsActivity() { + dismissWithAnimation(() -> { + ViewGroup root = (ViewGroup) mGlobalActionsLayout.getParent(); + ControlsAnimations.exitAnimation(root, this::completeDismiss).start(); + }); + } + + void dismissWithAnimation(Runnable animation) { if (!mShowing) { return; } mShowing = false; - if (mControlsUiController != null) mControlsUiController.hide(); - mGlobalActionsLayout.setTranslationX(0); - mGlobalActionsLayout.setTranslationY(0); - mGlobalActionsLayout.setAlpha(1); - mGlobalActionsLayout.animate() - .alpha(0) - .translationX(mGlobalActionsLayout.getAnimationOffsetX()) - .translationY(mGlobalActionsLayout.getAnimationOffsetY()) - .setDuration(550) - .withEndAction(this::completeDismiss) - .setInterpolator(new LogAccelerateInterpolator()) - .setUpdateListener(animation -> { - float animatedValue = 1f - animation.getAnimatedFraction(); - int alpha = (int) (animatedValue * mScrimAlpha * 255); - mBackgroundDrawable.setAlpha(alpha); - mDepthController.updateGlobalDialogVisibility(animatedValue, - mGlobalActionsLayout); - }) - .start(); - dismissPanel(); - resetOrientation(); + animation.run(); } - void dismissImmediately() { + private void completeDismiss() { mShowing = false; - if (mControlsUiController != null) mControlsUiController.hide(); - dismissPanel(); resetOrientation(); - completeDismiss(); - } - - private void completeDismiss() { + dismissPanel(); + dismissOverflow(true); + if (mControlsUiController != null) mControlsUiController.hide(); mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi); mDepthController.updateGlobalDialogVisibility(0, null /* view */); super.dismiss(); @@ -2018,6 +2285,16 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } + private void dismissOverflow(boolean immediate) { + if (mOverflowPopup != null) { + if (immediate) { + mOverflowPopup.dismissImmediate(); + } else { + mOverflowPopup.dismiss(); + } + } + } + private void setRotationSuggestionsEnabled(boolean enabled) { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); @@ -2060,10 +2337,14 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, public void refreshDialog() { initializeLayout(); mGlobalActionsLayout.updateList(); + if (mControlsUiController != null) { + mControlsUiController.show(mControlsView, this::dismissForControlsActivity); + } } public void onRotate(int from, int to) { if (mShowing) { + mOnRotateCallback.run(); refreshDialog(); } } @@ -2090,9 +2371,22 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return isPanelDebugModeEnabled(context); } - private boolean shouldShowControls() { - return mKeyguardStateController.isUnlocked() + @VisibleForTesting + protected boolean shouldShowControls() { + boolean isUnlocked = mKeyguardStateController.isUnlocked() + || mKeyguardStateController.canDismissLockScreen(); + return (isUnlocked || mShowLockScreenCardsAndControls) && mControlsUiController.getAvailable() && !mControlsServiceInfos.isEmpty(); } + // TODO: Remove legacy layout XML and classes. + protected boolean shouldUseControlsLayout() { + // always use new controls layout + return true; + } + + private void onPowerMenuLockScreenSettingsChanged() { + mShowLockScreenCardsAndControls = Settings.Secure.getInt(mContentResolver, + Settings.Secure.POWER_MENU_LOCKED_SHOW_CONTENT, 0) != 0; + } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsFlatLayout.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsFlatLayout.java index f1025615783b..2f32d972449e 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsFlatLayout.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsFlatLayout.java @@ -32,7 +32,6 @@ import com.android.systemui.R; * Flat, single-row implementation of the button layout created by the global actions dialog. */ public class GlobalActionsFlatLayout extends GlobalActionsLayout { - private static final int MAX_ITEMS = 4; public GlobalActionsFlatLayout(Context context, AttributeSet attrs) { super(context, attrs); } @@ -54,11 +53,28 @@ public class GlobalActionsFlatLayout extends GlobalActionsLayout { return null; } + private View getOverflowButton() { + return findViewById(com.android.systemui.R.id.global_actions_overflow_button); + } + @Override protected void addToListView(View v, boolean reverse) { - // only add items to the list view if we haven't hit our max yet - if (getListView().getChildCount() < MAX_ITEMS) { - super.addToListView(v, reverse); + super.addToListView(v, reverse); + View overflowButton = getOverflowButton(); + // if there's an overflow button, make sure it stays at the end + if (overflowButton != null) { + getListView().removeView(overflowButton); + super.addToListView(overflowButton, reverse); + } + } + + @Override + protected void removeAllListViews() { + View overflowButton = getOverflowButton(); + super.removeAllListViews(); + // if there's an overflow button, add it back after clearing the list views + if (overflowButton != null) { + super.addToListView(overflowButton, false); } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java index 15cf1a060f4c..09757a4d6204 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java @@ -88,7 +88,7 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks public void showGlobalActions(GlobalActionsManager manager) { if (mDisabled) return; mGlobalActionsDialog = mGlobalActionsDialogLazy.get(); - mGlobalActionsDialog.showDialog(mKeyguardStateController.isShowing(), + mGlobalActionsDialog.showOrHideDialog(mKeyguardStateController.isShowing(), mDeviceProvisionedController.isDeviceProvisioned(), mPanelExtension.get()); Dependency.get(KeyguardUpdateMonitor.class).requestFaceAuth(); diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/GLWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/GLWallpaperRenderer.java index 88ab9ef4b014..61524900b89b 100644 --- a/packages/SystemUI/src/com/android/systemui/glwallpaper/GLWallpaperRenderer.java +++ b/packages/SystemUI/src/com/android/systemui/glwallpaper/GLWallpaperRenderer.java @@ -49,20 +49,6 @@ public interface GLWallpaperRenderer { void onDrawFrame(); /** - * Notify ambient mode is changed. - * @param inAmbientMode true if in ambient mode. - * @param duration duration of transition. - */ - void updateAmbientMode(boolean inAmbientMode, long duration); - - /** - * Notify the wallpaper offsets changed. - * @param xOffset offset along x axis. - * @param yOffset offset along y axis. - */ - void updateOffsets(float xOffset, float yOffset); - - /** * Ask renderer to report the surface size it needs. */ Size reportSurfaceSize(); @@ -81,24 +67,4 @@ public interface GLWallpaperRenderer { */ void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args); - /** - * A proxy which owns surface holder. - */ - interface SurfaceProxy { - - /** - * Ask proxy to start rendering frame to surface. - */ - void requestRender(); - - /** - * Ask proxy to prepare render context. - */ - void preRender(); - - /** - * Ask proxy to destroy render context. - */ - void postRender(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java index 626d0cfed997..fa45ea1acb95 100644 --- a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageGLWallpaper.java @@ -33,7 +33,6 @@ import static android.opengl.GLES20.glUniform1i; import static android.opengl.GLES20.glVertexAttribPointer; import android.graphics.Bitmap; -import android.graphics.Rect; import android.opengl.GLUtils; import android.util.Log; @@ -50,14 +49,9 @@ import java.nio.FloatBuffer; class ImageGLWallpaper { private static final String TAG = ImageGLWallpaper.class.getSimpleName(); - static final String A_POSITION = "aPosition"; - static final String A_TEXTURE_COORDINATES = "aTextureCoordinates"; - static final String U_PER85 = "uPer85"; - static final String U_REVEAL = "uReveal"; - static final String U_AOD2OPACITY = "uAod2Opacity"; - static final String U_TEXTURE = "uTexture"; - - private static final int HANDLE_UNDEFINED = -1; + private static final String A_POSITION = "aPosition"; + private static final String A_TEXTURE_COORDINATES = "aTextureCoordinates"; + private static final String U_TEXTURE = "uTexture"; private static final int POSITION_COMPONENT_COUNT = 2; private static final int TEXTURE_COMPONENT_COUNT = 2; private static final int BYTES_PER_FLOAT = 4; @@ -88,14 +82,9 @@ class ImageGLWallpaper { private int mAttrPosition; private int mAttrTextureCoordinates; - private int mUniAod2Opacity; - private int mUniPer85; - private int mUniReveal; private int mUniTexture; private int mTextureId; - private float[] mCurrentTexCoordinate; - ImageGLWallpaper(ImageGLProgram program) { mProgram = program; @@ -135,31 +124,9 @@ class ImageGLWallpaper { } private void setupUniforms() { - mUniAod2Opacity = mProgram.getUniformHandle(U_AOD2OPACITY); - mUniPer85 = mProgram.getUniformHandle(U_PER85); - mUniReveal = mProgram.getUniformHandle(U_REVEAL); mUniTexture = mProgram.getUniformHandle(U_TEXTURE); } - int getHandle(String name) { - switch (name) { - case A_POSITION: - return mAttrPosition; - case A_TEXTURE_COORDINATES: - return mAttrTextureCoordinates; - case U_AOD2OPACITY: - return mUniAod2Opacity; - case U_PER85: - return mUniPer85; - case U_REVEAL: - return mUniReveal; - case U_TEXTURE: - return mUniTexture; - default: - return HANDLE_UNDEFINED; - } - } - void draw() { glDrawArrays(GL_TRIANGLES, 0, VERTICES.length / 2); } @@ -201,87 +168,6 @@ class ImageGLWallpaper { } /** - * This method adjust s(x-axis), t(y-axis) texture coordinates to get current display area - * of texture and will be used during transition. - * The adjustment happens if either the width or height of the surface is larger than - * corresponding size of the display area. - * If both width and height are larger than corresponding size of the display area, - * the adjustment will happen at both s, t side. - * - * @param surface The size of the surface. - * @param scissor The display area. - * @param xOffset The offset amount along s axis. - * @param yOffset The offset amount along t axis. - */ - void adjustTextureCoordinates(Rect surface, Rect scissor, float xOffset, float yOffset) { - mCurrentTexCoordinate = TEXTURES.clone(); - - if (surface == null || scissor == null) { - mTextureBuffer.put(mCurrentTexCoordinate); - mTextureBuffer.position(0); - return; - } - - int surfaceWidth = surface.width(); - int surfaceHeight = surface.height(); - int scissorWidth = scissor.width(); - int scissorHeight = scissor.height(); - - if (surfaceWidth > scissorWidth) { - // Calculate the new s pos in pixels. - float pixelS = (float) Math.round((surfaceWidth - scissorWidth) * xOffset); - // Calculate the s pos in texture coordinate. - float coordinateS = pixelS / surfaceWidth; - // Calculate the percentage occupied by the scissor width in surface width. - float surfacePercentageW = (float) scissorWidth / surfaceWidth; - // Need also consider the case if surface height is smaller than scissor height. - if (surfaceHeight < scissorHeight) { - // We will narrow the surface percentage to keep aspect ratio. - surfacePercentageW *= (float) surfaceHeight / scissorHeight; - } - // Determine the final s pos, also limit the legal s pos to prevent from out of range. - float s = coordinateS + surfacePercentageW > 1f ? 1f - surfacePercentageW : coordinateS; - // Traverse the s pos in texture coordinates array and adjust the s pos accordingly. - for (int i = 0; i < mCurrentTexCoordinate.length; i += 2) { - // indices 2, 4 and 6 are the end of s coordinates. - if (i == 2 || i == 4 || i == 6) { - mCurrentTexCoordinate[i] = Math.min(1f, s + surfacePercentageW); - } else { - mCurrentTexCoordinate[i] = s; - } - } - } - - if (surfaceHeight > scissorHeight) { - // Calculate the new t pos in pixels. - float pixelT = (float) Math.round((surfaceHeight - scissorHeight) * yOffset); - // Calculate the t pos in texture coordinate. - float coordinateT = pixelT / surfaceHeight; - // Calculate the percentage occupied by the scissor height in surface height. - float surfacePercentageH = (float) scissorHeight / surfaceHeight; - // Need also consider the case if surface width is smaller than scissor width. - if (surfaceWidth < scissorWidth) { - // We will narrow the surface percentage to keep aspect ratio. - surfacePercentageH *= (float) surfaceWidth / scissorWidth; - } - // Determine the final t pos, also limit the legal t pos to prevent from out of range. - float t = coordinateT + surfacePercentageH > 1f ? 1f - surfacePercentageH : coordinateT; - // Traverse the t pos in texture coordinates array and adjust the t pos accordingly. - for (int i = 1; i < mCurrentTexCoordinate.length; i += 2) { - // indices 1, 3 and 11 are the end of t coordinates. - if (i == 1 || i == 3 || i == 11) { - mCurrentTexCoordinate[i] = Math.min(1f, t + surfacePercentageH); - } else { - mCurrentTexCoordinate[i] = t; - } - } - } - - mTextureBuffer.put(mCurrentTexCoordinate); - mTextureBuffer.position(0); - } - - /** * Called to dump current state. * @param prefix prefix. * @param fd fd. @@ -289,17 +175,5 @@ class ImageGLWallpaper { * @param args args. */ public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { - StringBuilder sb = new StringBuilder(); - sb.append('{'); - if (mCurrentTexCoordinate != null) { - for (int i = 0; i < mCurrentTexCoordinate.length; i++) { - sb.append(mCurrentTexCoordinate[i]).append(','); - if (i == mCurrentTexCoordinate.length - 1) { - sb.deleteCharAt(sb.length() - 1); - } - } - } - sb.append('}'); - out.print(prefix); out.print("mTexCoordinates="); out.println(sb.toString()); } } diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageProcessHelper.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageProcessHelper.java deleted file mode 100644 index 703d5910500a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageProcessHelper.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.glwallpaper; - -import static com.android.systemui.glwallpaper.ImageWallpaperRenderer.WallpaperTexture; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Handler.Callback; -import android.os.Message; -import android.util.Log; - -/** - * A helper class that computes threshold from a bitmap. - * Threshold will be computed each time the user picks a new image wallpaper. - */ -class ImageProcessHelper { - private static final String TAG = ImageProcessHelper.class.getSimpleName(); - private static final float DEFAULT_THRESHOLD = 0.8f; - private static final float DEFAULT_OTSU_THRESHOLD = 0f; - private static final float MAX_THRESHOLD = 0.89f; - private static final int MSG_UPDATE_THRESHOLD = 1; - - /** - * This color matrix will be applied to each pixel to get luminance from rgb by below formula: - * Luminance = .2126f * r + .7152f * g + .0722f * b. - */ - private static final float[] LUMINOSITY_MATRIX = new float[] { - .2126f, .0000f, .0000f, .0000f, .0000f, - .0000f, .7152f, .0000f, .0000f, .0000f, - .0000f, .0000f, .0722f, .0000f, .0000f, - .0000f, .0000f, .0000f, 1.000f, .0000f - }; - - private final Handler mHandler = new Handler(new Callback() { - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_THRESHOLD: - mThreshold = (float) msg.obj; - return true; - default: - return false; - } - } - }); - - private float mThreshold = DEFAULT_THRESHOLD; - - void start(WallpaperTexture texture) { - new ThresholdComputeTask(mHandler).execute(texture); - } - - float getThreshold() { - return Math.min(mThreshold, MAX_THRESHOLD); - } - - private static class ThresholdComputeTask extends AsyncTask<WallpaperTexture, Void, Float> { - private Handler mUpdateHandler; - - ThresholdComputeTask(Handler handler) { - super(handler); - mUpdateHandler = handler; - } - - @Override - protected Float doInBackground(WallpaperTexture... textures) { - WallpaperTexture texture = textures[0]; - final float[] threshold = new float[] {DEFAULT_THRESHOLD}; - if (texture == null) { - Log.e(TAG, "ThresholdComputeTask: WallpaperTexture not initialized"); - return threshold[0]; - } - - texture.use(bitmap -> { - if (bitmap != null) { - threshold[0] = new Threshold().compute(bitmap); - } else { - Log.e(TAG, "ThresholdComputeTask: Can't get bitmap"); - } - }); - return threshold[0]; - } - - @Override - protected void onPostExecute(Float result) { - Message msg = mUpdateHandler.obtainMessage(MSG_UPDATE_THRESHOLD, result); - mUpdateHandler.sendMessage(msg); - } - } - - private static class Threshold { - public float compute(Bitmap bitmap) { - Bitmap grayscale = toGrayscale(bitmap); - int[] histogram = getHistogram(grayscale); - boolean isSolidColor = isSolidColor(grayscale, histogram); - - // We will see gray wallpaper during the transition if solid color wallpaper is set, - // please refer to b/130360362#comment16. - // As a result, we use Percentile85 rather than Otsus if a solid color wallpaper is set. - ThresholdAlgorithm algorithm = isSolidColor ? new Percentile85() : new Otsus(); - return algorithm.compute(grayscale, histogram); - } - - private Bitmap toGrayscale(Bitmap bitmap) { - int width = bitmap.getWidth(); - int height = bitmap.getHeight(); - - Bitmap grayscale = Bitmap.createBitmap(width, height, bitmap.getConfig(), - false /* hasAlpha */, bitmap.getColorSpace()); - Canvas canvas = new Canvas(grayscale); - ColorMatrix cm = new ColorMatrix(LUMINOSITY_MATRIX); - Paint paint = new Paint(); - paint.setColorFilter(new ColorMatrixColorFilter(cm)); - canvas.drawBitmap(bitmap, new Matrix(), paint); - - return grayscale; - } - - private int[] getHistogram(Bitmap grayscale) { - int width = grayscale.getWidth(); - int height = grayscale.getHeight(); - - // TODO: Fine tune the performance here, tracking on b/123615079. - int[] histogram = new int[256]; - for (int row = 0; row < height; row++) { - for (int col = 0; col < width; col++) { - int pixel = grayscale.getPixel(col, row); - int y = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel); - histogram[y]++; - } - } - - return histogram; - } - - private boolean isSolidColor(Bitmap bitmap, int[] histogram) { - boolean solidColor = false; - int pixels = bitmap.getWidth() * bitmap.getHeight(); - - // In solid color case, only one element of histogram has value, - // which is pixel counts and the value of other elements should be 0. - for (int value : histogram) { - if (value != 0 && value != pixels) { - break; - } - if (value == pixels) { - solidColor = true; - break; - } - } - return solidColor; - } - } - - private static class Percentile85 implements ThresholdAlgorithm { - @Override - public float compute(Bitmap bitmap, int[] histogram) { - float per85 = DEFAULT_THRESHOLD; - int pixelCount = bitmap.getWidth() * bitmap.getHeight(); - float[] acc = new float[256]; - for (int i = 0; i < acc.length; i++) { - acc[i] = (float) histogram[i] / pixelCount; - float prev = i == 0 ? 0f : acc[i - 1]; - float next = acc[i]; - float idx = (float) (i + 1) / 255; - float sum = prev + next; - if (prev < 0.85f && sum >= 0.85f) { - per85 = idx; - } - if (i > 0) { - acc[i] += acc[i - 1]; - } - } - return per85; - } - } - - private static class Otsus implements ThresholdAlgorithm { - @Override - public float compute(Bitmap bitmap, int[] histogram) { - float threshold = DEFAULT_OTSU_THRESHOLD; - float maxVariance = 0; - float pixelCount = bitmap.getWidth() * bitmap.getHeight(); - float[] w = new float[2]; - float[] m = new float[2]; - float[] u = new float[2]; - - for (int i = 0; i < histogram.length; i++) { - m[1] += i * histogram[i]; - } - - w[1] = pixelCount; - for (int tonalValue = 0; tonalValue < histogram.length; tonalValue++) { - float dU; - float variance; - float numPixels = histogram[tonalValue]; - float tmp = numPixels * tonalValue; - w[0] += numPixels; - w[1] -= numPixels; - - if (w[0] == 0 || w[1] == 0) { - continue; - } - - m[0] += tmp; - m[1] -= tmp; - u[0] = m[0] / w[0]; - u[1] = m[1] / w[1]; - dU = u[0] - u[1]; - variance = w[0] * w[1] * dU * dU; - - if (variance > maxVariance) { - threshold = (tonalValue + 1f) / histogram.length; - maxVariance = variance; - } - } - return threshold; - } - } - - private interface ThresholdAlgorithm { - float compute(Bitmap bitmap, int[] histogram); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageRevealHelper.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageRevealHelper.java deleted file mode 100644 index f815b5d476ec..000000000000 --- a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageRevealHelper.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.glwallpaper; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.util.Log; - -import com.android.systemui.Interpolators; - -/** - * Use ValueAnimator and appropriate interpolator to control the progress of reveal transition. - * The transition will happen while getting awake and quit events. - */ -class ImageRevealHelper { - private static final String TAG = ImageRevealHelper.class.getSimpleName(); - private static final float MAX_REVEAL = 0f; - private static final float MIN_REVEAL = 1f; - private static final boolean DEBUG = true; - - private final ValueAnimator mAnimator; - private final RevealStateListener mRevealListener; - private float mReveal = MAX_REVEAL; - private boolean mAwake = false; - - ImageRevealHelper(RevealStateListener listener) { - mRevealListener = listener; - mAnimator = ValueAnimator.ofFloat(); - mAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - mAnimator.addUpdateListener(animator -> { - mReveal = (float) animator.getAnimatedValue(); - if (mRevealListener != null) { - mRevealListener.onRevealStateChanged(); - } - }); - mAnimator.addListener(new AnimatorListenerAdapter() { - private boolean mIsCanceled; - - @Override - public void onAnimationCancel(Animator animation) { - mIsCanceled = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - if (!mIsCanceled && mRevealListener != null) { - if (DEBUG) { - Log.d(TAG, "transition end"); - } - mRevealListener.onRevealEnd(); - } - mIsCanceled = false; - } - - @Override - public void onAnimationStart(Animator animation) { - if (mRevealListener != null) { - if (DEBUG) { - Log.d(TAG, "transition start"); - } - mRevealListener.onRevealStart(true /* animate */); - } - } - }); - } - - public float getReveal() { - return mReveal; - } - - void updateAwake(boolean awake, long duration) { - if (DEBUG) { - Log.d(TAG, "updateAwake: awake=" + awake + ", duration=" + duration); - } - mAnimator.cancel(); - mAwake = awake; - if (duration == 0) { - // We are transiting from home to aod or aod to home directly, - // we don't need to do transition in these cases. - mReveal = mAwake ? MAX_REVEAL : MIN_REVEAL; - mRevealListener.onRevealStart(false /* animate */); - mRevealListener.onRevealStateChanged(); - mRevealListener.onRevealEnd(); - } else { - mAnimator.setDuration(duration); - mAnimator.setFloatValues(mReveal, mAwake ? MAX_REVEAL : MIN_REVEAL); - mAnimator.start(); - } - } - - /** - * A listener to trace value changes of reveal. - */ - public interface RevealStateListener { - - /** - * Called back while reveal status changes. - */ - void onRevealStateChanged(); - - /** - * Called back while reveal starts. - */ - void onRevealStart(boolean animate); - - /** - * Called back while reveal ends. - */ - void onRevealEnd(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java index e9ddb3831b1a..1a0356c4446d 100644 --- a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java +++ b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java @@ -19,18 +19,14 @@ package com.android.systemui.glwallpaper; import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT; import static android.opengl.GLES20.glClear; import static android.opengl.GLES20.glClearColor; -import static android.opengl.GLES20.glUniform1f; import static android.opengl.GLES20.glViewport; import android.app.WallpaperManager; import android.content.Context; -import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Rect; import android.util.Log; -import android.util.MathUtils; import android.util.Size; -import android.view.DisplayInfo; import com.android.systemui.R; @@ -42,57 +38,24 @@ import java.util.function.Consumer; /** * A GL renderer for image wallpaper. */ -public class ImageWallpaperRenderer implements GLWallpaperRenderer, - ImageRevealHelper.RevealStateListener { +public class ImageWallpaperRenderer implements GLWallpaperRenderer { private static final String TAG = ImageWallpaperRenderer.class.getSimpleName(); - private static final float SCALE_VIEWPORT_MIN = 1f; - private static final float SCALE_VIEWPORT_MAX = 1.1f; - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; private final ImageGLProgram mProgram; private final ImageGLWallpaper mWallpaper; - private final ImageProcessHelper mImageProcessHelper; - private final ImageRevealHelper mImageRevealHelper; - - private SurfaceProxy mProxy; - private final Rect mScissor; private final Rect mSurfaceSize = new Rect(); - private final Rect mViewport = new Rect(); - private boolean mScissorMode; - private float mXOffset; - private float mYOffset; private final WallpaperTexture mTexture; - public ImageWallpaperRenderer(Context context, SurfaceProxy proxy) { + public ImageWallpaperRenderer(Context context) { final WallpaperManager wpm = context.getSystemService(WallpaperManager.class); if (wpm == null) { Log.w(TAG, "WallpaperManager not available"); } mTexture = new WallpaperTexture(wpm); - DisplayInfo displayInfo = new DisplayInfo(); - context.getDisplay().getDisplayInfo(displayInfo); - - // We only do transition in portrait currently, b/137962047. - int orientation = context.getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - mScissor = new Rect(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight); - } else { - mScissor = new Rect(0, 0, displayInfo.logicalHeight, displayInfo.logicalWidth); - } - - mProxy = proxy; mProgram = new ImageGLProgram(context); mWallpaper = new ImageGLWallpaper(mProgram); - mImageProcessHelper = new ImageProcessHelper(); - mImageRevealHelper = new ImageRevealHelper(this); - - startProcessingImage(); - } - - protected void startProcessingImage() { - // Compute threshold of the image, this is an async work. - mImageProcessHelper.start(mTexture); } @Override @@ -121,103 +84,26 @@ public class ImageWallpaperRenderer implements GLWallpaperRenderer, @Override public void onDrawFrame() { - float threshold = mImageProcessHelper.getThreshold(); - float reveal = mImageRevealHelper.getReveal(); - - glUniform1f(mWallpaper.getHandle(ImageGLWallpaper.U_AOD2OPACITY), 1); - glUniform1f(mWallpaper.getHandle(ImageGLWallpaper.U_PER85), threshold); - glUniform1f(mWallpaper.getHandle(ImageGLWallpaper.U_REVEAL), reveal); - glClear(GL_COLOR_BUFFER_BIT); - // We only need to scale viewport while doing transition. - if (mScissorMode) { - scaleViewport(reveal); - } else { - glViewport(0, 0, mSurfaceSize.width(), mSurfaceSize.height()); - } + glViewport(0, 0, mSurfaceSize.width(), mSurfaceSize.height()); mWallpaper.useTexture(); mWallpaper.draw(); } @Override - public void updateAmbientMode(boolean inAmbientMode, long duration) { - mImageRevealHelper.updateAwake(!inAmbientMode, duration); - } - - @Override - public void updateOffsets(float xOffset, float yOffset) { - mXOffset = xOffset; - mYOffset = yOffset; - int left = (int) ((mSurfaceSize.width() - mScissor.width()) * xOffset); - int right = left + mScissor.width(); - mScissor.set(left, mScissor.top, right, mScissor.bottom); - } - - @Override public Size reportSurfaceSize() { - mTexture.use(null); + mTexture.use(null /* consumer */); mSurfaceSize.set(mTexture.getTextureDimensions()); return new Size(mSurfaceSize.width(), mSurfaceSize.height()); } @Override public void finish() { - mProxy = null; - } - - private void scaleViewport(float reveal) { - int left = mScissor.left; - int top = mScissor.top; - int width = mScissor.width(); - int height = mScissor.height(); - // Interpolation between SCALE_VIEWPORT_MAX and SCALE_VIEWPORT_MIN by reveal. - float vpScaled = MathUtils.lerp(SCALE_VIEWPORT_MIN, SCALE_VIEWPORT_MAX, reveal); - // Calculate the offset amount from the lower left corner. - float offset = (SCALE_VIEWPORT_MIN - vpScaled) / 2; - // Change the viewport. - mViewport.set((int) (left + width * offset), (int) (top + height * offset), - (int) (width * vpScaled), (int) (height * vpScaled)); - glViewport(mViewport.left, mViewport.top, mViewport.right, mViewport.bottom); - } - - @Override - public void onRevealStateChanged() { - mProxy.requestRender(); - } - - @Override - public void onRevealStart(boolean animate) { - if (animate) { - mScissorMode = true; - // Use current display area of texture. - mWallpaper.adjustTextureCoordinates(mSurfaceSize, mScissor, mXOffset, mYOffset); - } - mProxy.preRender(); - } - - @Override - public void onRevealEnd() { - if (mScissorMode) { - mScissorMode = false; - // reset texture coordinates to use full texture. - mWallpaper.adjustTextureCoordinates(null, null, 0, 0); - // We need draw full texture back before finishing render. - mProxy.requestRender(); - } - mProxy.postRender(); } @Override public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { - out.print(prefix); out.print("mProxy="); out.print(mProxy); out.print(prefix); out.print("mSurfaceSize="); out.print(mSurfaceSize); - out.print(prefix); out.print("mScissor="); out.print(mScissor); - out.print(prefix); out.print("mViewport="); out.print(mViewport); - out.print(prefix); out.print("mScissorMode="); out.print(mScissorMode); - out.print(prefix); out.print("mXOffset="); out.print(mXOffset); - out.print(prefix); out.print("mYOffset="); out.print(mYOffset); - out.print(prefix); out.print("threshold="); out.print(mImageProcessHelper.getThreshold()); - out.print(prefix); out.print("mReveal="); out.print(mImageRevealHelper.getReveal()); out.print(prefix); out.print("mWcgContent="); out.print(isWcgContent()); mWallpaper.dump(prefix, fd, out, args); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 1012a5213a58..b26dc5f91245 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -1717,9 +1717,9 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable { resetKeyguardDonePendingLocked(); } - mUpdateMonitor.clearBiometricRecognized(); if (mGoingToSleep) { + mUpdateMonitor.clearBiometricRecognized(); Log.i(TAG, "Device is going to sleep, aborting keyguardDone"); return; } @@ -1740,6 +1740,7 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable { } handleHide(); + mUpdateMonitor.clearBiometricRecognized(); Trace.endSection(); } 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 14e3e9390825..123cf78d74f8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -61,6 +61,18 @@ public class LogModule { return buffer; } + /** Provides a logging buffer for all logs related to the data layer of notifications. */ + @Provides + @Singleton + @NotifInteractionLog + public static LogBuffer provideNotifInteractionLogBuffer( + LogcatEchoTracker echoTracker, + DumpManager dumpManager) { + LogBuffer buffer = new LogBuffer("NotifInteractionLog", 50, 10, echoTracker); + buffer.attach(dumpManager); + return buffer; + } + /** Provides a logging buffer for all logs related to Quick Settings. */ @Provides @Singleton diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java new file mode 100644 index 000000000000..20fc6ff445a6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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 messages related to the user interacting with notifications (e.g. + * clicking on them). + */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface NotifInteractionLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 62efd8ce4cee..9509e6d479e3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -19,22 +19,30 @@ package com.android.systemui.media; import android.annotation.LayoutRes; import android.app.PendingIntent; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.graphics.Bitmap; +import android.graphics.ImageDecoder; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; +import android.media.MediaDescription; import android.media.MediaMetadata; +import android.media.ThumbnailUtils; import android.media.session.MediaController; +import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession; import android.media.session.PlaybackState; +import android.net.Uri; +import android.service.media.MediaBrowserService; +import android.text.TextUtils; import android.util.Log; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnAttachStateChangeListener; @@ -52,13 +60,12 @@ import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.widget.AdaptiveIcon; -import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.statusbar.NotificationMediaManager; -import com.android.systemui.statusbar.NotificationMediaManager.MediaListener; +import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.util.Assert; +import java.io.IOException; import java.util.List; import java.util.concurrent.Executor; @@ -67,10 +74,10 @@ import java.util.concurrent.Executor; */ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; - private final NotificationMediaManager mMediaManager; @Nullable private final LocalMediaManager mLocalMediaManager; private final Executor mForegroundExecutor; - private final Executor mBackgroundExecutor; + protected final Executor mBackgroundExecutor; + private final ActivityStarter mActivityStarter; private Context mContext; protected LinearLayout mMediaNotifView; @@ -79,12 +86,19 @@ public class MediaControlPanel { private MediaController mController; private int mForegroundColor; private int mBackgroundColor; - protected ComponentName mRecvComponent; private MediaDevice mDevice; + protected ComponentName mServiceComponent; private boolean mIsRegistered = false; + private String mKey; private final int[] mActionIds; + public static final String MEDIA_PREFERENCES = "media_control_prefs"; + public static final String MEDIA_PREFERENCE_KEY = "browser_components"; + private SharedPreferences mSharedPrefs; + private boolean mCheckedForResumption = false; + private boolean mIsRemotePlayback; + // Button IDs used in notifications protected static final int[] NOTIF_ACTION_IDS = { com.android.internal.R.id.action0, @@ -94,6 +108,13 @@ public class MediaControlPanel { com.android.internal.R.id.action4 }; + // URI fields to try loading album art from + private static final String[] ART_URIS = { + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + }; + private final MediaController.Callback mSessionCallback = new MediaController.Callback() { @Override public void onSessionDestroyed() { @@ -102,12 +123,11 @@ public class MediaControlPanel { clearControls(); makeInactive(); } - }; - - private final MediaListener mMediaListener = new MediaListener() { @Override - public void onMetadataOrStateChanged(MediaMetadata metadata, int state) { - if (state == PlaybackState.STATE_NONE) { + public void onPlaybackStateChanged(PlaybackState state) { + final int s = state != null ? state.getState() : PlaybackState.STATE_NONE; + if (s == PlaybackState.STATE_NONE) { + Log.d(TAG, "playback state change will trigger resumption, state=" + state); clearControls(); makeInactive(); } @@ -153,16 +173,17 @@ public class MediaControlPanel { * Initialize a new control panel * @param context * @param parent - * @param manager * @param routeManager Manager used to listen for device change events. * @param layoutId layout resource to use for this control panel * @param actionIds resource IDs for action buttons in the layout * @param foregroundExecutor foreground executor * @param backgroundExecutor background executor, used for processing artwork + * @param activityStarter activity starter */ - public MediaControlPanel(Context context, ViewGroup parent, NotificationMediaManager manager, + public MediaControlPanel(Context context, ViewGroup parent, @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds, - Executor foregroundExecutor, Executor backgroundExecutor) { + Executor foregroundExecutor, Executor backgroundExecutor, + ActivityStarter activityStarter) { mContext = context; LayoutInflater inflater = LayoutInflater.from(mContext); mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false); @@ -172,11 +193,11 @@ public class MediaControlPanel { // attach/detach of views instead of inflating them in the constructor, which would allow // mStateListener to be unregistered in detach. mMediaNotifView.addOnAttachStateChangeListener(mStateListener); - mMediaManager = manager; mLocalMediaManager = routeManager; mActionIds = actionIds; mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; + mActivityStarter = activityStarter; } /** @@ -198,107 +219,127 @@ public class MediaControlPanel { /** * Update the media panel view for the given media session * @param token - * @param icon + * @param iconDrawable + * @param largeIcon * @param iconColor * @param bgColor * @param contentIntent * @param appNameString - * @param device + * @param key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, - int bgColor, PendingIntent contentIntent, String appNameString) { - mToken = token; + public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon, + int iconColor, int bgColor, PendingIntent contentIntent, String appNameString, + String key) { + // Ensure that component names are updated if token has changed + if (mToken == null || !mToken.equals(token)) { + mToken = token; + mServiceComponent = null; + mCheckedForResumption = false; + } + mForegroundColor = iconColor; mBackgroundColor = bgColor; mController = new MediaController(mContext, mToken); - - MediaMetadata mediaMetadata = mController.getMetadata(); - - // Try to find a receiver for the media button that matches this app - PackageManager pm = mContext.getPackageManager(); - Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON); - List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser()); - if (info != null) { - for (ResolveInfo inf : info) { - if (inf.activityInfo.packageName.equals(mController.getPackageName())) { - mRecvComponent = inf.getComponentInfo().getComponentName(); + mKey = key; + + // Try to find a browser service component for this app + // TODO also check for a media button receiver intended for restarting (b/154127084) + // Only check if we haven't tried yet or the session token changed + final String pkgName = mController.getPackageName(); + if (mServiceComponent == null && !mCheckedForResumption) { + Log.d(TAG, "Checking for service component"); + PackageManager pm = mContext.getPackageManager(); + Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE); + List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0); + if (resumeInfo != null) { + for (ResolveInfo inf : resumeInfo) { + if (inf.serviceInfo.packageName.equals(mController.getPackageName())) { + mBackgroundExecutor.execute(() -> + tryUpdateResumptionList(inf.getComponentInfo().getComponentName())); + break; + } } } + mCheckedForResumption = true; } mController.registerCallback(mSessionCallback); - if (mediaMetadata == null) { - Log.e(TAG, "Media metadata was null"); - return; - } - - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView)); - } mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); // Click action if (contentIntent != null) { mMediaNotifView.setOnClickListener(v -> { - try { - contentIntent.send(); - // Also close shade - mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled", e); - } + mActivityStarter.postStartActivityDismissingKeyguard(contentIntent); }); } // App icon ImageView appIcon = mMediaNotifView.findViewById(R.id.icon); - Drawable iconDrawable = icon.loadDrawable(mContext); iconDrawable.setTint(mForegroundColor); appIcon.setImageDrawable(iconDrawable); - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - titleText.setText(songName); - titleText.setTextColor(mForegroundColor); + // Transfer chip + mSeamless = mMediaNotifView.findViewById(R.id.media_seamless); + if (mSeamless != null) { + if (mLocalMediaManager != null) { + mSeamless.setVisibility(View.VISIBLE); + updateDevice(mLocalMediaManager.getCurrentConnectedDevice()); + mSeamless.setOnClickListener(v -> { + final Intent intent = new Intent() + .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) + .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, + mController.getPackageName()) + .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken); + mActivityStarter.startActivity(intent, false, true /* dismissShade */, + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + }); + } else { + Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName); + } + } + PlaybackInfo playbackInfo = mController.getPlaybackInfo(); + if (playbackInfo != null) { + mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE; + } else { + Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback."); + mIsRemotePlayback = false; + } + + makeActive(); - // Not in mini player: - // App title + // App title (not in mini player) TextView appName = mMediaNotifView.findViewById(R.id.app_name); if (appName != null) { appName.setText(appNameString); appName.setTextColor(mForegroundColor); } - // Artist name + MediaMetadata mediaMetadata = mController.getMetadata(); + if (mediaMetadata == null) { + Log.e(TAG, "Media metadata was null"); + return; + } + + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + if (albumView != null) { + // Resize art in a background thread + mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView)); + } + + // Song name + TextView titleText = mMediaNotifView.findViewById(R.id.header_title); + String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + titleText.setText(songName); + titleText.setTextColor(mForegroundColor); + + // Artist name (not in mini player) TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); if (artistText != null) { String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); artistText.setText(artistName); artistText.setTextColor(mForegroundColor); } - - // Transfer chip - mSeamless = mMediaNotifView.findViewById(R.id.media_seamless); - if (mSeamless != null && mLocalMediaManager != null) { - mSeamless.setVisibility(View.VISIBLE); - updateDevice(mLocalMediaManager.getCurrentConnectedDevice()); - ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class); - mSeamless.setOnClickListener(v -> { - final Intent intent = new Intent() - .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) - .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, - mController.getPackageName()) - .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken); - mActivityStarter.startActivity(intent, false, true /* dismissShade */, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - }); - } - - makeActive(); } /** @@ -319,13 +360,24 @@ public class MediaControlPanel { /** * Get the name of the package associated with the current media controller - * @return the package name + * @return the package name, or null if no controller */ public String getMediaPlayerPackage() { + if (mController == null) { + return null; + } return mController.getPackageName(); } /** + * Return the original notification's key + * @return The notification key + */ + public String getKey() { + return mKey; + } + + /** * Check whether this player has an attached media session. * @return whether there is a controller with a current media session. */ @@ -361,18 +413,97 @@ public class MediaControlPanel { /** * Process album art for layout + * @param description media description + * @param albumView view to hold the album art + */ + protected void processAlbumArt(MediaDescription description, ImageView albumView) { + Bitmap albumArt = null; + + // First try loading from URI + albumArt = loadBitmapFromUri(description.getIconUri()); + + // Then check bitmap + if (albumArt == null) { + albumArt = description.getIconBitmap(); + } + + processAlbumArtInternal(albumArt, albumView); + } + + /** + * Process album art for layout * @param metadata media metadata + * @param largeIcon from notification, checked as a fallback if metadata does not have art * @param albumView view to hold the album art */ - private void processAlbumArt(MediaMetadata metadata, ImageView albumView) { - Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); + private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) { + Bitmap albumArt = null; + + // First look in URI fields + for (String field : ART_URIS) { + String uriString = metadata.getString(field); + if (!TextUtils.isEmpty(uriString)) { + albumArt = loadBitmapFromUri(Uri.parse(uriString)); + if (albumArt != null) { + Log.d(TAG, "loaded art from " + field); + break; + } + } + } + + // Then check bitmap field + if (albumArt == null) { + albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + } + + // Finally try the notification's largeIcon + if (albumArt == null && largeIcon != null) { + albumArt = largeIcon.getBitmap(); + } + + processAlbumArtInternal(albumArt, albumView); + } + + /** + * Load a bitmap from a URI + * @param uri + * @return bitmap, or null if couldn't be loaded + */ + private Bitmap loadBitmapFromUri(Uri uri) { + // ImageDecoder requires a scheme of the following types + if (uri.getScheme() == null) { + return null; + } + + if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT) + && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE) + && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { + return null; + } + + ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri); + try { + return ImageDecoder.decodeBitmap(source); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Resize and crop the image if provided and update the control view + * @param albumArt Bitmap of art to display, or null to hide view + * @param albumView View that will hold the art + */ + private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) { + // Resize RoundedBitmapDrawable roundedDrawable = null; if (albumArt != null) { + float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true); int albumSize = (int) mContext.getResources().getDimension( R.dimen.qs_media_album_size); - Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false); + Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize); roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); roundedDrawable.setCornerRadius(radius); } else { @@ -419,7 +550,16 @@ public class MediaControlPanel { TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text); deviceName.setTextColor(fgTintList); - if (device != null) { + if (mIsRemotePlayback) { + mSeamless.setEnabled(false); + mSeamless.setAlpha(0.38f); + iconView.setImageResource(R.drawable.ic_hardware_speaker); + iconView.setVisibility(View.VISIBLE); + iconView.setImageTintList(fgTintList); + deviceName.setText(R.string.media_seamless_remote_device); + } else if (device != null) { + mSeamless.setEnabled(true); + mSeamless.setAlpha(1f); Drawable icon = device.getIcon(); iconView.setVisibility(View.VISIBLE); iconView.setImageTintList(fgTintList); @@ -434,15 +574,33 @@ public class MediaControlPanel { deviceName.setText(device.getName()); } else { // Reset to default + Log.d(TAG, "device is null. Not binding output chip."); + mSeamless.setEnabled(true); + mSeamless.setAlpha(1f); iconView.setVisibility(View.GONE); deviceName.setText(com.android.internal.R.string.ext_media_seamless_action); } } /** - * Put controls into a resumption state + * Puts controls into a resumption state if possible, or calls removePlayer if no component was + * found that could resume playback */ public void clearControls() { + Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage()); + if (mServiceComponent == null) { + // If we don't have a way to resume, just remove the player altogether + Log.d(TAG, "Removing unresumable controls"); + removePlayer(); + return; + } + resetButtons(); + } + + /** + * Hide the media buttons and show only a restart button + */ + protected void resetButtons() { // Hide all the old buttons for (int i = 0; i < mActionIds.length; i++) { ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]); @@ -455,27 +613,8 @@ public class MediaControlPanel { ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]); btn.setOnClickListener(v -> { Log.d(TAG, "Attempting to restart session"); - // Send a media button event to previously found receiver - if (mRecvComponent != null) { - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); - intent.setComponent(mRecvComponent); - int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY; - intent.putExtra( - Intent.EXTRA_KEY_EVENT, - new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - mContext.sendBroadcast(intent); - } else { - // If we don't have a receiver, try relaunching the activity instead - if (mController.getSessionActivity() != null) { - try { - mController.getSessionActivity().send(); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled", e); - } - } else { - Log.e(TAG, "No receiver or activity to restart"); - } - } + QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent); + browser.restart(); }); btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); @@ -485,7 +624,6 @@ public class MediaControlPanel { private void makeActive() { Assert.isMainThread(); if (!mIsRegistered) { - mMediaManager.addCallback(mMediaListener); if (mLocalMediaManager != null) { mLocalMediaManager.registerCallback(mDeviceCallback); mLocalMediaManager.startScan(); @@ -501,9 +639,71 @@ public class MediaControlPanel { mLocalMediaManager.stopScan(); mLocalMediaManager.unregisterCallback(mDeviceCallback); } - mMediaManager.removeCallback(mMediaListener); mIsRegistered = false; } } + /** + * Verify that we can connect to the given component with a MediaBrowser, and if so, add that + * component to the list of resumption components + */ + private void tryUpdateResumptionList(ComponentName componentName) { + Log.d(TAG, "Testing if we can connect to " + componentName); + QSMediaBrowser.testConnection(mContext, + new QSMediaBrowser.Callback() { + @Override + public void onConnected() { + Log.d(TAG, "yes we can resume with " + componentName); + mServiceComponent = componentName; + updateResumptionList(componentName); + } + + @Override + public void onError() { + Log.d(TAG, "Cannot resume with " + componentName); + mServiceComponent = null; + if (!hasMediaSession()) { + // If it's not active and we can't resume, remove + removePlayer(); + } + } + }, + componentName); + } + + /** + * Add the component to the saved list of media browser services, checking for duplicates and + * removing older components that exceed the maximum limit + * @param componentName + */ + private synchronized void updateResumptionList(ComponentName componentName) { + // Add to front of saved list + if (mSharedPrefs == null) { + mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0); + } + String componentString = componentName.flattenToString(); + String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null); + if (listString == null) { + listString = componentString; + } else { + String[] components = listString.split(QSMediaBrowser.DELIMITER); + StringBuilder updated = new StringBuilder(componentString); + int nBrowsers = 1; + for (int i = 0; i < components.length + && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) { + if (componentString.equals(components[i])) { + continue; + } + updated.append(QSMediaBrowser.DELIMITER).append(components[i]); + nBrowsers++; + } + listString = updated.toString(); + } + mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply(); + } + + /** + * Called when a player can't be resumed to give it an opportunity to hide or remove itself + */ + protected void removePlayer() { } } diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt index b7658a9f178d..51c157a56560 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt @@ -61,6 +61,7 @@ class SeekBarObserver(view: View) : Observer<SeekBarViewModel.Progress> { if (!data.enabled) { seekBarView.setEnabled(false) seekBarView.getThumb().setAlpha(0) + seekBarView.setProgress(0) elapsedTimeView.setText("") totalTimeView.setText("") return diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt index dd83e42cde2d..142510030a5f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt @@ -77,13 +77,25 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) { val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L val position = playbackState?.position?.toInt() val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() - val enabled = if (duration != null && duration <= 0) false else true + val enabled = if (playbackState == null || + playbackState?.getState() == PlaybackState.STATE_NONE || + (duration != null && duration <= 0)) false else true _data = Progress(enabled, seekAvailable, position, duration, color) if (shouldPollPlaybackPosition()) { checkPlaybackPosition() } } + /** + * Puts the seek bar into a resumption state. + * + * This should be called when the media session behind the controller has been destroyed. + */ + @AnyThread + fun clearController() = bgExecutor.execute { + _data = _data.copy(enabled = false) + } + @AnyThread private fun checkPlaybackPosition(): Runnable = bgExecutor.executeDelayed({ val currentPosition = controller?.playbackState?.position?.toInt() diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java index a827f59ac7d1..f900f1e1db63 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java @@ -60,6 +60,11 @@ public class SysUiState implements Dumpable { mCallbacks.remove(callback); } + /** Returns the current sysui state flags. */ + public int getFlags() { + return mFlags; + } + /** Methods to this call can be chained together before calling {@link #commitUpdate(int)}. */ public SysUiState setFlag(int flag, boolean enabled) { if (enabled) { diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java index dba43430b490..f322489b8dc2 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java @@ -53,16 +53,23 @@ public class PipAnimationController { public static final int TRANSITION_DIRECTION_SAME = 1; public static final int TRANSITION_DIRECTION_TO_PIP = 2; public static final int TRANSITION_DIRECTION_TO_FULLSCREEN = 3; + public static final int TRANSITION_DIRECTION_TO_SPLIT_SCREEN = 4; @IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = { TRANSITION_DIRECTION_NONE, TRANSITION_DIRECTION_SAME, TRANSITION_DIRECTION_TO_PIP, - TRANSITION_DIRECTION_TO_FULLSCREEN + TRANSITION_DIRECTION_TO_FULLSCREEN, + TRANSITION_DIRECTION_TO_SPLIT_SCREEN }) @Retention(RetentionPolicy.SOURCE) public @interface TransitionDirection {} + public static boolean isOutPipDirection(@TransitionDirection int direction) { + return direction == TRANSITION_DIRECTION_TO_FULLSCREEN + || direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN; + } + private final Interpolator mFastOutSlowInInterpolator; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; @@ -253,14 +260,13 @@ public class PipAnimationController { } boolean shouldApplyCornerRadius() { - return mTransitionDirection != TRANSITION_DIRECTION_TO_FULLSCREEN; + return !isOutPipDirection(mTransitionDirection); } boolean inScaleTransition() { if (mAnimationType != ANIM_TYPE_BOUNDS) return false; final int direction = getTransitionDirection(); - return direction != TRANSITION_DIRECTION_TO_FULLSCREEN - && direction != TRANSITION_DIRECTION_TO_PIP; + return !isOutPipDirection(direction) && direction != TRANSITION_DIRECTION_TO_PIP; } /** diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java index e24d29f1aedf..9d9e74abc38f 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java @@ -126,6 +126,10 @@ public class PipBoundsHandler { mCurrentMinSize = minEdgeSize; } + protected float getAspectRatio() { + return mAspectRatio; + } + /** * Sets both shelf visibility and its height if applicable. * @return {@code true} if the internal shelf state is changed, {@code false} otherwise. @@ -419,7 +423,7 @@ public class PipBoundsHandler { /** * Populates the bounds on the screen that the PIP can be visible in. */ - private void getInsetBounds(Rect outRect) { + protected void getInsetBounds(Rect outRect) { try { mWindowManager.getStableInsets(mContext.getDisplayId(), mTmpInsets); outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index a95d6b7a73cd..d38c481752c6 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -16,7 +16,6 @@ package com.android.systemui.pip; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_ALPHA; @@ -25,6 +24,8 @@ import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_FULLSCREEN; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_SPLIT_SCREEN; +import static com.android.systemui.pip.PipAnimationController.isOutPipDirection; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,6 +49,7 @@ import android.window.WindowOrganizer; import com.android.internal.os.SomeArgs; import com.android.systemui.R; import com.android.systemui.pip.phone.PipUpdateThread; +import com.android.systemui.stackdivider.Divider; import java.util.ArrayList; import java.util.HashMap; @@ -85,6 +87,7 @@ public class PipTaskOrganizer extends TaskOrganizer { private final int mEnterExitAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final Map<IBinder, Rect> mBoundsToRestore = new HashMap<>(); + private final Divider mSplitDivider; // These callbacks are called on the update thread private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = @@ -126,7 +129,7 @@ public class PipTaskOrganizer extends TaskOrganizer { }; @SuppressWarnings("unchecked") - private Handler.Callback mUpdateCallbacks = (msg) -> { + private final Handler.Callback mUpdateCallbacks = (msg) -> { SomeArgs args = (SomeArgs) msg.obj; Consumer<Rect> updateBoundsCallback = (Consumer<Rect>) args.arg1; switch (msg.what) { @@ -189,7 +192,8 @@ public class PipTaskOrganizer extends TaskOrganizer { mSurfaceControlTransactionFactory; public PipTaskOrganizer(Context context, @NonNull PipBoundsHandler boundsHandler, - @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper) { + @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, + @Nullable Divider divider) { mMainHandler = new Handler(Looper.getMainLooper()); mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks); mPipBoundsHandler = boundsHandler; @@ -198,6 +202,7 @@ public class PipTaskOrganizer extends TaskOrganizer { mSurfaceTransactionHelper = surfaceTransactionHelper; mPipAnimationController = new PipAnimationController(context, surfaceTransactionHelper); mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mSplitDivider = divider; } public Handler getUpdateHandler() { @@ -226,25 +231,26 @@ public class PipTaskOrganizer extends TaskOrganizer { /** * Dismiss PiP, this is done in two phases using {@link WindowContainerTransaction} - * - setActivityWindowingMode to fullscreen at beginning of the transaction. without changing - * the windowing mode of the Task itself. This makes sure the activity render it's fullscreen + * - setActivityWindowingMode to undefined at beginning of the transaction. without changing + * the windowing mode of the Task itself. This makes sure the activity render it's final * configuration while the Task is still in PiP. - * - setWindowingMode to fullscreen at the end of transition + * - setWindowingMode to undefined at the end of transition * @param animationDurationMs duration in millisecond for the exiting PiP transition */ public void dismissPip(int animationDurationMs) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setActivityWindowingMode(mToken, WINDOWING_MODE_FULLSCREEN); + wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); WindowOrganizer.applyTransaction(wct); final Rect destinationBounds = mBoundsToRestore.remove(mToken.asBinder()); + final int direction = syncWithSplitScreenBounds(destinationBounds) + ? TRANSITION_DIRECTION_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_TO_FULLSCREEN; scheduleAnimateResizePip(mLastReportedBounds, destinationBounds, - TRANSITION_DIRECTION_TO_FULLSCREEN, animationDurationMs, - null /* updateBoundsCallback */); + direction, animationDurationMs, null /* updateBoundsCallback */); mInPip = false; } @Override - public void onTaskAppeared(ActivityManager.RunningTaskInfo info) { + public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { Objects.requireNonNull(info, "Requires RunningTaskInfo"); final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( info.topActivity, getAspectRatioOrDefault(info.pictureInPictureParams), @@ -253,7 +259,7 @@ public class PipTaskOrganizer extends TaskOrganizer { mTaskInfo = info; mToken = mTaskInfo.token; mInPip = true; - mLeash = mToken.getLeash(); + mLeash = leash; final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); mBoundsToRestore.put(mToken.asBinder(), currentBounds); @@ -279,24 +285,26 @@ public class PipTaskOrganizer extends TaskOrganizer { * Meanwhile this callback is invoked whenever the task is removed. For instance: * - as a result of removeStacksInWindowingModes from WM * - activity itself is died + * Nevertheless, we simply update the internal state here as all the heavy lifting should + * have been done in WM. */ @Override public void onTaskVanished(ActivityManager.RunningTaskInfo info) { - WindowContainerToken token = info.token; + if (!mInPip) { + return; + } + final WindowContainerToken token = info.token; Objects.requireNonNull(token, "Requires valid WindowContainerToken"); if (token.asBinder() != mToken.asBinder()) { Log.wtf(TAG, "Unrecognized token: " + token); return; } - final Rect boundsToRestore = mBoundsToRestore.remove(token.asBinder()); - scheduleAnimateResizePip(mLastReportedBounds, boundsToRestore, - TRANSITION_DIRECTION_TO_FULLSCREEN, mEnterExitAnimationDuration, - null /* updateBoundsCallback */); mInPip = false; } @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { + Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); final PictureInPictureParams newParams = info.pictureInPictureParams; if (!shouldUpdateDestinationBounds(newParams)) { Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); @@ -321,13 +329,19 @@ public class PipTaskOrganizer extends TaskOrganizer { * @param destinationBoundsOut the current destination bounds will be populated to this param */ @SuppressWarnings("unchecked") - public void onMovementBoundsChanged(Rect destinationBoundsOut, + public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, boolean fromImeAdjustment, boolean fromShelfAdjustment) { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); - destinationBoundsOut.set(mLastReportedBounds); if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { + if (mInPip && fromRotation) { + // this could happen if rotation finishes before the animation + mLastReportedBounds.set(destinationBoundsOut); + scheduleFinishResizePip(mLastReportedBounds); + } else if (!mLastReportedBounds.isEmpty()) { + destinationBoundsOut.set(mLastReportedBounds); + } return; } @@ -375,7 +389,7 @@ public class PipTaskOrganizer extends TaskOrganizer { @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback) { if (!mInPip) { - // Ignore animation when we are no longer in PIP + // can be initiated in other component, ignore if we are no longer in PIP return; } SomeArgs args = SomeArgs.obtain(); @@ -427,6 +441,10 @@ public class PipTaskOrganizer extends TaskOrganizer { private void scheduleFinishResizePip(SurfaceControl.Transaction tx, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback) { + if (!mInPip) { + // can be initiated in other component, ignore if we are no longer in PIP + return; + } SomeArgs args = SomeArgs.obtain(); args.arg1 = updateBoundsCallback; args.arg2 = tx; @@ -441,7 +459,7 @@ public class PipTaskOrganizer extends TaskOrganizer { public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback) { if (!mInPip) { - // Ignore offsets when we are no longer in PIP + // can be initiated in other component, ignore if we are no longer in PIP return; } SomeArgs args = SomeArgs.obtain(); @@ -508,14 +526,13 @@ public class PipTaskOrganizer extends TaskOrganizer { mLastReportedBounds.set(destinationBounds); final WindowContainerTransaction wct = new WindowContainerTransaction(); final Rect taskBounds; - if (direction == TRANSITION_DIRECTION_TO_FULLSCREEN) { + if (isOutPipDirection(direction)) { // If we are animating to fullscreen, then we need to reset the override bounds - // on the task to ensure that the task "matches" the parent's bounds, this applies - // also to the final windowing mode, which should be reset to undefined rather than - // fullscreen. - taskBounds = null; - wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED) - .setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + // on the task to ensure that the task "matches" the parent's bounds. + taskBounds = (direction == TRANSITION_DIRECTION_TO_FULLSCREEN) + ? null : destinationBounds; + // As for the final windowing mode, simply reset it to undefined. + wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); } else { taskBounds = destinationBounds; } @@ -567,6 +584,24 @@ public class PipTaskOrganizer extends TaskOrganizer { } /** + * Sync with {@link #mSplitDivider} on destination bounds if PiP is going to split screen. + * + * @param destinationBoundsOut contain the updated destination bounds if applicable + * @return {@code true} if destinationBounds is altered for split screen + */ + private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut) { + if (mSplitDivider == null || !mSplitDivider.inSplitMode()) { + // bail early if system is not in split screen mode + return false; + } + // PiP window will go to split-secondary mode instead of fullscreen, populates the + // split screen bounds here. + destinationBoundsOut.set( + mSplitDivider.getView().getNonMinimizedSplitScreenSecondaryBounds()); + return true; + } + + /** * Callback interface for PiP transitions (both from and to PiP mode) */ public interface PipTransitionCallback { diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java index a2667d9a4c74..a86a884c8016 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -19,7 +19,7 @@ package com.android.systemui.pip.phone; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_FULLSCREEN; +import static com.android.systemui.pip.PipAnimationController.isOutPipDirection; import android.annotation.Nullable; import android.app.ActivityManager; @@ -53,6 +53,7 @@ import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.PinnedStackListenerForwarder.PinnedStackListener; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.WindowManagerWrapper; +import com.android.systemui.stackdivider.Divider; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.FloatingContentCoordinator; import com.android.systemui.wm.DisplayChangeController; @@ -97,8 +98,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio final boolean changed = mPipBoundsHandler.onDisplayRotationChanged(mTmpNormalBounds, displayId, fromRotation, toRotation, t); if (changed) { - updateMovementBounds(mTmpNormalBounds, false /* fromImeAdjustment */, - false /* fromShelfAdjustment */); + updateMovementBounds(mTmpNormalBounds, true /* fromRotation */, + false /* fromImeAdjustment */, false /* fromShelfAdjustment */); } }; @@ -134,8 +135,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, - boolean homeTaskVisible, boolean clearedTask) { - if (task.configuration.windowConfiguration.getWindowingMode() + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_PINNED) { return; } @@ -163,7 +164,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onMovementBoundsChanged(boolean fromImeAdjustment) { mHandler.post(() -> updateMovementBounds(null /* toBounds */, - fromImeAdjustment, false /* fromShelfAdjustment */)); + false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */)); } @Override @@ -199,7 +200,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio DeviceConfigProxy deviceConfig, PipBoundsHandler pipBoundsHandler, PipSnapAlgorithm pipSnapAlgorithm, - PipSurfaceTransactionHelper surfaceTransactionHelper) { + PipSurfaceTransactionHelper surfaceTransactionHelper, + Divider divider) { mContext = context; mActivityManager = ActivityManager.getService(); @@ -214,7 +216,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio final IActivityTaskManager activityTaskManager = ActivityTaskManager.getService(); mPipBoundsHandler = pipBoundsHandler; mPipTaskOrganizer = new PipTaskOrganizer(context, pipBoundsHandler, - surfaceTransactionHelper); + surfaceTransactionHelper, divider); mPipTaskOrganizer.registerPipTransitionCallback(this); mInputConsumerController = InputConsumerController.getPipInputConsumer(); mMediaController = new PipMediaController(context, mActivityManager, broadcastDispatcher); @@ -294,7 +296,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio if (changed) { mTouchHandler.onShelfVisibilityChanged(visible, height); updateMovementBounds(mPipBoundsHandler.getLastDestinationBounds(), - false /* fromImeAdjustment */, true /* fromShelfAdjustment */); + false /* fromRotation */, false /* fromImeAdjustment */, + true /* fromShelfAdjustment */); } }); } @@ -311,7 +314,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onPipTransitionStarted(ComponentName activity, int direction) { - if (direction == TRANSITION_DIRECTION_TO_FULLSCREEN) { + if (isOutPipDirection(direction)) { // On phones, the expansion animation that happens on pip tap before restoring // to fullscreen makes it so that the bounds received here are the expanded // bounds. We want to restore to the unexpanded bounds when re-entering pip, @@ -338,22 +341,22 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onPipTransitionFinished(ComponentName activity, int direction) { - onPipTransitionFinishedOrCanceled(); + onPipTransitionFinishedOrCanceled(direction); } @Override public void onPipTransitionCanceled(ComponentName activity, int direction) { - onPipTransitionFinishedOrCanceled(); + onPipTransitionFinishedOrCanceled(direction); } - private void onPipTransitionFinishedOrCanceled() { + private void onPipTransitionFinishedOrCanceled(int direction) { // Re-enable touches after the animation completes mTouchHandler.setTouchEnabled(true); - mTouchHandler.onPinnedStackAnimationEnded(); + mTouchHandler.onPinnedStackAnimationEnded(direction); mMenuController.onPinnedStackAnimationEnded(); } - private void updateMovementBounds(@Nullable Rect toBounds, + private void updateMovementBounds(@Nullable Rect toBounds, boolean fromRotation, boolean fromImeAdjustment, boolean fromShelfAdjustment) { // Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before // passing to mTouchHandler/mPipTaskOrganizer @@ -361,7 +364,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds, outBounds, mTmpDisplayInfo); // mTouchHandler would rely on the bounds populated from mPipTaskOrganizer - mPipTaskOrganizer.onMovementBoundsChanged(outBounds, + mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment, fromShelfAdjustment); mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds, outBounds, fromImeAdjustment, fromShelfAdjustment, diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java index 2b9b1716cb18..ec15dd16f46e 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java @@ -54,6 +54,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; @@ -129,9 +130,7 @@ public class PipMenuActivity extends Activity { } }; - private Handler mHandler = new Handler(); - private Messenger mToControllerMessenger; - private Messenger mMessenger = new Messenger(new Handler() { + private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -174,7 +173,9 @@ public class PipMenuActivity extends Activity { } } } - }); + }; + private Messenger mToControllerMessenger; + private Messenger mMessenger = new Messenger(mHandler); private final Runnable mFinishRunnable = new Runnable() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java index d660b670446b..61ed40d5d782 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java @@ -30,6 +30,7 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.Debug; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; @@ -122,7 +123,7 @@ public class PipMenuActivityController { private boolean mStartActivityRequested; private long mStartActivityRequestedTime; private Messenger mToActivityMessenger; - private Handler mHandler = new Handler() { + private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -133,15 +134,15 @@ public class PipMenuActivityController { break; } case MESSAGE_EXPAND_PIP: { - mListeners.forEach(l -> l.onPipExpand()); + mListeners.forEach(Listener::onPipExpand); break; } case MESSAGE_DISMISS_PIP: { - mListeners.forEach(l -> l.onPipDismiss()); + mListeners.forEach(Listener::onPipDismiss); break; } case MESSAGE_SHOW_MENU: { - mListeners.forEach(l -> l.onPipShowMenu()); + mListeners.forEach(Listener::onPipShowMenu); break; } case MESSAGE_UPDATE_ACTIVITY_CALLBACK: { @@ -259,6 +260,8 @@ public class PipMenuActivityController { if (DEBUG) { Log.d(TAG, "showMenu() state=" + menuState + " hasActivity=" + (mToActivityMessenger != null) + + " allowMenuTimeout=" + allowMenuTimeout + + " willResizeMenu=" + willResizeMenu + " callers=\n" + Debug.getCallers(5, " ")); } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java index a8a5d896537f..00f693de8f4d 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java @@ -168,6 +168,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, void synchronizePinnedStackBounds() { cancelAnimations(); mBounds.set(mPipTaskOrganizer.getLastReportedBounds()); + mFloatingContentCoordinator.onContentMoved(this); } /** diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java index 0b076559ae36..d80f18a983ee 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java @@ -56,7 +56,6 @@ public class PipResizeGestureHandler { private final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); private final PipBoundsHandler mPipBoundsHandler; - private final PipTouchHandler mPipTouchHandler; private final PipMotionHelper mMotionHelper; private final int mDisplayId; private final Executor mMainExecutor; @@ -70,10 +69,10 @@ public class PipResizeGestureHandler { private final Rect mTmpBounds = new Rect(); private final int mDelta; - private boolean mAllowGesture = false; + private boolean mAllowGesture; private boolean mIsAttached; private boolean mIsEnabled; - private boolean mEnablePipResize; + private boolean mEnableUserResize; private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; @@ -82,21 +81,20 @@ public class PipResizeGestureHandler { private int mCtrlType; public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, - PipTouchHandler pipTouchHandler, PipMotionHelper motionHelper, - DeviceConfigProxy deviceConfig, PipTaskOrganizer pipTaskOrganizer) { + PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, + PipTaskOrganizer pipTaskOrganizer) { final Resources res = context.getResources(); context.getDisplay().getMetrics(mDisplayMetrics); mDisplayId = context.getDisplayId(); mMainExecutor = context.getMainExecutor(); mPipBoundsHandler = pipBoundsHandler; - mPipTouchHandler = pipTouchHandler; mMotionHelper = motionHelper; mPipTaskOrganizer = pipTaskOrganizer; context.getDisplay().getRealSize(mMaxSize); mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); - mEnablePipResize = DeviceConfig.getBoolean( + mEnableUserResize = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, PIP_USER_RESIZE, /* defaultValue = */ true); @@ -105,7 +103,7 @@ public class PipResizeGestureHandler { @Override public void onPropertiesChanged(DeviceConfig.Properties properties) { if (properties.getKeyset().contains(PIP_USER_RESIZE)) { - mEnablePipResize = properties.getBoolean( + mEnableUserResize = properties.getBoolean( PIP_USER_RESIZE, /* defaultValue = */ true); } } @@ -134,7 +132,7 @@ public class PipResizeGestureHandler { } private void updateIsEnabled() { - boolean isEnabled = mIsAttached && mEnablePipResize; + boolean isEnabled = mIsAttached && mEnableUserResize; if (isEnabled == mIsEnabled) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index ddba9eab7766..f5c83c1fffd7 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -16,6 +16,7 @@ package com.android.systemui.pip.phone; +import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL; import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE; @@ -56,6 +57,7 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.logging.MetricsLoggerWrapper; import com.android.systemui.R; +import com.android.systemui.pip.PipAnimationController; import com.android.systemui.pip.PipBoundsHandler; import com.android.systemui.pip.PipSnapAlgorithm; import com.android.systemui.pip.PipTaskOrganizer; @@ -229,7 +231,7 @@ public class PipTouchHandler { mMotionHelper = new PipMotionHelper(mContext, activityTaskManager, pipTaskOrganizer, mMenuController, mSnapAlgorithm, mFlingAnimationUtils, floatingContentCoordinator); mPipResizeGestureHandler = - new PipResizeGestureHandler(context, pipBoundsHandler, this, mMotionHelper, + new PipResizeGestureHandler(context, pipBoundsHandler, mMotionHelper, deviceConfig, pipTaskOrganizer); mTouchState = new PipTouchState(ViewConfiguration.get(context), mHandler, () -> mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), @@ -266,6 +268,10 @@ public class PipTouchHandler { mMagnetizedPip = mMotionHelper.getMagnetizedPip(); mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); + + // Set the magnetic field radius equal to twice the size of the target. + mMagneticTarget.setMagneticFieldRadiusPx(targetSize * 2); + mMagnetizedPip.setPhysicsAnimatorUpdateListener(mMotionHelper.mResizePipUpdateListener); mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { @Override @@ -321,7 +327,7 @@ public class PipTouchHandler { } public void onActivityPinned() { - createDismissTargetMaybe(); + createOrUpdateDismissTarget(); mShowPipMenuOnAnimationEnd = true; mPipResizeGestureHandler.onActivityPinned(); @@ -339,11 +345,16 @@ public class PipTouchHandler { mPipResizeGestureHandler.onActivityUnpinned(); } - public void onPinnedStackAnimationEnded() { + public void onPinnedStackAnimationEnded( + @PipAnimationController.TransitionDirection int direction) { // Always synchronize the motion helper bounds once PiP animations finish mMotionHelper.synchronizePinnedStackBounds(); updateMovementBounds(); - mResizedBounds.set(mMotionHelper.getBounds()); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + // updates mResizedBounds only if it's an entering PiP animation + // mResized should be otherwise updated in setMenuState. + mResizedBounds.set(mMotionHelper.getBounds()); + } if (mShowPipMenuOnAnimationEnd) { mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(), @@ -357,8 +368,7 @@ public class PipTouchHandler { mMotionHelper.synchronizePinnedStackBounds(); // Recreate the dismiss target for the new orientation. - cleanUpDismissTarget(); - createDismissTargetMaybe(); + createOrUpdateDismissTarget(); } public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { @@ -454,43 +464,57 @@ public class PipTouchHandler { } /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ - private void createDismissTargetMaybe() { + private void createOrUpdateDismissTarget() { if (!mTargetViewContainer.isAttachedToWindow()) { mHandler.removeCallbacks(mShowTargetAction); mMagneticTargetAnimator.cancel(); - final Point windowSize = new Point(); - mWindowManager.getDefaultDisplay().getRealSize(windowSize); - WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, - mDismissAreaHeight, - 0, windowSize.y - mDismissAreaHeight, - WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - lp.setTitle("pip-dismiss-overlay"); - lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - lp.setFitInsetsTypes(0 /* types */); - mTargetViewContainer.setVisibility(View.INVISIBLE); - mWindowManager.addView(mTargetViewContainer, lp); + + try { + mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); + } catch (IllegalStateException e) { + // This shouldn't happen, but if the target is already added, just update its layout + // params. + mWindowManager.updateViewLayout( + mTargetViewContainer, getDismissTargetLayoutParams()); + } + } else { + mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); } } + /** Returns layout params for the dismiss target, using the latest display metrics. */ + private WindowManager.LayoutParams getDismissTargetLayoutParams() { + final Point windowSize = new Point(); + mWindowManager.getDefaultDisplay().getRealSize(windowSize); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + mDismissAreaHeight, + 0, windowSize.y - mDismissAreaHeight, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + + lp.setTitle("pip-dismiss-overlay"); + lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + lp.setFitInsetsTypes(0 /* types */); + + return lp; + } + /** Makes the dismiss target visible and animates it in, if it isn't already visible. */ private void showDismissTargetMaybe() { - createDismissTargetMaybe(); + createOrUpdateDismissTarget(); if (mTargetViewContainer.getVisibility() != View.VISIBLE) { mTargetView.setTranslationY(mTargetViewContainer.getHeight()); mTargetViewContainer.setVisibility(View.VISIBLE); - // Set the magnetic field radius to half of PIP's width. - mMagneticTarget.setMagneticFieldRadiusPx(mMotionHelper.getBounds().width()); - // Cancel in case we were in the middle of animating it out. mMagneticTargetAnimator.cancel(); mMagneticTargetAnimator diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java index 3a2d786cebe4..fae8af4f575a 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java @@ -57,6 +57,7 @@ import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.PinnedStackListenerForwarder.PinnedStackListener; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.WindowManagerWrapper; +import com.android.systemui.stackdivider.Divider; import java.util.ArrayList; import java.util.List; @@ -232,7 +233,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Inject public PipManager(Context context, BroadcastDispatcher broadcastDispatcher, PipBoundsHandler pipBoundsHandler, - PipSurfaceTransactionHelper surfaceTransactionHelper) { + PipSurfaceTransactionHelper surfaceTransactionHelper, + Divider divider) { if (mInitialized) { return; } @@ -249,7 +251,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio mResizeAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); mPipTaskOrganizer = new PipTaskOrganizer(mContext, mPipBoundsHandler, - surfaceTransactionHelper); + surfaceTransactionHelper, divider); mPipTaskOrganizer.registerPipTransitionCallback(this); mActivityTaskManager = ActivityTaskManager.getService(); ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); @@ -706,15 +708,15 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio mActiveMediaSessionListener, null); updateMediaController(mMediaSessionManager.getActiveSessions(null)); for (int i = mListeners.size() - 1; i >= 0; i--) { - mListeners.get(i).onPipEntered(); + mListeners.get(i).onPipEntered(packageName); } updatePipVisibility(true); } @Override public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, - boolean clearedTask) { - if (task.configuration.windowConfiguration.getWindowingMode() + boolean clearedTask, boolean wasVisible) { + if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_PINNED) { return; } @@ -756,7 +758,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio * because there's no guarantee for the PIP manager be return relavent information * correctly. (e.g. {@link isPipShown}). */ - void onPipEntered(); + void onPipEntered(String packageName); /** Invoked when a PIPed activity is closed. */ void onPipActivityClosed(); /** Invoked when the PIP menu gets shown. */ diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java index c7e77ccfa488..158be45e0adb 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java @@ -137,8 +137,8 @@ public class PipMenuActivity extends Activity implements PipManager.Listener { } @Override - public void onPipEntered() { - if (DEBUG) Log.d(TAG, "onPipEntered()"); + public void onPipEntered(String packageName) { + if (DEBUG) Log.d(TAG, "onPipEntered(), packageName=" + packageName); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java index b01c2f4eb5fb..30ec29683942 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java @@ -23,6 +23,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.graphics.Bitmap; @@ -50,6 +52,8 @@ public class PipNotification { private static final String ACTION_MENU = "PipNotification.menu"; private static final String ACTION_CLOSE = "PipNotification.close"; + private final PackageManager mPackageManager; + private final PipManager mPipManager; private final NotificationManager mNotificationManager; @@ -59,13 +63,16 @@ public class PipNotification { private String mDefaultTitle; private int mDefaultIconResId; + /** Package name for the application that owns PiP window. */ + private String mPackageName; private boolean mNotified; - private String mTitle; + private String mMediaTitle; private Bitmap mArt; private PipManager.Listener mPipListener = new PipManager.Listener() { @Override - public void onPipEntered() { + public void onPipEntered(String packageName) { + mPackageName = packageName; updateMediaControllerMetadata(); notifyPipNotification(); } @@ -73,6 +80,7 @@ public class PipNotification { @Override public void onPipActivityClosed() { dismissPipNotification(); + mPackageName = null; } @Override @@ -88,6 +96,7 @@ public class PipNotification { @Override public void onMoveToFullscreen() { dismissPipNotification(); + mPackageName = null; } @Override @@ -146,6 +155,8 @@ public class PipNotification { public PipNotification(Context context, BroadcastDispatcher broadcastDispatcher, PipManager pipManager) { + mPackageManager = context.getPackageManager(); + mNotificationManager = (NotificationManager) context.getSystemService( Context.NOTIFICATION_SERVICE); @@ -188,7 +199,7 @@ public class PipNotification { .setShowWhen(true) .setWhen(System.currentTimeMillis()) .setSmallIcon(mDefaultIconResId) - .setContentTitle(!TextUtils.isEmpty(mTitle) ? mTitle : mDefaultTitle); + .setContentTitle(getNotificationTitle()); if (mArt != null) { mNotificationBuilder.setStyle(new Notification.BigPictureStyle() .bigPicture(mArt)); @@ -220,14 +231,36 @@ public class PipNotification { } } } - if (!TextUtils.equals(title, mTitle) || art != mArt) { - mTitle = title; + if (!TextUtils.equals(title, mMediaTitle) || art != mArt) { + mMediaTitle = title; mArt = art; return true; } return false; } + private String getNotificationTitle() { + if (!TextUtils.isEmpty(mMediaTitle)) { + return mMediaTitle; + } + + final String applicationTitle = getApplicationLabel(mPackageName); + if (!TextUtils.isEmpty(applicationTitle)) { + return applicationTitle; + } + + return mDefaultTitle; + } + + private String getApplicationLabel(String packageName) { + try { + final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0); + return mPackageManager.getApplicationLabel(appInfo).toString(); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + private static PendingIntent createPendingIntent(Context context, String action) { return PendingIntent.getBroadcast(context, 0, new Intent(action), PendingIntent.FLAG_CANCEL_CURRENT); diff --git a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt index 448531a132df..c5ae3ab2c9fb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt @@ -20,10 +20,14 @@ import android.content.Context import android.content.res.Configuration import android.view.View import android.view.ViewGroup +import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.qs.TileLayout.exactly -class DoubleLineTileLayout(context: Context) : ViewGroup(context), QSPanel.QSTileLayout { +class DoubleLineTileLayout( + context: Context, + private val uiEventLogger: UiEventLogger +) : ViewGroup(context), QSPanel.QSTileLayout { companion object { private const val NUM_LINES = 2 @@ -86,6 +90,13 @@ class DoubleLineTileLayout(context: Context) : ViewGroup(context), QSPanel.QSTil for (record in mRecords) { record.tile.setListening(this, listening) } + if (listening) { + for (i in 0 until numVisibleTiles) { + val tile = mRecords[i].tile + uiEventLogger.logWithInstanceId( + QSEvent.QQS_TILE_VISIBLE, 0, tile.metricsSpec, tile.instanceId) + } + } } override fun getNumVisibleTiles() = tilesToShow diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index cd737217b84a..b157f4b3c5ed 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -22,7 +22,9 @@ import android.widget.Scroller; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; +import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.QSPanel.TileRecord; @@ -63,7 +65,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private int mLayoutDirection; private int mHorizontalClipBound; private final Rect mClippingRect; - private int mLastMaxHeight = -1; + private final UiEventLogger mUiEventLogger = QSEvents.INSTANCE.getQsUiEventsLogger(); public PagedTileLayout(Context context, AttributeSet attrs) { super(context, attrs); @@ -75,6 +77,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mLayoutDirection = getLayoutDirection(); mClippingRect = new Rect(); } + private int mLastMaxHeight = -1; public void saveInstanceState(Bundle outState) { outState.putInt(CURRENT_PAGE, getCurrentItem()); @@ -126,6 +129,15 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { return page; } + // This will dump to the ui log all the tiles that are visible in this page + private void logVisibleTiles(TilePage page) { + for (int i = 0; i < page.mRecords.size(); i++) { + QSTile t = page.mRecords.get(i).tile; + mUiEventLogger.logWithInstanceId(QSEvent.QS_TILE_VISIBLE, 0, t.getMetricsSpec(), + t.getInstanceId()); + } + } + @Override public void setListening(boolean listening) { if (mListening == listening) return; @@ -218,7 +230,11 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); int currentItem = getCurrentPageNumber(); for (int i = 0; i < mPages.size(); i++) { - mPages.get(i).setSelected(i == currentItem ? selected : false); + TilePage page = mPages.get(i); + page.setSelected(i == currentItem ? selected : false); + if (page.isSelected()) { + logVisibleTiles(page); + } } setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); } @@ -419,6 +435,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1 : position == 0); } + } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java index 17aaff1f7383..ee3b499edfb7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java @@ -39,6 +39,7 @@ import android.widget.Switch; import android.widget.TextView; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; @@ -53,6 +54,7 @@ public class QSDetail extends LinearLayout { private static final long FADE_DURATION = 300; private final SparseArray<View> mDetailViews = new SparseArray<>(); + private final UiEventLogger mUiEventLogger = QSEvents.INSTANCE.getQsUiEventsLogger(); private ViewGroup mDetailContent; protected TextView mDetailSettingsButton; @@ -205,6 +207,7 @@ public class QSDetail extends LinearLayout { mDetailContent.addView(detailView); mDetailViews.put(viewCacheIndex, detailView); Dependency.get(MetricsLogger.class).visible(adapter.getMetricsCategory()); + mUiEventLogger.log(adapter.openDetailEvent()); announceForAccessibility(mContext.getString( R.string.accessibility_quick_settings_detail, adapter.getTitle())); @@ -214,6 +217,7 @@ public class QSDetail extends LinearLayout { } else { if (mDetailAdapter != null) { Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory()); + mUiEventLogger.log(mDetailAdapter.closeDetailEvent()); } mClosingDetail = true; mDetailAdapter = null; @@ -249,6 +253,7 @@ public class QSDetail extends LinearLayout { mDetailSettingsButton.setOnClickListener(v -> { Dependency.get(MetricsLogger.class).action(ACTION_QS_MORE_SETTINGS, adapter.getMetricsCategory()); + mUiEventLogger.log(adapter.moreSettingsEvent()); Dependency.get(ActivityStarter.class) .postStartActivityDismissingKeyguard(settingsIntent, 0); }); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt b/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt new file mode 100644 index 000000000000..54e8a2be0d2a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLoggerImpl +import com.android.internal.logging.testing.UiEventLoggerFake + +object QSEvents { + + var qsUiEventsLogger: UiEventLogger = UiEventLoggerImpl() + private set + + fun setLoggerForTesting(): UiEventLoggerFake { + return UiEventLoggerFake().also { + qsUiEventsLogger = it + } + } + + fun resetLogger() { + qsUiEventsLogger = UiEventLoggerImpl() + } +} + +enum class QSEvent(private val _id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "Tile clicked. It has an instance id and a spec (or packageName)") + QS_ACTION_CLICK(387), + + @UiEvent(doc = "Tile secondary button clicked. " + + "It has an instance id and a spec (or packageName)") + QS_ACTION_SECONDARY_CLICK(388), + + @UiEvent(doc = "Tile long clicked. It has an instance id and a spec (or packageName)") + QS_ACTION_LONG_PRESS(389), + + @UiEvent(doc = "Quick Settings panel expanded") + QS_PANEL_EXPANDED(390), + + @UiEvent(doc = "Quick Settings panel collapsed") + QS_PANEL_COLLAPSED(391), + + @UiEvent(doc = "Tile visible in Quick Settings panel. The tile may be in a different page. " + + "It has an instance id and a spec (or packageName)") + QS_TILE_VISIBLE(392), + + @UiEvent(doc = "Quick Quick Settings panel expanded") + QQS_PANEL_EXPANDED(393), + + @UiEvent(doc = "Quick Quick Settings panel collapsed") + QQS_PANEL_COLLAPSED(394), + + @UiEvent(doc = "Tile visible in Quick Quick Settings panel. " + + "It has an instance id and a spec (or packageName)") + QQS_TILE_VISIBLE(395); + + override fun getId() = _id +} + +enum class QSEditEvent(private val _id: Int) : UiEventLogger.UiEventEnum { + + @UiEvent(doc = "Tile removed from current tiles") + QS_EDIT_REMOVE(210), + + @UiEvent(doc = "Tile added to current tiles") + QS_EDIT_ADD(211), + + @UiEvent(doc = "Tile moved") + QS_EDIT_MOVE(212), + + @UiEvent(doc = "QS customizer open") + QS_EDIT_OPEN(213), + + @UiEvent(doc = "QS customizer closed") + QS_EDIT_CLOSED(214), + + @UiEvent(doc = "QS tiles reset") + QS_EDIT_RESET(215); + + override fun getId() = _id +} + +enum class QSDndEvent(private val _id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "TODO(beverlyt)") + QS_DND_CONDITION_SELECT(420), + + @UiEvent(doc = "TODO(beverlyt)") + QS_DND_TIME_UP(422), + + @UiEvent(doc = "TODO(beverlyt)") + QS_DND_TIME_DOWN(423); + + override fun getId() = _id +} + +enum class QSUserSwitcherEvent(private val _id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "The current user has been switched in the detail panel") + QS_USER_SWITCH(424), + + @UiEvent(doc = "User switcher QS detail panel open") + QS_USER_DETAIL_OPEN(425), + + @UiEvent(doc = "User switcher QS detail panel closed") + QS_USER_DETAIL_CLOSE(426), + + @UiEvent(doc = "User switcher QS detail panel more settings pressed") + QS_USER_MORE_SETTINGS(427); + + override fun getId() = _id +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index ece1ce8bb4d0..1e8c4d86da36 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -16,6 +16,8 @@ package com.android.systemui.qs; import android.content.Context; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.external.TileServices; import com.android.systemui.qs.logging.QSLogger; @@ -30,6 +32,7 @@ public interface QSHost { Context getContext(); Context getUserContext(); QSLogger getQSLogger(); + UiEventLogger getUiEventLogger(); Collection<QSTile> getTiles(); void addCallback(Callback callback); void removeCallback(Callback callback); @@ -39,6 +42,8 @@ public interface QSHost { int indexOf(String tileSpec); + InstanceId getNewInstanceId(); + interface Callback { void onTilesChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java new file mode 100644 index 000000000000..9e532868427f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.MediaDescription; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.os.Bundle; +import android.service.media.MediaBrowserService; +import android.util.Log; + +import java.util.List; + +/** + * Media browser for managing resumption in QS media controls + */ +public class QSMediaBrowser { + + /** Maximum number of controls to show on boot */ + public static final int MAX_RESUMPTION_CONTROLS = 5; + + /** Delimiter for saved component names */ + public static final String DELIMITER = ":"; + + private static final String TAG = "QSMediaBrowser"; + private final Context mContext; + private final Callback mCallback; + private MediaBrowser mMediaBrowser; + private ComponentName mComponentName; + + /** + * Initialize a new media browser + * @param context the context + * @param callback used to report media items found + * @param componentName Component name of the MediaBrowserService this browser will connect to + */ + public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) { + mContext = context; + mCallback = callback; + mComponentName = componentName; + + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + mMediaBrowser = new MediaBrowser(mContext, + mComponentName, + mConnectionCallback, + rootHints); + } + + /** + * Connects to the MediaBrowserService and looks for valid media. If a media item is returned + * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription + */ + public void findRecentMedia() { + Log.d(TAG, "Connecting to " + mComponentName); + mMediaBrowser.connect(); + } + + private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = + new MediaBrowser.SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, + List<MediaBrowser.MediaItem> children) { + if (children.size() == 0) { + Log.e(TAG, "No children found for " + mComponentName); + return; + } + // We ask apps to return a playable item as the first child when sending + // a request with EXTRA_RECENT; if they don't, no resume controls + MediaBrowser.MediaItem child = children.get(0); + MediaDescription desc = child.getDescription(); + if (child.isPlayable()) { + mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this); + } else { + Log.e(TAG, "Child found but not playable for " + mComponentName); + } + mMediaBrowser.disconnect(); + } + + @Override + public void onError(String parentId) { + Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId); + mMediaBrowser.disconnect(); + } + + @Override + public void onError(String parentId, Bundle options) { + Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId + + ", options: " + options); + mMediaBrowser.disconnect(); + } + }; + + private final MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + /** + * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed. + * For resumption controls, apps are expected to return a playable media item as the first + * child. If there are no children or it isn't playable it will be ignored. + */ + @Override + public void onConnected() { + if (mMediaBrowser.isConnected()) { + mCallback.onConnected(); + Log.d(TAG, "Service connected for " + mComponentName); + String root = mMediaBrowser.getRoot(); + mMediaBrowser.subscribe(root, mSubscriptionCallback); + } + } + + /** + * Invoked when the client is disconnected from the media browser. + */ + @Override + public void onConnectionSuspended() { + Log.d(TAG, "Connection suspended for " + mComponentName); + } + + /** + * Invoked when the connection to the media browser failed. + */ + @Override + public void onConnectionFailed() { + Log.e(TAG, "Connection failed for " + mComponentName); + mCallback.onError(); + } + }; + + /** + * Connects to the MediaBrowserService and starts playback + */ + public void restart() { + if (mMediaBrowser.isConnected()) { + mMediaBrowser.disconnect(); + } + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + mMediaBrowser = new MediaBrowser(mContext, mComponentName, + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected()); + MediaSession.Token token = mMediaBrowser.getSessionToken(); + MediaController controller = new MediaController(mContext, token); + controller.getTransportControls(); + controller.getTransportControls().prepare(); + controller.getTransportControls().play(); + } + }, rootHints); + mMediaBrowser.connect(); + } + + /** + * Get the media session token + * @return the token, or null if the MediaBrowser is null or disconnected + */ + public MediaSession.Token getToken() { + if (mMediaBrowser == null || !mMediaBrowser.isConnected()) { + return null; + } + return mMediaBrowser.getSessionToken(); + } + + /** + * Get an intent to launch the app associated with this browser service + * @return + */ + public PendingIntent getAppIntent() { + PackageManager pm = mContext.getPackageManager(); + Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName()); + return PendingIntent.getActivity(mContext, 0, launchIntent, 0); + } + + /** + * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser + * @param mContext the context + * @param callback methods onConnected or onError will be called to indicate whether the + * connection was successful or not + * @param mComponentName Component name of the MediaBrowserService this browser will connect to + */ + public static MediaBrowser testConnection(Context mContext, Callback callback, + ComponentName mComponentName) { + final MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + Log.d(TAG, "connected"); + callback.onConnected(); + } + + @Override + public void onConnectionSuspended() { + Log.d(TAG, "suspended"); + callback.onError(); + } + + @Override + public void onConnectionFailed() { + Log.d(TAG, "failed"); + callback.onError(); + } + }; + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + MediaBrowser browser = new MediaBrowser(mContext, + mComponentName, + mConnectionCallback, + rootHints); + browser.connect(); + return browser; + } + + /** + * Interface to handle results from QSMediaBrowser + */ + public static class Callback { + /** + * Called when the browser has successfully connected to the service + */ + public void onConnected() { + } + + /** + * Called when the browser encountered an error connecting to the service + */ + public void onError() { + } + + /** + * Called when the browser finds a suitable track to add to the media carousel + * @param track media info for the item + * @param component component of the MediaBrowserService which returned this + * @param browser reference to the browser + */ + public void addTrack(MediaDescription track, ComponentName component, + QSMediaBrowser browser) { + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java index e636707a9722..e76cd5116818 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java @@ -18,11 +18,13 @@ package com.android.systemui.qs; import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; -import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.media.MediaDescription; import android.media.session.MediaController; import android.media.session.MediaSession; import android.util.Log; @@ -39,7 +41,7 @@ import com.android.systemui.R; import com.android.systemui.media.MediaControlPanel; import com.android.systemui.media.SeekBarObserver; import com.android.systemui.media.SeekBarViewModel; -import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.concurrent.Executor; @@ -61,24 +63,28 @@ public class QSMediaPlayer extends MediaControlPanel { }; private final QSPanel mParent; + private final Executor mForegroundExecutor; private final DelayableExecutor mBackgroundExecutor; private final SeekBarViewModel mSeekBarViewModel; private final SeekBarObserver mSeekBarObserver; + private String mPackageName; /** * Initialize quick shade version of player * @param context * @param parent - * @param manager + * @param routeManager Provides information about device * @param foregroundExecutor * @param backgroundExecutor + * @param activityStarter */ - public QSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager, - LocalMediaManager routeManager, Executor foregroundExecutor, - DelayableExecutor backgroundExecutor) { - super(context, parent, manager, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS, - foregroundExecutor, backgroundExecutor); + public QSMediaPlayer(Context context, ViewGroup parent, LocalMediaManager routeManager, + Executor foregroundExecutor, DelayableExecutor backgroundExecutor, + ActivityStarter activityStarter) { + super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS, + foregroundExecutor, backgroundExecutor, activityStarter); mParent = (QSPanel) parent; + mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); mSeekBarObserver = new SeekBarObserver(getView()); @@ -92,48 +98,103 @@ public class QSMediaPlayer extends MediaControlPanel { } /** + * Add a media panel view based on a media description. Used for resumption + * @param description + * @param iconColor + * @param bgColor + * @param contentIntent + * @param pkgName + */ + public void setMediaSession(MediaSession.Token token, MediaDescription description, + int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) { + mPackageName = pkgName; + PackageManager pm = getContext().getPackageManager(); + Drawable icon = null; + CharSequence appName = pkgName.substring(pkgName.lastIndexOf(".")); + try { + icon = pm.getApplicationIcon(pkgName); + appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0)); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Error getting package information", e); + } + + // Set what we can normally + super.setMediaSession(token, icon, null, iconColor, bgColor, contentIntent, + appName.toString(), null); + + // Then add info from MediaDescription + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + if (albumView != null) { + // Resize art in a background thread + mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView)); + } + + // Song name + TextView titleText = mMediaNotifView.findViewById(R.id.header_title); + CharSequence songName = description.getTitle(); + titleText.setText(songName); + titleText.setTextColor(iconColor); + + // Artist name (not in mini player) + TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); + if (artistText != null) { + CharSequence artistName = description.getSubtitle(); + artistText.setText(artistName); + artistText.setTextColor(iconColor); + } + + initLongPressMenu(iconColor); + + // Set buttons to resume state + resetButtons(); + } + + /** * Update media panel view for the given media session * @param token token for this media session * @param icon app notification icon + * @param largeIcon notification's largeIcon, used as a fallback for album art * @param iconColor foreground color (for text, icons) * @param bgColor background color * @param actionsContainer a LinearLayout containing the media action buttons - * @param notif reference to original notification - * @param device current playback device + * @param contentIntent Intent to send when user taps on player + * @param appName Application title + * @param key original notification's key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, - int bgColor, View actionsContainer, Notification notif) { + public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, + int iconColor, int bgColor, View actionsContainer, PendingIntent contentIntent, + String appName, String key) { - String appName = Notification.Builder.recoverBuilder(getContext(), notif) - .loadHeaderAppName(); - super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent, - appName); + super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName, + key); // Media controls - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } + if (actionsContainer != null) { + LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; + int i = 0; + for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); + ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); + if (thatBtn == null || thatBtn.getDrawable() == null + || thatBtn.getVisibility() != View.VISIBLE) { + thisBtn.setVisibility(View.GONE); + continue; + } - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - Log.d(TAG, "clicking on other button"); - thatBtn.performClick(); - }); - } + Drawable thatIcon = thatBtn.getDrawable(); + thisBtn.setImageDrawable(thatIcon.mutate()); + thisBtn.setVisibility(View.VISIBLE); + thisBtn.setOnClickListener(v -> { + Log.d(TAG, "clicking on other button"); + thatBtn.performClick(); + }); + } - // Hide any unused buttons - for (; i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); + // Hide any unused buttons + for (; i < QS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); + thisBtn.setVisibility(View.GONE); + } } // Seek Bar @@ -141,6 +202,10 @@ public class QSMediaPlayer extends MediaControlPanel { mBackgroundExecutor.execute( () -> mSeekBarViewModel.updateController(controller, iconColor)); + initLongPressMenu(iconColor); + } + + private void initLongPressMenu(int iconColor) { // Set up long press menu View guts = mMediaNotifView.findViewById(R.id.media_guts); View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); @@ -148,7 +213,7 @@ public class QSMediaPlayer extends MediaControlPanel { View clearView = options.findViewById(R.id.remove); clearView.setOnClickListener(b -> { - mParent.removeMediaPlayer(QSMediaPlayer.this); + removePlayer(); }); ImageView removeIcon = options.findViewById(R.id.remove_icon); removeIcon.setImageTintList(ColorStateList.valueOf(iconColor)); @@ -168,9 +233,9 @@ public class QSMediaPlayer extends MediaControlPanel { } @Override - public void clearControls() { - super.clearControls(); - + protected void resetButtons() { + super.resetButtons(); + mSeekBarViewModel.clearController(); View guts = mMediaNotifView.findViewById(R.id.media_guts); View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); @@ -193,4 +258,19 @@ public class QSMediaPlayer extends MediaControlPanel { public void setListening(boolean listening) { mSeekBarViewModel.setListening(listening); } + + @Override + public void removePlayer() { + Log.d(TAG, "removing player from parent: " + mParent); + // Ensure this happens on the main thread (could happen in QSMediaBrowser callback) + mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this)); + } + + @Override + public String getMediaPlayerPackage() { + if (getController() == null) { + return mPackageName; + } + return super.getMediaPlayerPackage(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 0566b2e621db..a3004bdc004d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -21,16 +21,26 @@ import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEX import static com.android.systemui.util.Utils.useQsMediaPlayer; import android.annotation.Nullable; +import android.app.Notification; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.media.MediaDescription; import android.media.session.MediaSession; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; +import android.os.UserManager; import android.service.notification.StatusBarNotification; import android.service.quicksettings.Tile; import android.util.AttributeSet; @@ -42,7 +52,9 @@ import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.statusbar.NotificationVisibility; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.InfoMediaManager; @@ -54,6 +66,8 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaControlPanel; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; @@ -63,7 +77,9 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; -import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.statusbar.notification.NotificationEntryListener; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; import com.android.systemui.tuner.TunerService; @@ -91,6 +107,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected final Context mContext; protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); + private final BroadcastDispatcher mBroadcastDispatcher; private String mCachedSpecs = ""; protected final View mBrightnessView; private final H mHandler = new H(); @@ -99,11 +116,12 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private final LinearLayout mMediaCarousel; private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>(); - private final NotificationMediaManager mNotificationMediaManager; private final LocalBluetoothManager mLocalBluetoothManager; private final Executor mForegroundExecutor; private final DelayableExecutor mBackgroundExecutor; private boolean mUpdateCarousel = false; + private ActivityStarter mActivityStarter; + private NotificationEntryManager mNotificationEntryManager; protected boolean mExpanded; protected boolean mListening; @@ -112,6 +130,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private BrightnessController mBrightnessController; private final DumpManager mDumpManager; private final QSLogger mQSLogger; + protected final UiEventLogger mUiEventLogger; protected QSTileHost mHost; protected QSSecurityFooter mFooter; @@ -125,6 +144,28 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private BrightnessMirrorController mBrightnessMirrorController; private View mDivider; + private boolean mHasLoadedMediaControls; + + private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_USER_UNLOCKED.equals(action)) { + if (!mHasLoadedMediaControls) { + loadMediaResumptionControls(); + } + } + } + }; + + private final NotificationEntryListener mNotificationEntryListener = + new NotificationEntryListener() { + @Override + public void onEntryRemoved(NotificationEntry entry, NotificationVisibility visibility, + boolean removedByUser, int reason) { + checkToRemoveMediaNotification(entry); + } + }; @Inject public QSPanel( @@ -133,19 +174,24 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - NotificationMediaManager notificationMediaManager, @Main Executor foregroundExecutor, @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager + @Nullable LocalBluetoothManager localBluetoothManager, + ActivityStarter activityStarter, + NotificationEntryManager entryManager, + UiEventLogger uiEventLogger ) { super(context, attrs); mContext = context; mQSLogger = qsLogger; mDumpManager = dumpManager; - mNotificationMediaManager = notificationMediaManager; mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mLocalBluetoothManager = localBluetoothManager; + mBroadcastDispatcher = broadcastDispatcher; + mActivityStarter = activityStarter; + mNotificationEntryManager = entryManager; + mUiEventLogger = uiEventLogger; setOrientation(VERTICAL); @@ -180,7 +226,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne updateResources(); mBrightnessController = new BrightnessController(getContext(), - findViewById(R.id.brightness_slider), broadcastDispatcher); + findViewById(R.id.brightness_slider), mBroadcastDispatcher); } @Override @@ -204,13 +250,16 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne * Add or update a player for the associated media session * @param token * @param icon + * @param largeIcon * @param iconColor * @param bgColor * @param actionsContainer * @param notif + * @param key */ - public void addMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, - View actionsContainer, StatusBarNotification notif) { + public void addMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, + int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif, + String key) { if (!useQsMediaPlayer(mContext)) { // Shouldn't happen, but just in case Log.e(TAG, "Tried to add media session without player!"); @@ -224,14 +273,20 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne QSMediaPlayer player = null; String packageName = notif.getPackageName(); for (QSMediaPlayer p : mMediaPlayers) { - if (p.getMediaSessionToken().equals(token)) { - Log.d(TAG, "a player for this session already exists"); + if (p.getKey() == null) { + // No notification key = loaded via mediabrowser, so just match on package + if (packageName.equals(p.getMediaPlayerPackage())) { + Log.d(TAG, "Found matching resume player by package: " + packageName); + player = p; + break; + } + } else if (p.getMediaSessionToken().equals(token)) { + Log.d(TAG, "Found matching player by token " + packageName); player = p; break; - } - - if (packageName.equals(p.getMediaPlayerPackage())) { - Log.d(TAG, "found an old session for this app"); + } else if (packageName.equals(p.getMediaPlayerPackage()) && key.equals(p.getKey())) { + // Also match if it's the same package and notification key + Log.d(TAG, "Found matching player by package " + packageName + ", " + key); player = p; break; } @@ -252,8 +307,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne LocalMediaManager routeManager = new LocalMediaManager(mContext, mLocalBluetoothManager, imm, notif.getPackageName()); - player = new QSMediaPlayer(mContext, this, mNotificationMediaManager, routeManager, - mForegroundExecutor, mBackgroundExecutor); + player = new QSMediaPlayer(mContext, this, routeManager, mForegroundExecutor, + mBackgroundExecutor, mActivityStarter); player.setListening(mListening); if (player.isPlaying()) { mMediaCarousel.addView(player.getView(), 0, lp); // add in front @@ -266,8 +321,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } Log.d(TAG, "setting player session"); - player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer, - notif.getNotification()); + String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification()) + .loadHeaderAppName(); + player.setMediaSession(token, icon, largeIcon, iconColor, bgColor, actionsContainer, + notif.getNotification().contentIntent, appName, key); if (mMediaPlayers.size() > 0) { ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); @@ -297,6 +354,100 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return true; } + private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() { + @Override + public void addTrack(MediaDescription desc, ComponentName component, + QSMediaBrowser browser) { + if (component == null) { + Log.e(TAG, "Component cannot be null"); + return; + } + + if (desc == null || desc.getTitle() == null) { + Log.e(TAG, "Description incomplete"); + return; + } + + Log.d(TAG, "adding track from browser: " + desc + ", " + component); + QSMediaPlayer player = new QSMediaPlayer(mContext, QSPanel.this, + null, mForegroundExecutor, mBackgroundExecutor, mActivityStarter); + + String pkgName = component.getPackageName(); + + // Add controls to carousel + int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width); + int padding = (int) getResources().getDimension(R.dimen.qs_media_padding); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(playerWidth, + LayoutParams.MATCH_PARENT); + lp.setMarginStart(padding); + lp.setMarginEnd(padding); + mMediaCarousel.addView(player.getView(), lp); + ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); + mMediaPlayers.add(player); + + int iconColor = Color.DKGRAY; + int bgColor = Color.LTGRAY; + + MediaSession.Token token = browser.getToken(); + player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(), + pkgName); + } + }; + + /** + * Load controls for resuming media, if available + */ + private void loadMediaResumptionControls() { + if (!useQsMediaPlayer(mContext)) { + return; + } + Log.d(TAG, "Loading resumption controls"); + + // Look up saved components to resume + Context userContext = mContext.createContextAsUser(mContext.getUser(), 0); + SharedPreferences prefs = userContext.getSharedPreferences( + MediaControlPanel.MEDIA_PREFERENCES, Context.MODE_PRIVATE); + String listString = prefs.getString(MediaControlPanel.MEDIA_PREFERENCE_KEY, null); + if (listString == null) { + Log.d(TAG, "No saved media components"); + return; + } + + String[] components = listString.split(QSMediaBrowser.DELIMITER); + Log.d(TAG, "components are: " + listString + " count " + components.length); + for (int i = 0; i < components.length && i < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) { + String[] info = components[i].split("/"); + String packageName = info[0]; + String className = info[1]; + ComponentName component = new ComponentName(packageName, className); + QSMediaBrowser browser = new QSMediaBrowser(mContext, mMediaBrowserCallback, + component); + browser.findRecentMedia(); + } + mHasLoadedMediaControls = true; + } + + private void checkToRemoveMediaNotification(NotificationEntry entry) { + if (!useQsMediaPlayer(mContext)) { + return; + } + + if (!entry.isMediaNotification()) { + return; + } + + // If this entry corresponds to an existing set of controls, clear the controls + // This will handle apps that use an action to clear their notification + for (QSMediaPlayer p : mMediaPlayers) { + if (p.getKey() != null && p.getKey().equals(entry.getKey())) { + Log.d(TAG, "Clearing controls since notification removed " + entry.getKey()); + p.clearControls(); + return; + } + } + Log.d(TAG, "Media notification removed but no player found " + entry.getKey()); + } + protected void addDivider() { mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false); mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(), @@ -347,6 +498,23 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mBrightnessMirrorController.addCallback(this); } mDumpManager.registerDumpable(getDumpableTag(), this); + + if (getClass() == QSPanel.class) { + //TODO(ethibodeau) remove class check after media refactor in ag/11059751 + // Only run this in QSPanel proper, not QQS + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_UNLOCKED); + mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, filter, null, + UserHandle.ALL); + mHasLoadedMediaControls = false; + + UserManager userManager = mContext.getSystemService(UserManager.class); + if (userManager.isUserUnlocked(mContext.getUserId())) { + // If it's already unlocked (like if dark theme was toggled), we can load now + loadMediaResumptionControls(); + } + } + mNotificationEntryManager.addNotificationEntryListener(mNotificationEntryListener); } @Override @@ -362,6 +530,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mBrightnessMirrorController.removeCallback(this); } mDumpManager.unregisterDumpable(getDumpableTag()); + mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); + mNotificationEntryManager.removeNotificationEntryListener(mNotificationEntryListener); super.onDetachedFromWindow(); } @@ -512,8 +682,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded); if (!mExpanded) { + mUiEventLogger.log(closePanelEvent()); closeDetail(); } else { + mUiEventLogger.log(openPanelEvent()); logTiles(); } } @@ -620,6 +792,18 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return mHost.createTileView(tile, collapsedView); } + protected QSEvent openPanelEvent() { + return QSEvent.QS_PANEL_EXPANDED; + } + + protected QSEvent closePanelEvent() { + return QSEvent.QS_PANEL_COLLAPSED; + } + + protected QSEvent tileVisibleEvent() { + return QSEvent.QS_TILE_VISIBLE; + } + protected boolean shouldShowDetail() { return mExpanded; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 9e8eb3a28781..8835e5db50c0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -31,6 +31,9 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -73,6 +76,7 @@ import javax.inject.Singleton; public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final int MAX_QS_INSTANCE_ID = 1 << 20; public static final String TILES_SETTING = Secure.QS_TILES; @@ -85,6 +89,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D private final DumpManager mDumpManager; private final BroadcastDispatcher mBroadcastDispatcher; private final QSLogger mQSLogger; + private final UiEventLogger mUiEventLogger; + private final InstanceIdSequence mInstanceIdSequence; private final List<Callback> mCallbacks = new ArrayList<>(); private AutoTileManager mAutoTiles; @@ -106,7 +112,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, Optional<StatusBar> statusBarOptional, - QSLogger qsLogger) { + QSLogger qsLogger, + UiEventLogger uiEventLogger) { mIconController = iconController; mContext = context; mUserContext = context; @@ -114,8 +121,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D mPluginManager = pluginManager; mDumpManager = dumpManager; mQSLogger = qsLogger; + mUiEventLogger = uiEventLogger; mBroadcastDispatcher = broadcastDispatcher; + mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID); mServices = new TileServices(this, bgLooper, mBroadcastDispatcher); mStatusBarOptional = statusBarOptional; @@ -137,6 +146,11 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D return mIconController; } + @Override + public InstanceId getNewInstanceId() { + return mInstanceIdSequence.newInstanceId(); + } + public void destroy() { mTiles.values().forEach(tile -> tile.destroy()); mAutoTiles.destroy(); @@ -170,6 +184,11 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D } @Override + public UiEventLogger getUiEventLogger() { + return mUiEventLogger; + } + + @Override public void addCallback(Callback callback) { mCallbacks.add(callback); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java index 0ba4cb159024..f77ff8cd7949 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java @@ -29,7 +29,7 @@ import android.widget.LinearLayout; import com.android.systemui.R; import com.android.systemui.media.MediaControlPanel; -import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.plugins.ActivityStarter; import java.util.concurrent.Executor; @@ -47,29 +47,32 @@ public class QuickQSMediaPlayer extends MediaControlPanel { * Initialize mini media player for QQS * @param context * @param parent - * @param manager * @param foregroundExecutor * @param backgroundExecutor + * @param activityStarter */ - public QuickQSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager, - Executor foregroundExecutor, Executor backgroundExecutor) { - super(context, parent, manager, null, R.layout.qqs_media_panel, QQS_ACTION_IDS, - foregroundExecutor, backgroundExecutor); + public QuickQSMediaPlayer(Context context, ViewGroup parent, Executor foregroundExecutor, + Executor backgroundExecutor, ActivityStarter activityStarter) { + super(context, parent, null, R.layout.qqs_media_panel, QQS_ACTION_IDS, + foregroundExecutor, backgroundExecutor, activityStarter); } /** * Update media panel view for the given media session * @param token token for this media session * @param icon app notification icon + * @param largeIcon notification's largeIcon, used as a fallback for album art * @param iconColor foreground color (for text, icons) * @param bgColor background color * @param actionsContainer a LinearLayout containing the media action buttons * @param actionsToShow indices of which actions to display in the mini player * (max 3: Notification.MediaStyle.MAX_MEDIA_BUTTONS_IN_COMPACT) * @param contentIntent Intent to send when user taps on the view + * @param key original notification's key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, - View actionsContainer, int[] actionsToShow, PendingIntent contentIntent) { + public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, + int iconColor, int bgColor, View actionsContainer, int[] actionsToShow, + PendingIntent contentIntent, String key) { // Only update if this is a different session and currently playing String oldPackage = ""; if (getController() != null) { @@ -84,7 +87,7 @@ public class QuickQSMediaPlayer extends MediaControlPanel { return; } - super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, null); + super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, null, key); LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; int i = 0; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index e6876bd98d21..6683a1ce4f4f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -27,6 +27,7 @@ import android.view.Gravity; import android.view.View; import android.widget.LinearLayout; +import com.android.internal.logging.UiEventLogger; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.Dependency; import com.android.systemui.R; @@ -34,12 +35,13 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.Utils; @@ -83,13 +85,16 @@ public class QuickQSPanel extends QSPanel { DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - NotificationMediaManager notificationMediaManager, @Main Executor foregroundExecutor, @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager + @Nullable LocalBluetoothManager localBluetoothManager, + ActivityStarter activityStarter, + NotificationEntryManager entryManager, + UiEventLogger uiEventLogger ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, notificationMediaManager, - foregroundExecutor, backgroundExecutor, localBluetoothManager); + super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, + foregroundExecutor, backgroundExecutor, localBluetoothManager, activityStarter, + entryManager, uiEventLogger); if (mFooter != null) { removeView(mFooter.getView()); } @@ -109,15 +114,15 @@ public class QuickQSPanel extends QSPanel { int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout, - notificationMediaManager, foregroundExecutor, backgroundExecutor); + foregroundExecutor, backgroundExecutor, activityStarter); LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); lp2.setMarginEnd(marginSize); lp2.setMarginStart(0); mHorizontalLinearLayout.addView(mMediaPlayer.getView(), lp2); - mTileLayout = new DoubleLineTileLayout(context); + mTileLayout = new DoubleLineTileLayout(context, mUiEventLogger); mMediaTileLayout = mTileLayout; - mRegularTileLayout = new HeaderTileLayout(context); + mRegularTileLayout = new HeaderTileLayout(context, mUiEventLogger); LayoutParams lp = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); lp.setMarginEnd(0); lp.setMarginStart(marginSize); @@ -132,7 +137,7 @@ public class QuickQSPanel extends QSPanel { super.setPadding(0, 0, 0, 0); } else { sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); - mTileLayout = new HeaderTileLayout(context); + mTileLayout = new HeaderTileLayout(context, mUiEventLogger); mTileLayout.setListening(mListening); addView((View) mTileLayout, 0 /* Between brightness and footer */); super.setPadding(0, 0, 0, 0); @@ -309,13 +314,30 @@ public class QuickQSPanel extends QSPanel { super.setVisibility(visibility); } + @Override + protected QSEvent openPanelEvent() { + return QSEvent.QQS_PANEL_EXPANDED; + } + + @Override + protected QSEvent closePanelEvent() { + return QSEvent.QQS_PANEL_COLLAPSED; + } + + @Override + protected QSEvent tileVisibleEvent() { + return QSEvent.QQS_TILE_VISIBLE; + } + private static class HeaderTileLayout extends TileLayout { - private boolean mListening; + private final UiEventLogger mUiEventLogger; + private Rect mClippingBounds = new Rect(); - public HeaderTileLayout(Context context) { + public HeaderTileLayout(Context context, UiEventLogger uiEventLogger) { super(context); + mUiEventLogger = uiEventLogger; setClipChildren(false); setClipToPadding(false); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, @@ -440,5 +462,18 @@ public class QuickQSPanel extends QSPanel { } return getPaddingStart() + column * (mCellWidth + mCellMarginHorizontal); } + + @Override + public void setListening(boolean listening) { + boolean startedListening = !mListening && listening; + super.setListening(listening); + if (startedListening) { + for (int i = 0; i < getNumVisibleTiles(); i++) { + QSTile tile = mRecords.get(i).tile; + mUiEventLogger.logWithInstanceId(QSEvent.QQS_TILE_VISIBLE, 0, + tile.getMetricsSpec(), tile.getInstanceId()); + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java index 4f812bc1059c..c22463964a19 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java @@ -37,11 +37,15 @@ public abstract class SecureSetting extends ContentObserver implements Listenabl protected abstract void handleValueChanged(int value, boolean observedChange); - public SecureSetting(Context context, Handler handler, String settingName) { + protected SecureSetting(Context context, Handler handler, String settingName) { + this(context, handler, settingName, ActivityManager.getCurrentUser()); + } + + public SecureSetting(Context context, Handler handler, String settingName, int userId) { super(handler); mContext = context; mSettingName = settingName; - mUserId = ActivityManager.getCurrentUser(); + mUserId = userId; } public int getValue() { @@ -80,4 +84,8 @@ public abstract class SecureSetting extends ContentObserver implements Listenabl setListening(true); } } + + public int getCurrentUser() { + return mUserId; + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 9f59277c918a..098431658e6a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -31,7 +31,7 @@ public class TileLayout extends ViewGroup implements QSTileLayout { protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); private int mCellMarginTop; - private boolean mListening; + protected boolean mListening; protected int mMaxAllowedRows = 3; // Prototyping with less rows diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index bfac85bd4c10..3e2f9dec5807 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -44,6 +44,7 @@ import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSDetailClipper; +import com.android.systemui.qs.QSEditEvent; import com.android.systemui.qs.QSTileHost; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; @@ -93,7 +94,8 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene LightBarController lightBarController, KeyguardStateController keyguardStateController, ScreenLifecycle screenLifecycle, - TileQueryHelper tileQueryHelper) { + TileQueryHelper tileQueryHelper, + UiEventLogger uiEventLogger) { super(new ContextThemeWrapper(context, R.style.edit_theme), attrs); LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this); @@ -115,7 +117,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene mToolbar.setTitle(R.string.qs_edit); mRecyclerView = findViewById(android.R.id.list); mTransparentView = findViewById(R.id.customizer_transparent_view); - mTileAdapter = new TileAdapter(getContext()); + mTileAdapter = new TileAdapter(getContext(), uiEventLogger); mTileQueryHelper = tileQueryHelper; mTileQueryHelper.setListener(mTileAdapter); mRecyclerView.setAdapter(mTileAdapter); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSEditEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/customize/QSEditEvent.kt deleted file mode 100644 index ff8ddec8397a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSEditEvent.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.customize - -import com.android.internal.logging.UiEvent -import com.android.internal.logging.UiEventLogger - -enum class QSEditEvent(private val _id: Int) : UiEventLogger.UiEventEnum { - - @UiEvent(doc = "Tile removed from current tiles") - QS_EDIT_REMOVE(210), - @UiEvent(doc = "Tile added to current tiles") - QS_EDIT_ADD(211), - @UiEvent(doc = "Tile moved") - QS_EDIT_MOVE(212), - @UiEvent(doc = "QS customizer open") - QS_EDIT_OPEN(213), - @UiEvent(doc = "QS customizer closed") - QS_EDIT_CLOSED(214), - @UiEvent(doc = "QS tiles reset") - QS_EDIT_RESET(215); - - override fun getId() = _id -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index 58de95d7ed6d..e738cec4962a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -41,8 +41,8 @@ import androidx.recyclerview.widget.RecyclerView.State; import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.android.internal.logging.UiEventLogger; -import com.android.internal.logging.UiEventLoggerImpl; import com.android.systemui.R; +import com.android.systemui.qs.QSEditEvent; import com.android.systemui.qs.QSTileHost; import com.android.systemui.qs.customize.TileAdapter.Holder; import com.android.systemui.qs.customize.TileQueryHelper.TileInfo; @@ -92,10 +92,11 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta private int mAccessibilityFromIndex; private CharSequence mAccessibilityFromLabel; private QSTileHost mHost; - private UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); + private final UiEventLogger mUiEventLogger; - public TileAdapter(Context context) { + public TileAdapter(Context context, UiEventLogger uiEventLogger) { mContext = context; + mUiEventLogger = uiEventLogger; mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mItemTouchHelper = new ItemTouchHelper(mCallbacks); mDecoration = new TileItemDecoration(context); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 79a7df24e217..db77e08c204b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -118,6 +118,7 @@ public class TileQueryHelper { if (tile == null) { continue; } else if (!tile.isAvailable()) { + tile.setTileSpec(spec); tile.destroy(); continue; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 08c8f86c1125..30e0a766de37 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -371,6 +371,11 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener return MetricsEvent.QS_CUSTOM; } + @Override + public final String getMetricsSpec() { + return mComponent.getPackageName(); + } + public void startUnlockAndRun() { Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { try { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index 60f6647743d2..7e5f2e1961e1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -48,7 +48,9 @@ import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.InstanceId; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.Utils; @@ -62,6 +64,7 @@ import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.PagedTileLayout.TilePage; +import com.android.systemui.qs.QSEvent; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QuickStatusBarHeader; import com.android.systemui.qs.logging.QSLogger; @@ -97,12 +100,14 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final StatusBarStateController mStatusBarStateController = Dependency.get(StatusBarStateController.class); + private final UiEventLogger mUiEventLogger; private final QSLogger mQSLogger; private final ArrayList<Callback> mCallbacks = new ArrayList<>(); private final Object mStaleListener = new Object(); protected TState mState; private TState mTmpState; + private final InstanceId mInstanceId; private boolean mAnnounceNextStateChange; private String mTileSpec; @@ -156,10 +161,12 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy protected QSTileImpl(QSHost host) { mHost = host; mContext = host.getContext(); + mInstanceId = host.getNewInstanceId(); mState = newTileState(); mTmpState = newTileState(); mQSSettingsPanelOption = QSSettingsControllerKt.getQSSettingsPanelOption(); mQSLogger = host.getQSLogger(); + mUiEventLogger = host.getUiEventLogger(); } protected final void resetStates() { @@ -173,6 +180,11 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy return mLifecycle; } + @Override + public InstanceId getInstanceId() { + return mInstanceId; + } + /** * Adds or removes a listening client for the tile. If the tile has one or more * listening client it will go into the listening state. @@ -247,6 +259,8 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION) .addTaggedData(FIELD_STATUS_BAR_STATE, mStatusBarStateController.getState()))); + mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(), + getInstanceId()); mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state); mHandler.sendEmptyMessage(H.CLICK); } @@ -255,6 +269,8 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION) .addTaggedData(FIELD_STATUS_BAR_STATE, mStatusBarStateController.getState()))); + mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(), + getInstanceId()); mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(), mState.state); mHandler.sendEmptyMessage(H.SECONDARY_CLICK); @@ -264,6 +280,8 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION) .addTaggedData(FIELD_STATUS_BAR_STATE, mStatusBarStateController.getState()))); + mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(), + getInstanceId()); mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state); mHandler.sendEmptyMessage(H.LONG_CLICK); @@ -483,6 +501,11 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy } } + @Override + public String getMetricsSpec() { + return mTileSpec; + } + /** * Provides a default label for the tile. * @return default label for the tile. diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java index 4449d483118b..f2495048bf26 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java @@ -20,6 +20,7 @@ import android.provider.Settings.Secure; import android.service.quicksettings.Tile; import android.widget.Switch; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.R; import com.android.systemui.plugins.qs.QSTile.BooleanState; @@ -34,7 +35,8 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements BatteryController.BatteryStateChangeCallback { private final BatteryController mBatteryController; - private final SecureSetting mSetting; + @VisibleForTesting + protected final SecureSetting mSetting; private int mLevel; private boolean mPowerSave; @@ -48,7 +50,9 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements super(host); mBatteryController = batteryController; mBatteryController.observe(getLifecycle(), this); - mSetting = new SecureSetting(mContext, mHandler, Secure.LOW_POWER_WARNING_ACKNOWLEDGED) { + int currentUser = host.getUserContext().getUserId(); + mSetting = new SecureSetting(mContext, mHandler, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, + currentUser) { @Override protected void handleValueChanged(int value, boolean observedChange) { handleRefreshState(null); @@ -68,6 +72,11 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements } @Override + protected void handleUserSwitch(int newUserId) { + mSetting.setUserId(newUserId); + } + + @Override public int getMetricsCategory() { return MetricsEvent.QS_BATTERY_TILE; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index 666323766c12..da7890324950 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -18,9 +18,12 @@ package com.android.systemui.qs.tiles; import android.content.Intent; import android.service.quicksettings.Tile; +import android.text.TextUtils; import android.util.Log; +import android.widget.Switch; import com.android.systemui.R; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.tileimpl.QSTileImpl; @@ -35,14 +38,17 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> implements RecordingController.RecordingStateChangeCallback { private static final String TAG = "ScreenRecordTile"; private RecordingController mController; + private ActivityStarter mActivityStarter; private long mMillisUntilFinished = 0; private Callback mCallback = new Callback(); @Inject - public ScreenRecordTile(QSHost host, RecordingController controller) { + public ScreenRecordTile(QSHost host, RecordingController controller, + ActivityStarter activityStarter) { super(host); mController = controller; mController.observe(this, mCallback); + mActivityStarter = activityStarter; } @Override @@ -72,6 +78,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> state.value = isRecording || isStarting; state.state = (isRecording || isStarting) ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + state.label = mContext.getString(R.string.quick_settings_screen_record_label); if (isRecording) { state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord); @@ -87,6 +94,10 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord); state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start); } + state.contentDescription = TextUtils.isEmpty(state.secondaryLabel) + ? state.label + : TextUtils.concat(state.label, ", ", state.secondaryLabel); + state.expandedAccessibilityClassName = Switch.class.getName(); } @Override @@ -108,7 +119,8 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> Log.d(TAG, "Starting countdown"); // Close QS, otherwise the permission dialog appears beneath it getHost().collapsePanels(); - mController.launchRecordPrompt(); + Intent intent = mController.getPromptIntent(); + mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); } private void cancelCountdown() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 447f48b5db94..89ce125ae985 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -26,10 +26,12 @@ import android.view.View; import android.view.ViewGroup; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.RestrictedLockUtils; import com.android.systemui.R; import com.android.systemui.qs.PseudoGridView; +import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.statusbar.policy.UserSwitcherController; /** @@ -48,8 +50,9 @@ public class UserDetailView extends PseudoGridView { R.layout.qs_user_detail, parent, attach); } - public void createAndSetAdapter(UserSwitcherController controller) { - mAdapter = new Adapter(mContext, controller); + public void createAndSetAdapter(UserSwitcherController controller, + UiEventLogger uiEventLogger) { + mAdapter = new Adapter(mContext, controller, uiEventLogger); ViewGroupAdapterBridge.link(this, mAdapter); } @@ -63,11 +66,14 @@ public class UserDetailView extends PseudoGridView { private final Context mContext; protected UserSwitcherController mController; private View mCurrentUserView; + private final UiEventLogger mUiEventLogger; - public Adapter(Context context, UserSwitcherController controller) { + public Adapter(Context context, UserSwitcherController controller, + UiEventLogger uiEventLogger) { super(controller); mContext = context; mController = controller; + mUiEventLogger = uiEventLogger; } @Override @@ -127,6 +133,7 @@ public class UserDetailView extends PseudoGridView { mController.startActivity(intent); } else if (tag.isSwitchToEnabled) { MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER); + mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); if (!tag.isAddUser && !tag.isRestricted && !tag.isDisabledByAdmin) { if (mCurrentUserView != null) { mCurrentUserView.setActivated(false); diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java index 5bf44c6a3003..f3e2f104621e 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java @@ -20,8 +20,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; - import android.annotation.Nullable; import android.app.ActivityManager; import android.app.trust.TrustManager; @@ -39,7 +37,6 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.stackdivider.Divider; import com.android.systemui.statusbar.phone.StatusBar; @@ -66,21 +63,6 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { private TrustManager mTrustManager; private OverviewProxyService mOverviewProxyService; - private TaskStackChangeListener mListener = new TaskStackChangeListener() { - @Override - public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, - boolean homeTaskVisible, boolean clearedTask) { - if (task.configuration.windowConfiguration.getWindowingMode() - != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { - return; - } - - if (homeTaskVisible) { - showRecentApps(false /* triggeredFromAltTab */); - } - } - }; - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject public OverviewProxyRecentsImpl(Optional<Lazy<StatusBar>> statusBarLazy, @@ -95,7 +77,6 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { mHandler = new Handler(); mTrustManager = (TrustManager) context.getSystemService(Context.TRUST_SERVICE); mOverviewProxyService = Dependency.get(OverviewProxyService.class); - ActivityManagerWrapper.getInstance().registerTaskStackListener(mListener); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 66bc177da81d..34a9e28b943a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -20,8 +20,10 @@ import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; +import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SUPPORTS_WINDOW_CORNERS; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY; @@ -58,6 +60,7 @@ import android.view.MotionEvent; import android.view.Surface; import android.view.accessibility.AccessibilityManager; +import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.util.ScreenshotHelper; import com.android.systemui.Dumpable; @@ -353,10 +356,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } long token = Binder.clearCallingIdentity(); try { - Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); + final Intent intent = + new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); + final String chooserClassName = AccessibilityButtonChooserActivity.class.getName(); + intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra(AccessibilityManager.EXTRA_SHORTCUT_TYPE, - AccessibilityManager.ACCESSIBILITY_BUTTON); mContext.startActivityAsUser(intent, UserHandle.CURRENT); } finally { Binder.restoreCallingIdentity(token); @@ -380,7 +384,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void handleImageAsScreenshot(Bitmap screenImage, Rect locationInScreen, Insets visibleInsets, int taskId) { mScreenshotHelper.provideScreenshot(screenImage, locationInScreen, visibleInsets, - taskId, mHandler, null); + taskId, SCREENSHOT_OVERVIEW, mHandler, null); } @Override @@ -491,8 +495,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } dispatchNavButtonBounds(); - // Update the systemui state flags + // Force-update the systemui state flags updateSystemUiStateFlags(); + notifySystemUiStateFlags(mSysUiState.getFlags()); notifyConnectionChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index 8dad08e49d80..ae0a1c4d9822 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -59,15 +59,15 @@ public class RecordingController } /** - * Show dialog of screen recording options to user. + * Get an intent to show screen recording options to the user. */ - public void launchRecordPrompt() { + public Intent getPromptIntent() { final ComponentName launcherComponent = new ComponentName(SYSUI_PACKAGE, SYSUI_SCREENRECORD_LAUNCHER); final Intent intent = new Intent(); intent.setComponent(launcherComponent); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); + return intent; } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 1780fb1848e7..581422116c8f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -36,6 +36,7 @@ import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Insets; @@ -47,7 +48,6 @@ import android.graphics.Region; import android.graphics.drawable.Icon; import android.media.MediaActionSound; import android.net.Uri; -import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -68,6 +68,7 @@ import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; @@ -76,11 +77,13 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.statusbar.phone.StatusBar; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -104,7 +107,6 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset */ static class SaveImageInBackgroundData { public Bitmap image; - public Uri imageUri; public Consumer<Uri> finisher; public GlobalScreenshot.ActionsReadyListener mActionsReadyListener; public int errorMsgResId; @@ -112,13 +114,33 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset void clearImage() { image = null; - imageUri = null; + } + } + + /** + * Structure returned by the SaveImageInBackgroundTask + */ + static class SavedImageData { + public Uri uri; + public Notification.Action shareAction; + public Notification.Action editAction; + public Notification.Action deleteAction; + public List<Notification.Action> smartActions; + + /** + * Used to reset the return data on error + */ + public void reset() { + uri = null; + shareAction = null; + editAction = null; + deleteAction = null; + smartActions = null; } } abstract static class ActionsReadyListener { - abstract void onActionsReady(Uri imageUri, List<Notification.Action> smartActions, - List<Notification.Action> actions); + abstract void onActionsReady(SavedImageData imageData); } // These strings are used for communicating the action invoked to @@ -142,11 +164,20 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; + private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; + private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; + private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350; + private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183; + private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade + private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; private static final float ROUNDED_CORNER_RADIUS = .05f; private static final long SCREENSHOT_CORNER_TIMEOUT_MILLIS = 6000; private static final int MESSAGE_CORNER_TIMEOUT = 2; + private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); + private final ScreenshotNotificationsController mNotificationsController; + private final UiEventLogger mUiEventLogger; private final Context mContext; private final WindowManager mWindowManager; @@ -163,17 +194,21 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset private final LinearLayout mActionsView; private final ImageView mBackgroundProtection; private final FrameLayout mDismissButton; + private final ImageView mDismissImage; private Bitmap mScreenBitmap; + private SaveImageInBackgroundTask mSaveInBgTask; private Animator mScreenshotAnimation; + private Runnable mOnCompleteRunnable; + private boolean mInDarkMode = false; + private Animator mDismissAnimation; private float mScreenshotOffsetXPx; private float mScreenshotOffsetYPx; private float mScreenshotHeightPx; private float mDismissButtonSize; private float mCornerSizeX; - - private AsyncTask<Void, Void, Void> mSaveInBgTask; + private float mDismissDeltaY; private MediaActionSound mCameraSound; @@ -185,7 +220,9 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_CORNER_TIMEOUT: - GlobalScreenshot.this.clearScreenshot("timeout"); + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); + GlobalScreenshot.this.dismissScreenshot("timeout", false); + mOnCompleteRunnable.run(); break; default: break; @@ -199,9 +236,11 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset @Inject public GlobalScreenshot( Context context, @Main Resources resources, LayoutInflater layoutInflater, - ScreenshotNotificationsController screenshotNotificationsController) { + ScreenshotNotificationsController screenshotNotificationsController, + UiEventLogger uiEventLogger) { mContext = context; mNotificationsController = screenshotNotificationsController; + mUiEventLogger = uiEventLogger; // Inflate the screenshot layout mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null); @@ -222,7 +261,12 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mBackgroundProtection = mScreenshotLayout.findViewById( R.id.global_screenshot_actions_background); mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button); - mDismissButton.setOnClickListener(view -> clearScreenshot("dismiss_button")); + mDismissButton.setOnClickListener(view -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL); + dismissScreenshot("dismiss_button", false); + mOnCompleteRunnable.run(); + }); + mDismissImage = mDismissButton.findViewById(R.id.global_screenshot_dismiss_image); mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash); mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector); @@ -231,6 +275,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mScreenshotSelectorView.setFocusableInTouchMode(true); mScreenshotView.setPivotX(0); mScreenshotView.setPivotY(0); + mActionsContainer.setPivotX(0); // Setup the window that we are going to use mWindowLayoutParams = new WindowManager.LayoutParams( @@ -257,6 +302,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mDismissButtonSize = resources.getDimensionPixelSize( R.dimen.screenshot_dismiss_button_tappable_size); mCornerSizeX = resources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale); + mDismissDeltaY = resources.getDimensionPixelSize(R.dimen.screenshot_dismissal_height_delta); mFastOutSlowIn = AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); @@ -294,19 +340,19 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset data.finisher = finisher; data.mActionsReadyListener = actionsReadyListener; data.createDeleteAction = false; + if (mSaveInBgTask != null) { - mSaveInBgTask.cancel(false); + mSaveInBgTask.ignoreResult(); } - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data).execute(); + mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data); + mSaveInBgTask.execute(); } /** * Takes a screenshot of the current display and shows an animation. */ private void takeScreenshot(Consumer<Uri> finisher, Rect crop) { - clearScreenshot("new screenshot requested"); - int rot = mDisplay.getRotation(); int width = crop.width(); int height = crop.height(); @@ -317,11 +363,15 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } private void takeScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect) { + dismissScreenshot("new screenshot requested", true); + mScreenBitmap = screenshot; + if (mScreenBitmap == null) { mNotificationsController.notifyScreenshotError( R.string.screenshot_failed_to_capture_text); finisher.accept(null); + mOnCompleteRunnable.run(); return; } @@ -329,15 +379,22 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); + updateDarkTheme(); + mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this); + if (mDismissAnimation != null && mDismissAnimation.isRunning()) { + mDismissAnimation.cancel(); + } // Start the post-screenshot animation - startAnimation(finisher, screenRect.width(), screenRect.height(), - screenRect); + startAnimation(finisher, screenRect.width(), screenRect.height(), screenRect); } - void takeScreenshot(Consumer<Uri> finisher) { + void takeScreenshot(Consumer<Uri> finisher, Runnable onComplete) { + dismissScreenshot("new screenshot requested", true); + mOnCompleteRunnable = onComplete; + mDisplay.getRealMetrics(mDisplayMetrics); takeScreenshot( finisher, @@ -345,9 +402,10 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, - Insets visibleInsets, int taskId, Consumer<Uri> finisher) { + Insets visibleInsets, int taskId, Consumer<Uri> finisher, Runnable onComplete) { // TODO use taskId and visibleInsets - clearScreenshot("new screenshot requested"); + dismissScreenshot("new screenshot requested", true); + mOnCompleteRunnable = onComplete; takeScreenshot(screenshot, finisher, screenshotScreenBounds); } @@ -355,7 +413,10 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset * Displays a screenshot selector */ @SuppressLint("ClickableViewAccessibility") - void takeScreenshotPartial(final Consumer<Uri> finisher) { + void takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete) { + dismissScreenshot("new screenshot requested", true); + mOnCompleteRunnable = onComplete; + mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() { @Override @@ -406,8 +467,24 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset /** * Clears current screenshot */ - private void clearScreenshot(String reason) { - Log.e(TAG, "clearing screenshot: " + reason); + private void dismissScreenshot(String reason, boolean immediate) { + Log.v(TAG, "clearing screenshot: " + reason); + if (!immediate) { + mDismissAnimation = createScreenshotDismissAnimation(); + mDismissAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + clearScreenshot(); + } + }); + mDismissAnimation.start(); + } else { + clearScreenshot(); + } + } + + private void clearScreenshot() { if (mScreenshotLayout.isAttachedToWindow()) { mWindowManager.removeView(mScreenshotLayout); } @@ -422,6 +499,48 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mDismissButton.setVisibility(View.GONE); mScreenshotView.setVisibility(View.GONE); mScreenshotView.setLayerType(View.LAYER_TYPE_NONE, null); + mScreenshotView.setContentDescription( + mContext.getResources().getString(R.string.screenshot_preview_description)); + mScreenshotLayout.setAlpha(1); + mDismissButton.setTranslationY(0); + mActionsContainer.setTranslationY(0); + mScreenshotView.setTranslationY(0); + } + + /** + * Update assets (called when the dark theme status changes). We only need to update the + * dismiss + * button and the actions container background, since the buttons are re-inflated on demand. + */ + private void reloadAssets() { + mDismissImage.setImageDrawable(mContext.getDrawable(R.drawable.screenshot_cancel)); + mActionsContainer.setBackground( + mContext.getDrawable(R.drawable.action_chip_container_background)); + + } + + /** + * Checks the current dark theme status and updates if it has changed. + */ + private void updateDarkTheme() { + int currentNightMode = mContext.getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK; + switch (currentNightMode) { + case Configuration.UI_MODE_NIGHT_NO: + // Night mode is not active, we're using the light theme + if (mInDarkMode) { + mInDarkMode = false; + reloadAssets(); + } + break; + case Configuration.UI_MODE_NIGHT_YES: + // Night mode is active, we're using dark theme + if (!mInDarkMode) { + mInDarkMode = true; + reloadAssets(); + } + break; + } } /** @@ -429,13 +548,17 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset */ private void startAnimation(final Consumer<Uri> finisher, int w, int h, @Nullable Rect screenRect) { - // If power save is on, show a toast so there is some visual indication that a screenshot + // If power save is on, show a toast so there is some visual indication that a + // screenshot // has been taken. - PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + PowerManager powerManager = (PowerManager) mContext.getSystemService( + Context.POWER_SERVICE); if (powerManager.isPowerSaveMode()) { - Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show(); + Toast.makeText(mContext, R.string.screenshot_saved_title, + Toast.LENGTH_SHORT).show(); } + // Add the view for the animation mScreenshotView.setImageBitmap(mScreenBitmap); @@ -443,12 +566,14 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { @Override - void onActionsReady(Uri uri, List<Notification.Action> smartActions, - List<Notification.Action> actions) { - if (uri == null) { + void onActionsReady(SavedImageData imageData) { + finisher.accept(imageData.uri); + if (imageData.uri == null) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); mNotificationsController.notifyScreenshotError( R.string.screenshot_failed_to_capture_text); } else { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); mScreenshotHandler.post(() -> { if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { mScreenshotAnimation.addListener( @@ -456,20 +581,19 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - createScreenshotActionsShadeAnimation( - smartActions, actions).start(); + createScreenshotActionsShadeAnimation(imageData) + .start(); } }); } else { - createScreenshotActionsShadeAnimation(smartActions, - actions).start(); + createScreenshotActionsShadeAnimation(imageData).start(); } + mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); + mScreenshotHandler.sendMessageDelayed( + mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), + SCREENSHOT_CORNER_TIMEOUT_MILLIS); }); } - mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); - mScreenshotHandler.sendMessageDelayed( - mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), - SCREENSHOT_CORNER_TIMEOUT_MILLIS); } }); mScreenshotHandler.post(() -> { @@ -500,14 +624,16 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset final PointF startPos = new PointF(bounds.centerX(), bounds.centerY()); final PointF finalPos = new PointF(mScreenshotOffsetXPx + width * cornerScale / 2f, - mDisplayMetrics.heightPixels - mScreenshotOffsetYPx - height * cornerScale / 2f); + mDisplayMetrics.heightPixels - mScreenshotOffsetYPx + - height * cornerScale / 2f); ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); float xPositionPct = SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; float scalePct = - SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; + SCREENSHOT_TO_CORNER_SCALE_DURATION_MS + / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; toCorner.addUpdateListener(animation -> { float t = animation.getAnimatedFraction(); if (t < scalePct) { @@ -565,8 +691,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset return dropInAnimation; } - private ValueAnimator createScreenshotActionsShadeAnimation( - List<Notification.Action> smartActions, List<Notification.Action> actions) { + private ValueAnimator createScreenshotActionsShadeAnimation(SavedImageData imageData) { LayoutInflater inflater = LayoutInflater.from(mContext); mActionsView.removeAllViews(); mActionsContainer.setScrollX(0); @@ -581,61 +706,123 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } catch (RemoteException e) { } - for (Notification.Action smartAction : smartActions) { + ArrayList<ScreenshotActionChip> chips = new ArrayList<>(); + + for (Notification.Action smartAction : imageData.smartActions) { ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( R.layout.global_screenshot_action_chip, mActionsView, false); actionChip.setText(smartAction.title); actionChip.setIcon(smartAction.getIcon(), false); actionChip.setPendingIntent(smartAction.actionIntent, - () -> clearScreenshot("chip tapped")); + () -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED); + dismissScreenshot("chip tapped", false); + mOnCompleteRunnable.run(); + }); mActionsView.addView(actionChip); + chips.add(actionChip); } - for (Notification.Action action : actions) { - ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( - R.layout.global_screenshot_action_chip, mActionsView, false); - actionChip.setText(action.title); - actionChip.setIcon(action.getIcon(), true); - actionChip.setPendingIntent(action.actionIntent, () -> clearScreenshot("chip tapped")); - if (action.actionIntent.getIntent().getAction().equals(Intent.ACTION_EDIT)) { - mScreenshotView.setOnClickListener(v -> { - try { - action.actionIntent.send(); - clearScreenshot("screenshot preview tapped"); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Intent cancelled", e); - } - }); + ScreenshotActionChip shareChip = (ScreenshotActionChip) inflater.inflate( + R.layout.global_screenshot_action_chip, mActionsView, false); + shareChip.setText(imageData.shareAction.title); + shareChip.setIcon(imageData.shareAction.getIcon(), true); + shareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED); + dismissScreenshot("chip tapped", false); + mOnCompleteRunnable.run(); + }); + mActionsView.addView(shareChip); + chips.add(shareChip); + + ScreenshotActionChip editChip = (ScreenshotActionChip) inflater.inflate( + R.layout.global_screenshot_action_chip, mActionsView, false); + editChip.setText(imageData.editAction.title); + editChip.setIcon(imageData.editAction.getIcon(), true); + editChip.setPendingIntent(imageData.editAction.actionIntent, () -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED); + dismissScreenshot("chip tapped", false); + mOnCompleteRunnable.run(); + }); + mActionsView.addView(editChip); + chips.add(editChip); + + mScreenshotView.setOnClickListener(v -> { + try { + imageData.editAction.actionIntent.send(); + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED); + dismissScreenshot("screenshot preview tapped", false); + mOnCompleteRunnable.run(); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Intent cancelled", e); } - mActionsView.addView(actionChip); - } + }); + mScreenshotView.setContentDescription(imageData.editAction.title); if (DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SCREENSHOT_SCROLLING_ENABLED, false)) { ScreenshotActionChip scrollChip = (ScreenshotActionChip) inflater.inflate( R.layout.global_screenshot_action_chip, mActionsView, false); Toast scrollNotImplemented = Toast.makeText( mContext, "Not implemented", Toast.LENGTH_SHORT); - scrollChip.setText("Extend"); // TODO (mkephart): add resource and translate + scrollChip.setText("Extend"); // TODO: add resource and translate scrollChip.setIcon( Icon.createWithResource(mContext, R.drawable.ic_arrow_downward), true); - scrollChip.setOnClickListener(v -> scrollNotImplemented.show()); + scrollChip.setOnClickListener(v -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SCROLL_TAPPED); + scrollNotImplemented.show(); + }); mActionsView.addView(scrollChip); + chips.add(scrollChip); } ValueAnimator animator = ValueAnimator.ofFloat(0, 1); - mActionsContainer.setY(mDisplayMetrics.heightPixels); + animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS); + float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS + / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS; mActionsContainer.setVisibility(VISIBLE); - mActionsContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - float actionsViewHeight = mActionsContainer.getMeasuredHeight() + mScreenshotHeightPx; + mActionsContainer.setAlpha(0); animator.addUpdateListener(animation -> { float t = animation.getAnimatedFraction(); mBackgroundProtection.setAlpha(t); - mActionsContainer.setY(mDisplayMetrics.heightPixels - actionsViewHeight * t); + mActionsContainer.setAlpha(t < alphaFraction ? t / alphaFraction : 1); + float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X + + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X)); + mActionsContainer.setScaleX(containerScale); + for (ScreenshotActionChip chip : chips) { + chip.setAlpha(t); + chip.setScaleX(1 / containerScale); // invert to keep size of children constant + } }); return animator; } + private AnimatorSet createScreenshotDismissAnimation() { + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS); + alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS); + alphaAnim.addUpdateListener(animation -> { + mScreenshotLayout.setAlpha(1 - animation.getAnimatedFraction()); + }); + + ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1); + yAnim.setInterpolator(mAccelerateInterpolator); + yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS); + float screenshotStartY = mScreenshotView.getTranslationY(); + float dismissStartY = mDismissButton.getTranslationY(); + yAnim.addUpdateListener(animation -> { + float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction()); + mScreenshotView.setTranslationY(screenshotStartY + yDelta); + mDismissButton.setTranslationY(dismissStartY + yDelta); + mActionsContainer.setTranslationY(yDelta); + }); + + AnimatorSet animSet = new AnimatorSet(); + animSet.play(yAnim).with(alphaAnim); + + return animSet; + } + /** * Receiver to proxy the share or edit intent, used to clean up the notification and send * appropriate signals to the system (ie. to dismiss the keyguard if necessary). @@ -681,7 +868,8 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { - String actionType = Intent.ACTION_EDIT.equals(intent.getAction()) ? ACTION_TYPE_EDIT + String actionType = Intent.ACTION_EDIT.equals(intent.getAction()) + ? ACTION_TYPE_EDIT : ACTION_TYPE_SHARE; ScreenshotSmartActions.notifyScreenshotAction( context, intent.getStringExtra(EXTRA_ID), actionType, false); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java index f3614ffbdb1b..095c32f4a2ce 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java @@ -24,7 +24,6 @@ import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.Nullable; -import android.app.Notification; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -53,7 +52,6 @@ import android.widget.Toast; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; -import java.util.List; import java.util.function.Consumer; import javax.inject.Inject; @@ -347,14 +345,13 @@ public class GlobalScreenshotLegacy { // Save the screenshot once we have a bit of time now saveScreenshotInWorkerThread(finisher, new GlobalScreenshot.ActionsReadyListener() { @Override - void onActionsReady(Uri uri, List<Notification.Action> smartActions, - List<Notification.Action> actions) { - if (uri == null) { + void onActionsReady(GlobalScreenshot.SavedImageData actionData) { + if (actionData.uri == null) { mNotificationsController.notifyScreenshotError( R.string.screenshot_failed_to_capture_text); } else { mNotificationsController - .showScreenshotActionsNotification(uri, smartActions, actions); + .showScreenshotActionsNotification(actionData); } } }); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index c828c4cccce5..bc3c33d68b67 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -83,6 +83,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final Context mContext; private final GlobalScreenshot.SaveImageInBackgroundData mParams; + private final GlobalScreenshot.SavedImageData mImageData; private final String mImageFileName; private final long mImageTime; private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; @@ -93,6 +94,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) { mContext = context; + mImageData = new GlobalScreenshot.SavedImageData(); // Prepare all the output metadata mParams = data; @@ -128,11 +130,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Resources r = mContext.getResources(); try { - CompletableFuture<List<Notification.Action>> smartActionsFuture = - ScreenshotSmartActions.getSmartActionsFuture( - mScreenshotId, mImageFileName, image, mSmartActionsProvider, - mSmartActionsEnabled, isManagedProfile(mContext)); - // Save the screenshot to the MediaStore final ContentValues values = new ContentValues(); values.put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES @@ -145,6 +142,12 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { values.put(MediaColumns.IS_PENDING, 1); final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + + CompletableFuture<List<Notification.Action>> smartActionsFuture = + ScreenshotSmartActions.getSmartActionsFuture( + mScreenshotId, uri, image, mSmartActionsProvider, + mSmartActionsEnabled, isManagedProfile(mContext)); + try { // First, write the actual data for our screenshot try (OutputStream out = resolver.openOutputStream(uri)) { @@ -192,8 +195,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { throw e; } - List<Notification.Action> actions = - populateNotificationActions(mContext, r, uri); List<Notification.Action> smartActions = new ArrayList<>(); if (mSmartActionsEnabled) { int timeoutMs = DeviceConfig.getInt( @@ -202,12 +203,18 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 1000); smartActions.addAll(buildSmartActions( ScreenshotSmartActions.getSmartActions( - mScreenshotId, mImageFileName, smartActionsFuture, timeoutMs, + mScreenshotId, smartActionsFuture, timeoutMs, mSmartActionsProvider), mContext)); } - mParams.mActionsReadyListener.onActionsReady(uri, smartActions, actions); - mParams.imageUri = uri; + + mImageData.uri = uri; + mImageData.smartActions = smartActions; + mImageData.shareAction = createShareAction(mContext, mContext.getResources(), uri); + mImageData.editAction = createEditAction(mContext, mContext.getResources(), uri); + mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri); + + mParams.mActionsReadyListener.onActionsReady(mImageData); mParams.image = null; mParams.errorMsgResId = 0; } catch (Exception e) { @@ -216,15 +223,24 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Slog.e(TAG, "unable to save screenshot", e); mParams.clearImage(); mParams.errorMsgResId = R.string.screenshot_failed_to_save_text; - mParams.mActionsReadyListener.onActionsReady(null, null, null); + mImageData.reset(); + mParams.mActionsReadyListener.onActionsReady(mImageData); } return null; } - @Override - protected void onPostExecute(Void params) { - mParams.finisher.accept(mParams.imageUri); + /** + * If we get a new screenshot request while this one is saving, we want to continue saving in + * the background but not return anything. + */ + void ignoreResult() { + mParams.mActionsReadyListener = new GlobalScreenshot.ActionsReadyListener() { + @Override + void onActionsReady(GlobalScreenshot.SavedImageData imageData) { + // do nothing + } + }; } @Override @@ -232,13 +248,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // If we are cancelled while the task is running in the background, we may get null // params. The finisher is expected to always be called back, so just use the baked-in // params from the ctor in any case. - mParams.mActionsReadyListener.onActionsReady(null, null, null); + mImageData.reset(); + mParams.mActionsReadyListener.onActionsReady(mImageData); mParams.finisher.accept(null); mParams.clearImage(); } @VisibleForTesting - List<Notification.Action> populateNotificationActions(Context context, Resources r, Uri uri) { + Notification.Action createShareAction(Context context, Resources r, Uri uri) { // Note: Both the share and edit actions are proxied through ActionProxyReceiver in // order to do some common work like dismissing the keyguard and sending // closeSystemWindows @@ -263,8 +280,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // by setting the (otherwise unused) request code to the current user id. int requestCode = context.getUserId(); - ArrayList<Notification.Action> actions = new ArrayList<>(); - PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode, new Intent(context, GlobalScreenshot.TargetChosenReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); @@ -288,7 +303,15 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder( Icon.createWithResource(r, R.drawable.ic_screenshot_share), r.getString(com.android.internal.R.string.share), shareAction); - actions.add(shareActionBuilder.build()); + + return shareActionBuilder.build(); + } + + @VisibleForTesting + Notification.Action createEditAction(Context context, Resources r, Uri uri) { + // Note: Both the share and edit actions are proxied through ActionProxyReceiver in + // order to do some common work like dismissing the keyguard and sending + // closeSystemWindows // Create an edit intent, if a specific package is provided as the editor, then // launch that directly @@ -301,6 +324,11 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { editIntent.setData(uri); editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + // Make sure pending intents for the system user are still unique across users + // by setting the (otherwise unused) request code to the current user id. + int requestCode = mContext.getUserId(); // Create a edit action PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, @@ -317,24 +345,30 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Notification.Action.Builder editActionBuilder = new Notification.Action.Builder( Icon.createWithResource(r, R.drawable.ic_screenshot_edit), r.getString(com.android.internal.R.string.screenshot_edit), editAction); - actions.add(editActionBuilder.build()); - - if (mCreateDeleteAction) { - // Create a delete action for the notification - PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, - new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) - .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, - mSmartActionsEnabled) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); - Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( - Icon.createWithResource(r, R.drawable.ic_screenshot_delete), - r.getString(com.android.internal.R.string.delete), deleteAction); - actions.add(deleteActionBuilder.build()); - } - return actions; + + return editActionBuilder.build(); + } + + @VisibleForTesting + Notification.Action createDeleteAction(Context context, Resources r, Uri uri) { + // Make sure pending intents for the system user are still unique across users + // by setting the (otherwise unused) request code to the current user id. + int requestCode = mContext.getUserId(); + + // Create a delete action for the notification + PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, + new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) + .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()) + .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) + .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + mSmartActionsEnabled) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( + Icon.createWithResource(r, R.drawable.ic_screenshot_delete), + r.getString(com.android.internal.R.string.delete), deleteAction); + + return deleteActionBuilder.build(); } private int getUserHandleOfForegroundApplication(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java new file mode 100644 index 000000000000..20fa991dcc1f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +public enum ScreenshotEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "screenshot requested from global actions") + SCREENSHOT_REQUESTED_GLOBAL_ACTIONS(302), + @UiEvent(doc = "screenshot requested from key chord") + SCREENSHOT_REQUESTED_KEY_CHORD(303), + @UiEvent(doc = "screenshot requested from other key press (e.g. ctrl-s)") + SCREENSHOT_REQUESTED_KEY_OTHER(384), + @UiEvent(doc = "screenshot requested from overview") + SCREENSHOT_REQUESTED_OVERVIEW(304), + @UiEvent(doc = "screenshot requested from accessibility actions") + SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS(382), + @UiEvent(doc = "screenshot requested (other)") + SCREENSHOT_REQUESTED_OTHER(305), + @UiEvent(doc = "screenshot was saved") + SCREENSHOT_SAVED(306), + @UiEvent(doc = "screenshot failed to save") + SCREENSHOT_NOT_SAVED(336), + @UiEvent(doc = "screenshot preview tapped") + SCREENSHOT_PREVIEW_TAPPED(307), + @UiEvent(doc = "screenshot edit button tapped") + SCREENSHOT_EDIT_TAPPED(308), + @UiEvent(doc = "screenshot share button tapped") + SCREENSHOT_SHARE_TAPPED(309), + @UiEvent(doc = "screenshot smart action chip tapped") + SCREENSHOT_SMART_ACTION_TAPPED(374), + @UiEvent(doc = "screenshot scroll tapped") + SCREENSHOT_SCROLL_TAPPED(373), + @UiEvent(doc = "screenshot interaction timed out") + SCREENSHOT_INTERACTION_TIMEOUT(310), + @UiEvent(doc = "screenshot explicitly dismissed") + SCREENSHOT_EXPLICIT_DISMISSAL(311); + + private final int mId; + + ScreenshotEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + + static ScreenshotEvent getScreenshotSource(int source) { + switch (source) { + case SCREENSHOT_GLOBAL_ACTIONS: + return ScreenshotEvent.SCREENSHOT_REQUESTED_GLOBAL_ACTIONS; + case SCREENSHOT_KEY_CHORD: + return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD; + case SCREENSHOT_KEY_OTHER: + return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER; + case SCREENSHOT_OVERVIEW: + return ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW; + case SCREENSHOT_ACCESSIBILITY_ACTIONS: + return ScreenshotEvent.SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS; + case SCREENSHOT_OTHER: + default: + return ScreenshotEvent.SCREENSHOT_REQUESTED_OTHER; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java index 09a0644159e2..3edb33da9cd0 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java @@ -19,6 +19,7 @@ package com.android.systemui.screenshot; import android.app.Notification; import android.content.ComponentName; import android.graphics.Bitmap; +import android.net.Uri; import android.util.Log; import java.util.Collections; @@ -67,7 +68,7 @@ public class ScreenshotNotificationSmartActionsProvider { */ public CompletableFuture<List<Notification.Action>> getActions( String screenshotId, - String screenshotFileName, + Uri screenshotUri, Bitmap bitmap, ComponentName componentName, boolean isManagedProfile) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java index 811a8d936b77..fbcd6ba0ff47 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java @@ -32,7 +32,6 @@ import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Picture; -import android.net.Uri; import android.os.UserHandle; import android.util.DisplayMetrics; import android.view.WindowManager; @@ -42,8 +41,6 @@ import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.util.NotificationChannels; -import java.util.List; - import javax.inject.Inject; /** @@ -185,23 +182,20 @@ public class ScreenshotNotificationsController { /** * Shows a notification with the saved screenshot and actions that can be taken with it. * - * @param imageUri URI for the saved image - * @param actions a list of notification actions which can be taken + * @param actionData SavedImageData struct with image URI and actions */ public void showScreenshotActionsNotification( - Uri imageUri, - List<Notification.Action> smartActions, - List<Notification.Action> actions) { - for (Notification.Action action : actions) { - mNotificationBuilder.addAction(action); - } - for (Notification.Action smartAction : smartActions) { + GlobalScreenshot.SavedImageData actionData) { + mNotificationBuilder.addAction(actionData.shareAction); + mNotificationBuilder.addAction(actionData.editAction); + mNotificationBuilder.addAction(actionData.deleteAction); + for (Notification.Action smartAction : actionData.smartActions) { mNotificationBuilder.addAction(smartAction); } // Create the intent to show the screenshot in gallery Intent launchIntent = new Intent(Intent.ACTION_VIEW); - launchIntent.setDataAndType(imageUri, "image/png"); + launchIntent.setDataAndType(actionData.uri, "image/png"); launchIntent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java index d31344446fac..c228fe2c4334 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java @@ -23,6 +23,7 @@ import android.app.Notification; import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import android.util.Slog; @@ -45,7 +46,7 @@ public class ScreenshotSmartActions { @VisibleForTesting static CompletableFuture<List<Notification.Action>> getSmartActionsFuture( - String screenshotId, String screenshotFileName, Bitmap image, + String screenshotId, Uri screenshotUri, Bitmap image, ScreenshotNotificationSmartActionsProvider smartActionsProvider, boolean smartActionsEnabled, boolean isManagedProfile) { if (!smartActionsEnabled) { @@ -70,7 +71,7 @@ public class ScreenshotSmartActions { ? runningTask.topActivity : new ComponentName("", ""); smartActionsFuture = smartActionsProvider.getActions( - screenshotId, screenshotFileName, image, componentName, isManagedProfile); + screenshotId, screenshotUri, image, componentName, isManagedProfile); } catch (Throwable e) { long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs; smartActionsFuture = CompletableFuture.completedFuture(Collections.emptyList()); @@ -84,7 +85,7 @@ public class ScreenshotSmartActions { } @VisibleForTesting - static List<Notification.Action> getSmartActions(String screenshotId, String screenshotFileName, + static List<Notification.Action> getSmartActions(String screenshotId, CompletableFuture<List<Notification.Action>> smartActionsFuture, int timeoutMs, ScreenshotNotificationSmartActionsProvider smartActionsProvider) { long startTimeMs = SystemClock.uptimeMillis(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java new file mode 100644 index 000000000000..5ced40cb1b3b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import android.os.IBinder; +import android.view.IWindowManager; + +import javax.inject.Inject; + +/** + * Stub + */ +public class ScrollCaptureController { + + public static final int STATUS_A = 0; + public static final int STATUS_B = 1; + + private final IWindowManager mWindowManagerService; + private StatusListener mListener; + + /** + * + * @param windowManagerService + */ + @Inject + public ScrollCaptureController(IWindowManager windowManagerService) { + mWindowManagerService = windowManagerService; + } + + interface StatusListener { + void onScrollCaptureStatus(boolean available); + } + + /** + * + * @param window + * @param listener + */ + public void getStatus(IBinder window, StatusListener listener) { + mListener = listener; +// try { +// mWindowManagerService.requestScrollCapture(window, new ClientCallbacks()); +// } catch (RemoteException e) { +// } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 8b8b6f8071e1..98030d45b05e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -16,6 +16,9 @@ package com.android.systemui.screenshot; +import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE; +import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI; + import android.app.Service; import android.content.Intent; import android.graphics.Bitmap; @@ -32,6 +35,9 @@ import android.os.UserManager; import android.util.Log; import android.view.WindowManager; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.ScreenshotHelper; + import java.util.function.Consumer; import javax.inject.Inject; @@ -42,13 +48,21 @@ public class TakeScreenshotService extends Service { private final GlobalScreenshot mScreenshot; private final GlobalScreenshotLegacy mScreenshotLegacy; private final UserManager mUserManager; + private final UiEventLogger mUiEventLogger; private Handler mHandler = new Handler(Looper.myLooper()) { @Override public void handleMessage(Message msg) { final Messenger callback = msg.replyTo; - Consumer<Uri> finisher = uri -> { - Message reply = Message.obtain(null, 1, uri); + Consumer<Uri> uriConsumer = uri -> { + Message reply = Message.obtain(null, SCREENSHOT_MSG_URI, uri); + try { + callback.send(reply); + } catch (RemoteException e) { + } + }; + Runnable onComplete = () -> { + Message reply = Message.obtain(null, SCREENSHOT_MSG_PROCESS_COMPLETE); try { callback.send(reply); } catch (RemoteException e) { @@ -60,42 +74,49 @@ public class TakeScreenshotService extends Service { // animation and error notification. if (!mUserManager.isUserUnlocked()) { Log.w(TAG, "Skipping screenshot because storage is locked!"); - post(() -> finisher.accept(null)); + post(() -> uriConsumer.accept(null)); + post(onComplete); return; } - // TODO (mkephart): clean up once notifications flow is fully deprecated + // TODO: clean up once notifications flow is fully deprecated boolean useCornerFlow = true; + + ScreenshotHelper.ScreenshotRequest screenshotRequest = + (ScreenshotHelper.ScreenshotRequest) msg.obj; + + mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource())); + switch (msg.what) { case WindowManager.TAKE_SCREENSHOT_FULLSCREEN: if (useCornerFlow) { - mScreenshot.takeScreenshot(finisher); + mScreenshot.takeScreenshot(uriConsumer, onComplete); } else { - mScreenshotLegacy.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0); + mScreenshotLegacy.takeScreenshot( + uriConsumer, screenshotRequest.getHasStatusBar(), + screenshotRequest.getHasNavBar()); } break; case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION: if (useCornerFlow) { - mScreenshot.takeScreenshotPartial(finisher); + mScreenshot.takeScreenshotPartial(uriConsumer, onComplete); } else { mScreenshotLegacy.takeScreenshotPartial( - finisher, msg.arg1 > 0, msg.arg2 > 0); + uriConsumer, screenshotRequest.getHasStatusBar(), + screenshotRequest.getHasNavBar()); } break; case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE: - Bitmap screenshot = msg.getData().getParcelable( - WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP); - Rect screenBounds = msg.getData().getParcelable( - WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS); - Insets insets = msg.getData().getParcelable( - WindowManager.PARCEL_KEY_SCREENSHOT_INSETS); - int taskId = msg.getData().getInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID); + Bitmap screenshot = screenshotRequest.getBitmap(); + Rect screenBounds = screenshotRequest.getBoundsInScreen(); + Insets insets = screenshotRequest.getInsets(); + int taskId = screenshotRequest.getTaskId(); if (useCornerFlow) { mScreenshot.handleImageAsScreenshot( - screenshot, screenBounds, insets, taskId, finisher); + screenshot, screenBounds, insets, taskId, uriConsumer, onComplete); } else { mScreenshotLegacy.handleImageAsScreenshot( - screenshot, screenBounds, insets, taskId, finisher); + screenshot, screenBounds, insets, taskId, uriConsumer); } break; default: @@ -106,10 +127,12 @@ public class TakeScreenshotService extends Service { @Inject public TakeScreenshotService(GlobalScreenshot globalScreenshot, - GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager) { + GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager, + UiEventLogger uiEventLogger) { mScreenshot = globalScreenshot; mScreenshotLegacy = globalScreenshotLegacy; mUserManager = userManager; + mUiEventLogger = uiEventLogger; } @Override @@ -120,7 +143,7 @@ public class TakeScreenshotService extends Service { @Override public boolean onUnbind(Intent intent) { if (mScreenshot != null) mScreenshot.stopScreenshot(); - // TODO (mkephart) remove once notifications flow is fully deprecated + // TODO remove once notifications flow is fully deprecated if (mScreenshotLegacy != null) mScreenshotLegacy.stopScreenshot(); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt new file mode 100644 index 000000000000..825a7f3dbadb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings + +import android.content.Context +import android.os.UserHandle +import androidx.annotation.VisibleForTesting +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.util.Assert +import java.lang.IllegalStateException + +/** + * Tracks a reference to the context for the current user + * + * Constructor is injected at SettingsModule + */ +class CurrentUserContextTracker internal constructor( + private val sysuiContext: Context, + broadcastDispatcher: BroadcastDispatcher +) { + private val userTracker: CurrentUserTracker + private var initialized = false + + private var _curUserContext: Context? = null + val currentUserContext: Context + get() { + if (!initialized) { + throw IllegalStateException("Must initialize before getting context") + } + return _curUserContext!! + } + + init { + userTracker = object : CurrentUserTracker(broadcastDispatcher) { + override fun onUserSwitched(newUserId: Int) { + handleUserSwitched(newUserId) + } + } + } + + fun initialize() { + initialized = true + _curUserContext = makeUserContext(userTracker.currentUserId) + userTracker.startTracking() + } + + @VisibleForTesting + fun handleUserSwitched(newUserId: Int) { + _curUserContext = makeUserContext(newUserId) + } + + private fun makeUserContext(uid: Int): Context { + Assert.isMainThread() + return sysuiContext.createContextAsUser(UserHandle.of(uid), 0) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java b/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java new file mode 100644 index 000000000000..2c5c3ceb6e66 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings.dagger; + +import android.content.Context; + +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.settings.CurrentUserContextTracker; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +/** + * Dagger Module for classes found within the com.android.systemui.settings package. + */ +@Module +public interface SettingsModule { + + /** + * Provides and initializes a CurrentUserContextTracker + */ + @Singleton + @Provides + static CurrentUserContextTracker provideCurrentUserContextTracker( + Context context, + BroadcastDispatcher broadcastDispatcher) { + CurrentUserContextTracker tracker = + new CurrentUserContextTracker(context, broadcastDispatcher); + tracker.initialize(); + return tracker; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java index b71c4ebb5930..555202a2b02c 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java @@ -21,23 +21,24 @@ import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Handler; -import android.os.RemoteException; import android.provider.Settings; import android.util.Slog; -import android.window.TaskOrganizer; -import android.window.WindowContainerToken; import android.view.LayoutInflater; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.View; +import android.window.TaskOrganizer; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import android.window.WindowOrganizer; @@ -48,6 +49,8 @@ import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.TransactionPool; import com.android.systemui.recents.Recents; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.wm.DisplayChangeController; import com.android.systemui.wm.DisplayController; @@ -90,7 +93,6 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, private boolean mHomeStackResizable = false; private ForcedResizableInfoActivityController mForcedResizableController; private SystemWindows mSystemWindows; - final SurfaceSession mSurfaceSession = new SurfaceSession(); private DisplayController mDisplayController; private DisplayImeController mImeController; final TransactionPool mTransactionPool; @@ -259,11 +261,25 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, wct.setScreenSizeDp(mSplits.mSecondary.token, mSplits.mSecondary.configuration.screenWidthDp, mSplits.mSecondary.configuration.screenHeightDp); + + wct.setBounds(mSplits.mPrimary.token, mSplitLayout.mAdjustedPrimary); + adjustAppBounds = new Rect(mSplits.mPrimary.configuration + .windowConfiguration.getAppBounds()); + adjustAppBounds.offset(0, mSplitLayout.mAdjustedPrimary.top + - mSplitLayout.mPrimary.top); + wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds); + wct.setScreenSizeDp(mSplits.mPrimary.token, + mSplits.mPrimary.configuration.screenWidthDp, + mSplits.mPrimary.configuration.screenHeightDp); } else { wct.setBounds(mSplits.mSecondary.token, mSplitLayout.mSecondary); wct.setAppBounds(mSplits.mSecondary.token, null); wct.setScreenSizeDp(mSplits.mSecondary.token, SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); + wct.setBounds(mSplits.mPrimary.token, mSplitLayout.mPrimary); + wct.setAppBounds(mSplits.mPrimary.token, null); + wct.setScreenSizeDp(mSplits.mPrimary.token, + SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); } WindowOrganizer.applyTransaction(wct); @@ -415,6 +431,21 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, } private final DividerImeController mImePositionProcessor = new DividerImeController(); + private TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() { + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() + != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY || !mSplits.isSplitScreenSupported()) { + return; + } + + if (isMinimized()) { + onUndockingTask(); + } + } + }; + public Divider(Context context, Optional<Lazy<Recents>> recentsOptionalLazy, DisplayController displayController, SystemWindows systemWindows, DisplayImeController imeController, Handler handler, @@ -474,7 +505,7 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, return; } try { - mSplits.init(mSurfaceSession); + mSplits.init(); // Set starting tile bounds based on middle target final WindowContainerTransaction tct = new WindowContainerTransaction(); int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; @@ -485,7 +516,7 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, removeDivider(); return; } - update(mDisplayController.getDisplayContext(displayId).getResources().getConfiguration()); + ActivityManagerWrapper.getInstance().registerTaskStackListener(mActivityRestartListener); } @Override @@ -554,14 +585,29 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, } private void update(Configuration configuration) { + final boolean isDividerHidden = mView != null && mView.isHidden(); + removeDivider(); addDivider(configuration); - if (mMinimized && mView != null) { - mView.setMinimizedDockStack(true, mHomeStackResizable); - updateTouchable(); + + if (mView != null) { + if (mMinimized) { + mView.setMinimizedDockStack(true, mHomeStackResizable); + updateTouchable(); + } + mView.setHidden(isDividerHidden); } } + void onTaskVanished() { + mHandler.post(this::removeDivider); + } + + void onTasksReady() { + mHandler.post(() -> update(mDisplayController.getDisplayContext( + mContext.getDisplayId()).getResources().getConfiguration())); + } + void updateVisibility(final boolean visible) { if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible); if (mVisible != visible) { diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java index a4b1310687aa..717edc591d7f 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java @@ -33,10 +33,8 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.TaskOrganizer; -import java.util.ArrayList; - class SplitScreenTaskOrganizer extends TaskOrganizer { - private static final String TAG = "SplitScreenTaskOrganizer"; + private static final String TAG = "SplitScreenTaskOrg"; private static final boolean DEBUG = Divider.DEBUG; RunningTaskInfo mPrimary; @@ -45,44 +43,31 @@ class SplitScreenTaskOrganizer extends TaskOrganizer { SurfaceControl mSecondarySurface; SurfaceControl mPrimaryDim; SurfaceControl mSecondaryDim; - ArrayList<SurfaceControl> mHomeAndRecentsSurfaces = new ArrayList<>(); Rect mHomeBounds = new Rect(); final Divider mDivider; private boolean mSplitScreenSupported = false; + final SurfaceSession mSurfaceSession = new SurfaceSession(); + SplitScreenTaskOrganizer(Divider divider) { mDivider = divider; } - void init(SurfaceSession session) throws RemoteException { + void init() throws RemoteException { registerOrganizer(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); registerOrganizer(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY); - try { - mPrimary = TaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, - WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); - mSecondary = TaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, - WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY); - mPrimarySurface = mPrimary.token.getLeash(); - mSecondarySurface = mSecondary.token.getLeash(); - } catch (Exception e) { - // teardown to prevent callbacks - unregisterOrganizer(); - throw e; + synchronized (this) { + try { + mPrimary = TaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, + WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); + mSecondary = TaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, + WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY); + } catch (Exception e) { + // teardown to prevent callbacks + unregisterOrganizer(); + throw e; + } } - mSplitScreenSupported = true; - - // Initialize dim surfaces: - mPrimaryDim = new SurfaceControl.Builder(session).setParent(mPrimarySurface) - .setColorLayer().setName("Primary Divider Dim").build(); - mSecondaryDim = new SurfaceControl.Builder(session).setParent(mSecondarySurface) - .setColorLayer().setName("Secondary Divider Dim").build(); - SurfaceControl.Transaction t = getTransaction(); - t.setLayer(mPrimaryDim, Integer.MAX_VALUE); - t.setColor(mPrimaryDim, new float[]{0f, 0f, 0f}); - t.setLayer(mSecondaryDim, Integer.MAX_VALUE); - t.setColor(mSecondaryDim, new float[]{0f, 0f, 0f}); - t.apply(); - releaseTransaction(t); } boolean isSplitScreenSupported() { @@ -98,6 +83,67 @@ class SplitScreenTaskOrganizer extends TaskOrganizer { } @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + synchronized (this) { + if (mPrimary == null || mSecondary == null) { + Log.w(TAG, "Received onTaskAppeared before creating root tasks " + taskInfo); + return; + } + + if (taskInfo.token.equals(mPrimary.token)) { + mPrimarySurface = leash; + } else if (taskInfo.token.equals(mSecondary.token)) { + mSecondarySurface = leash; + } + + if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) { + mSplitScreenSupported = true; + + // Initialize dim surfaces: + mPrimaryDim = new SurfaceControl.Builder(mSurfaceSession) + .setParent(mPrimarySurface).setColorLayer() + .setName("Primary Divider Dim").build(); + mSecondaryDim = new SurfaceControl.Builder(mSurfaceSession) + .setParent(mSecondarySurface).setColorLayer() + .setName("Secondary Divider Dim").build(); + SurfaceControl.Transaction t = getTransaction(); + t.setLayer(mPrimaryDim, Integer.MAX_VALUE); + t.setColor(mPrimaryDim, new float[]{0f, 0f, 0f}); + t.setLayer(mSecondaryDim, Integer.MAX_VALUE); + t.setColor(mSecondaryDim, new float[]{0f, 0f, 0f}); + t.apply(); + releaseTransaction(t); + + mDivider.onTasksReady(); + } + } + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + synchronized (this) { + final boolean isPrimaryTask = mPrimary != null + && taskInfo.token.equals(mPrimary.token); + final boolean isSecondaryTask = mSecondary != null + && taskInfo.token.equals(mSecondary.token); + + if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) { + mSplitScreenSupported = false; + + SurfaceControl.Transaction t = getTransaction(); + t.remove(mPrimaryDim); + t.remove(mSecondaryDim); + t.remove(mPrimarySurface); + t.remove(mSecondarySurface); + t.apply(); + releaseTransaction(t); + + mDivider.onTaskVanished(); + } + } + } + + @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { if (taskInfo.displayId != DEFAULT_DISPLAY) { return; @@ -110,6 +156,15 @@ class SplitScreenTaskOrganizer extends TaskOrganizer { * presentations based on the contents of the split regions. */ private void handleTaskInfoChanged(RunningTaskInfo info) { + if (!mSplitScreenSupported) { + // This shouldn't happen; but apparently there is a chance that SysUI crashes without + // system server receiving binder-death (or maybe it receives binder-death too late?). + // In this situation, when sys-ui restarts, the split root-tasks will still exist so + // there is a small window of time during init() where WM might send messages here + // before init() fails. So, avoid a cycle of crashes by returning early. + Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info); + return; + } final boolean secondaryWasHomeOrRecents = mSecondary.topActivityType == ACTIVITY_TYPE_HOME || mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS; final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java index 85dcbb6316d0..3027bd225216 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java @@ -174,18 +174,19 @@ public class WindowManagerProxy { if (rootTasks.isEmpty()) { return false; } - tiles.mHomeAndRecentsSurfaces.clear(); for (int i = rootTasks.size() - 1; i >= 0; --i) { final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i); - if (isHomeOrRecentTask(rootTask)) { - tiles.mHomeAndRecentsSurfaces.add(rootTask.token.getLeash()); - } + // Only move resizeable task to split secondary. WM will just ignore this anyways... + if (!rootTask.isResizable()) continue; + // Only move fullscreen tasks to split secondary. if (rootTask.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { continue; } wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); } + // Move the secondary split-forward. + wct.reorder(tiles.mSecondary.token, true /* onTop */); boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct); WindowOrganizer.applyTransaction(wct); return isHomeResizable; @@ -206,7 +207,6 @@ public class WindowManagerProxy { // Set launch root first so that any task created after getChildContainers and // before reparent (pretty unlikely) are put into fullscreen. TaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null); - tiles.mHomeAndRecentsSurfaces.clear(); // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished // plus specific APIs to clean this up. List<ActivityManager.RunningTaskInfo> primaryChildren = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 5475812b5563..43b47232d27d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -311,6 +311,7 @@ public class KeyguardIndicationController implements StateListener, mTextView.switchIndication(mTransientIndication); } else if (!TextUtils.isEmpty(mAlignmentIndication)) { mTextView.switchIndication(mAlignmentIndication); + mTextView.setTextColor(mContext.getColor(R.color.misalignment_text_color)); } else if (mPowerPluggedIn) { String indication = computePowerIndication(); if (animate) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java index 1b7524521d76..8c24c540e743 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.NavigationBarFragment; import com.android.systemui.statusbar.phone.NavigationBarView; +import com.android.systemui.statusbar.phone.NavigationModeController; import com.android.systemui.statusbar.policy.BatteryController; import javax.inject.Inject; @@ -139,7 +140,8 @@ public class NavigationBarController implements Callbacks { ? Dependency.get(LightBarController.class) : new LightBarController(context, Dependency.get(DarkIconDispatcher.class), - Dependency.get(BatteryController.class)); + Dependency.get(BatteryController.class), + Dependency.get(NavigationModeController.class)); navBar.setLightBarController(lightBarController); // TODO(b/118592525): to support multi-display, we start to add something which is diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java index ba3db0937422..670a65f55844 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java @@ -148,7 +148,7 @@ public class NotificationHeaderUtil { } public void updateChildrenHeaderAppearance() { - List<ExpandableNotificationRow> notificationChildren = mRow.getNotificationChildren(); + List<ExpandableNotificationRow> notificationChildren = mRow.getAttachedChildren(); if (notificationChildren == null) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 12298817d5a6..2647c04ff586 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -19,11 +19,13 @@ import static android.app.Notification.VISIBILITY_SECRET; import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_MEDIA_CONTROLS; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT; import android.app.ActivityManager; import android.app.KeyguardManager; import android.app.Notification; +import android.app.NotificationManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -182,7 +184,7 @@ public class NotificationLockscreenUserManagerImpl implements protected final Context mContext; private final Handler mMainHandler; protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>(); - protected final ArrayList<UserInfo> mCurrentManagedProfiles = new ArrayList<>(); + protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>(); protected int mCurrentUserId = 0; protected NotificationPresenter mPresenter; @@ -351,7 +353,10 @@ public class NotificationLockscreenUserManagerImpl implements boolean exceedsPriorityThreshold; if (NotificationUtils.useNewInterruptionModel(mContext) && hideSilentNotificationsOnLockscreen()) { - exceedsPriorityThreshold = entry.getBucket() != BUCKET_SILENT; + exceedsPriorityThreshold = + entry.getBucket() == BUCKET_MEDIA_CONTROLS + || (entry.getBucket() != BUCKET_SILENT + && entry.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT); } else { exceedsPriorityThreshold = !entry.getRanking().isAmbient(); } @@ -425,8 +430,9 @@ public class NotificationLockscreenUserManagerImpl implements */ public boolean allowsManagedPrivateNotificationsInPublic() { synchronized (mLock) { - for (UserInfo profile : mCurrentManagedProfiles) { - if (!userAllowsPrivateNotificationsInPublic(profile.id)) { + for (int i = mCurrentManagedProfiles.size() - 1; i >= 0; i--) { + if (!userAllowsPrivateNotificationsInPublic( + mCurrentManagedProfiles.valueAt(i).id)) { return false; } } @@ -490,15 +496,22 @@ public class NotificationLockscreenUserManagerImpl implements public boolean needsRedaction(NotificationEntry ent) { int userId = ent.getSbn().getUserId(); - boolean currentUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(mCurrentUserId); - boolean notiUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(userId); - boolean redactedLockscreen = currentUserWantsRedaction || notiUserWantsRedaction; + boolean isCurrentUserRedactingNotifs = + !userAllowsPrivateNotificationsInPublic(mCurrentUserId); + boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId); + boolean isNotifUserRedacted = !userAllowsPrivateNotificationsInPublic(userId); + + // redact notifications if the current user is redacting notifications; however if the + // notification is associated with a managed profile, we rely on the managed profile + // setting to determine whether to redact it + boolean isNotifRedacted = (!isNotifForManagedProfile && isCurrentUserRedactingNotifs) + || isNotifUserRedacted; boolean notificationRequestsRedaction = ent.getSbn().getNotification().visibility == Notification.VISIBILITY_PRIVATE; boolean userForcesRedaction = packageHasVisibilityOverride(ent.getSbn().getKey()); - return userForcesRedaction || notificationRequestsRedaction && redactedLockscreen; + return userForcesRedaction || notificationRequestsRedaction && isNotifRedacted; } private boolean packageHasVisibilityOverride(String key) { @@ -519,7 +532,7 @@ public class NotificationLockscreenUserManagerImpl implements for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) { mCurrentProfiles.put(user.id, user); if (UserManager.USER_TYPE_PROFILE_MANAGED.equals(user.userType)) { - mCurrentManagedProfiles.add(user); + mCurrentManagedProfiles.put(user.id, user); } } } @@ -551,7 +564,7 @@ public class NotificationLockscreenUserManagerImpl implements public boolean isAnyManagedProfilePublicMode() { synchronized (mLock) { for (int i = mCurrentManagedProfiles.size() - 1; i >= 0; i--) { - if (isLockscreenPublicMode(mCurrentManagedProfiles.get(i).id)) { + if (isLockscreenPublicMode(mCurrentManagedProfiles.valueAt(i).id)) { return true; } } @@ -668,8 +681,8 @@ public class NotificationLockscreenUserManagerImpl implements } pw.print(" mCurrentManagedProfiles="); synchronized (mLock) { - for (UserInfo userInfo : mCurrentManagedProfiles) { - pw.print("" + userInfo.id + " "); + for (int i = mCurrentManagedProfiles.size() - 1; i >= 0; i--) { + pw.print("" + mCurrentManagedProfiles.valueAt(i).id + " "); } } pw.println(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 0d7715958995..25f1a974bc36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -250,7 +250,8 @@ class NotificationShadeDepthController @Inject constructor( private fun updateShadeBlur() { var newBlur = 0 val state = statusBarStateController.state - if (state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) { + if ((state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) && + !keyguardStateController.isKeyguardFadingAway) { newBlur = blurUtils.blurRadiusOfRatio(shadeExpansion) } shadeSpring.animateTo(newBlur) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 1297f996b743..8fcc67a0708e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.inflation.LowPriorityInflationHelper; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -60,7 +61,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle private final Handler mHandler; - /** Re-usable map of notifications to their sorted children.*/ + /** Re-usable map of top-level notifications to their sorted children if any.*/ private final HashMap<NotificationEntry, List<NotificationEntry>> mTmpChildOrderMap = new HashMap<>(); @@ -71,6 +72,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle protected final VisualStabilityManager mVisualStabilityManager; private final SysuiStatusBarStateController mStatusBarStateController; private final NotificationEntryManager mEntryManager; + private final LowPriorityInflationHelper mLowPriorityInflationHelper; /** * {@code true} if notifications not part of a group should by default be rendered in their @@ -108,7 +110,8 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle BubbleController bubbleController, DynamicPrivacyController privacyController, ForegroundServiceSectionController fgsSectionController, - DynamicChildBindController dynamicChildBindController) { + DynamicChildBindController dynamicChildBindController, + LowPriorityInflationHelper lowPriorityInflationHelper) { mContext = context; mHandler = mainHandler; mLockscreenUserManager = notificationLockscreenUserManager; @@ -123,14 +126,15 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); mBubbleController = bubbleController; mDynamicPrivacyController = privacyController; - privacyController.addListener(this); mDynamicChildBindController = dynamicChildBindController; + mLowPriorityInflationHelper = lowPriorityInflationHelper; } public void setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer) { mPresenter = presenter; mListContainer = listContainer; + mDynamicPrivacyController.addListener(this); } /** @@ -177,6 +181,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle currentUserId); ent.setSensitive(sensitive, deviceSensitive); ent.getRow().setNeedsRedaction(needsRedaction); + mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow()); boolean isChildInGroup = mGroupManager.isChildInGroupWithSummary(ent.getSbn()); boolean groupChangesAllowed = mVisualStabilityManager.areGroupChangesAllowed() @@ -206,6 +211,8 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } orderedChildren.add(ent); } else { + // Top-level notif + mTmpChildOrderMap.put(ent, null); toShow.add(ent.getRow()); } } @@ -283,7 +290,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } - mDynamicChildBindController.updateChildContentViews(mTmpChildOrderMap); + mDynamicChildBindController.updateContentViews(mTmpChildOrderMap); mVisualStabilityManager.onReorderingFinished(); // clear the map again for the next usage mTmpChildOrderMap.clear(); @@ -307,17 +314,20 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } ExpandableNotificationRow parent = (ExpandableNotificationRow) view; - List<ExpandableNotificationRow> children = parent.getNotificationChildren(); + List<ExpandableNotificationRow> children = parent.getAttachedChildren(); List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry()); - - for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size(); - childIndex++) { + if (orderedChildren == null) { + // Not a group + continue; + } + parent.setUntruncatedChildCount(orderedChildren.size()); + for (int childIndex = 0; childIndex < orderedChildren.size(); childIndex++) { ExpandableNotificationRow childView = orderedChildren.get(childIndex).getRow(); if (children == null || !children.contains(childView)) { if (childView.getParent() != null) { - Log.wtf(TAG, "trying to add a notification child that already has " + - "a parent. class:" + childView.getParent().getClass() + - "\n child: " + childView); + Log.wtf(TAG, "trying to add a notification child that already has " + + "a parent. class:" + childView.getParent().getClass() + + "\n child: " + childView); // This shouldn't happen. We can recover by removing it though. ((ViewGroup) childView.getParent()).removeView(childView); } @@ -349,7 +359,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } ExpandableNotificationRow parent = (ExpandableNotificationRow) view; - List<ExpandableNotificationRow> children = parent.getNotificationChildren(); + List<ExpandableNotificationRow> children = parent.getAttachedChildren(); List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry()); if (children != null) { @@ -454,7 +464,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } if (row.isSummaryWithChildren()) { List<ExpandableNotificationRow> notificationChildren = - row.getNotificationChildren(); + row.getAttachedChildren(); int size = notificationChildren.size(); for (int i = size - 1; i >= 0; i--) { stack.push(notificationChildren.get(i)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index e81e5cae5bfc..229aa6d98e0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -24,6 +24,7 @@ import android.util.Log; import android.view.animation.Interpolator; import com.android.internal.annotations.GuardedBy; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; @@ -69,6 +70,7 @@ public class StatusBarStateControllerImpl implements SysuiStatusBarStateControll }; private final ArrayList<RankedListener> mListeners = new ArrayList<>(); + private final UiEventLogger mUiEventLogger; private int mState; private int mLastState; private boolean mLeaveOpenOnKeyguardHide; @@ -119,7 +121,8 @@ public class StatusBarStateControllerImpl implements SysuiStatusBarStateControll private Interpolator mDozeInterpolator = Interpolators.FAST_OUT_SLOW_IN; @Inject - public StatusBarStateControllerImpl() { + public StatusBarStateControllerImpl(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; for (int i = 0; i < HISTORY_SIZE; i++) { mHistoricalRecords[i] = new HistoricalState(); } @@ -155,6 +158,7 @@ public class StatusBarStateControllerImpl implements SysuiStatusBarStateControll } mLastState = mState; mState = state; + mUiEventLogger.log(StatusBarStateEvent.fromState(mState)); for (RankedListener rl : new ArrayList<>(mListeners)) { rl.mListener.onStateChanged(mState); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateEvent.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateEvent.java new file mode 100644 index 000000000000..8330169980d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateEvent.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +/** + * Events for changes in the {@link StatusBarState}. + */ +public enum StatusBarStateEvent implements UiEventLogger.UiEventEnum { + + @UiEvent(doc = "StatusBarState changed to unknown state") + STATUS_BAR_STATE_UNKNOWN(428), + + @UiEvent(doc = "StatusBarState changed to SHADE state") + STATUS_BAR_STATE_SHADE(429), + + @UiEvent(doc = "StatusBarState changed to KEYGUARD state") + STATUS_BAR_STATE_KEYGUARD(430), + + @UiEvent(doc = "StatusBarState changed to SHADE_LOCKED state") + STATUS_BAR_STATE_SHADE_LOCKED(431), + + @UiEvent(doc = "StatusBarState changed to FULLSCREEN_USER_SWITCHER state") + STATUS_BAR_STATE_FULLSCREEN_USER_SWITCHER(432); + + private int mId; + StatusBarStateEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + + /** + * Return the event associated with the state. + */ + public static StatusBarStateEvent fromState(int state) { + switch(state) { + case StatusBarState.SHADE: + return STATUS_BAR_STATE_SHADE; + case StatusBarState.KEYGUARD: + return STATUS_BAR_STATE_KEYGUARD; + case StatusBarState.SHADE_LOCKED: + return STATUS_BAR_STATE_SHADE_LOCKED; + case StatusBarState.FULLSCREEN_USER_SWITCHER: + return STATUS_BAR_STATE_FULLSCREEN_USER_SWITCHER; + default: + return STATUS_BAR_STATE_UNKNOWN; + } + } +} 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 e64b423aab60..de7e36d97b22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.notification.DynamicChildBindController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; +import com.android.systemui.statusbar.notification.collection.inflation.LowPriorityInflationHelper; import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationGroupManager; @@ -143,7 +144,8 @@ public interface StatusBarDependenciesModule { BubbleController bubbleController, DynamicPrivacyController privacyController, ForegroundServiceSectionController fgsSectionController, - DynamicChildBindController dynamicChildBindController) { + DynamicChildBindController dynamicChildBindController, + LowPriorityInflationHelper lowPriorityInflationHelper) { return new NotificationViewHierarchyManager( context, mainHandler, @@ -156,7 +158,8 @@ public interface StatusBarDependenciesModule { bubbleController, privacyController, fgsSectionController, - dynamicChildBindController); + dynamicChildBindController, + lowPriorityInflationHelper); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java index 6aef6b407f37..85560fefd952 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.app.ActivityManager; import android.graphics.Matrix; import android.graphics.Rect; @@ -41,6 +42,8 @@ import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; import com.android.systemui.statusbar.phone.NotificationPanelViewController; import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController; +import java.util.concurrent.Executor; + /** * A class that allows activities to be launched in a seamless way where the notification * transforms nicely into the starting window. @@ -59,6 +62,7 @@ public class ActivityLaunchAnimator { private final float mWindowCornerRadius; private final NotificationShadeWindowViewController mNotificationShadeWindowViewController; private final NotificationShadeDepthController mDepthController; + private final Executor mMainExecutor; private Callback mCallback; private final Runnable mTimeoutRunnable = () -> { setAnimationPending(false); @@ -73,12 +77,14 @@ public class ActivityLaunchAnimator { Callback callback, NotificationPanelViewController notificationPanel, NotificationShadeDepthController depthController, - NotificationListContainer container) { + NotificationListContainer container, + Executor mainExecutor) { mNotificationPanel = notificationPanel; mNotificationContainer = container; mDepthController = depthController; mNotificationShadeWindowViewController = notificationShadeWindowViewController; mCallback = callback; + mMainExecutor = mainExecutor; mWindowCornerRadius = ScreenDecorationsUtils .getWindowCornerRadius(mNotificationShadeWindowViewController.getView() .getResources()); @@ -91,7 +97,7 @@ public class ActivityLaunchAnimator { return null; } AnimationRunner animationRunner = new AnimationRunner( - (ExpandableNotificationRow) sourceView); + (ExpandableNotificationRow) sourceView, mMainExecutor); return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION, ANIMATION_DURATION - 150 /* statusBarTransitionDelay */); } @@ -134,17 +140,18 @@ public class ActivityLaunchAnimator { class AnimationRunner extends IRemoteAnimationRunner.Stub { - private final ExpandableNotificationRow mSourceNotification; - private final ExpandAnimationParameters mParams; + private final ExpandAnimationParameters mParams = new ExpandAnimationParameters(); private final Rect mWindowCrop = new Rect(); private final float mNotificationCornerRadius; + private final Executor mMainExecutor; + @Nullable private ExpandableNotificationRow mSourceNotification; + @Nullable private SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier; private float mCornerRadius; private boolean mIsFullScreenLaunch = true; - private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier; - public AnimationRunner(ExpandableNotificationRow sourceNofitication) { - mSourceNotification = sourceNofitication; - mParams = new ExpandAnimationParameters(); + AnimationRunner(ExpandableNotificationRow sourceNotification, Executor mainExecutor) { + mMainExecutor = mainExecutor; + mSourceNotification = sourceNotification; mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification); mNotificationCornerRadius = Math.max(mSourceNotification.getCurrentTopRoundness(), mSourceNotification.getCurrentBottomRoundness()); @@ -155,13 +162,15 @@ public class ActivityLaunchAnimator { RemoteAnimationTarget[] remoteAnimationWallpaperTargets, IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback) throws RemoteException { - mSourceNotification.post(() -> { + mMainExecutor.execute(() -> { RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget( remoteAnimationTargets); - if (primary == null) { + if (primary == null || mSourceNotification == null) { setAnimationPending(false); invokeCallback(iRemoteAnimationFinishedCallback); mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */); + mSourceNotification = null; + mSyncRtTransactionApplier = null; return; } @@ -172,28 +181,14 @@ public class ActivityLaunchAnimator { if (!mIsFullScreenLaunch) { mNotificationPanel.collapseWithDuration(ANIMATION_DURATION); } - ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - mParams.startPosition = mSourceNotification.getLocationOnScreen(); - mParams.startTranslationZ = mSourceNotification.getTranslationZ(); - mParams.startClipTopAmount = mSourceNotification.getClipTopAmount(); - if (mSourceNotification.isChildInGroup()) { - int parentClip = mSourceNotification - .getNotificationParent().getClipTopAmount(); - mParams.parentStartClipTopAmount = parentClip; - // We need to calculate how much the child is clipped by the parent - // because children always have 0 clipTopAmount - if (parentClip != 0) { - float childClip = parentClip - - mSourceNotification.getTranslationY(); - if (childClip > 0.0f) { - mParams.startClipTopAmount = (int) Math.ceil(childClip); - } - } - } - int targetWidth = primary.sourceContainerBounds.width(); - int notificationHeight = mSourceNotification.getActualHeight() + mParams.initFrom(mSourceNotification); + final int targetWidth = primary.sourceContainerBounds.width(); + final int notificationHeight; + final int notificationWidth; + notificationHeight = mSourceNotification.getActualHeight() - mSourceNotification.getClipBottomAmount(); - int notificationWidth = mSourceNotification.getWidth(); + notificationWidth = mSourceNotification.getWidth(); + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); anim.setDuration(ANIMATION_DURATION); anim.setInterpolator(Interpolators.LINEAR); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @@ -231,6 +226,11 @@ public class ActivityLaunchAnimator { }); } + @Nullable + ExpandableNotificationRow getRow() { + return mSourceNotification; + } + private void invokeCallback(IRemoteAnimationFinishedCallback callback) { try { callback.onAnimationFinished(); @@ -253,7 +253,9 @@ public class ActivityLaunchAnimator { private void setExpandAnimationRunning(boolean running) { mNotificationPanel.setLaunchingNotification(running); - mSourceNotification.setExpandAnimationRunning(running); + if (mSourceNotification != null) { + mSourceNotification.setExpandAnimationRunning(running); + } mNotificationShadeWindowViewController.setExpandAnimationRunning(running); mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null); mAnimationRunning = running; @@ -261,6 +263,8 @@ public class ActivityLaunchAnimator { mCallback.onExpandAnimationFinished(mIsFullScreenLaunch); applyParamsToNotification(null); applyParamsToNotificationShade(null); + mSourceNotification = null; + mSyncRtTransactionApplier = null; } } @@ -272,7 +276,9 @@ public class ActivityLaunchAnimator { } private void applyParamsToNotification(ExpandAnimationParameters params) { - mSourceNotification.applyExpandAnimationParams(params); + if (mSourceNotification != null) { + mSourceNotification.applyExpandAnimationParams(params); + } } private void applyParamsToWindow(RemoteAnimationTarget app) { @@ -287,14 +293,18 @@ public class ActivityLaunchAnimator { .withCornerRadius(mCornerRadius) .withVisibility(true) .build(); - mSyncRtTransactionApplier.scheduleApply(true /* earlyWakeup */, params); + if (mSyncRtTransactionApplier != null) { + mSyncRtTransactionApplier.scheduleApply(true /* earlyWakeup */, params); + } } @Override public void onAnimationCancelled() throws RemoteException { - mSourceNotification.post(() -> { + mMainExecutor.execute(() -> { setAnimationPending(false); mCallback.onLaunchAnimationCancelled(); + mSourceNotification = null; + mSyncRtTransactionApplier = null; }); } }; @@ -359,6 +369,28 @@ public class ActivityLaunchAnimator { public float getStartTranslationZ() { return startTranslationZ; } + + /** Initialize with data pulled from the row. */ + void initFrom(@Nullable ExpandableNotificationRow row) { + if (row == null) { + return; + } + startPosition = row.getLocationOnScreen(); + startTranslationZ = row.getTranslationZ(); + startClipTopAmount = row.getClipTopAmount(); + if (row.isChildInGroup()) { + int parentClip = row.getNotificationParent().getClipTopAmount(); + parentStartClipTopAmount = parentClip; + // We need to calculate how much the child is clipped by the parent + // because children always have 0 clipTopAmount + if (parentClip != 0) { + float childClip = parentClip - row.getTranslationY(); + if (childClip > 0.0f) { + startClipTopAmount = (int) Math.ceil(childClip); + } + } + } + } } public interface Callback { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java index 148cdea92052..57b41f36e51f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java @@ -63,44 +63,52 @@ public class DynamicChildBindController { } /** - * Update the child content views, unbinding content views on children that won't be visible - * and binding content views on children that will be visible eventually. + * Update the content views, unbinding content views on children that won't be visible + * and binding content views on children that will be visible eventually and previously unbound + * children that are no longer children. * - * @param groupNotifs map of notification summaries to their children + * @param groupNotifs map of top-level notifs to their children, if any */ - public void updateChildContentViews( + public void updateContentViews( Map<NotificationEntry, List<NotificationEntry>> groupNotifs) { for (NotificationEntry entry : groupNotifs.keySet()) { List<NotificationEntry> children = groupNotifs.get(entry); + if (children == null) { + if (!hasContent(entry)) { + // Case where child is updated to be top level + bindContent(entry); + } + continue; + } for (int j = 0; j < children.size(); j++) { NotificationEntry childEntry = children.get(j); if (j >= mChildBindCutoff) { - if (hasChildContent(childEntry)) { - freeChildContent(childEntry); + if (hasContent(childEntry)) { + freeContent(childEntry); } } else { - if (!hasChildContent(childEntry)) { - bindChildContent(childEntry); + if (!hasContent(childEntry)) { + bindContent(childEntry); } } } } } - private boolean hasChildContent(NotificationEntry entry) { + private boolean hasContent(NotificationEntry entry) { ExpandableNotificationRow row = entry.getRow(); return row.getPrivateLayout().getContractedChild() != null || row.getPrivateLayout().getExpandedChild() != null; } - private void freeChildContent(NotificationEntry entry) { + private void freeContent(NotificationEntry entry) { RowContentBindParams params = mStage.getStageParams(entry); params.markContentViewsFreeable(FLAG_CONTENT_VIEW_CONTRACTED); params.markContentViewsFreeable(FLAG_CONTENT_VIEW_EXPANDED); mStage.requestRebind(entry, null); } - private void bindChildContent(NotificationEntry entry) { + private void bindContent(NotificationEntry entry) { RowContentBindParams params = mStage.getStageParams(entry); params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED); params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java index ba1b23bd80ed..5748c4aa0b13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java @@ -33,6 +33,8 @@ public interface NotificationActivityStarter { void startNotificationGutsIntent(Intent intent, int appUid, ExpandableNotificationRow row); + void startHistoryIntent(boolean showHistory); + default boolean isCollapsingToShowActivityOverLockscreen() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java new file mode 100644 index 000000000000..1c2a00ed601a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification; + +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +/** + * Helps SystemUI create notification channels. + */ +public class NotificationChannelHelper { + private static final String TAG = "NotificationChannelHelper"; + + /** Creates a conversation channel based on the shortcut info or notification title. */ + public static NotificationChannel createConversationChannelIfNeeded( + Context context, + INotificationManager notificationManager, + NotificationEntry entry, + NotificationChannel channel) { + if (!TextUtils.isEmpty(channel.getConversationId())) { + return channel; + } + final String conversationId = entry.getSbn().getShortcutId(); + final String pkg = entry.getSbn().getPackageName(); + final int appUid = entry.getSbn().getUid(); + if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(pkg) + || entry.getRanking().getShortcutInfo() == null) { + return channel; + } + + // If this channel is not already a customized conversation channel, create + // a custom channel + try { + channel.setName(getName(entry)); + notificationManager.createConversationNotificationChannelForPackage( + pkg, appUid, entry.getSbn().getKey(), channel, + conversationId); + channel = notificationManager.getConversationNotificationChannel( + context.getOpPackageName(), UserHandle.getUserId(appUid), pkg, + channel.getId(), false, conversationId); + } catch (RemoteException e) { + Slog.e(TAG, "Could not create conversation channel", e); + } + return channel; + } + + private static String getName(NotificationEntry entry) { + if (entry.getRanking().getShortcutInfo().getShortLabel() != null) { + return entry.getRanking().getShortcutInfo().getShortLabel().toString(); + } + Bundle extras = entry.getSbn().getNotification().extras; + String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE); + if (TextUtils.isEmpty(nameString)) { + nameString = extras.getString(Notification.EXTRA_TITLE); + } + if (TextUtils.isEmpty(nameString)) { + nameString = "fallback"; + } + return nameString; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index 4e6df0ad1ba4..d364689a65d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -23,11 +23,14 @@ import android.view.View; import com.android.systemui.DejankUtils; import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.phone.StatusBar; import java.util.Optional; +import javax.inject.Inject; + /** * Click handler for generic clicks on notifications. Clicks on specific areas (expansion caret, * app ops icon, etc) are handled elsewhere. @@ -35,15 +38,19 @@ import java.util.Optional; public final class NotificationClicker implements View.OnClickListener { private static final String TAG = "NotificationClicker"; - private final Optional<StatusBar> mStatusBar; private final BubbleController mBubbleController; + private final NotificationClickerLogger mLogger; + private final Optional<StatusBar> mStatusBar; private final NotificationActivityStarter mNotificationActivityStarter; - public NotificationClicker(Optional<StatusBar> statusBar, + private NotificationClicker( BubbleController bubbleController, + NotificationClickerLogger logger, + Optional<StatusBar> statusBar, NotificationActivityStarter notificationActivityStarter) { - mStatusBar = statusBar; mBubbleController = bubbleController; + mLogger = logger; + mStatusBar = statusBar; mNotificationActivityStarter = notificationActivityStarter; } @@ -58,25 +65,26 @@ public final class NotificationClicker implements View.OnClickListener { SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK")); final ExpandableNotificationRow row = (ExpandableNotificationRow) v; - final StatusBarNotification sbn = row.getEntry().getSbn(); - if (sbn == null) { - Log.e(TAG, "NotificationClicker called on an unclickable notification,"); - return; - } + final NotificationEntry entry = row.getEntry(); + mLogger.logOnClick(entry); // Check if the notification is displaying the menu, if so slide notification back if (isMenuVisible(row)) { + mLogger.logMenuVisible(entry); row.animateTranslateNotification(0); return; } else if (row.isChildInGroup() && isMenuVisible(row.getNotificationParent())) { + mLogger.logParentMenuVisible(entry); row.getNotificationParent().animateTranslateNotification(0); return; } else if (row.isSummaryWithChildren() && row.areChildrenExpanded()) { // We never want to open the app directly if the user clicks in between // the notifications. + mLogger.logChildrenExpanded(entry); return; } else if (row.areGutsExposed()) { // ignore click if guts are exposed + mLogger.logGutsExposed(entry); return; } @@ -88,7 +96,7 @@ public final class NotificationClicker implements View.OnClickListener { mBubbleController.collapseStack(); } - mNotificationActivityStarter.onNotificationClicked(sbn, row); + mNotificationActivityStarter.onNotificationClicked(entry.getSbn(), row); } private boolean isMenuVisible(ExpandableNotificationRow row) { @@ -107,4 +115,30 @@ public final class NotificationClicker implements View.OnClickListener { row.setOnClickListener(null); } } + + /** Daggerized builder for NotificationClicker. */ + public static class Builder { + private final BubbleController mBubbleController; + private final NotificationClickerLogger mLogger; + + @Inject + public Builder( + BubbleController bubbleController, + NotificationClickerLogger logger) { + mBubbleController = bubbleController; + mLogger = logger; + } + + /** Builds an instance. */ + public NotificationClicker build( + Optional<StatusBar> statusBar, + NotificationActivityStarter notificationActivityStarter + ) { + return new NotificationClicker( + mBubbleController, + mLogger, + statusBar, + notificationActivityStarter); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt new file mode 100644 index 000000000000..fbf033bd2291 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import javax.inject.Inject + +class NotificationClickerLogger @Inject constructor( + @NotifInteractionLog private val buffer: LogBuffer +) { + fun logOnClick(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + str2 = entry.ranking.channel.id + }, { + "CLICK $str1 (channel=$str2)" + }) + } + + fun logMenuVisible(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; menu is visible" + }) + } + + fun logParentMenuVisible(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; parent menu is visible" + }) + } + + fun logChildrenExpanded(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; children are expanded" + }) + } + + fun logGutsExposed(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; guts are exposed" + }) + } +} + +private const val TAG = "NotificationClicker" 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 d37e16b17620..f1cb783742bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -515,7 +515,7 @@ public class NotificationEntryManager implements // always cancelled. We only remove them if they were dismissed by the user. return; } - List<NotificationEntry> childEntries = entry.getChildren(); + List<NotificationEntry> childEntries = entry.getAttachedNotifChildren(); if (childEntries == null) { return; } @@ -619,7 +619,8 @@ public class NotificationEntryManager implements entry.setSbn(notification); for (NotifCollectionListener listener : mNotifCollectionListeners) { listener.onEntryBind(entry, notification); - } mGroupManager.onEntryUpdated(entry, oldSbn); + } + mGroupManager.onEntryUpdated(entry, oldSbn); mLogger.logNotifUpdated(entry.getKey()); for (NotificationEntryListener listener : mNotificationEntryListeners) { @@ -699,7 +700,8 @@ public class NotificationEntryManager implements entry, oldImportances.get(entry.getKey()), oldAdjustments.get(entry.getKey()), - NotificationUiAdjustment.extractFromNotificationEntry(entry)); + NotificationUiAdjustment.extractFromNotificationEntry(entry), + mInflationCallback); } updateNotifications("updateNotificationRanking"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java index 2c747bdcf7b6..81494eddd989 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.statusbar.notification.collection.coordinator.PreparationCoordinator; import java.util.ArrayList; import java.util.Collections; @@ -36,6 +37,7 @@ public class GroupEntry extends ListEntry { private final List<NotificationEntry> mUnmodifiableChildren = Collections.unmodifiableList(mChildren); + private int mUntruncatedChildCount; @VisibleForTesting public GroupEntry(String key) { @@ -62,6 +64,24 @@ public class GroupEntry extends ListEntry { mSummary = summary; } + /** + * @see #getUntruncatedChildCount() + */ + public void setUntruncatedChildCount(int childCount) { + mUntruncatedChildCount = childCount; + } + + /** + * Get the untruncated number of children from the data model, including those that will not + * have views bound. This includes children that {@link PreparationCoordinator} will filter out + * entirely when they are beyond the last visible child. + * + * TODO: This should move to some shared class between the model and view hierarchy + */ + public int getUntruncatedChildCount() { + return mUntruncatedChildCount; + } + void clearChildren() { mChildren.clear(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifViewManager.kt index cf670bd5a424..339809e0770b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifViewManager.kt @@ -113,7 +113,7 @@ class NotifViewManager @Inject constructor( } else if (entries[idx] is GroupEntry) { // A top-level entry exists. If it's a group, diff the children val groupChildren = (entries[idx] as GroupEntry).children - listItem.notificationChildren?.forEach { listChild -> + listItem.attachedChildren?.forEach { listChild -> if (!groupChildren.contains(listChild.entry)) { listItem.removeChildNotification(listChild) @@ -155,8 +155,8 @@ class NotifViewManager @Inject constructor( for ((idx, childEntry) in entry.children.withIndex()) { val childListItem = rowRegistry.requireView(childEntry) // Child hasn't been added yet. add it! - if (listItem.notificationChildren == null || - !listItem.notificationChildren.contains(childListItem)) { + if (listItem.attachedChildren == null || + !listItem.attachedChildren.contains(childListItem)) { // TODO: old code here just Log.wtf()'d here. This might wreak havoc if (childListItem.view.parent != null) { throw IllegalStateException("trying to add a notification child that " + @@ -179,6 +179,7 @@ class NotifViewManager @Inject constructor( stabilityManager, null /*TODO: stability callback */ ) + listItem.setUntruncatedChildCount(entry.untruncatedChildCount) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 749c3e4c9d0d..cb0c2838c24d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -378,6 +378,7 @@ public final class NotificationEntry extends ListEntry { /** * Returns the data needed for a bubble for this notification, if it exists. */ + @Nullable public Notification.BubbleMetadata getBubbleMetadata() { return mBubbleMetadata; } @@ -385,7 +386,7 @@ public final class NotificationEntry extends ListEntry { /** * Sets bubble metadata for this notification. */ - public void setBubbleMetadata(Notification.BubbleMetadata metadata) { + public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) { mBubbleMetadata = metadata; } @@ -434,13 +435,18 @@ public final class NotificationEntry extends ListEntry { mRowController = controller; } - @Nullable - public List<NotificationEntry> getChildren() { + /** + * Get the children that are actually attached to this notification's row. + * + * TODO: Seems like most callers here should probably be using + * {@link com.android.systemui.statusbar.phone.NotificationGroupManager#getChildren} + */ + public @Nullable List<NotificationEntry> getAttachedNotifChildren() { if (row == null) { return null; } - List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren(); + List<ExpandableNotificationRow> rowChildren = row.getAttachedChildren(); if (rowChildren == null) { return null; } @@ -748,7 +754,7 @@ public final class NotificationEntry extends ListEntry { return false; } - List<NotificationEntry> children = getChildren(); + List<NotificationEntry> children = getAttachedNotifChildren(); if (children != null && children.size() > 0) { for (int i = 0; i < children.size(); i++) { NotificationEntry child = children.get(i); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt index ec17f4ed868c..9738bcc69279 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt @@ -34,12 +34,13 @@ import com.android.systemui.statusbar.notification.stack.NotificationSectionsMan import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_HEADS_UP import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT + import com.android.systemui.statusbar.phone.NotificationGroupManager import com.android.systemui.statusbar.policy.HeadsUpManager import dagger.Lazy -import java.util.Comparator -import java.util.Objects +import java.util.Objects; import javax.inject.Inject +import kotlin.Comparator private const val TAG = "NotifRankingManager" @@ -90,19 +91,13 @@ open class NotificationRankingManager @Inject constructor( val aIsHighPriority = a.isHighPriority() val bIsHighPriority = b.isHighPriority() - when { aHeadsUp != bHeadsUp -> if (aHeadsUp) -1 else 1 // Provide consistent ranking with headsUpManager aHeadsUp -> headsUpManager.compare(a, b) - usePeopleFiltering && aPersonType != bPersonType -> when (aPersonType) { - TYPE_IMPORTANT_PERSON -> -1 - TYPE_PERSON -> when (bPersonType) { - TYPE_IMPORTANT_PERSON -> 1 - else -> -1 - } - else -> 1 - } + + usePeopleFiltering && aPersonType != bPersonType -> + peopleNotificationIdentifier.compareTo(aPersonType, bPersonType) // Upsort current media notification. aMedia != bMedia -> if (aMedia) -1 else 1 // Upsort PRIORITY_MAX system notifications diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java index 2a3b2b7d815d..3fde2ed249d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY; -import static com.android.systemui.statusbar.notification.interruption.NotificationAlertingManager.alertAgain; +import static com.android.systemui.statusbar.notification.interruption.HeadsUpController.alertAgain; import android.annotation.Nullable; @@ -29,7 +29,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; -import com.android.systemui.statusbar.notification.headsup.HeadsUpViewBinder; +import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 0e8dd5e24e91..4159d43e34ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -16,11 +16,15 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import static com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; + import android.annotation.IntDef; import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; +import android.util.ArraySet; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; @@ -40,6 +44,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; @@ -60,22 +65,47 @@ public class PreparationCoordinator implements Coordinator { private final NotifInflationErrorManager mNotifErrorManager; private final NotifViewBarn mViewBarn; private final Map<NotificationEntry, Integer> mInflationStates = new ArrayMap<>(); + + /** + * The set of notifications that are currently inflating something. Note that this is + * separate from inflation state as a view could either be uninflated or inflated and still be + * inflating something. + */ + private final Set<NotificationEntry> mInflatingNotifs = new ArraySet<>(); + private final IStatusBarService mStatusBarService; + /** + * The number of children in a group we actually keep inflated since we don't actually show + * all the children and don't need every child inflated at all times. + */ + private final int mChildBindCutoff; + @Inject public PreparationCoordinator( PreparationCoordinatorLogger logger, NotifInflaterImpl notifInflater, NotifInflationErrorManager errorManager, NotifViewBarn viewBarn, - IStatusBarService service - ) { + IStatusBarService service) { + this(logger, notifInflater, errorManager, viewBarn, service, CHILD_BIND_CUTOFF); + } + + @VisibleForTesting + PreparationCoordinator( + PreparationCoordinatorLogger logger, + NotifInflaterImpl notifInflater, + NotifInflationErrorManager errorManager, + NotifViewBarn viewBarn, + IStatusBarService service, + int childBindCutoff) { mLogger = logger; mNotifInflater = notifInflater; mNotifErrorManager = errorManager; mNotifErrorManager.addInflationErrorListener(mInflationErrorListener); mViewBarn = viewBarn; mStatusBarService = service; + mChildBindCutoff = childBindCutoff; } @Override @@ -96,6 +126,8 @@ public class PreparationCoordinator implements Coordinator { @Override public void onEntryUpdated(NotificationEntry entry) { + abortInflation(entry, "entryUpdated"); + mInflatingNotifs.remove(entry); @InflationState int state = getInflationState(entry); if (state == STATE_INFLATED) { mInflationStates.put(entry, STATE_INFLATED_INVALID); @@ -113,6 +145,7 @@ public class PreparationCoordinator implements Coordinator { @Override public void onEntryCleanUp(NotificationEntry entry) { mInflationStates.remove(entry); + mInflatingNotifs.remove(entry); mViewBarn.removeViewForEntry(entry); } }; @@ -133,23 +166,11 @@ public class PreparationCoordinator implements Coordinator { private final NotifFilter mNotifInflatingFilter = new NotifFilter(TAG + "Inflating") { /** - * Filters out notifications that haven't been inflated yet + * Filters out notifications that aren't inflated */ @Override public boolean shouldFilterOut(NotificationEntry entry, long now) { - @InflationState int state = getInflationState(entry); - return (state != STATE_INFLATED) && (state != STATE_INFLATED_INVALID); - } - }; - - private final NotifInflater.InflationCallback mInflationCallback = - new NotifInflater.InflationCallback() { - @Override - public void onInflationFinished(NotificationEntry entry) { - mLogger.logNotifInflated(entry.getKey()); - mViewBarn.registerViewForEntry(entry, entry.getRow()); - mInflationStates.put(entry, STATE_INFLATED); - mNotifInflatingFilter.invalidateList(); + return !isInflated(entry); } }; @@ -187,19 +208,42 @@ public class PreparationCoordinator implements Coordinator { ListEntry entry = entries.get(i); if (entry instanceof GroupEntry) { GroupEntry groupEntry = (GroupEntry) entry; - inflateNotifRequiredViews(groupEntry.getSummary()); - List<NotificationEntry> children = groupEntry.getChildren(); - for (int j = 0, groupSize = children.size(); j < groupSize; j++) { - inflateNotifRequiredViews(children.get(j)); - } + groupEntry.setUntruncatedChildCount(groupEntry.getChildren().size()); + inflateRequiredGroupViews(groupEntry); } else { NotificationEntry notifEntry = (NotificationEntry) entry; - inflateNotifRequiredViews(notifEntry); + inflateRequiredNotifViews(notifEntry); + } + } + } + + private void inflateRequiredGroupViews(GroupEntry groupEntry) { + NotificationEntry summary = groupEntry.getSummary(); + List<NotificationEntry> children = groupEntry.getChildren(); + inflateRequiredNotifViews(summary); + for (int j = 0; j < children.size(); j++) { + NotificationEntry child = children.get(j); + boolean childShouldBeBound = j < mChildBindCutoff; + if (childShouldBeBound) { + inflateRequiredNotifViews(child); + } else { + if (mInflatingNotifs.contains(child)) { + abortInflation(child, "Past last visible group child"); + } + if (isInflated(child)) { + // TODO: May want to put an animation hint here so view manager knows to treat + // this differently from a regular removal animation + freeNotifViews(child); + } } } } - private void inflateNotifRequiredViews(NotificationEntry entry) { + private void inflateRequiredNotifViews(NotificationEntry entry) { + if (mInflatingNotifs.contains(entry)) { + // Already inflating this entry + return; + } @InflationState int state = mInflationStates.get(entry); switch (state) { case STATE_UNINFLATED: @@ -217,16 +261,38 @@ public class PreparationCoordinator implements Coordinator { private void inflateEntry(NotificationEntry entry, String reason) { abortInflation(entry, reason); - mNotifInflater.inflateViews(entry, mInflationCallback); + mInflatingNotifs.add(entry); + mNotifInflater.inflateViews(entry, this::onInflationFinished); } private void rebind(NotificationEntry entry, String reason) { - mNotifInflater.rebindViews(entry, mInflationCallback); + mInflatingNotifs.add(entry); + mNotifInflater.rebindViews(entry, this::onInflationFinished); } private void abortInflation(NotificationEntry entry, String reason) { mLogger.logInflationAborted(entry.getKey(), reason); entry.abortTask(); + mInflatingNotifs.remove(entry); + } + + private void onInflationFinished(NotificationEntry entry) { + mLogger.logNotifInflated(entry.getKey()); + mInflatingNotifs.remove(entry); + mViewBarn.registerViewForEntry(entry, entry.getRow()); + mInflationStates.put(entry, STATE_INFLATED); + mNotifInflatingFilter.invalidateList(); + } + + private void freeNotifViews(NotificationEntry entry) { + mViewBarn.removeViewForEntry(entry); + entry.setRow(null); + mInflationStates.put(entry, STATE_UNINFLATED); + } + + private boolean isInflated(NotificationEntry entry) { + @InflationState int state = getInflationState(entry); + return (state == STATE_INFLATED) || (state == STATE_INFLATED_INVALID); } private @InflationState int getInflationState(NotificationEntry entry) { @@ -241,7 +307,7 @@ public class PreparationCoordinator implements Coordinator { value = {STATE_UNINFLATED, STATE_INFLATED_INVALID, STATE_INFLATED, STATE_ERROR}) @interface InflationState {} - /** The notification has never been inflated before. */ + /** The notification has no views attached. */ private static final int STATE_UNINFLATED = 0; /** The notification is inflated. */ @@ -255,4 +321,13 @@ public class PreparationCoordinator implements Coordinator { /** The notification errored out while inflating */ private static final int STATE_ERROR = -1; + + /** + * How big the buffer of extra views we keep around to be ready to show when we do need to + * dynamically inflate a row. + */ + private static final int EXTRA_VIEW_BUFFER_COUNT = 1; + + private static final int CHILD_BIND_CUTOFF = + NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED + EXTRA_VIEW_BUFFER_COUNT; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/LowPriorityInflationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/LowPriorityInflationHelper.java new file mode 100644 index 000000000000..73c0fdc56b8d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/LowPriorityInflationHelper.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.inflation; + +import com.android.systemui.statusbar.FeatureFlags; +import com.android.systemui.statusbar.notification.collection.GroupEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.RowContentBindParams; +import com.android.systemui.statusbar.notification.row.RowContentBindStage; +import com.android.systemui.statusbar.phone.NotificationGroupManager; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Helper class that provide methods to help check when we need to inflate a low priority version + * ot notification content. + */ +@Singleton +public class LowPriorityInflationHelper { + private final FeatureFlags mFeatureFlags; + private final NotificationGroupManager mGroupManager; + private final RowContentBindStage mRowContentBindStage; + + @Inject + LowPriorityInflationHelper( + FeatureFlags featureFlags, + NotificationGroupManager groupManager, + RowContentBindStage rowContentBindStage) { + mFeatureFlags = featureFlags; + mGroupManager = groupManager; + mRowContentBindStage = rowContentBindStage; + } + + /** + * Check if we inflated the wrong version of the view and if we need to reinflate the + * content views to be their low priority version or not. + * + * Whether we inflate the low priority view or not depends on the notification being visually + * part of a group. Since group membership is determined AFTER inflation, we're forced to check + * again at a later point in the pipeline to see if we inflated the wrong view and reinflate + * the correct one here. + * + * TODO: The group manager should run before inflation so that we don't deal with this + */ + public void recheckLowPriorityViewAndInflate( + NotificationEntry entry, + ExpandableNotificationRow row) { + RowContentBindParams params = mRowContentBindStage.getStageParams(entry); + final boolean shouldBeLowPriority = shouldUseLowPriorityView(entry); + if (!row.isRemoved() && row.isLowPriority() != shouldBeLowPriority) { + params.setUseLowPriority(shouldBeLowPriority); + mRowContentBindStage.requestRebind(entry, + en -> row.setIsLowPriority(shouldBeLowPriority)); + } + } + + /** + * Whether the notification should inflate a low priority version of its content views. + */ + public boolean shouldUseLowPriorityView(NotificationEntry entry) { + boolean isGroupChild; + if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { + isGroupChild = (entry.getParent() != GroupEntry.ROOT_ENTRY); + } else { + isGroupChild = mGroupManager.isChildInGroupWithSummary(entry.getSbn()); + } + return entry.isAmbient() && !isGroupChild; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java index f4c4924b4b9a..710b137d2795 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java @@ -51,5 +51,6 @@ public interface NotificationRowBinder { NotificationEntry entry, @Nullable Integer oldImportance, NotificationUiAdjustment oldAdjustment, - NotificationUiAdjustment newAdjustment); + NotificationUiAdjustment newAdjustment, + NotificationRowContentBinder.InflationCallback callback); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 73f12f86e52e..673aa3903156 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -64,6 +64,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { private final ExpandableNotificationRowComponent.Builder mExpandableNotificationRowComponentBuilder; private final IconManager mIconManager; + private final LowPriorityInflationHelper mLowPriorityInflationHelper; private NotificationPresenter mPresenter; private NotificationListContainer mListContainer; @@ -81,7 +82,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { NotificationInterruptStateProvider notificationInterruptionStateProvider, Provider<RowInflaterTask> rowInflaterTaskProvider, ExpandableNotificationRowComponent.Builder expandableNotificationRowComponentBuilder, - IconManager iconManager) { + IconManager iconManager, + LowPriorityInflationHelper lowPriorityInflationHelper) { mContext = context; mNotifBindPipeline = notifBindPipeline; mRowContentBindStage = rowContentBindStage; @@ -92,6 +94,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mRowInflaterTaskProvider = rowInflaterTaskProvider; mExpandableNotificationRowComponentBuilder = expandableNotificationRowComponentBuilder; mIconManager = iconManager; + mLowPriorityInflationHelper = lowPriorityInflationHelper; } /** @@ -180,13 +183,14 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { NotificationEntry entry, @Nullable Integer oldImportance, NotificationUiAdjustment oldAdjustment, - NotificationUiAdjustment newAdjustment) { + NotificationUiAdjustment newAdjustment, + NotificationRowContentBinder.InflationCallback callback) { if (NotificationUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) { if (entry.rowExists()) { ExpandableNotificationRow row = entry.getRow(); row.reset(); updateRow(entry, row); - inflateContentViews(entry, row, null /* callback */); + inflateContentViews(entry, row, callback); } else { // Once the RowInflaterTask is done, it will pick up the updated entry, so // no-op here. @@ -221,14 +225,18 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { private void inflateContentViews( NotificationEntry entry, ExpandableNotificationRow row, - NotificationRowContentBinder.InflationCallback inflationCallback) { + @Nullable NotificationRowContentBinder.InflationCallback inflationCallback) { final boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(entry.getSbn(), entry.getImportance()); - final boolean isLowPriority = entry.isAmbient(); + // If this is our first time inflating, we don't actually know the groupings for real + // yet, so we might actually inflate a low priority content view incorrectly here and have + // to correct it later in the pipeline. On subsequent inflations (i.e. updates), this + // should inflate the correct view. + final boolean isLowPriority = mLowPriorityInflationHelper.shouldUseLowPriorityView(entry); RowContentBindParams params = mRowContentBindStage.getStageParams(entry); params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); - params.setUseLowPriority(entry.isAmbient()); + params.setUseLowPriority(isLowPriority); // TODO: Replace this API with RowContentBindParams directly. Also move to a separate // redaction controller. @@ -238,7 +246,9 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mRowContentBindStage.requestRebind(entry, en -> { row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight); row.setIsLowPriority(isLowPriority); - inflationCallback.onAsyncInflationFinished(en); + if (inflationCallback != null) { + inflationCallback.onAsyncInflationFinished(en); + } }); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 565a082533a7..1dbfa32cdf41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -24,12 +24,11 @@ import android.os.Handler; import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.logging.UiEventLoggerImpl; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.CurrentUserContextTracker; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -45,7 +44,6 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.init.NotificationsControllerImpl; import com.android.systemui.statusbar.notification.init.NotificationsControllerStub; -import com.android.systemui.statusbar.notification.interruption.NotificationAlertingManager; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl; import com.android.systemui.statusbar.notification.logging.NotificationLogger; @@ -53,13 +51,14 @@ import com.android.systemui.statusbar.notification.logging.NotificationPanelLogg import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl; import com.android.systemui.statusbar.notification.row.NotificationBlockingHelperManager; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; +import com.android.systemui.statusbar.notification.row.PriorityOnboardingDialogController; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBar; -import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.leak.LeakDetector; import java.util.concurrent.Executor; +import javax.inject.Provider; import javax.inject.Singleton; import dagger.Binds; @@ -109,7 +108,9 @@ public interface NotificationsModule { HighPriorityProvider highPriorityProvider, INotificationManager notificationManager, LauncherApps launcherApps, - ShortcutManager shortcutManager) { + ShortcutManager shortcutManager, + CurrentUserContextTracker contextTracker, + Provider<PriorityOnboardingDialogController.Builder> builderProvider) { return new NotificationGutsManager( context, visualStabilityManager, @@ -119,7 +120,9 @@ public interface NotificationsModule { highPriorityProvider, notificationManager, launcherApps, - shortcutManager); + shortcutManager, + contextTracker, + builderProvider); } /** Provides an instance of {@link VisualStabilityManager} */ @@ -130,27 +133,6 @@ public interface NotificationsModule { return new VisualStabilityManager(notificationEntryManager, handler); } - /** Provides an instance of {@link NotificationAlertingManager} */ - @Singleton - @Provides - static NotificationAlertingManager provideNotificationAlertingManager( - NotificationEntryManager notificationEntryManager, - NotificationRemoteInputManager remoteInputManager, - VisualStabilityManager visualStabilityManager, - StatusBarStateController statusBarStateController, - NotificationInterruptStateProvider notificationInterruptStateProvider, - NotificationListener notificationListener, - HeadsUpManager headsUpManager) { - return new NotificationAlertingManager( - notificationEntryManager, - remoteInputManager, - visualStabilityManager, - statusBarStateController, - notificationInterruptStateProvider, - notificationListener, - headsUpManager); - } - /** Provides an instance of {@link NotificationLogger} */ @Singleton @Provides @@ -177,13 +159,6 @@ public interface NotificationsModule { return new NotificationPanelLoggerImpl(); } - /** Provides an instance of {@link com.android.internal.logging.UiEventLogger} */ - @Singleton - @Provides - static UiEventLogger provideUiEventLogger() { - return new UiEventLoggerImpl(); - } - /** Provides an instance of {@link NotificationBlockingHelperManager} */ @Singleton @Provides diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpBindController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpBindController.java deleted file mode 100644 index a7b1f37edf0e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpBindController.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.headsup; - -import androidx.annotation.NonNull; - -import com.android.systemui.statusbar.notification.NotificationEntryManager; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; -import com.android.systemui.statusbar.notification.interruption.NotificationAlertingManager; -import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; -import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Controller class for old pipeline heads up view binding. It listens to - * {@link NotificationEntryManager} entry events and appropriately binds or unbinds the heads up - * view. - * - * This has a subtle contract with {@link NotificationAlertingManager} where this controller handles - * the heads up binding, but {@link NotificationAlertingManager} listens for general inflation - * events to actually mark it heads up/update. In the new pipeline, we combine the classes. - * See {@link HeadsUpCoordinator}. - */ -@Singleton -public class HeadsUpBindController { - private final HeadsUpViewBinder mHeadsUpViewBinder; - private final NotificationInterruptStateProvider mInterruptStateProvider; - - @Inject - HeadsUpBindController( - HeadsUpViewBinder headsUpViewBinder, - NotificationInterruptStateProvider notificationInterruptStateProvider) { - mInterruptStateProvider = notificationInterruptStateProvider; - mHeadsUpViewBinder = headsUpViewBinder; - } - - /** - * Attach this controller and add its listeners. - */ - public void attach( - NotificationEntryManager entryManager, - HeadsUpManager headsUpManager) { - entryManager.addCollectionListener(mCollectionListener); - headsUpManager.addListener(mOnHeadsUpChangedListener); - } - - private NotifCollectionListener mCollectionListener = new NotifCollectionListener() { - @Override - public void onEntryAdded(NotificationEntry entry) { - if (mInterruptStateProvider.shouldHeadsUp(entry)) { - mHeadsUpViewBinder.bindHeadsUpView(entry, null); - } - } - - @Override - public void onEntryUpdated(NotificationEntry entry) { - if (mInterruptStateProvider.shouldHeadsUp(entry)) { - mHeadsUpViewBinder.bindHeadsUpView(entry, null); - } - } - - @Override - public void onEntryCleanUp(NotificationEntry entry) { - mHeadsUpViewBinder.abortBindCallback(entry); - } - }; - - private OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() { - @Override - public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) { - if (!isHeadsUp) { - mHeadsUpViewBinder.unbindHeadsUpView(entry); - } - } - }; -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 5fac5b1cf159..c9754048e1d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification -import com.android.systemui.bubbles.BubbleController import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.NotificationListener @@ -28,7 +27,7 @@ import com.android.systemui.statusbar.notification.NotificationEntryManager import com.android.systemui.statusbar.notification.NotificationListController import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer -import com.android.systemui.statusbar.notification.headsup.HeadsUpBindController +import com.android.systemui.statusbar.notification.interruption.HeadsUpController import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper @@ -36,7 +35,7 @@ import com.android.systemui.statusbar.phone.NotificationGroupManager import com.android.systemui.statusbar.phone.StatusBar import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.HeadsUpManager -import com.android.systemui.statusbar.notification.headsup.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.policy.RemoteInputUriController import dagger.Lazy import java.io.FileDescriptor @@ -62,12 +61,12 @@ class NotificationsControllerImpl @Inject constructor( private val deviceProvisionedController: DeviceProvisionedController, private val notificationRowBinder: NotificationRowBinderImpl, private val remoteInputUriController: RemoteInputUriController, - private val bubbleController: BubbleController, private val groupManager: NotificationGroupManager, private val groupAlertTransferHelper: NotificationGroupAlertTransferHelper, private val headsUpManager: HeadsUpManager, - private val headsUpBindController: HeadsUpBindController, - private val headsUpViewBinder: HeadsUpViewBinder + private val headsUpController: HeadsUpController, + private val headsUpViewBinder: HeadsUpViewBinder, + private val clickerBuilder: NotificationClicker.Builder ) : NotificationsController { override fun initialize( @@ -87,10 +86,7 @@ class NotificationsControllerImpl @Inject constructor( listController.bind() notificationRowBinder.setNotificationClicker( - NotificationClicker( - Optional.of(statusBar), - bubbleController, - notificationActivityStarter)) + clickerBuilder.build(Optional.of(statusBar), notificationActivityStarter)) notificationRowBinder.setUpWithPresenter( presenter, listContainer, @@ -112,7 +108,7 @@ class NotificationsControllerImpl @Inject constructor( groupAlertTransferHelper.bind(entryManager, groupManager) headsUpManager.addListener(groupManager) headsUpManager.addListener(groupAlertTransferHelper) - headsUpBindController.attach(entryManager, headsUpManager) + headsUpController.attach(entryManager, headsUpManager) groupManager.setHeadsUpManager(headsUpManager) groupAlertTransferHelper.setHeadsUpManager(headsUpManager) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationAlertingManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java index 5d070981f81b..9b6ae9a7f99d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationAlertingManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,117 +22,117 @@ import android.app.Notification; import android.service.notification.StatusBarNotification; import android.util.Log; -import com.android.internal.statusbar.NotificationVisibility; +import androidx.annotation.NonNull; + import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationRemoteInputManager; -import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.dagger.NotificationsModule; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; -/** Handles heads-up and pulsing behavior driven by notification changes. */ -public class NotificationAlertingManager { - - private static final String TAG = "NotifAlertManager"; +import javax.inject.Inject; +import javax.inject.Singleton; +/** + * Controller class for old pipeline heads up logic. It listens to {@link NotificationEntryManager} + * entry events and appropriately binds or unbinds the heads up view and promotes it to the top + * of the screen. + */ +@Singleton +public class HeadsUpController { + private final HeadsUpViewBinder mHeadsUpViewBinder; + private final NotificationInterruptStateProvider mInterruptStateProvider; private final NotificationRemoteInputManager mRemoteInputManager; private final VisualStabilityManager mVisualStabilityManager; private final StatusBarStateController mStatusBarStateController; - private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final NotificationListener mNotificationListener; + private final HeadsUpManager mHeadsUpManager; - private HeadsUpManager mHeadsUpManager; - - /** - * Injected constructor. See {@link NotificationsModule}. - */ - public NotificationAlertingManager( - NotificationEntryManager notificationEntryManager, + @Inject + HeadsUpController( + HeadsUpViewBinder headsUpViewBinder, + NotificationInterruptStateProvider notificationInterruptStateProvider, + HeadsUpManager headsUpManager, NotificationRemoteInputManager remoteInputManager, - VisualStabilityManager visualStabilityManager, StatusBarStateController statusBarStateController, - NotificationInterruptStateProvider notificationInterruptionStateProvider, - NotificationListener notificationListener, - HeadsUpManager headsUpManager) { + VisualStabilityManager visualStabilityManager, + NotificationListener notificationListener) { + mHeadsUpViewBinder = headsUpViewBinder; + mHeadsUpManager = headsUpManager; + mInterruptStateProvider = notificationInterruptStateProvider; mRemoteInputManager = remoteInputManager; - mVisualStabilityManager = visualStabilityManager; mStatusBarStateController = statusBarStateController; - mNotificationInterruptStateProvider = notificationInterruptionStateProvider; + mVisualStabilityManager = visualStabilityManager; mNotificationListener = notificationListener; - mHeadsUpManager = headsUpManager; + } - notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { - @Override - public void onEntryInflated(NotificationEntry entry) { - showAlertingView(entry); - } + /** + * Attach this controller and add its listeners. + */ + public void attach( + NotificationEntryManager entryManager, + HeadsUpManager headsUpManager) { + entryManager.addCollectionListener(mCollectionListener); + headsUpManager.addListener(mOnHeadsUpChangedListener); + } - @Override - public void onPreEntryUpdated(NotificationEntry entry) { - updateAlertState(entry); + private NotifCollectionListener mCollectionListener = new NotifCollectionListener() { + @Override + public void onEntryAdded(NotificationEntry entry) { + if (mInterruptStateProvider.shouldHeadsUp(entry)) { + mHeadsUpViewBinder.bindHeadsUpView( + entry, HeadsUpController.this::showAlertingView); } + } - @Override - public void onEntryRemoved( - NotificationEntry entry, - NotificationVisibility visibility, - boolean removedByUser, - int reason) { - stopAlerting(entry.getKey()); - } - }); - } + @Override + public void onEntryUpdated(NotificationEntry entry) { + updateHunState(entry); + } + + @Override + public void onEntryRemoved(NotificationEntry entry, int reason) { + stopAlerting(entry); + } + + @Override + public void onEntryCleanUp(NotificationEntry entry) { + mHeadsUpViewBinder.abortBindCallback(entry); + } + }; /** - * Adds the entry to the respective alerting manager if the content view was inflated and - * the entry should still alert. + * Adds the entry to the HUN manager and show it for the first time. */ private void showAlertingView(NotificationEntry entry) { - // TODO: Instead of this back and forth, we should listen to changes in heads up and - // cancel on-going heads up view inflation using the bind pipeline. - if (entry.getRow().getPrivateLayout().getHeadsUpChild() != null) { - mHeadsUpManager.showNotification(entry); - if (!mStatusBarStateController.isDozing()) { - // Mark as seen immediately - setNotificationShown(entry.getSbn()); - } + mHeadsUpManager.showNotification(entry); + if (!mStatusBarStateController.isDozing()) { + // Mark as seen immediately + setNotificationShown(entry.getSbn()); } } - private void updateAlertState(NotificationEntry entry) { - boolean alertAgain = alertAgain(entry, entry.getSbn().getNotification()); + private void updateHunState(NotificationEntry entry) { + boolean hunAgain = alertAgain(entry, entry.getSbn().getNotification()); // includes check for whether this notification should be filtered: - boolean shouldAlert = mNotificationInterruptStateProvider.shouldHeadsUp(entry); - final boolean wasAlerting = mHeadsUpManager.isAlerting(entry.getKey()); - if (wasAlerting) { - if (shouldAlert) { - mHeadsUpManager.updateNotification(entry.getKey(), alertAgain); + boolean shouldHeadsUp = mInterruptStateProvider.shouldHeadsUp(entry); + final boolean wasHeadsUp = mHeadsUpManager.isAlerting(entry.getKey()); + if (wasHeadsUp) { + if (shouldHeadsUp) { + mHeadsUpManager.updateNotification(entry.getKey(), hunAgain); } else if (!mHeadsUpManager.isEntryAutoHeadsUpped(entry.getKey())) { // We don't want this to be interrupting anymore, let's remove it mHeadsUpManager.removeNotification(entry.getKey(), false /* removeImmediately */); } - } else if (shouldAlert && alertAgain) { - // This notification was updated to be alerting, show it! - mHeadsUpManager.showNotification(entry); + } else if (shouldHeadsUp && hunAgain) { + mHeadsUpViewBinder.bindHeadsUpView(entry, mHeadsUpManager::showNotification); } } - /** - * Checks whether an update for a notification warrants an alert for the user. - * - * @param oldEntry the entry for this notification. - * @param newNotification the new notification for this entry. - * @return whether this notification should alert the user. - */ - public static boolean alertAgain( - NotificationEntry oldEntry, Notification newNotification) { - return oldEntry == null || !oldEntry.hasInterrupted() - || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; - } - private void setNotificationShown(StatusBarNotification n) { try { mNotificationListener.setNotificationsShown(new String[]{n.getKey()}); @@ -141,10 +141,11 @@ public class NotificationAlertingManager { } } - private void stopAlerting(final String key) { - // Attempt to remove notifications from their alert manager. + private void stopAlerting(NotificationEntry entry) { + // Attempt to remove notifications from their HUN manager. // Though the remove itself may fail, it lets the manager know to remove as soon as // possible. + String key = entry.getKey(); if (mHeadsUpManager.isAlerting(key)) { // A cancel() in response to a remote input shouldn't be delayed, as it makes the // sending look longer than it takes. @@ -157,4 +158,28 @@ public class NotificationAlertingManager { mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime); } } + + /** + * Checks whether an update for a notification warrants an alert for the user. + * + * @param oldEntry the entry for this notification. + * @param newNotification the new notification for this entry. + * @return whether this notification should alert the user. + */ + public static boolean alertAgain( + NotificationEntry oldEntry, Notification newNotification) { + return oldEntry == null || !oldEntry.hasInterrupted() + || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; + } + + private OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() { + @Override + public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) { + if (!isHeadsUp && !entry.getRow().isRemoved()) { + mHeadsUpViewBinder.unbindHeadsUpView(entry); + } + } + }; + + private static final String TAG = "HeadsUpBindController"; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpViewBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java index 37acfa8dc0a4..ff139957031a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpViewBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.headsup; +package com.android.systemui.statusbar.notification.interruption; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; @@ -42,7 +42,7 @@ import javax.inject.Singleton; * content view. * * TODO: This should be moved into {@link HeadsUpCoordinator} when the old pipeline is deprecated - * (i.e. when {@link HeadsUpBindController} is removed). + * (i.e. when {@link HeadsUpController} is removed). */ @Singleton public class HeadsUpViewBinder { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt index 5879c15c2493..d36627fb849d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt @@ -20,6 +20,7 @@ import android.annotation.IntDef import android.service.notification.NotificationListenerService.Ranking import android.service.notification.StatusBarNotification import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.PeopleNotificationType +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_FULL_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON @@ -33,21 +34,28 @@ interface PeopleNotificationIdentifier { /** * Identifies if the given notification can be classified as a "People" notification. * - * @return [TYPE_NON_PERSON] if not a people notification, [TYPE_PERSON] if a standard people - * notification, and [TYPE_IMPORTANT_PERSON] if an "important" people notification. + * @return [TYPE_NON_PERSON] if not a people notification, [TYPE_PERSON] if it is a people + * notification that doesn't use shortcuts, [TYPE_FULL_PERSON] if it is a person notification + * that users shortcuts, and [TYPE_IMPORTANT_PERSON] if an "important" people notification + * that users shortcuts. */ @PeopleNotificationType fun getPeopleNotificationType(sbn: StatusBarNotification, ranking: Ranking): Int + fun compareTo(@PeopleNotificationType a: Int, + @PeopleNotificationType b: Int): Int + companion object { @Retention(AnnotationRetention.SOURCE) - @IntDef(prefix = ["TYPE_"], value = [TYPE_NON_PERSON, TYPE_PERSON, TYPE_IMPORTANT_PERSON]) + @IntDef(prefix = ["TYPE_"], value = [TYPE_NON_PERSON, TYPE_PERSON, TYPE_FULL_PERSON, + TYPE_IMPORTANT_PERSON]) annotation class PeopleNotificationType const val TYPE_NON_PERSON = 0 const val TYPE_PERSON = 1 - const val TYPE_IMPORTANT_PERSON = 2 + const val TYPE_FULL_PERSON = 2 + const val TYPE_IMPORTANT_PERSON = 3 } } @@ -69,6 +77,11 @@ class PeopleNotificationIdentifierImpl @Inject constructor( } } + override fun compareTo(@PeopleNotificationType a: Int, + @PeopleNotificationType b: Int): Int { + return b.compareTo(a); + } + /** * Given two [PeopleNotificationType]s, determine the upper bound. Used to constrain a * notification to a type given multiple signals, i.e. notification groups, where each child @@ -84,8 +97,9 @@ class PeopleNotificationIdentifierImpl @Inject constructor( private val Ranking.personTypeInfo get() = when { !isConversation -> TYPE_NON_PERSON + shortcutInfo == null -> TYPE_PERSON channel?.isImportantConversation == true -> TYPE_IMPORTANT_PERSON - else -> TYPE_PERSON + else -> TYPE_FULL_PERSON } private fun extractPersonTypeInfo(sbn: StatusBarNotification) = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index b9dd97482d88..92b597b01559 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -331,19 +331,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView .setDuration(ACTIVATE_ANIMATION_LENGTH); } - @Override - public boolean performClick() { - if (!mNeedsDimming || (mAccessibilityManager != null - && mAccessibilityManager.isTouchExplorationEnabled())) { - return super.performClick(); - } - return false; - } - - boolean superPerformClick() { - return super.performClick(); - } - /** * Cancels the hotspot and makes the notification inactive. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java index 2f0e433b3927..dd30c890e75b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java @@ -72,7 +72,7 @@ public class ActivatableNotificationViewController { } else { mView.makeInactive(true /* animate */); } - }, mView::superPerformClick, mView::handleSlideBack, + }, mView::performClick, mView::handleSlideBack, mFalsingManager::onNotificationDoubleTap); mView.setOnTouchListener(mTouchHandler); mView.setTouchHandler(mTouchHandler); 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 998230f205ab..f8844c715230 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 @@ -70,6 +70,7 @@ import com.android.internal.widget.CachingIconView; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; +import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -239,7 +240,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private ExpandableNotificationRow mNotificationParent; private OnExpandClickListener mOnExpandClickListener; private View.OnClickListener mOnAppOpsClickListener; - private boolean mIsChildInGroup; // Listener will be called when receiving a long click event. // Use #setLongPressPosition to optionally assign positional data with the long press. @@ -410,7 +410,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setIconAnimationRunningForChild(running, mChildrenContainer.getHeaderView()); setIconAnimationRunningForChild(running, mChildrenContainer.getLowPriorityHeaderView()); List<ExpandableNotificationRow> notificationChildren = - mChildrenContainer.getNotificationChildren(); + mChildrenContainer.getAttachedChildren(); for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow child = notificationChildren.get(i); child.setIconAnimationRunning(running); @@ -535,6 +535,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return isNonblockable; } + private boolean isConversation() { + return mPeopleNotificationIdentifier + .getPeopleNotificationType(mEntry.getSbn(), mEntry.getRanking()) + != PeopleNotificationIdentifier.TYPE_NON_PERSON; + } + public void onNotificationUpdated() { for (NotificationContentView l : mLayouts) { l.onNotificationUpdated(mEntry); @@ -547,7 +553,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mMenuRow.setAppName(mAppName); } if (mIsSummaryWithChildren) { - mChildrenContainer.recreateNotificationHeader(mExpandClickListener); + mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation()); mChildrenContainer.onNotificationUpdated(); } if (mIconAnimationRunning) { @@ -559,7 +565,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mNotificationParent != null) { mNotificationParent.updateChildrenHeaderAppearance(); } - onChildrenCountChanged(); + onAttachedChildrenCountChanged(); // The public layouts expand button is always visible mPublicLayout.updateExpandButtons(true); updateLimits(); @@ -579,6 +585,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + /** Call when bubble state has changed and the button on the notification should be updated. */ + public void updateBubbleButton() { + for (NotificationContentView l : mLayouts) { + l.updateBubbleButton(mEntry); + } + } + @VisibleForTesting void updateShelfIconColor() { StatusBarIconView expandedIcon = mEntry.getIcons().getShelfIcon(); @@ -763,6 +776,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * @see NotificationChildrenContainer#setUntruncatedChildCount(int) + */ + public void setUntruncatedChildCount(int childCount) { + if (mChildrenContainer == null) { + mChildrenContainerStub.inflate(); + } + mChildrenContainer.setUntruncatedChildCount(childCount); + } + + /** * Add a child notification to this view. * * @param row the row to add @@ -773,7 +796,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainerStub.inflate(); } mChildrenContainer.addNotification(row, childIndex); - onChildrenCountChanged(); + onAttachedChildrenCountChanged(); row.setIsChildInGroup(true, this); } @@ -792,7 +815,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mChildrenContainer != null) { mChildrenContainer.removeNotification(row); } - onChildrenCountChanged(); + onAttachedChildrenCountChanged(); row.setIsChildInGroup(false, null); row.setBottomRoundness(0.0f, false /* animate */); } @@ -830,15 +853,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } mNotificationParent = isChildInGroup ? parent : null; mPrivateLayout.setIsChildInGroup(isChildInGroup); - // TODO: Move inflation logic out of this call - if (mIsChildInGroup != isChildInGroup) { - mIsChildInGroup = isChildInGroup; - if (!isRemoved() && mIsLowPriority) { - RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); - params.setUseLowPriority(mIsLowPriority); - mRowContentBindStage.requestRebind(mEntry, null /* callback */); - } - } + resetBackgroundAlpha(); updateBackgroundForGroupState(); updateClickAndFocus(); @@ -886,15 +901,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mChildrenExpanded; } - public List<ExpandableNotificationRow> getNotificationChildren() { - return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren(); - } - - public int getNumberOfNotificationChildren() { - if (mChildrenContainer == null) { - return 0; - } - return mChildrenContainer.getNotificationChildren().size(); + public List<ExpandableNotificationRow> getAttachedChildren() { + return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren(); } /** @@ -1028,7 +1036,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setChronometerRunning(running, mPublicLayout); if (mChildrenContainer != null) { List<ExpandableNotificationRow> notificationChildren = - mChildrenContainer.getNotificationChildren(); + mChildrenContainer.getAttachedChildren(); for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow child = notificationChildren.get(i); child.setChronometerRunning(running); @@ -1086,6 +1094,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView updateClickAndFocus(); } + /** The click listener for the bubble button. */ + public View.OnClickListener getBubbleClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + Dependency.get(BubbleController.class) + .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); + mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); + } + }; + } + private void updateClickAndFocus() { boolean normalChild = !isChildInGroup() || isGroupExpanded(); boolean clickable = mOnClickListener != null && normalChild; @@ -1121,6 +1141,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mMenuRow.shouldUseDefaultMenuItems()) { ArrayList<MenuItem> items = new ArrayList<>(); items.add(NotificationMenuRow.createConversationItem(mContext)); + items.add(NotificationMenuRow.createPartialConversationItem(mContext)); items.add(NotificationMenuRow.createInfoItem(mContext)); items.add(NotificationMenuRow.createSnoozeItem(mContext)); items.add(NotificationMenuRow.createAppOpsItem(mContext)); @@ -1228,7 +1249,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mUpdateBackgroundOnUpdate = true; reInflateViews(); if (mChildrenContainer != null) { - for (ExpandableNotificationRow child : mChildrenContainer.getNotificationChildren()) { + for (ExpandableNotificationRow child : mChildrenContainer.getAttachedChildren()) { child.onUiModeChanged(); } } @@ -1267,7 +1288,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mNotificationColor; } - private void updateNotificationColor() { + public void updateNotificationColor() { Configuration currentConfig = getResources().getConfiguration(); boolean nightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; @@ -1286,8 +1307,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void removeAllChildren() { - List<ExpandableNotificationRow> notificationChildren - = mChildrenContainer.getNotificationChildren(); + List<ExpandableNotificationRow> notificationChildren = + mChildrenContainer.getAttachedChildren(); ArrayList<ExpandableNotificationRow> clonedList = new ArrayList<>(notificationChildren); for (int i = 0; i < clonedList.size(); i++) { ExpandableNotificationRow row = clonedList.get(i); @@ -1297,7 +1318,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.removeNotification(row); row.setIsChildInGroup(false, null); } - onChildrenCountChanged(); + onAttachedChildrenCountChanged(); } @Override @@ -1308,7 +1329,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void setForceUnlocked(boolean forceUnlocked) { mForceUnlocked = forceUnlocked; if (mIsSummaryWithChildren) { - List<ExpandableNotificationRow> notificationChildren = getNotificationChildren(); + List<ExpandableNotificationRow> notificationChildren = getAttachedChildren(); for (ExpandableNotificationRow child : notificationChildren) { child.setForceUnlocked(forceUnlocked); } @@ -1324,7 +1345,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mEntry.getIcons().getStatusBarIcon().setDismissed(); if (isChildInGroup()) { List<ExpandableNotificationRow> notificationChildren = - mNotificationParent.getNotificationChildren(); + mNotificationParent.getAttachedChildren(); int i = notificationChildren.indexOf(this); if (i != -1 && i < notificationChildren.size() - 1) { mChildAfterViewWhenDismissed = notificationChildren.get(i + 1); @@ -1613,6 +1634,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mFalsingManager = falsingManager; mStatusbarStateController = statusBarStateController; mPeopleNotificationIdentifier = peopleNotificationIdentifier; + for (NotificationContentView l : mLayouts) { + l.setPeopleNotificationIdentifier(mPeopleNotificationIdentifier); + } } private void initDimens() { @@ -1814,6 +1838,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void resetTranslation() { + if (mMenuRow != null && mMenuRow.isMenuVisible()) { + return; + } + if (mTranslateAnim != null) { mTranslateAnim.cancel(); } @@ -2328,12 +2356,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mGroupManager.isGroupExpanded(mEntry.getSbn()); } - private void onChildrenCountChanged() { + private void onAttachedChildrenCountChanged() { mIsSummaryWithChildren = mChildrenContainer != null && mChildrenContainer.getNotificationChildCount() > 0; if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() == null) { - mChildrenContainer.recreateNotificationHeader(mExpandClickListener - ); + mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation()); } getShowingLayout().updateBackgroundColor(false /* animate */); mPrivateLayout.updateExpandButtons(isExpandable()); @@ -2361,7 +2388,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // If this is a summary, then add in the children notification channels for the // same user and pkg. if (mIsSummaryWithChildren) { - final List<ExpandableNotificationRow> childrenRows = getNotificationChildren(); + final List<ExpandableNotificationRow> childrenRows = getAttachedChildren(); final int numChildren = childrenRows.size(); for (int i = 0; i < numChildren; i++) { final ExpandableNotificationRow childRow = childrenRows.get(i); @@ -2468,7 +2495,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mHideSensitiveForIntrinsicHeight = hideSensitive; if (mIsSummaryWithChildren) { List<ExpandableNotificationRow> notificationChildren = - mChildrenContainer.getNotificationChildren(); + mChildrenContainer.getAttachedChildren(); for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow child = notificationChildren.get(i); child.setHideSensitiveForIntrinsicHeight(hideSensitive); @@ -2806,7 +2833,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView updateBackgroundForGroupState(); if (mIsSummaryWithChildren) { List<ExpandableNotificationRow> notificationChildren = - mChildrenContainer.getNotificationChildren(); + mChildrenContainer.getAttachedChildren(); for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow child = notificationChildren.get(i); child.updateBackgroundForGroupState(); @@ -2831,7 +2858,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mShowNoBackground = !mShowGroupBackgroundWhenExpanded && isGroupExpanded() && !isGroupExpansionChanging() && !isUserLocked(); mChildrenContainer.updateHeaderForExpansion(mShowNoBackground); - List<ExpandableNotificationRow> children = mChildrenContainer.getNotificationChildren(); + List<ExpandableNotificationRow> children = mChildrenContainer.getAttachedChildren(); for (int i = 0; i < children.size(); i++) { children.get(i).updateBackgroundForGroupState(); } @@ -3241,7 +3268,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.print(", alpha: " + mChildrenContainer.getAlpha()); pw.print(", translationY: " + mChildrenContainer.getTranslationY()); pw.println(); - List<ExpandableNotificationRow> notificationChildren = getNotificationChildren(); + List<ExpandableNotificationRow> notificationChildren = getAttachedChildren(); pw.println(" Children: " + notificationChildren.size()); pw.println(" {"); for(ExpandableNotificationRow child : notificationChildren) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index ee3b753ab926..5797944298d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -590,7 +590,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { // handling reset for child notifications if (this instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) this; - List<ExpandableNotificationRow> children = row.getNotificationChildren(); + List<ExpandableNotificationRow> children = row.getAttachedChildren(); if (row.isSummaryWithChildren() && children != null) { for (ExpandableNotificationRow childRow : children) { childRow.resetViewState(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java index 41b248fb9634..3ec8c2374718 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java @@ -29,6 +29,7 @@ public class FooterView extends StackScrollerDecorView { private final int mClearAllTopPadding; private FooterViewButton mDismissButton; private FooterViewButton mManageButton; + private boolean mShowHistory; public FooterView(Context context, AttributeSet attrs) { super(context, attrs); @@ -72,15 +73,30 @@ public class FooterView extends StackScrollerDecorView { || touchY > mContent.getY() + mContent.getHeight(); } + public void showHistory(boolean showHistory) { + mShowHistory = showHistory; + if (mShowHistory) { + mManageButton.setText(R.string.manage_notifications_history_text); + mManageButton.setContentDescription( + mContext.getString(R.string.manage_notifications_history_text)); + } else { + mManageButton.setText(R.string.manage_notifications_text); + mManageButton.setContentDescription( + mContext.getString(R.string.manage_notifications_text)); + } + } + + public boolean isHistoryShown() { + return mShowHistory; + } + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mDismissButton.setText(R.string.clear_all_notifications_text); mDismissButton.setContentDescription( mContext.getString(R.string.accessibility_clear_all)); - mManageButton.setText(R.string.manage_notifications_history_text); - mManageButton.setContentDescription( - mContext.getString(R.string.manage_notifications_history_text)); + showHistory(mShowHistory); } public boolean isButtonVisible() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java new file mode 100644 index 000000000000..32477a6f39b7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.drawable.Icon; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.widget.ConversationLayout; +import com.android.systemui.R; + +/** + * A hybrid view which may contain information about one ore more conversations. + */ +public class HybridConversationNotificationView extends HybridNotificationView { + + private ImageView mConversationIconView; + private TextView mConversationSenderName; + private View mConversationFacePile; + private int mConversationIconSize; + private int mFacePileSize; + private int mFacePileProtectionWidth; + + public HybridConversationNotificationView(Context context) { + this(context, null); + } + + public HybridConversationNotificationView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public HybridConversationNotificationView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public HybridConversationNotificationView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon); + mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile); + mConversationSenderName = requireViewById(R.id.conversation_notification_sender); + mFacePileSize = getResources() + .getDimensionPixelSize(R.dimen.conversation_single_line_face_pile_size); + mConversationIconSize = getResources() + .getDimensionPixelSize(R.dimen.conversation_single_line_avatar_size); + mFacePileProtectionWidth = getResources().getDimensionPixelSize( + R.dimen.conversation_single_line_face_pile_protection_width); + mTransformationHelper.addViewTransformingToSimilar(mConversationIconView); + } + + @Override + public void bind(@Nullable CharSequence title, @Nullable CharSequence text, + @Nullable View contentView) { + if (!(contentView instanceof ConversationLayout)) { + super.bind(title, text, contentView); + return; + } + + ConversationLayout conversationLayout = (ConversationLayout) contentView; + Icon conversationIcon = conversationLayout.getConversationIcon(); + if (conversationIcon != null) { + mConversationFacePile.setVisibility(GONE); + mConversationIconView.setVisibility(VISIBLE); + mConversationIconView.setImageIcon(conversationIcon); + } else { + // If there isn't an icon, generate a "face pile" based on the sender avatars + mConversationIconView.setVisibility(GONE); + mConversationFacePile.setVisibility(VISIBLE); + + mConversationFacePile = + requireViewById(com.android.internal.R.id.conversation_face_pile); + ImageView facePileBottomBg = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom_background); + ImageView facePileBottom = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom); + ImageView facePileTop = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_top); + conversationLayout.bindFacePile(facePileBottomBg, facePileBottom, facePileTop); + setSize(mConversationFacePile, mFacePileSize); + setSize(facePileBottom, mConversationIconSize); + setSize(facePileTop, mConversationIconSize); + setSize(facePileBottomBg, mConversationIconSize + 2 * mFacePileProtectionWidth); + mTransformationHelper.addViewTransformingToSimilar(facePileTop); + mTransformationHelper.addViewTransformingToSimilar(facePileBottom); + mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); + } + CharSequence conversationTitle = conversationLayout.getConversationTitle(); + if (TextUtils.isEmpty(conversationTitle)) { + conversationTitle = title; + } + if (conversationLayout.isOneToOne()) { + mConversationSenderName.setVisibility(GONE); + } else { + mConversationSenderName.setVisibility(VISIBLE); + mConversationSenderName.setText(conversationLayout.getConversationSenderName()); + } + CharSequence conversationText = conversationLayout.getConversationText(); + if (TextUtils.isEmpty(conversationText)) { + conversationText = text; + } + super.bind(conversationTitle, conversationText, conversationLayout); + } + + private static void setSize(View view, int size) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams(); + lp.width = size; + lp.height = size; + view.setLayoutParams(lp); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java index fe819574f3b6..0ccebc130b1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java @@ -16,18 +16,20 @@ package com.android.systemui.statusbar.notification.row; +import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.content.res.Resources; +import android.service.notification.StatusBarNotification; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.android.internal.widget.ConversationLayout; import com.android.systemui.R; -import com.android.systemui.statusbar.notification.NotificationDozeHelper; -import com.android.systemui.statusbar.notification.NotificationUtils; /** * A class managing hybrid groups that include {@link HybridNotificationView} and the notification @@ -36,41 +38,41 @@ import com.android.systemui.statusbar.notification.NotificationUtils; public class HybridGroupManager { private final Context mContext; - private final ViewGroup mParent; private float mOverflowNumberSize; private int mOverflowNumberPadding; private int mOverflowNumberColor; - public HybridGroupManager(Context ctx, ViewGroup parent) { + public HybridGroupManager(Context ctx) { mContext = ctx; - mParent = parent; initDimens(); } public void initDimens() { Resources res = mContext.getResources(); - mOverflowNumberSize = res.getDimensionPixelSize( - R.dimen.group_overflow_number_size); - mOverflowNumberPadding = res.getDimensionPixelSize( - R.dimen.group_overflow_number_padding); + mOverflowNumberSize = res.getDimensionPixelSize(R.dimen.group_overflow_number_size); + mOverflowNumberPadding = res.getDimensionPixelSize(R.dimen.group_overflow_number_padding); } - private HybridNotificationView inflateHybridViewWithStyle(int style) { + private HybridNotificationView inflateHybridViewWithStyle(int style, + View contentView, ViewGroup parent) { LayoutInflater inflater = new ContextThemeWrapper(mContext, style) .getSystemService(LayoutInflater.class); - HybridNotificationView hybrid = (HybridNotificationView) inflater.inflate( - R.layout.hybrid_notification, mParent, false); - mParent.addView(hybrid); + int layout = contentView instanceof ConversationLayout + ? R.layout.hybrid_conversation_notification + : R.layout.hybrid_notification; + HybridNotificationView hybrid = (HybridNotificationView) + inflater.inflate(layout, parent, false); + parent.addView(hybrid); return hybrid; } - private TextView inflateOverflowNumber() { + private TextView inflateOverflowNumber(ViewGroup parent) { LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class); TextView numberView = (TextView) inflater.inflate( - R.layout.hybrid_overflow_number, mParent, false); - mParent.addView(numberView); + R.layout.hybrid_overflow_number, parent, false); + parent.addView(numberView); updateOverFlowNumberColor(numberView); return numberView; } @@ -87,22 +89,26 @@ public class HybridGroupManager { } public HybridNotificationView bindFromNotification(HybridNotificationView reusableView, - Notification notification) { - return bindFromNotificationWithStyle(reusableView, notification, - R.style.HybridNotification); + View contentView, StatusBarNotification notification, + ViewGroup parent) { + return bindFromNotificationWithStyle(reusableView, contentView, notification, + R.style.HybridNotification, parent); } private HybridNotificationView bindFromNotificationWithStyle( - HybridNotificationView reusableView, Notification notification, int style) { + HybridNotificationView reusableView, View contentView, + StatusBarNotification notification, + int style, ViewGroup parent) { if (reusableView == null) { - reusableView = inflateHybridViewWithStyle(style); + reusableView = inflateHybridViewWithStyle(style, contentView, parent); } - CharSequence titleText = resolveTitle(notification); - CharSequence contentText = resolveText(notification); - reusableView.bind(titleText, contentText); + CharSequence titleText = resolveTitle(notification.getNotification()); + CharSequence contentText = resolveText(notification.getNotification()); + reusableView.bind(titleText, contentText, contentView); return reusableView; } + @Nullable private CharSequence resolveText(Notification notification) { CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT); if (contentText == null) { @@ -111,6 +117,7 @@ public class HybridGroupManager { return contentText; } + @Nullable private CharSequence resolveTitle(Notification notification) { CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE); if (titleText == null) { @@ -119,9 +126,10 @@ public class HybridGroupManager { return titleText; } - public TextView bindOverflowNumber(TextView reusableView, int number) { + public TextView bindOverflowNumber(TextView reusableView, int number, + ViewGroup parent) { if (reusableView == null) { - reusableView = inflateOverflowNumber(); + reusableView = inflateOverflowNumber(parent); } String text = mContext.getResources().getString( R.string.notification_group_overflow_indicator, number); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java index be25d6377912..207144931c3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java @@ -36,8 +36,7 @@ import com.android.systemui.statusbar.notification.TransformState; public class HybridNotificationView extends AlphaOptimizedLinearLayout implements TransformableView { - private ViewTransformationHelper mTransformationHelper; - + protected final ViewTransformationHelper mTransformationHelper = new ViewTransformationHelper(); protected TextView mTitleView; protected TextView mTextView; @@ -69,9 +68,8 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout @Override protected void onFinishInflate() { super.onFinishInflate(); - mTitleView = (TextView) findViewById(R.id.notification_title); - mTextView = (TextView) findViewById(R.id.notification_text); - mTransformationHelper = new ViewTransformationHelper(); + mTitleView = findViewById(R.id.notification_title); + mTextView = findViewById(R.id.notification_text); mTransformationHelper.setCustomTransformation( new ViewTransformationHelper.CustomTransformation() { @Override @@ -106,11 +104,8 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mTextView); } - public void bind(CharSequence title) { - bind(title, null); - } - - public void bind(CharSequence title, CharSequence text) { + public void bind(@Nullable CharSequence title, @Nullable CharSequence text, + @Nullable View contentView) { mTitleView.setText(title); mTitleView.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE); if (TextUtils.isEmpty(text)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 9d5443729d45..582e3e5b6c34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -132,7 +132,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder mConversationProcessor, row, bindParams.isLowPriority, - bindParams.isChildInGroup, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, callback, @@ -156,7 +155,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder InflationProgress result = createRemoteViews(reInflateFlags, builder, bindParams.isLowPriority, - bindParams.isChildInGroup, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, packageContext); @@ -285,11 +283,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, - Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, - boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, - Context packageContext) { + Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, + boolean usesIncreasedHeadsUpHeight, Context packageContext) { InflationProgress result = new InflationProgress(); - isLowPriority = isLowPriority && !isChildInGroup; if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); @@ -702,7 +698,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final Context mContext; private final boolean mInflateSynchronously; private final boolean mIsLowPriority; - private final boolean mIsChildInGroup; private final boolean mUsesIncreasedHeight; private final InflationCallback mCallback; private final boolean mUsesIncreasedHeadsUpHeight; @@ -728,7 +723,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, boolean isLowPriority, - boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, @@ -743,7 +737,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache = cache; mContext = mRow.getContext(); mIsLowPriority = isLowPriority; - mIsChildInGroup = isChildInGroup; mUsesIncreasedHeight = usesIncreasedHeight; mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; mRemoteViewClickHandler = remoteViewClickHandler; @@ -781,7 +774,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mConversationProcessor.processNotification(mEntry, recoveredBuilder); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, - recoveredBuilder, mIsLowPriority, mIsChildInGroup, mUsesIncreasedHeight, + recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext); return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mEntry, mRow.getContext(), packageContext, mRow.getHeadsUpManager(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 3c3f1b21fb3c..e9849ec84987 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -16,13 +16,18 @@ package com.android.systemui.statusbar.notification.row; + +import static android.provider.Settings.Global.NOTIFICATION_BUBBLES; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Build; +import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import android.util.ArraySet; @@ -47,6 +52,7 @@ import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.phone.NotificationGroupManager; @@ -118,6 +124,7 @@ public class NotificationContentView extends FrameLayout { private NotificationGroupManager mGroupManager; private RemoteInputController mRemoteInputController; private Runnable mExpandedVisibleListener; + private PeopleNotificationIdentifier mPeopleIdentifier; /** * List of listeners for when content views become inactive (i.e. not the showing view). */ @@ -170,7 +177,7 @@ public class NotificationContentView extends FrameLayout { public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); - mHybridGroupManager = new HybridGroupManager(getContext(), this); + mHybridGroupManager = new HybridGroupManager(getContext()); mMediaTransferManager = new MediaTransferManager(getContext()); mSmartReplyConstants = Dependency.get(SmartReplyConstants.class); mSmartReplyController = Dependency.get(SmartReplyController.class); @@ -454,6 +461,9 @@ public class NotificationContentView extends FrameLayout { mExpandedChild = child; mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); + if (mContainingNotification != null) { + applyBubbleAction(mExpandedChild, mContainingNotification.getEntry()); + } } /** @@ -493,6 +503,9 @@ public class NotificationContentView extends FrameLayout { mHeadsUpChild = child; mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); + if (mContainingNotification != null) { + applyBubbleAction(mHeadsUpChild, mContainingNotification.getEntry()); + } } @Override @@ -1138,6 +1151,8 @@ public class NotificationContentView extends FrameLayout { mForceSelectNextLayout = true; mPreviousExpandedRemoteInputIntent = null; mPreviousHeadsUpRemoteInputIntent = null; + applyBubbleAction(mExpandedChild, entry); + applyBubbleAction(mHeadsUpChild, entry); } private void updateAllSingleLineViews() { @@ -1148,7 +1163,7 @@ public class NotificationContentView extends FrameLayout { if (mIsChildInGroup) { boolean isNewView = mSingleLineView == null; mSingleLineView = mHybridGroupManager.bindFromNotification( - mSingleLineView, mStatusBarNotification.getNotification()); + mSingleLineView, mContractedChild, mStatusBarNotification, this); if (isNewView) { updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView); @@ -1308,6 +1323,58 @@ public class NotificationContentView extends FrameLayout { return null; } + /** + * Call to update state of the bubble button (i.e. does it show bubble or unbubble or no + * icon at all). + * + * @param entry the new entry to use. + */ + public void updateBubbleButton(NotificationEntry entry) { + applyBubbleAction(mExpandedChild, entry); + } + + private boolean isBubblesEnabled() { + return Settings.Global.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, 0) == 1; + } + + private void applyBubbleAction(View layout, NotificationEntry entry) { + if (layout == null || mContainingNotification == null || mPeopleIdentifier == null) { + return; + } + ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button); + View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container); + if (bubbleButton == null || actionContainer == null) { + return; + } + boolean isPersonWithShortcut = + mPeopleIdentifier.getPeopleNotificationType(entry.getSbn(), entry.getRanking()) + >= PeopleNotificationIdentifier.TYPE_FULL_PERSON; + boolean showButton = isBubblesEnabled() + && isPersonWithShortcut + && entry.getBubbleMetadata() != null; + if (showButton) { + Drawable d = mContext.getResources().getDrawable(entry.isBubble() + ? R.drawable.ic_stop_bubble + : R.drawable.ic_create_bubble); + mContainingNotification.updateNotificationColor(); + final int tint = mContainingNotification.getNotificationColor(); + d.setTint(tint); + + String contentDescription = mContext.getResources().getString(entry.isBubble() + ? R.string.notification_conversation_unbubble + : R.string.notification_conversation_bubble); + + bubbleButton.setContentDescription(contentDescription); + bubbleButton.setImageDrawable(d); + bubbleButton.setOnClickListener(mContainingNotification.getBubbleClickListener()); + bubbleButton.setVisibility(VISIBLE); + actionContainer.setVisibility(VISIBLE); + } else { + bubbleButton.setVisibility(GONE); + } + } + private void applySmartReplyView( SmartRepliesAndActions smartRepliesAndActions, NotificationEntry entry) { @@ -1512,6 +1579,10 @@ public class NotificationContentView extends FrameLayout { mContainingNotification = containingNotification; } + public void setPeopleNotificationIdentifier(PeopleNotificationIdentifier peopleIdentifier) { + mPeopleIdentifier = peopleIdentifier; + } + public void requestSelectLayout(boolean needsAnimation) { selectLayout(needsAnimation, false); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 6fc1264d69e2..863951e655e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -17,9 +17,13 @@ package com.android.systemui.statusbar.notification.row; import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; +import static android.provider.Settings.Global.NOTIFICATION_BUBBLES; import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; @@ -27,10 +31,12 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -38,11 +44,10 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.graphics.drawable.Icon; -import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.os.RemoteException; -import android.os.UserHandle; +import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.transition.ChangeBounds; @@ -51,7 +56,7 @@ import android.transition.TransitionManager; import android.transition.TransitionSet; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; +import android.view.LayoutInflater; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; @@ -61,13 +66,17 @@ import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Dependency; +import com.android.systemui.Prefs; import com.android.systemui.R; +import com.android.systemui.statusbar.notification.NotificationChannelHelper; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.lang.annotation.Retention; import java.util.List; +import javax.inject.Provider; + /** * The guts of a conversation notification revealed when performing a long press. */ @@ -79,8 +88,8 @@ public class NotificationConversationInfo extends LinearLayout implements private INotificationManager mINotificationManager; ShortcutManager mShortcutManager; private PackageManager mPm; - private VisualStabilityManager mVisualStabilityManager; private ConversationIconFactory mIconFactory; + private VisualStabilityManager mVisualStabilityManager; private String mPackageName; private String mAppName; @@ -88,14 +97,19 @@ public class NotificationConversationInfo extends LinearLayout implements private String mDelegatePkg; private NotificationChannel mNotificationChannel; private ShortcutInfo mShortcutInfo; - private String mConversationId; private StatusBarNotification mSbn; + @Nullable private Notification.BubbleMetadata mBubbleMetadata; + private Context mUserContext; + private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider; private boolean mIsDeviceProvisioned; + private int mAppBubble; private TextView mPriorityDescriptionView; private TextView mDefaultDescriptionView; private TextView mSilentDescriptionView; + private @Action int mSelectedAction = -1; + private boolean mPressedApply; private OnSnoozeClickListener mOnSnoozeClickListener; private OnSettingsClickListener mOnSettingsClickListener; @@ -132,21 +146,22 @@ public class NotificationConversationInfo extends LinearLayout implements */ private OnClickListener mOnFavoriteClick = v -> { - mSelectedAction = ACTION_FAVORITE; + setSelectedAction(ACTION_FAVORITE); updateToggleActions(mSelectedAction, true); }; private OnClickListener mOnDefaultClick = v -> { - mSelectedAction = ACTION_DEFAULT; + setSelectedAction(ACTION_DEFAULT); updateToggleActions(mSelectedAction, true); }; private OnClickListener mOnMuteClick = v -> { - mSelectedAction = ACTION_MUTE; + setSelectedAction(ACTION_MUTE); updateToggleActions(mSelectedAction, true); }; private OnClickListener mOnDone = v -> { + mPressedApply = true; closeControls(v, true); }; @@ -166,6 +181,23 @@ public class NotificationConversationInfo extends LinearLayout implements void onClick(View v, int hoursToSnooze); } + @VisibleForTesting + void setSelectedAction(int selectedAction) { + if (mSelectedAction == selectedAction) { + return; + } + + mSelectedAction = selectedAction; + onSelectedActionChanged(); + } + + private void onSelectedActionChanged() { + // If the user selected Priority, maybe show the priority onboarding + if (mSelectedAction == ACTION_FAVORITE && shouldShowPriorityOnboarding()) { + showPriorityOnboarding(); + } + } + public void bindNotification( ShortcutManager shortcutManager, PackageManager pm, @@ -174,9 +206,12 @@ public class NotificationConversationInfo extends LinearLayout implements String pkg, NotificationChannel notificationChannel, NotificationEntry entry, + Notification.BubbleMetadata bubbleMetadata, OnSettingsClickListener onSettingsClick, OnSnoozeClickListener onSnoozeClickListener, ConversationIconFactory conversationIconFactory, + Context userContext, + Provider<PriorityOnboardingDialogController.Builder> builderProvider, boolean isDeviceProvisioned) { mSelectedAction = -1; mINotificationManager = iNotificationManager; @@ -192,18 +227,25 @@ public class NotificationConversationInfo extends LinearLayout implements mIsDeviceProvisioned = isDeviceProvisioned; mOnSnoozeClickListener = onSnoozeClickListener; mIconFactory = conversationIconFactory; + mUserContext = userContext; + mBubbleMetadata = bubbleMetadata; + mBuilderProvider = builderProvider; mShortcutManager = shortcutManager; - mConversationId = mNotificationChannel.getConversationId(); - if (TextUtils.isEmpty(mNotificationChannel.getConversationId())) { - mConversationId = mSbn.getShortcutId(mContext); - } - if (TextUtils.isEmpty(mConversationId)) { + mShortcutInfo = entry.getRanking().getShortcutInfo(); + if (mShortcutInfo == null) { throw new IllegalArgumentException("Does not have required information"); } - mShortcutInfo = entry.getRanking().getShortcutInfo(); - createConversationChannelIfNeeded(); + mNotificationChannel = NotificationChannelHelper.createConversationChannelIfNeeded( + getContext(), mINotificationManager, entry, mNotificationChannel); + + try { + mAppBubble = mINotificationManager.getBubblePreferenceForPackage(mPackageName, mAppUid); + } catch (RemoteException e) { + Log.e(TAG, "can't reach OS", e); + mAppBubble = BUBBLE_PREFERENCE_SELECTED; + } bindHeader(); bindActions(); @@ -212,27 +254,6 @@ public class NotificationConversationInfo extends LinearLayout implements done.setOnClickListener(mOnDone); } - void createConversationChannelIfNeeded() { - // If this channel is not already a customized conversation channel, create - // a custom channel - if (TextUtils.isEmpty(mNotificationChannel.getConversationId())) { - try { - // TODO: remove - mNotificationChannel.setName(mContext.getString( - R.string.notification_summary_message_format, - getName(), mNotificationChannel.getName())); - mINotificationManager.createConversationNotificationChannelForPackage( - mPackageName, mAppUid, mSbn.getKey(), mNotificationChannel, - mConversationId); - mNotificationChannel = mINotificationManager.getConversationNotificationChannel( - mContext.getOpPackageName(), UserHandle.getUserId(mAppUid), mPackageName, - mNotificationChannel.getId(), false, mConversationId); - } catch (RemoteException e) { - Slog.e(TAG, "Could not create conversation channel", e); - } - } - } - private void bindActions() { // TODO: b/152050825 @@ -247,6 +268,11 @@ public class NotificationConversationInfo extends LinearLayout implements snooze.setOnClickListener(mOnSnoozeClick); */ + if (mAppBubble == BUBBLE_PREFERENCE_ALL) { + ((TextView) findViewById(R.id.default_summary)).setText(getResources().getString( + R.string.notification_channel_summary_default_with_bubbles, mAppName)); + } + findViewById(R.id.priority).setOnClickListener(mOnFavoriteClick); findViewById(R.id.default_behavior).setOnClickListener(mOnDefaultClick); findViewById(R.id.silence).setOnClickListener(mOnMuteClick); @@ -284,54 +310,13 @@ public class NotificationConversationInfo extends LinearLayout implements // bindName(); bindPackage(); bindIcon(mNotificationChannel.isImportantConversation()); - } private void bindIcon(boolean important) { ImageView image = findViewById(R.id.conversation_icon); - if (mShortcutInfo != null) { - image.setImageDrawable(mIconFactory.getConversationDrawable( - mShortcutInfo, mPackageName, mAppUid, - important)); - } else { - if (mSbn.getNotification().extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION, false)) { - // TODO: maybe use a generic group icon, or a composite of recent senders - image.setImageDrawable(mPm.getDefaultActivityIcon()); - } else { - final List<Notification.MessagingStyle.Message> messages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - (Parcelable[]) mSbn.getNotification().extras.get( - Notification.EXTRA_MESSAGES)); - - final Notification.MessagingStyle.Message latestMessage = - Notification.MessagingStyle.findLatestIncomingMessage(messages); - Icon personIcon = latestMessage.getSenderPerson().getIcon(); - if (personIcon != null) { - image.setImageIcon(latestMessage.getSenderPerson().getIcon()); - } else { - // TODO: choose something better - image.setImageDrawable(mPm.getDefaultActivityIcon()); - } - } - } - } + image.setImageDrawable(mIconFactory.getConversationDrawable( + mShortcutInfo, mPackageName, mAppUid, important)); - private void bindName() { - TextView name = findViewById(R.id.name); - name.setText(getName()); - } - - private String getName() { - if (mShortcutInfo != null) { - return mShortcutInfo.getShortLabel().toString(); - } else { - Bundle extras = mSbn.getNotification().extras; - String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE); - if (TextUtils.isEmpty(nameString)) { - nameString = extras.getString(Notification.EXTRA_TITLE); - } - return nameString; - } } private void bindPackage() { @@ -512,6 +497,40 @@ public class NotificationConversationInfo extends LinearLayout implements bgHandler.post( new UpdateChannelRunnable(mINotificationManager, mPackageName, mAppUid, mSelectedAction, mNotificationChannel)); + mVisualStabilityManager.temporarilyAllowReordering(); + } + + private boolean shouldShowPriorityOnboarding() { + return !Prefs.getBoolean(mUserContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, false); + } + + private void showPriorityOnboarding() { + View onboardingView = LayoutInflater.from(mContext) + .inflate(R.layout.priority_onboarding_half_shell, null); + + boolean ignoreDnd = false; + try { + ignoreDnd = (mINotificationManager + .getConsolidatedNotificationPolicy().priorityConversationSenders + & NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT) != 0; + } catch (RemoteException e) { + Log.e(TAG, "Could not check conversation senders", e); + } + + boolean showAsBubble = mBubbleMetadata != null + && mBubbleMetadata.getAutoExpandBubble() + && Settings.Global.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, 0) == 1; + + PriorityOnboardingDialogController controller = mBuilderProvider.get() + .setContext(mUserContext) + .setView(onboardingView) + .setIgnoresDnd(ignoreDnd) + .setShowsAsBubble(showAsBubble) + .build(); + + controller.init(); + controller.show(); } /** @@ -545,7 +564,7 @@ public class NotificationConversationInfo extends LinearLayout implements @Override public boolean shouldBeSaved() { - return mSelectedAction == ACTION_FAVORITE || mSelectedAction == ACTION_MUTE; + return mPressedApply; } @Override @@ -598,6 +617,10 @@ public class NotificationConversationInfo extends LinearLayout implements !mChannelToUpdate.isImportantConversation()); if (mChannelToUpdate.isImportantConversation()) { mChannelToUpdate.setAllowBubbles(true); + if (mAppBubble == BUBBLE_PREFERENCE_NONE) { + mINotificationManager.setBubblesAllowed(mAppPkg, mAppUid, + BUBBLE_PREFERENCE_SELECTED); + } } mChannelToUpdate.setImportance(Math.max( mChannelToUpdate.getOriginalImportance(), IMPORTANCE_DEFAULT)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 2487d1a898a3..75affdf20364 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -49,6 +49,7 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.CurrentUserContextTracker; import com.android.systemui.statusbar.NotificationLifetimeExtender; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; @@ -67,6 +68,8 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import java.io.FileDescriptor; import java.io.PrintWriter; +import javax.inject.Provider; + import dagger.Lazy; /** @@ -111,6 +114,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx private final INotificationManager mNotificationManager; private final LauncherApps mLauncherApps; private final ShortcutManager mShortcutManager; + private final CurrentUserContextTracker mContextTracker; + private final Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider; /** * Injected constructor. See {@link NotificationsModule}. @@ -121,7 +126,9 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx HighPriorityProvider highPriorityProvider, INotificationManager notificationManager, LauncherApps launcherApps, - ShortcutManager shortcutManager) { + ShortcutManager shortcutManager, + CurrentUserContextTracker contextTracker, + Provider<PriorityOnboardingDialogController.Builder> builderProvider) { mContext = context; mVisualStabilityManager = visualStabilityManager; mStatusBarLazy = statusBarLazy; @@ -131,6 +138,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mNotificationManager = notificationManager; mLauncherApps = launcherApps; mShortcutManager = shortcutManager; + mContextTracker = contextTracker; + mBuilderProvider = builderProvider; } public void setUpWithPresenter(NotificationPresenter presenter, @@ -243,6 +252,9 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx } else if (gutsView instanceof NotificationConversationInfo) { initializeConversationNotificationInfo( row, (NotificationConversationInfo) gutsView); + } else if (gutsView instanceof PartialConversationInfo) { + initializePartialConversationNotificationInfo(row, + (PartialConversationInfo) gutsView); } return true; } catch (Exception e) { @@ -348,7 +360,47 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx } /** - * Sets up the {@link NotificationConversationInfo} inside the notification row's guts. + * Sets up the {@link PartialConversationInfo} inside the notification row's guts. + * @param row view to set up the guts for + * @param notificationInfoView view to set up/bind within {@code row} + */ + @VisibleForTesting + void initializePartialConversationNotificationInfo( + final ExpandableNotificationRow row, + PartialConversationInfo notificationInfoView) throws Exception { + NotificationGuts guts = row.getGuts(); + StatusBarNotification sbn = row.getEntry().getSbn(); + String packageName = sbn.getPackageName(); + // Settings link is only valid for notifications that specify a non-system user + NotificationInfo.OnSettingsClickListener onSettingsClick = null; + UserHandle userHandle = sbn.getUser(); + PackageManager pmUser = StatusBar.getPackageManagerForUser( + mContext, userHandle.getIdentifier()); + + if (!userHandle.equals(UserHandle.ALL) + || mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) { + onSettingsClick = (View v, NotificationChannel channel, int appUid) -> { + mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO); + guts.resetFalsingCheck(); + mOnSettingsClickListener.onSettingsClick(sbn.getKey()); + startAppNotificationSettingsActivity(packageName, appUid, channel, row); + }; + } + + notificationInfoView.bindNotification( + pmUser, + mNotificationManager, + packageName, + row.getEntry().getChannel(), + row.getUniqueChannels(), + row.getEntry(), + onSettingsClick, + mDeviceProvisionedController.isDeviceProvisioned(), + row.getIsNonblockable()); + } + + /** + * Sets up the {@link ConversationInfo} inside the notification row's guts. * @param row view to set up the guts for * @param notificationInfoView view to set up/bind within {@code row} */ @@ -357,7 +409,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx final ExpandableNotificationRow row, NotificationConversationInfo notificationInfoView) throws Exception { NotificationGuts guts = row.getGuts(); - StatusBarNotification sbn = row.getEntry().getSbn(); + NotificationEntry entry = row.getEntry(); + StatusBarNotification sbn = entry.getSbn(); String packageName = sbn.getPackageName(); // Settings link is only valid for notifications that specify a non-system user NotificationConversationInfo.OnSettingsClickListener onSettingsClick = null; @@ -384,7 +437,6 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx guts.resetFalsingCheck(); mOnSettingsClickListener.onSettingsClick(sbn.getKey()); startAppNotificationSettingsActivity(packageName, appUid, channel, row); - notificationInfoView.closeControls(v, false); }; } ConversationIconFactory iconFactoryLoader = new ConversationIconFactory(mContext, @@ -398,11 +450,14 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mNotificationManager, mVisualStabilityManager, packageName, - row.getEntry().getChannel(), - row.getEntry(), + entry.getChannel(), + entry, + entry.getBubbleMetadata(), onSettingsClick, onSnoozeClickListener, iconFactoryLoader, + mContextTracker.getCurrentUserContext(), + mBuilderProvider, mDeviceProvisionedController.isDeviceProvisioned()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index 83a6eb297ab3..5e1e3b255867 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -268,7 +268,9 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl NotificationEntry entry = mParent.getEntry(); int personNotifType = mPeopleNotificationIdentifier .getPeopleNotificationType(entry.getSbn(), entry.getRanking()); - if (personNotifType != PeopleNotificationIdentifier.TYPE_NON_PERSON) { + if (personNotifType == PeopleNotificationIdentifier.TYPE_PERSON) { + mInfoItem = createPartialConversationItem(mContext); + } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) { mInfoItem = createConversationItem(mContext); } else { mInfoItem = createInfoItem(mContext); @@ -667,6 +669,16 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl R.drawable.ic_settings); } + static NotificationMenuItem createPartialConversationItem(Context context) { + Resources res = context.getResources(); + String infoDescription = res.getString(R.string.notification_menu_gear_description); + PartialConversationInfo infoContent = + (PartialConversationInfo) LayoutInflater.from(context).inflate( + R.layout.partial_conversation_info, null, false); + return new NotificationMenuItem(context, infoDescription, infoContent, + R.drawable.ic_settings); + } + static NotificationMenuItem createInfoItem(Context context) { Resources res = context.getResources(); String infoDescription = res.getString(R.string.notification_menu_gear_description); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index 9bd8d4782672..a9f83c8b9e6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -114,11 +114,6 @@ public interface NotificationRowContentBinder { public boolean isLowPriority; /** - * Bind child version of content views. - */ - public boolean isChildInGroup; - - /** * Use increased height when binding contracted view. */ public boolean usesIncreasedHeight; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java new file mode 100644 index 000000000000..2189b872da43 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.notification.row; + +import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION; +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; + +import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.RemoteException; +import android.service.notification.StatusBarNotification; +import android.text.TextUtils; +import android.transition.ChangeBounds; +import android.transition.Fade; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Dependency; +import com.android.systemui.R; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +import java.lang.annotation.Retention; +import java.util.List; +import java.util.Set; + +/** + * The guts of a conversation notification that doesn't use valid shortcuts that is revealed when + * performing a long press. + */ +public class PartialConversationInfo extends LinearLayout implements + NotificationGuts.GutsContent { + private static final String TAG = "PartialConvoGuts"; + + private INotificationManager mINotificationManager; + private PackageManager mPm; + private String mPackageName; + private String mAppName; + private int mAppUid; + private String mDelegatePkg; + private NotificationChannel mNotificationChannel; + private StatusBarNotification mSbn; + private boolean mIsDeviceProvisioned; + private boolean mIsNonBlockable; + private Set<NotificationChannel> mUniqueChannelsInRow; + private Drawable mPkgIcon; + + private @Action int mSelectedAction = -1; + private boolean mPressedApply; + private boolean mPresentingChannelEditorDialog = false; + + private NotificationInfo.OnSettingsClickListener mOnSettingsClickListener; + private NotificationGuts mGutsContainer; + private ChannelEditorDialogController mChannelEditorDialogController; + + @VisibleForTesting + boolean mSkipPost = false; + + @Retention(SOURCE) + @IntDef({ACTION_SETTINGS}) + private @interface Action {} + static final int ACTION_SETTINGS = 5; + + private OnClickListener mOnDone = v -> { + mPressedApply = true; + closeControls(v, true); + }; + + public PartialConversationInfo(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void bindNotification( + PackageManager pm, + INotificationManager iNotificationManager, + String pkg, + NotificationChannel notificationChannel, + Set<NotificationChannel> uniqueChannelsInRow, + NotificationEntry entry, + NotificationInfo.OnSettingsClickListener onSettingsClick, + boolean isDeviceProvisioned, + boolean isNonBlockable) { + mSelectedAction = -1; + mINotificationManager = iNotificationManager; + mPackageName = pkg; + mSbn = entry.getSbn(); + mPm = pm; + mAppName = mPackageName; + mOnSettingsClickListener = onSettingsClick; + mNotificationChannel = notificationChannel; + mAppUid = mSbn.getUid(); + mDelegatePkg = mSbn.getOpPkg(); + mIsDeviceProvisioned = isDeviceProvisioned; + mIsNonBlockable = isNonBlockable; + mChannelEditorDialogController = Dependency.get(ChannelEditorDialogController.class); + mUniqueChannelsInRow = uniqueChannelsInRow; + + bindHeader(); + bindActions(); + + View turnOffButton = findViewById(R.id.turn_off_notifications); + turnOffButton.setOnClickListener(getTurnOffNotificationsClickListener()); + turnOffButton.setVisibility(turnOffButton.hasOnClickListeners() && !mIsNonBlockable + ? VISIBLE : GONE); + + View done = findViewById(R.id.done); + done.setOnClickListener(mOnDone); + } + + private void bindActions() { + final View settingsButton = findViewById(R.id.info); + settingsButton.setOnClickListener(getSettingsOnClickListener()); + settingsButton.setVisibility(settingsButton.hasOnClickListeners() ? VISIBLE : GONE); + + TextView msg = findViewById(R.id.non_configurable_text); + msg.setText(getResources().getString(R.string.no_shortcut, mAppName)); + } + + private void bindHeader() { + bindConversationDetails(); + + // Delegate + bindDelegate(); + } + + private OnClickListener getSettingsOnClickListener() { + if (mAppUid >= 0 && mOnSettingsClickListener != null && mIsDeviceProvisioned) { + final int appUidF = mAppUid; + return ((View view) -> { + mOnSettingsClickListener.onClick(view, mNotificationChannel, appUidF); + }); + } + return null; + } + + private OnClickListener getTurnOffNotificationsClickListener() { + return ((View view) -> { + if (!mPresentingChannelEditorDialog && mChannelEditorDialogController != null) { + mPresentingChannelEditorDialog = true; + + mChannelEditorDialogController.prepareDialogForApp(mAppName, mPackageName, mAppUid, + mUniqueChannelsInRow, mPkgIcon, mOnSettingsClickListener); + mChannelEditorDialogController.setOnFinishListener(() -> { + mPresentingChannelEditorDialog = false; + closeControls(this, false); + }); + mChannelEditorDialogController.show(); + } + }); + } + + private void bindConversationDetails() { + final TextView channelName = findViewById(R.id.parent_channel_name); + channelName.setText(mNotificationChannel.getName()); + + bindGroup(); + bindName(); + bindPackage(); + bindIcon(); + } + + private void bindName() { + TextView name = findViewById(R.id.name); + Bundle extras = mSbn.getNotification().extras; + String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE); + if (TextUtils.isEmpty(nameString)) { + nameString = extras.getString(Notification.EXTRA_TITLE); + } + name.setText(nameString); + } + + private void bindIcon() { + ImageView image = findViewById(R.id.conversation_icon); + if (mSbn.getNotification().extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION, false)) { + // TODO: maybe use a generic group icon, or a composite of recent senders + image.setImageDrawable(mPkgIcon); + } else { + final List<Notification.MessagingStyle.Message> messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + (Parcelable[]) mSbn.getNotification().extras.get( + Notification.EXTRA_MESSAGES)); + + final Notification.MessagingStyle.Message latestMessage = + Notification.MessagingStyle.findLatestIncomingMessage(messages); + Icon personIcon = null; + if (latestMessage != null && latestMessage.getSenderPerson() != null) { + personIcon = latestMessage.getSenderPerson().getIcon(); + } + if (personIcon != null) { + image.setImageIcon(latestMessage.getSenderPerson().getIcon()); + } else { + image.setImageDrawable(mPkgIcon); + } + } + } + + private void bindPackage() { + ApplicationInfo info; + try { + info = mPm.getApplicationInfo( + mPackageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE); + if (info != null) { + mAppName = String.valueOf(mPm.getApplicationLabel(info)); + mPkgIcon = mPm.getApplicationIcon(info); + } + } catch (PackageManager.NameNotFoundException e) { + mPkgIcon = mPm.getDefaultActivityIcon(); + } + ((TextView) findViewById(R.id.pkg_name)).setText(mAppName); + } + + private void bindDelegate() { + TextView delegateView = findViewById(R.id.delegate_name); + + if (!TextUtils.equals(mPackageName, mDelegatePkg)) { + // this notification was posted by a delegate! + delegateView.setVisibility(View.VISIBLE); + } else { + delegateView.setVisibility(View.GONE); + } + } + + private void bindGroup() { + // Set group information if this channel has an associated group. + CharSequence groupName = null; + if (mNotificationChannel != null && mNotificationChannel.getGroup() != null) { + try { + final NotificationChannelGroup notificationChannelGroup = + mINotificationManager.getNotificationChannelGroupForPackage( + mNotificationChannel.getGroup(), mPackageName, mAppUid); + if (notificationChannelGroup != null) { + groupName = notificationChannelGroup.getName(); + } + } catch (RemoteException e) { + } + } + TextView groupNameView = findViewById(R.id.group_name); + View groupDivider = findViewById(R.id.group_divider); + if (groupName != null) { + groupNameView.setText(groupName); + groupNameView.setVisibility(VISIBLE); + groupDivider.setVisibility(VISIBLE); + } else { + groupNameView.setVisibility(GONE); + groupDivider.setVisibility(GONE); + } + } + + @Override + public boolean post(Runnable action) { + if (mSkipPost) { + action.run(); + return true; + } else { + return super.post(action); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + } + + @Override + public void onFinishedClosing() { + // TODO: do we need to do anything here? + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (mGutsContainer != null && + event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + if (mGutsContainer.isExposed()) { + event.getText().add(mContext.getString( + R.string.notification_channel_controls_opened_accessibility, mAppName)); + } else { + event.getText().add(mContext.getString( + R.string.notification_channel_controls_closed_accessibility, mAppName)); + } + } + } + + /** + * Closes the controls and commits the updated importance values (indirectly). + * + * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the + * user does not have the ability to undo the action anymore. + */ + @VisibleForTesting + void closeControls(View v, boolean save) { + int[] parentLoc = new int[2]; + int[] targetLoc = new int[2]; + mGutsContainer.getLocationOnScreen(parentLoc); + v.getLocationOnScreen(targetLoc); + final int centerX = v.getWidth() / 2; + final int centerY = v.getHeight() / 2; + final int x = targetLoc[0] - parentLoc[0] + centerX; + final int y = targetLoc[1] - parentLoc[1] + centerY; + mGutsContainer.closeControls(x, y, save, false /* force */); + } + + @Override + public void setGutsParent(NotificationGuts guts) { + mGutsContainer = guts; + } + + @Override + public boolean willBeRemoved() { + return false; + } + + @Override + public boolean shouldBeSaved() { + return mPressedApply; + } + + @Override + public View getContentView() { + return this; + } + + @Override + public boolean handleCloseControls(boolean save, boolean force) { + return false; + } + + @Override + public int getActualHeight() { + return getHeight(); + } + + @VisibleForTesting + public boolean isAnimating() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PriorityOnboardingDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PriorityOnboardingDialogController.kt new file mode 100644 index 000000000000..d1b405256f39 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PriorityOnboardingDialogController.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.graphics.PixelFormat +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.view.View +import android.view.View.GONE +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.Window +import android.view.WindowInsets.Type.statusBars +import android.view.WindowManager +import android.widget.LinearLayout +import android.widget.TextView +import com.android.systemui.Prefs +import com.android.systemui.R +import java.lang.IllegalStateException +import javax.inject.Inject + +/** + * Controller to handle presenting the priority conversations onboarding dialog + */ +class PriorityOnboardingDialogController @Inject constructor( + val view: View, + val context: Context, + val ignoresDnd: Boolean, + val showsAsBubble: Boolean +) { + + private lateinit var dialog: Dialog + + fun init() { + initDialog() + } + + fun show() { + dialog.show() + } + + private fun done() { + // Log that the user has seen the onboarding + Prefs.putBoolean(context, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true) + dialog.dismiss() + } + + class Builder @Inject constructor() { + private lateinit var view: View + private lateinit var context: Context + private var ignoresDnd = false + private var showAsBubble = false + + fun setView(v: View): Builder { + view = v + return this + } + + fun setContext(c: Context): Builder { + context = c + return this + } + + fun setIgnoresDnd(ignore: Boolean): Builder { + ignoresDnd = ignore + return this + } + + fun setShowsAsBubble(bubble: Boolean): Builder { + showAsBubble = bubble + return this + } + + fun build(): PriorityOnboardingDialogController { + val controller = PriorityOnboardingDialogController( + view, context, ignoresDnd, showAsBubble) + return controller + } + } + + private fun initDialog() { + dialog = Dialog(context) + + if (dialog.window == null) { + throw IllegalStateException("Need a window for the onboarding dialog to show") + } + + dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) + // Prevent a11y readers from reading the first element in the dialog twice + dialog.setTitle("\u00A0") + dialog.apply { + setContentView(view) + setCanceledOnTouchOutside(true) + + findViewById<TextView>(R.id.done_button)?.setOnClickListener { + done() + } + + if (!ignoresDnd) { + findViewById<LinearLayout>(R.id.ignore_dnd_tip).visibility = GONE + } + + if (!showsAsBubble) { + findViewById<LinearLayout>(R.id.floating_bubble_tip).visibility = GONE + } + + window?.apply { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + addFlags(wmFlags) + setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + setWindowAnimations(com.android.internal.R.style.Animation_InputMethod) + + attributes = attributes.apply { + format = PixelFormat.TRANSLUCENT + title = ChannelEditorDialogController::class.java.simpleName + gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + fitInsetsTypes = attributes.fitInsetsTypes and statusBars().inv() + width = MATCH_PARENT + height = WRAP_CONTENT + } + } + } + } + + private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS + or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index d3fec695f012..f26ecc32821d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -27,7 +27,6 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin */ public final class RowContentBindParams { private boolean mUseLowPriority; - private boolean mUseChildInGroup; private boolean mUseIncreasedHeight; private boolean mUseIncreasedHeadsUpHeight; private boolean mViewsNeedReinflation; @@ -56,20 +55,6 @@ public final class RowContentBindParams { } /** - * Set whether content should use group child version of its content views. - */ - public void setUseChildInGroup(boolean useChildInGroup) { - if (mUseChildInGroup != useChildInGroup) { - mDirtyContentViews |= (FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED); - } - mUseChildInGroup = useChildInGroup; - } - - public boolean useChildInGroup() { - return mUseChildInGroup; - } - - /** * Set whether content should use an increased height version of its contracted view. */ public void setUseIncreasedCollapsedHeight(boolean useIncreasedHeight) { @@ -163,10 +148,10 @@ public final class RowContentBindParams { @Override public String toString() { return String.format("RowContentBindParams[mContentViews=%x mDirtyContentViews=%x " - + "mUseLowPriority=%b mUseChildInGroup=%b mUseIncreasedHeight=%b " + + "mUseLowPriority=%b mUseIncreasedHeight=%b " + "mUseIncreasedHeadsUpHeight=%b mViewsNeedReinflation=%b]", - mContentViews, mDirtyContentViews, mUseLowPriority, mUseChildInGroup, - mUseIncreasedHeight, mUseIncreasedHeadsUpHeight, mViewsNeedReinflation); + mContentViews, mDirtyContentViews, mUseLowPriority, mUseIncreasedHeight, + mUseIncreasedHeadsUpHeight, mViewsNeedReinflation); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index c632f3eb22a2..c6f0a135cd34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -71,7 +71,6 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { BindParams bindParams = new BindParams(); bindParams.isLowPriority = params.useLowPriority(); - bindParams.isChildInGroup = params.useChildInGroup(); bindParams.usesIncreasedHeight = params.useIncreasedHeight(); bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight(); boolean forceInflate = params.needsReinflation(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt index 13e7fe5cd6d8..15499b87d56d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -46,13 +46,13 @@ class NotificationConversationTemplateViewWrapper constructor( ) private val conversationLayout: ConversationLayout = view as ConversationLayout - private lateinit var conversationIcon: CachingIconView + private lateinit var conversationIconView: CachingIconView private lateinit var conversationBadgeBg: View private lateinit var expandButton: View private lateinit var expandButtonContainer: View private lateinit var imageMessageContainer: ViewGroup private lateinit var messagingLinearLayout: MessagingLinearLayout - private lateinit var conversationTitle: View + private lateinit var conversationTitleView: View private lateinit var importanceRing: View private lateinit var appName: View private var facePileBottomBg: View? = null @@ -63,7 +63,7 @@ class NotificationConversationTemplateViewWrapper constructor( messagingLinearLayout = conversationLayout.messagingLinearLayout imageMessageContainer = conversationLayout.imageMessageContainer with(conversationLayout) { - conversationIcon = requireViewById(com.android.internal.R.id.conversation_icon) + conversationIconView = requireViewById(com.android.internal.R.id.conversation_icon) conversationBadgeBg = requireViewById(com.android.internal.R.id.conversation_icon_badge_bg) expandButton = requireViewById(com.android.internal.R.id.expand_button) @@ -71,7 +71,7 @@ class NotificationConversationTemplateViewWrapper constructor( requireViewById(com.android.internal.R.id.expand_button_container) importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring) appName = requireViewById(com.android.internal.R.id.app_name_text) - conversationTitle = requireViewById(com.android.internal.R.id.conversation_text) + conversationTitleView = requireViewById(com.android.internal.R.id.conversation_text) facePileTop = findViewById(com.android.internal.R.id.conversation_face_pile_top) facePileBottom = findViewById(com.android.internal.R.id.conversation_face_pile_bottom) facePileBottomBg = @@ -93,7 +93,7 @@ class NotificationConversationTemplateViewWrapper constructor( addTransformedViews( messagingLinearLayout, appName, - conversationTitle) + conversationTitleView) // Let's ignore the image message container since that is transforming as part of the // messages already @@ -124,7 +124,7 @@ class NotificationConversationTemplateViewWrapper constructor( ) addViewsTransformingToSimilar( - conversationIcon, + conversationIconView, conversationBadgeBg, expandButton, importanceRing, @@ -136,29 +136,27 @@ class NotificationConversationTemplateViewWrapper constructor( override fun setShelfIconVisible(visible: Boolean) { if (conversationLayout.isImportantConversation) { - if (conversationIcon.visibility != GONE) { - conversationIcon.setForceHidden(visible); + if (conversationIconView.visibility != GONE) { + conversationIconView.isForceHidden = visible // We don't want the small icon to be hidden by the extended wrapper, as force // hiding the conversationIcon will already do that via its listener. - return; + return } } super.setShelfIconVisible(visible) } - override fun getShelfTransformationTarget(): View? { - if (conversationLayout.isImportantConversation) { - if (conversationIcon.visibility != GONE) { - return conversationIcon - } else { - // A notification with a fallback icon was set to important. Currently - // the transformation doesn't work for these and needs to be fixed. In the meantime - // those are using the icon. - return super.getShelfTransformationTarget(); - } - } - return super.getShelfTransformationTarget() - } + override fun getShelfTransformationTarget(): View? = + if (conversationLayout.isImportantConversation) + if (conversationIconView.visibility != GONE) + conversationIconView + else + // A notification with a fallback icon was set to important. Currently + // the transformation doesn't work for these and needs to be fixed. + // In the meantime those are using the icon. + super.getShelfTransformationTarget() + else + super.getShelfTransformationTarget() override fun setRemoteInputVisible(visible: Boolean) = conversationLayout.showHistoricMessages(visible) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 7ac066277c86..f8b783113ccb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.row.wrapper; import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y; -import android.annotation.NonNull; import android.app.AppOpsManager; import android.app.Notification; import android.content.Context; @@ -28,11 +27,13 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.NotificationExpandButton; +import com.android.settingslib.Utils; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.TransformableView; @@ -61,12 +62,14 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { private NotificationExpandButton mExpandButton; protected NotificationHeaderView mNotificationHeader; private TextView mHeaderText; + private TextView mAppNameText; private ImageView mWorkProfileImage; private View mCameraIcon; private View mMicIcon; private View mOverlayIcon; private View mAppOps; private View mAudiblyAlertedIcon; + private FrameLayout mIconContainer; private boolean mIsLowPriority; private boolean mTransformLowPriorityTitle; @@ -109,8 +112,10 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } protected void resolveHeaderViews() { + mIconContainer = mView.findViewById(com.android.internal.R.id.header_icon_container); mIcon = mView.findViewById(com.android.internal.R.id.icon); mHeaderText = mView.findViewById(com.android.internal.R.id.header_text); + mAppNameText = mView.findViewById(com.android.internal.R.id.app_name_text); mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button); mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge); mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header); @@ -133,15 +138,6 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { if (mAppOps != null) { mAppOps.setOnClickListener(listener); } - if (mCameraIcon != null) { - mCameraIcon.setOnClickListener(listener); - } - if (mMicIcon != null) { - mMicIcon.setOnClickListener(listener); - } - if (mOverlayIcon != null) { - mOverlayIcon.setOnClickListener(listener); - } } /** @@ -192,6 +188,61 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } } + public void applyConversationSkin() { + if (mAppNameText != null) { + mAppNameText.setTextAppearance( + com.android.internal.R.style + .TextAppearance_DeviceDefault_Notification_Conversation_AppName); + ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) mAppNameText.getLayoutParams(); + layoutParams.setMarginStart(0); + } + if (mIconContainer != null) { + ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) mIconContainer.getLayoutParams(); + layoutParams.width = + mIconContainer.getContext().getResources().getDimensionPixelSize( + com.android.internal.R.dimen.conversation_content_start); + final int marginStart = + mIconContainer.getContext().getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_content_margin_start); + layoutParams.setMarginStart(marginStart * -1); + } + if (mIcon != null) { + ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) mIcon.getLayoutParams(); + layoutParams.setMarginEnd(0); + } + } + + public void clearConversationSkin() { + if (mAppNameText != null) { + final int textAppearance = Utils.getThemeAttr( + mAppNameText.getContext(), + com.android.internal.R.attr.notificationHeaderTextAppearance, + com.android.internal.R.style.TextAppearance_DeviceDefault_Notification_Info); + mAppNameText.setTextAppearance(textAppearance); + ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) mAppNameText.getLayoutParams(); + final int marginStart = mAppNameText.getContext().getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_header_app_name_margin_start); + layoutParams.setMarginStart(marginStart); + } + if (mIconContainer != null) { + ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) mIconContainer.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.setMarginStart(0); + } + if (mIcon != null) { + ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) mIcon.getLayoutParams(); + final int marginEnd = mIcon.getContext().getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_header_icon_margin_end); + layoutParams.setMarginEnd(marginEnd); + } + } + /** * Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each * child is faded automatically and doesn't have to be manually added. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index 874d81db0bd2..b96cff830f31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; @@ -187,21 +188,26 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi com.android.systemui.R.id.quick_qs_panel); StatusBarNotification sbn = mRow.getEntry().getSbn(); Notification notif = sbn.getNotification(); + Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext); panel.getMediaPlayer().setMediaSession(token, - notif.getSmallIcon(), + iconDrawable, + notif.getLargeIcon(), tintColor, mBackgroundColor, mActions, compactActions, - notif.contentIntent); + notif.contentIntent, + sbn.getKey()); QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById( com.android.systemui.R.id.quick_settings_panel); bigPanel.addMediaSession(token, - notif.getSmallIcon(), + iconDrawable, + notif.getLargeIcon(), tintColor, mBackgroundColor, mActions, - sbn); + sbn, + sbn.getKey()); } boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 400e794b820b..c9b1318feb13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.HybridGroupManager; import com.android.systemui.statusbar.notification.row.HybridNotificationView; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import java.util.ArrayList; @@ -65,7 +66,7 @@ public class NotificationChildrenContainer extends ViewGroup { }.setDuration(200); private final List<View> mDividers = new ArrayList<>(); - private final List<ExpandableNotificationRow> mChildren = new ArrayList<>(); + private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>(); private final HybridGroupManager mHybridGroupManager; private int mChildPadding; private int mDividerHeight; @@ -99,12 +100,14 @@ public class NotificationChildrenContainer extends ViewGroup { private boolean mIsLowPriority; private OnClickListener mHeaderClickListener; private ViewGroup mCurrentHeader; + private boolean mIsConversation; private boolean mShowDividersWhenExpanded; private boolean mHideDividersDuringExpand; private int mTranslationForHeader; private int mCurrentHeaderTranslation = 0; private float mHeaderVisibleAmount = 1.0f; + private int mUntruncatedChildCount; public NotificationChildrenContainer(Context context) { this(context, null); @@ -121,7 +124,7 @@ public class NotificationChildrenContainer extends ViewGroup { public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mHybridGroupManager = new HybridGroupManager(getContext(), this); + mHybridGroupManager = new HybridGroupManager(getContext()); initDimens(); setClipChildren(false); } @@ -153,9 +156,10 @@ public class NotificationChildrenContainer extends ViewGroup { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); + int childCount = + Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); for (int i = 0; i < childCount; i++) { - View child = mChildren.get(i); + View child = mAttachedChildren.get(i); // We need to layout all children even the GONE ones, such that the heights are // calculated correctly as they are used to calculate how many we can fit on the screen child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); @@ -195,11 +199,12 @@ public class NotificationChildrenContainer extends ViewGroup { } int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); int height = mNotificationHeaderMargin + mNotificatonTopPadding; - int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); + int childCount = + Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1; for (int i = 0; i < childCount; i++) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); // We need to measure all children even the GONE ones, such that the heights are // calculated correctly as they are used to calculate how many we can fit on the screen. boolean isOverflow = i == overflowIndex; @@ -242,14 +247,24 @@ public class NotificationChildrenContainer extends ViewGroup { } /** + * Set the untruncated number of children in the group so that the view can update the UI + * appropriately. Note that this may differ from the number of views attached as truncated + * children will not have views. + */ + public void setUntruncatedChildCount(int childCount) { + mUntruncatedChildCount = childCount; + updateGroupOverflow(); + } + + /** * Add a child notification to this view. * * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addNotification(ExpandableNotificationRow row, int childIndex) { - int newIndex = childIndex < 0 ? mChildren.size() : childIndex; - mChildren.add(newIndex, row); + int newIndex = childIndex < 0 ? mAttachedChildren.size() : childIndex; + mAttachedChildren.add(newIndex, row); addView(row); row.setUserLocked(mUserLocked); @@ -257,7 +272,6 @@ public class NotificationChildrenContainer extends ViewGroup { addView(divider); mDividers.add(newIndex, divider); - updateGroupOverflow(); row.setContentTransformationAmount(0, false /* isLastChild */); // It doesn't make sense to keep old animations around, lets cancel them! ExpandableViewState viewState = row.getViewState(); @@ -268,8 +282,8 @@ public class NotificationChildrenContainer extends ViewGroup { } public void removeNotification(ExpandableNotificationRow row) { - int childIndex = mChildren.indexOf(row); - mChildren.remove(row); + int childIndex = mAttachedChildren.indexOf(row); + mAttachedChildren.remove(row); removeView(row); final View divider = mDividers.remove(childIndex); @@ -284,7 +298,6 @@ public class NotificationChildrenContainer extends ViewGroup { row.setSystemChildExpanded(false); row.setUserLocked(false); - updateGroupOverflow(); if (!row.isRemoved()) { mHeaderUtil.restoreNotificationHeader(row); } @@ -294,11 +307,12 @@ public class NotificationChildrenContainer extends ViewGroup { * @return The number of notification children in the container. */ public int getNotificationChildCount() { - return mChildren.size(); + return mAttachedChildren.size(); } - public void recreateNotificationHeader(OnClickListener listener) { + public void recreateNotificationHeader(OnClickListener listener, boolean isConversation) { mHeaderClickListener = listener; + mIsConversation = isConversation; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); @@ -317,7 +331,16 @@ public class NotificationChildrenContainer extends ViewGroup { header.reapply(getContext(), mNotificationHeader); } mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); - recreateLowPriorityHeader(builder); + if (mNotificationHeaderWrapper instanceof NotificationHeaderViewWrapper) { + NotificationHeaderViewWrapper headerWrapper = + (NotificationHeaderViewWrapper) mNotificationHeaderWrapper; + if (isConversation) { + headerWrapper.applyConversationSkin(); + } else { + headerWrapper.clearConversationSkin(); + } + } + recreateLowPriorityHeader(builder, isConversation); updateHeaderVisibility(false /* animate */); updateChildrenHeaderAppearance(); } @@ -327,7 +350,7 @@ public class NotificationChildrenContainer extends ViewGroup { * * @param builder a builder to reuse. Otherwise the builder will be recovered. */ - private void recreateLowPriorityHeader(Notification.Builder builder) { + private void recreateLowPriorityHeader(Notification.Builder builder, boolean isConversation) { RemoteViews header; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); if (mIsLowPriority) { @@ -351,6 +374,15 @@ public class NotificationChildrenContainer extends ViewGroup { header.reapply(getContext(), mNotificationHeaderLowPriority); } mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); + if (mNotificationHeaderWrapper instanceof NotificationHeaderViewWrapper) { + NotificationHeaderViewWrapper headerWrapper = + (NotificationHeaderViewWrapper) mNotificationHeaderWrapper; + if (isConversation) { + headerWrapper.applyConversationSkin(); + } else { + headerWrapper.clearConversationSkin(); + } + } resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader()); } else { removeView(mNotificationHeaderLowPriority); @@ -364,11 +396,10 @@ public class NotificationChildrenContainer extends ViewGroup { } public void updateGroupOverflow() { - int childCount = mChildren.size(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); - if (childCount > maxAllowedVisibleChildren) { - int number = childCount - maxAllowedVisibleChildren; - mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number); + if (mUntruncatedChildCount > maxAllowedVisibleChildren) { + int number = mUntruncatedChildCount - maxAllowedVisibleChildren; + mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number, this); if (mGroupOverFlowState == null) { mGroupOverFlowState = new ViewState(); mNeverAppliedGroupState = true; @@ -401,8 +432,11 @@ public class NotificationChildrenContainer extends ViewGroup { R.layout.notification_children_divider, this, false); } - public List<ExpandableNotificationRow> getNotificationChildren() { - return mChildren; + /** + * Get notification children that are attached currently. + */ + public List<ExpandableNotificationRow> getAttachedChildren() { + return mAttachedChildren; } /** @@ -420,13 +454,13 @@ public class NotificationChildrenContainer extends ViewGroup { return false; } boolean result = false; - for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) { - ExpandableNotificationRow child = mChildren.get(i); + for (int i = 0; i < mAttachedChildren.size() && i < childOrder.size(); i++) { + ExpandableNotificationRow child = mAttachedChildren.get(i); ExpandableNotificationRow desiredChild = (ExpandableNotificationRow) childOrder.get(i); if (child != desiredChild) { if (visualStabilityManager.canReorderNotification(desiredChild)) { - mChildren.remove(desiredChild); - mChildren.add(i, desiredChild); + mAttachedChildren.remove(desiredChild); + mAttachedChildren.add(i, desiredChild); result = true; } else { visualStabilityManager.addReorderingAllowedCallback(callback); @@ -442,9 +476,9 @@ public class NotificationChildrenContainer extends ViewGroup { // we don't modify it the group is expanded or if we are expanding it return; } - int size = mChildren.size(); + int size = mAttachedChildren.size(); for (int i = 0; i < size; i++) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); child.setSystemChildExpanded(i == 0 && size == 1); } } @@ -468,7 +502,7 @@ public class NotificationChildrenContainer extends ViewGroup { } int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation; int visibleChildren = 0; - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); boolean firstChild = true; float expandFactor = 0; if (mUserLocked) { @@ -499,7 +533,7 @@ public class NotificationChildrenContainer extends ViewGroup { } firstChild = false; } - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); intrinsicHeight += child.getIntrinsicHeight(); visibleChildren++; } @@ -518,7 +552,7 @@ public class NotificationChildrenContainer extends ViewGroup { * @param ambientState the ambient state containing ambient information */ public void updateState(ExpandableViewState parentState, AmbientState ambientState) { - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); int yPosition = mNotificationHeaderMargin + mCurrentHeaderTranslation; boolean firstChild = true; int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); @@ -535,7 +569,7 @@ public class NotificationChildrenContainer extends ViewGroup { && !mContainingNotification.isGroupExpansionChanging(); int launchTransitionCompensation = 0; for (int i = 0; i < childCount; i++) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); if (!firstChild) { if (expandingToExpandedGroup) { yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight, @@ -586,7 +620,7 @@ public class NotificationChildrenContainer extends ViewGroup { } if (mOverflowNumber != null) { - ExpandableNotificationRow overflowView = mChildren.get(Math.min( + ExpandableNotificationRow overflowView = mAttachedChildren.get(Math.min( getMaxAllowedVisibleChildren(true /* likeCollapsed */), childCount) - 1); mGroupOverFlowState.copyFrom(overflowView.getViewState()); @@ -672,7 +706,7 @@ public class NotificationChildrenContainer extends ViewGroup { /** Applies state to children. */ public void applyState() { - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); float expandFraction = 0.0f; if (mUserLocked) { @@ -683,7 +717,7 @@ public class NotificationChildrenContainer extends ViewGroup { || (mContainingNotification.isGroupExpansionChanging() && !mHideDividersDuringExpand); for (int i = 0; i < childCount; i++) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); ExpandableViewState viewState = child.getViewState(); viewState.applyToView(child); @@ -716,10 +750,10 @@ public class NotificationChildrenContainer extends ViewGroup { if (mContainingNotification.hasExpandingChild()) { return; } - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount; for (int i = 0; i < childCount; i++) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == GONE) { continue; } @@ -754,7 +788,7 @@ public class NotificationChildrenContainer extends ViewGroup { /** Animate to a given state. */ public void startAnimationToState(AnimationProperties properties) { - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); float expandFraction = getGroupExpandFraction(); final boolean dividersVisible = mUserLocked && !showingAsLowPriority() @@ -762,7 +796,7 @@ public class NotificationChildrenContainer extends ViewGroup { || (mContainingNotification.isGroupExpansionChanging() && !mHideDividersDuringExpand); for (int i = childCount - 1; i >= 0; i--) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); ExpandableViewState viewState = child.getViewState(); viewState.animateTo(child, properties); @@ -799,9 +833,9 @@ public class NotificationChildrenContainer extends ViewGroup { public ExpandableNotificationRow getViewAtPosition(float y) { // find the view under the pointer, accounting for GONE views - final int count = mChildren.size(); + final int count = mAttachedChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { - ExpandableNotificationRow slidingChild = mChildren.get(childIdx); + ExpandableNotificationRow slidingChild = mAttachedChildren.get(childIdx); float childTop = slidingChild.getTranslationY(); float top = childTop + slidingChild.getClipTopAmount(); float bottom = childTop + slidingChild.getActualHeight(); @@ -818,9 +852,9 @@ public class NotificationChildrenContainer extends ViewGroup { if (mNotificationHeader != null) { mNotificationHeader.setExpanded(childrenExpanded); } - final int count = mChildren.size(); + final int count = mAttachedChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { - ExpandableNotificationRow child = mChildren.get(childIdx); + ExpandableNotificationRow child = mAttachedChildren.get(childIdx); child.setChildrenExpanded(childrenExpanded, false); } updateHeaderTouchability(); @@ -919,12 +953,12 @@ public class NotificationChildrenContainer extends ViewGroup { private void startChildAlphaAnimations(boolean toVisible) { float target = toVisible ? 1.0f : 0.0f; float start = 1.0f - target; - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) { break; } - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); child.setAlpha(start); ViewState viewState = new ViewState(); viewState.initFrom(child); @@ -979,12 +1013,12 @@ public class NotificationChildrenContainer extends ViewGroup { int maxContentHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation + mNotificatonTopPadding; int visibleChildren = 0; - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) { break; } - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); float childHeight = child.isExpanded(true /* allowOnKeyguard */) ? child.getMaxExpandHeight() : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); @@ -1006,9 +1040,9 @@ public class NotificationChildrenContainer extends ViewGroup { boolean showingLowPriority = showingAsLowPriority(); updateHeaderTransformation(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); float childHeight; if (showingLowPriority) { childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */); @@ -1042,13 +1076,13 @@ public class NotificationChildrenContainer extends ViewGroup { int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation + mNotificatonTopPadding + mDividerHeight; int visibleChildren = 0; - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); float childHeight = child.isExpanded(true /* allowOnKeyguard */) ? child.getMaxExpandHeight() : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); @@ -1097,7 +1131,7 @@ public class NotificationChildrenContainer extends ViewGroup { int minExpandHeight = mNotificationHeaderMargin + headerTranslation; int visibleChildren = 0; boolean firstChild = true; - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; @@ -1107,7 +1141,7 @@ public class NotificationChildrenContainer extends ViewGroup { } else { firstChild = false; } - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); minExpandHeight += child.getSingleLineView().getHeight(); visibleChildren++; } @@ -1128,7 +1162,7 @@ public class NotificationChildrenContainer extends ViewGroup { removeView(mNotificationHeaderLowPriority); mNotificationHeaderLowPriority = null; } - recreateNotificationHeader(listener); + recreateNotificationHeader(listener, mIsConversation); initDimens(); for (int i = 0; i < mDividers.size(); i++) { View prevDivider = mDividers.get(i); @@ -1149,9 +1183,9 @@ public class NotificationChildrenContainer extends ViewGroup { if (!mUserLocked) { updateHeaderVisibility(false /* animate */); } - int childCount = mChildren.size(); + int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { - ExpandableNotificationRow child = mChildren.get(i); + ExpandableNotificationRow child = mAttachedChildren.get(i); child.setUserLocked(userLocked && !showingAsLowPriority()); } updateHeaderTouchability(); @@ -1172,8 +1206,8 @@ public class NotificationChildrenContainer extends ViewGroup { int position = mNotificationHeaderMargin + mCurrentHeaderTranslation + mNotificatonTopPadding; - for (int i = 0; i < mChildren.size(); i++) { - ExpandableNotificationRow child = mChildren.get(i); + for (int i = 0; i < mAttachedChildren.size(); i++) { + ExpandableNotificationRow child = mAttachedChildren.get(i); boolean notGone = child.getVisibility() != View.GONE; if (notGone) { position += mDividerHeight; @@ -1212,7 +1246,7 @@ public class NotificationChildrenContainer extends ViewGroup { public void setIsLowPriority(boolean isLowPriority) { mIsLowPriority = isLowPriority; if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ - recreateLowPriorityHeader(null /* existingBuilder */); + recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation); updateHeaderVisibility(false /* animate */); } if (mUserLocked) { @@ -1251,8 +1285,8 @@ public class NotificationChildrenContainer extends ViewGroup { public void setCurrentBottomRoundness(float currentBottomRoundness) { boolean last = true; - for (int i = mChildren.size() - 1; i >= 0; i--) { - ExpandableNotificationRow child = mChildren.get(i); + for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { + ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java index c4a720cb659f..09ab1d89473e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java @@ -23,6 +23,7 @@ import android.view.View; import android.view.ViewGroup; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; +import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.VisibilityLocationProvider; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.SimpleNotificationListContainer; @@ -194,4 +195,6 @@ public interface NotificationListContainer extends ExpandableView.OnHeightChange * @param v the item to remove */ void removeListItem(@NonNull NotificationListItem v); + + void setNotificationActivityStarter(NotificationActivityStarter notificationActivityStarter); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListItem.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListItem.java index 8991abe52ce1..c2dd2296aa17 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListItem.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListItem.java @@ -43,7 +43,7 @@ public interface NotificationListItem { // This generic is kind of ugly - we should change this once the old VHM is gone /** @return list of the children of this item */ - List<? extends NotificationListItem> getNotificationChildren(); + List<? extends NotificationListItem> getAttachedChildren(); /** remove all children from this list item */ void removeAllChildren(); @@ -54,6 +54,9 @@ public interface NotificationListItem { /** add an item as a child */ void addChildNotification(NotificationListItem child, int childIndex); + /** set the child count view should display */ + void setUntruncatedChildCount(int count); + /** Update the order of the children with the new list */ boolean applyChildOrder( List<? extends NotificationListItem> childOrderList, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 6054b507185e..7093dd8b39e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -29,6 +29,7 @@ import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEX import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.app.TaskStackBuilder; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeAnimator; @@ -50,6 +51,7 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; +import android.os.AsyncTask; import android.os.Bundle; import android.os.ServiceManager; import android.provider.Settings; @@ -117,6 +119,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController; +import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationUtils; @@ -255,6 +258,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd private final AmbientState mAmbientState; private NotificationGroupManager mGroupManager; + private NotificationActivityStarter mNotificationActivityStarter; private HashSet<ExpandableView> mChildrenToAddAnimated = new HashSet<>(); private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>(); private ArrayList<ExpandableView> mChildrenToRemoveAnimated = new ArrayList<>(); @@ -628,8 +632,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd mHighPriorityBeforeSpeedBump = "1".equals(newValue); } else if (key.equals(Settings.Secure.NOTIFICATION_DISMISS_RTL)) { updateDismissRtlSetting("1".equals(newValue)); + } else if (key.equals(Settings.Secure.NOTIFICATION_HISTORY_ENABLED)) { + updateFooter(); } - }, HIGH_PRIORITY, Settings.Secure.NOTIFICATION_DISMISS_RTL); + }, HIGH_PRIORITY, Settings.Secure.NOTIFICATION_DISMISS_RTL, + Settings.Secure.NOTIFICATION_HISTORY_ENABLED); mFeatureFlags = featureFlags; mNotifPipeline = notifPipeline; @@ -742,12 +749,17 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @VisibleForTesting @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void updateFooter() { + if (mFooterView == null) { + return; + } boolean showDismissView = mClearAllEnabled && hasActiveClearableNotifications(ROWS_ALL); boolean showFooterView = (showDismissView || hasActiveNotifications()) && mStatusBarState != StatusBarState.KEYGUARD && !mRemoteInputManager.getController().isRemoteInputActive(); + boolean showHistory = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0) == 1; - updateFooterView(showFooterView, showDismissView); + updateFooterView(showFooterView, showDismissView, showHistory); } /** @@ -2377,7 +2389,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd ExpandableNotificationRow row = (ExpandableNotificationRow) child; if (row.isSummaryWithChildren() && row.areChildrenExpanded()) { List<ExpandableNotificationRow> notificationChildren = - row.getNotificationChildren(); + row.getAttachedChildren(); for (int childIndex = 0; childIndex < notificationChildren.size(); childIndex++) { ExpandableNotificationRow rowChild = notificationChildren.get(childIndex); @@ -4638,7 +4650,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd ExpandableNotificationRow row = (ExpandableNotificationRow) view; row.setHeadsUpAnimatingAway(false); if (row.isSummaryWithChildren()) { - for (ExpandableNotificationRow child : row.getNotificationChildren()) { + for (ExpandableNotificationRow child : row.getAttachedChildren()) { child.setHeadsUpAnimatingAway(false); } } @@ -4979,13 +4991,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - public void updateFooterView(boolean visible, boolean showDismissView) { + public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) { if (mFooterView == null) { return; } boolean animate = mIsExpanded && mAnimationsEnabled; mFooterView.setVisible(visible, animate); mFooterView.setSecondaryVisible(showDismissView, animate); + mFooterView.showHistory(showHistory); } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @@ -5566,12 +5579,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - public void manageNotifications(View v) { - Intent intent = new Intent(Settings.ACTION_NOTIFICATION_HISTORY); - mStatusBar.startActivity(intent, true, true, Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - - @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @VisibleForTesting void clearNotifications( @SelectedRows int selection, @@ -5598,7 +5605,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd && (!hasClipBounds || mTmpRect.height() > 0)) { parentVisible = true; } - List<ExpandableNotificationRow> children = row.getNotificationChildren(); + List<ExpandableNotificationRow> children = row.getAttachedChildren(); if (children != null) { for (ExpandableNotificationRow childRow : children) { if (includeChildInDismissAll(row, selection)) { @@ -5696,6 +5703,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd } } + @Override + public void setNotificationActivityStarter( + NotificationActivityStarter notificationActivityStarter) { + mNotificationActivityStarter = notificationActivityStarter; + } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) protected void inflateFooterView() { @@ -5705,7 +5718,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES); clearNotifications(ROWS_ALL, true /* closeShade */); }); - footerView.setManageButtonClickListener(this::manageNotifications); + footerView.setManageButtonClickListener(v -> { + mNotificationActivityStarter.startHistoryIntent(mFooterView.isHistoryShown()); + }); setFooterView(footerView); } @@ -5714,6 +5729,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate( R.layout.status_bar_no_notifications, this, false); view.setText(R.string.empty_shade_text); + view.setOnClickListener(v -> { + final boolean showHistory = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0) == 1; + Intent intent = showHistory ? new Intent( + Settings.ACTION_NOTIFICATION_HISTORY) : new Intent( + Settings.ACTION_NOTIFICATION_SETTINGS); + mStatusBar.startActivity(intent, true, true, Intent.FLAG_ACTIVITY_SINGLE_TOP); + }); setEmptyShadeView(view); } @@ -6388,7 +6411,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd if (parent != null && parent.areChildrenExpanded() && (parent.areGutsExposed() || mSwipeHelper.getExposedMenuView() == parent - || (parent.getNotificationChildren().size() == 1 + || (parent.getAttachedChildren().size() == 1 && parent.getEntry().isClearable()))) { // In this case the group is expanded and showing the menu for the // group, further interaction should apply to the group, not any diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 9646c01c8c41..1a15377566e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -297,7 +297,7 @@ public class StackScrollAlgorithm { ExpandableNotificationRow row = (ExpandableNotificationRow) v; // handle the notgoneIndex for the children as well - List<ExpandableNotificationRow> children = row.getNotificationChildren(); + List<ExpandableNotificationRow> children = row.getAttachedChildren(); if (row.isSummaryWithChildren() && children != null) { for (ExpandableNotificationRow childRow : children) { if (childRow.getVisibility() != View.GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 8c9bb6c75828..303a0831b52f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -29,6 +29,9 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardConstants; @@ -50,6 +53,8 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Map; +import java.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; @@ -64,6 +69,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private static final boolean DEBUG_BIO_WAKELOCK = KeyguardConstants.DEBUG_BIOMETRIC_WAKELOCK; private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000; private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; + private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); @IntDef(prefix = { "MODE_" }, value = { MODE_NONE, @@ -171,6 +177,68 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } } + @VisibleForTesting + public enum BiometricUiEvent implements UiEventLogger.UiEventEnum { + + @UiEvent(doc = "A biometric event of type fingerprint succeeded.") + BIOMETRIC_FINGERPRINT_SUCCESS(396), + + @UiEvent(doc = "A biometric event of type fingerprint failed.") + BIOMETRIC_FINGERPRINT_FAILURE(397), + + @UiEvent(doc = "A biometric event of type fingerprint errored.") + BIOMETRIC_FINGERPRINT_ERROR(398), + + @UiEvent(doc = "A biometric event of type face unlock succeeded.") + BIOMETRIC_FACE_SUCCESS(399), + + @UiEvent(doc = "A biometric event of type face unlock failed.") + BIOMETRIC_FACE_FAILURE(400), + + @UiEvent(doc = "A biometric event of type face unlock errored.") + BIOMETRIC_FACE_ERROR(401), + + @UiEvent(doc = "A biometric event of type iris succeeded.") + BIOMETRIC_IRIS_SUCCESS(402), + + @UiEvent(doc = "A biometric event of type iris failed.") + BIOMETRIC_IRIS_FAILURE(403), + + @UiEvent(doc = "A biometric event of type iris errored.") + BIOMETRIC_IRIS_ERROR(404); + + private final int mId; + + BiometricUiEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + + static final Map<BiometricSourceType, BiometricUiEvent> ERROR_EVENT_BY_SOURCE_TYPE = Map.of( + BiometricSourceType.FINGERPRINT, BiometricUiEvent.BIOMETRIC_FINGERPRINT_ERROR, + BiometricSourceType.FACE, BiometricUiEvent.BIOMETRIC_FACE_ERROR, + BiometricSourceType.IRIS, BiometricUiEvent.BIOMETRIC_IRIS_ERROR + ); + + static final Map<BiometricSourceType, BiometricUiEvent> SUCCESS_EVENT_BY_SOURCE_TYPE = + Map.of( + BiometricSourceType.FINGERPRINT, BiometricUiEvent.BIOMETRIC_FINGERPRINT_SUCCESS, + BiometricSourceType.FACE, BiometricUiEvent.BIOMETRIC_FACE_SUCCESS, + BiometricSourceType.IRIS, BiometricUiEvent.BIOMETRIC_IRIS_SUCCESS + ); + + static final Map<BiometricSourceType, BiometricUiEvent> FAILURE_EVENT_BY_SOURCE_TYPE = + Map.of( + BiometricSourceType.FINGERPRINT, BiometricUiEvent.BIOMETRIC_FINGERPRINT_FAILURE, + BiometricSourceType.FACE, BiometricUiEvent.BIOMETRIC_FACE_FAILURE, + BiometricSourceType.IRIS, BiometricUiEvent.BIOMETRIC_IRIS_FAILURE + ); + } + @Inject public BiometricUnlockController(Context context, DozeScrimController dozeScrimController, KeyguardViewMediator keyguardViewMediator, ScrimController scrimController, @@ -274,6 +342,9 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } mMetricsLogger.write(new LogMaker(MetricsEvent.BIOMETRIC_AUTH) .setType(MetricsEvent.TYPE_SUCCESS).setSubtype(toSubtype(biometricSourceType))); + Optional.ofNullable(BiometricUiEvent.SUCCESS_EVENT_BY_SOURCE_TYPE.get(biometricSourceType)) + .ifPresent(UI_EVENT_LOGGER::log); + boolean unlockAllowed = mKeyguardBypassController.onBiometricAuthenticated( biometricSourceType, isStrongBiometric); if (unlockAllowed) { @@ -504,6 +575,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp public void onBiometricAuthFailed(BiometricSourceType biometricSourceType) { mMetricsLogger.write(new LogMaker(MetricsEvent.BIOMETRIC_AUTH) .setType(MetricsEvent.TYPE_FAILURE).setSubtype(toSubtype(biometricSourceType))); + Optional.ofNullable(BiometricUiEvent.FAILURE_EVENT_BY_SOURCE_TYPE.get(biometricSourceType)) + .ifPresent(UI_EVENT_LOGGER::log); cleanup(); } @@ -513,6 +586,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mMetricsLogger.write(new LogMaker(MetricsEvent.BIOMETRIC_AUTH) .setType(MetricsEvent.TYPE_ERROR).setSubtype(toSubtype(biometricSourceType)) .addTaggedData(MetricsEvent.FIELD_BIOMETRIC_AUTH_ERROR, msgId)); + Optional.ofNullable(BiometricUiEvent.ERROR_EVENT_BY_SOURCE_TYPE.get(biometricSourceType)) + .ifPresent(UI_EVENT_LOGGER::log); cleanup(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index abae4d8eb96e..9bf14e43da03 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -77,7 +77,6 @@ public final class DozeServiceHost implements DozeHost { private final BatteryController mBatteryController; private final ScrimController mScrimController; private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; - private BiometricUnlockController mBiometricUnlockController; private final KeyguardViewMediator mKeyguardViewMediator; private final Lazy<AssistManager> mAssistManagerLazy; private final DozeScrimController mDozeScrimController; @@ -148,9 +147,9 @@ public final class DozeServiceHost implements DozeHost { mNotificationPanel = notificationPanel; mNotificationShadeWindowViewController = notificationShadeWindowViewController; mAmbientIndicationContainer = ambientIndicationContainer; - mBiometricUnlockController = mBiometricUnlockControllerLazy.get(); } + @Override public String toString() { return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]"; @@ -206,11 +205,11 @@ public final class DozeServiceHost implements DozeHost { boolean dozing = mDozingRequested && mStatusBarStateController.getState() == StatusBarState.KEYGUARD - || mBiometricUnlockController.getMode() + || mBiometricUnlockControllerLazy.get().getMode() == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING; // When in wake-and-unlock we may not have received a change to StatusBarState // but we still should not be dozing, manually set to false. - if (mBiometricUnlockController.getMode() + if (mBiometricUnlockControllerLazy.get().getMode() == BiometricUnlockController.MODE_WAKE_AND_UNLOCK) { dozing = false; } @@ -311,7 +310,7 @@ public final class DozeServiceHost implements DozeHost { @Override public boolean isPulsingBlocked() { - return mBiometricUnlockController.getMode() + return mBiometricUnlockControllerLazy.get().getMode() == BiometricUnlockController.MODE_WAKE_AND_UNLOCK; } @@ -323,7 +322,7 @@ public final class DozeServiceHost implements DozeHost { @Override public boolean isBlockingDoze() { - if (mBiometricUnlockController.hasPendingAuthentication()) { + if (mBiometricUnlockControllerLazy.get().hasPendingAuthentication()) { Log.i(StatusBar.TAG, "Blocking AOD because fingerprint has authenticated"); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java index f103bd01fc3f..ba8a63428cf6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java @@ -53,11 +53,13 @@ import android.view.WindowManagerGlobal; import com.android.internal.policy.GestureNavigationSettingsObserver; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.plugins.PluginListener; import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; @@ -74,7 +76,7 @@ import java.util.concurrent.Executor; /** * Utility class to handle edge swipes for back gesture */ -public class EdgeBackGestureHandler implements DisplayListener, +public class EdgeBackGestureHandler extends CurrentUserTracker implements DisplayListener, PluginListener<NavigationEdgeBackPlugin>, ProtoTraceable<SystemUiTraceProto> { private static final String TAG = "EdgeBackGestureHandler"; @@ -165,6 +167,7 @@ public class EdgeBackGestureHandler implements DisplayListener, private boolean mIsGesturalModeEnabled; private boolean mIsEnabled; private boolean mIsNavBarShownTransiently; + private boolean mIsBackGestureAllowed; private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; @@ -200,7 +203,7 @@ public class EdgeBackGestureHandler implements DisplayListener, public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService, SysUiState sysUiFlagContainer, PluginManager pluginManager) { - final Resources res = context.getResources(); + super(Dependency.get(BroadcastDispatcher.class)); mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = context.getMainExecutor(); @@ -216,20 +219,30 @@ public class EdgeBackGestureHandler implements DisplayListener, ViewConfiguration.getLongPressTimeout()); mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver( - mContext.getMainThreadHandler(), mContext, () -> updateCurrentUserResources(res)); + mContext.getMainThreadHandler(), mContext, this::updateCurrentUserResources); - updateCurrentUserResources(res); + updateCurrentUserResources(); sysUiFlagContainer.addCallback(sysUiFlags -> mSysUiFlags = sysUiFlags); } - public void updateCurrentUserResources(Resources res) { + public void updateCurrentUserResources() { + Resources res = Dependency.get(NavigationModeController.class).getCurrentUserContext() + .getResources(); mEdgeWidthLeft = mGestureNavigationSettingsObserver.getLeftSensitivity(res); mEdgeWidthRight = mGestureNavigationSettingsObserver.getRightSensitivity(res); + mIsBackGestureAllowed = + !mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible(); mBottomGestureHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_gesture_height); } + @Override + public void onUserSwitched(int newUserId) { + updateIsEnabled(); + updateCurrentUserResources(); + } + /** * @see NavigationBarView#onAttachedToWindow() */ @@ -243,6 +256,7 @@ public class EdgeBackGestureHandler implements DisplayListener, Settings.Global.getUriFor(FIXED_ROTATION_TRANSFORM_SETTING_NAME), false /* notifyForDescendants */, mFixedRotationObserver, UserHandle.USER_ALL); updateIsEnabled(); + startTracking(); } /** @@ -255,6 +269,7 @@ public class EdgeBackGestureHandler implements DisplayListener, } mContext.getContentResolver().unregisterContentObserver(mFixedRotationObserver); updateIsEnabled(); + stopTracking(); } private void setRotationCallbacks(boolean enable) { @@ -269,10 +284,13 @@ public class EdgeBackGestureHandler implements DisplayListener, } } - public void onNavigationModeChanged(int mode, Context currentUserContext) { + /** + * @see NavigationModeController.ModeChangedListener#onNavigationModeChanged + */ + public void onNavigationModeChanged(int mode) { mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode); updateIsEnabled(); - updateCurrentUserResources(currentUserContext.getResources()); + updateCurrentUserResources(); } public void onNavBarTransientStateChanged(boolean isTransient) { @@ -312,7 +330,7 @@ public class EdgeBackGestureHandler implements DisplayListener, WindowManagerGlobal.getWindowManagerService() .unregisterSystemGestureExclusionListener( mGestureExclusionListener, mDisplayId); - } catch (RemoteException e) { + } catch (RemoteException | IllegalArgumentException e) { Log.e(TAG, "Failed to unregister window manager callbacks", e); } @@ -326,7 +344,7 @@ public class EdgeBackGestureHandler implements DisplayListener, WindowManagerGlobal.getWindowManagerService() .registerSystemGestureExclusionListener( mGestureExclusionListener, mDisplayId); - } catch (RemoteException e) { + } catch (RemoteException | IllegalArgumentException e) { Log.e(TAG, "Failed to register window manager callbacks", e); } @@ -363,6 +381,10 @@ public class EdgeBackGestureHandler implements DisplayListener, updateDisplaySize(); } + public boolean isHandlingGestures() { + return mIsEnabled && mIsBackGestureAllowed; + } + private WindowManager.LayoutParams createLayoutParams() { Resources resources = mContext.getResources(); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( @@ -469,9 +491,9 @@ public class EdgeBackGestureHandler implements DisplayListener, mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset; mLogGesture = false; mInRejectedExclusion = false; - mAllowGesture = !QuickStepContract.isBackGestureDisabled(mSysUiFlags) - && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()) - && !mDisabledForQuickstep; + mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed + && !QuickStepContract.isBackGestureDisabled(mSysUiFlags) + && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()); if (mAllowGesture) { mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge); mEdgeBackPlugin.onMotionEvent(ev); @@ -534,7 +556,8 @@ public class EdgeBackGestureHandler implements DisplayListener, private void updateDisabledForQuickstep() { int rotation = mContext.getResources().getConfiguration().windowConfiguration.getRotation(); - mDisabledForQuickstep = mStartingQuickstepRotation != rotation; + mDisabledForQuickstep = mStartingQuickstepRotation > -1 && + mStartingQuickstepRotation != rotation; } @Override @@ -599,6 +622,7 @@ public class EdgeBackGestureHandler implements DisplayListener, public void dump(PrintWriter pw) { pw.println("EdgeBackGestureHandler:"); pw.println(" mIsEnabled=" + mIsEnabled); + pw.println(" mIsBackGestureAllowed=" + mIsBackGestureAllowed); pw.println(" mAllowGesture=" + mAllowGesture); pw.println(" mDisabledForQuickstep=" + mDisabledForQuickstep); pw.println(" mInRejectedExclusion" + mInRejectedExclusion); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index 90bc075b399d..ae7867d68af4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -85,6 +85,8 @@ import com.android.systemui.statusbar.policy.PreviewInflater; import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory; import com.android.systemui.tuner.TunerService; +import java.util.concurrent.Executor; + /** * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status * text. @@ -553,7 +555,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } }; if (!mKeyguardStateController.canDismissLockScreen()) { - AsyncTask.execute(runnable); + Dependency.get(Executor.class).execute(runnable); } else { boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr) && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 82e02b47974c..39949c82661f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -400,6 +400,9 @@ public class KeyguardBouncer { mExpansionCallback.onFullyHidden(); } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) { mExpansionCallback.onStartingToHide(); + if (mKeyguardView != null) { + mKeyguardView.onStartingToHide(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java index d35e1e1e176a..3e5eb5fba8f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java @@ -33,6 +33,7 @@ import com.android.internal.view.AppearanceRegion; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.plugins.DarkIconDispatcher; +import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.policy.BatteryController; import java.io.FileDescriptor; @@ -58,6 +59,7 @@ public class LightBarController implements BatteryController.BatteryStateChangeC private AppearanceRegion[] mAppearanceRegions = new AppearanceRegion[0]; private int mStatusBarMode; private int mNavigationBarMode; + private int mNavigationMode; private final Color mDarkModeColor; /** @@ -84,11 +86,14 @@ public class LightBarController implements BatteryController.BatteryStateChangeC @Inject public LightBarController(Context ctx, DarkIconDispatcher darkIconDispatcher, - BatteryController batteryController) { + BatteryController batteryController, NavigationModeController navModeController) { mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone)); mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher; mBatteryController = batteryController; mBatteryController.addCallback(this); + mNavigationMode = navModeController.addListener((mode) -> { + mNavigationMode = mode; + }); } public void setNavigationBar(LightBarTransitionsController navigationBar) { @@ -234,7 +239,8 @@ public class LightBarController implements BatteryController.BatteryStateChangeC } private void updateNavigation() { - if (mNavigationBarController != null) { + if (mNavigationBarController != null + && !QuickStepContract.isGesturalMode(mNavigationMode)) { mNavigationBarController.setIconsDark(mNavigationLight, animateChange()); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java index a19d35ac4e81..ec54b302b055 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java @@ -65,7 +65,6 @@ public class LockIcon extends KeyguardAffordanceView { mPredrawRegistered = false; int newState = mState; - mOldState = mState; Drawable icon = getIcon(newState); setImageDrawable(icon, false); @@ -135,6 +134,7 @@ public class LockIcon extends KeyguardAffordanceView { } void update(int newState, boolean pulsing, boolean dozing, boolean keyguardJustShown) { + mOldState = mState; mState = newState; mPulsing = pulsing; mDozing = dozing; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java index a633e1979bad..a2e7306d4931 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java @@ -470,8 +470,10 @@ public class LockscreenLockIconController { } private int getState() { - if ((mKeyguardStateController.canDismissLockScreen() || !mKeyguardShowing - || mKeyguardStateController.isKeyguardGoingAway()) && !mSimLocked) { + if ((mKeyguardStateController.canDismissLockScreen() + || !mKeyguardStateController.isShowing() + || mKeyguardStateController.isKeyguardGoingAway() + || mKeyguardStateController.isKeyguardFadingAway()) && !mSimLocked) { return STATE_LOCK_OPEN; } else if (mTransientBiometricsError) { return STATE_BIOMETRICS_ERROR; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java index 3105155f28e8..b2aa769f1bff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java @@ -28,6 +28,7 @@ import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_NAVIGATION_B import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; +import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_FORCE_OPAQUE; import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; @@ -91,11 +92,13 @@ import android.view.accessibility.AccessibilityManager.AccessibilityServicesStat import androidx.annotation.VisibleForTesting; +import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.LatencyTracker; import com.android.internal.view.AppearanceRegion; import com.android.systemui.R; +import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistHandleViewController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -183,6 +186,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback private WindowManager mWindowManager; private final CommandQueue mCommandQueue; private long mLastLockToAppLongPress; + private final SystemActions mSystemActions; private Locale mLocale; private int mLayoutDirection; @@ -371,6 +375,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback Optional<Recents> recentsOptional, Lazy<StatusBar> statusBarLazy, ShadeController shadeController, NotificationRemoteInputManager notificationRemoteInputManager, + SystemActions systemActions, @Main Handler mainHandler) { mAccessibilityManagerWrapper = accessibilityManagerWrapper; mDeviceProvisionedController = deviceProvisionedController; @@ -389,6 +394,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback mCommandQueue = commandQueue; mDivider = divider; mRecentsOptional = recentsOptional; + mSystemActions = systemActions; mHandler = mainHandler; } @@ -1136,10 +1142,10 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback } private boolean onAccessibilityLongClick(View v) { - Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); + final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra(AccessibilityManager.EXTRA_SHORTCUT_TYPE, - AccessibilityManager.ACCESSIBILITY_BUTTON); + final String chooserClassName = AccessibilityButtonChooserActivity.class.getName(); + intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName); v.getContext().startActivityAsUser(intent, UserHandle.CURRENT); return true; } @@ -1166,6 +1172,16 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback .setFlag(SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE, longClickable) .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isNavBarWindowVisible()) .commitUpdate(mDisplayId); + registerAction(clickable, SystemActions.SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON); + registerAction(longClickable, SystemActions.SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON_CHOOSER); + } + + private void registerAction(boolean register, int actionId) { + if (register) { + mSystemActions.register(actionId); + } else { + mSystemActions.unregister(actionId); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 84aecd4e0759..2978772cac5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -123,7 +123,7 @@ public class NavigationBarView extends FrameLayout implements private KeyButtonDrawable mRecentIcon; private KeyButtonDrawable mDockedIcon; - private final EdgeBackGestureHandler mEdgeBackGestureHandler; + private EdgeBackGestureHandler mEdgeBackGestureHandler; private final DeadZone mDeadZone; private boolean mDeadZoneConsuming = false; private final NavigationBarTransitions mBarTransitions; @@ -244,7 +244,7 @@ public class NavigationBarView extends FrameLayout implements private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> { // When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully // gestural mode, the entire nav bar should be touchable. - if (!isGesturalMode(mNavBarMode) || mImeVisible) { + if (!mEdgeBackGestureHandler.isHandlingGestures() || mImeVisible) { info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); return; } @@ -296,8 +296,6 @@ public class NavigationBarView extends FrameLayout implements R.style.RotateButtonCCWStart90, isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton); - final ContextualButton backButton = new ContextualButton(R.id.back, 0); - mConfiguration = new Configuration(); mTmpLastConfiguration = new Configuration(); mConfiguration.updateFrom(context.getResources().getConfiguration()); @@ -305,7 +303,7 @@ public class NavigationBarView extends FrameLayout implements mScreenPinningNotify = new ScreenPinningNotify(mContext); mBarTransitions = new NavigationBarTransitions(this, Dependency.get(CommandQueue.class)); - mButtonDispatchers.put(R.id.back, backButton); + mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back)); mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home)); mButtonDispatchers.put(R.id.home_handle, new ButtonDispatcher(R.id.home_handle)); mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps)); @@ -659,7 +657,7 @@ public class NavigationBarView extends FrameLayout implements boolean disableHomeHandle = disableRecent && ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); - boolean disableBack = !useAltBack && (isGesturalMode(mNavBarMode) + boolean disableBack = !useAltBack && (mEdgeBackGestureHandler.isHandlingGestures() || ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)); // When screen pinning, don't hide back and home when connected service or back and @@ -686,9 +684,9 @@ public class NavigationBarView extends FrameLayout implements } } - getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); - getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); - getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); + getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); + getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); + getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); getHomeHandle().setVisibility(disableHomeHandle ? View.INVISIBLE : View.VISIBLE); } @@ -838,10 +836,9 @@ public class NavigationBarView extends FrameLayout implements @Override public void onNavigationModeChanged(int mode) { - Context curUserCtx = Dependency.get(NavigationModeController.class).getCurrentUserContext(); mNavBarMode = mode; mBarTransitions.onNavigationModeChanged(mNavBarMode); - mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode, curUserCtx); + mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode); mRecentsOnboarding.onNavigationModeChanged(mNavBarMode); getRotateSuggestionButton().onNavigationModeChanged(mNavBarMode); @@ -864,6 +861,7 @@ public class NavigationBarView extends FrameLayout implements @Override public void onFinishInflate() { + super.onFinishInflate(); mNavigationInflaterView = findViewById(R.id.navigation_inflater); mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java index d24ccf343a3a..6061b1e73d1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java @@ -17,9 +17,6 @@ package com.android.systemui.statusbar.phone; import static android.content.Intent.ACTION_OVERLAY_CHANGED; -import static android.content.Intent.ACTION_PREFERRED_ACTIVITY_CHANGED; -import static android.os.UserHandle.USER_CURRENT; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY; @@ -38,17 +35,14 @@ import android.os.UserHandle; import android.provider.Settings; import android.provider.Settings.Secure; import android.util.Log; -import android.util.SparseBooleanArray; import com.android.systemui.Dumpable; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Arrays; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -70,104 +64,34 @@ public class NavigationModeController implements Dumpable { private final Context mContext; private Context mCurrentUserContext; private final IOverlayManager mOverlayManager; - private final DeviceProvisionedController mDeviceProvisionedController; private final Executor mUiBgExecutor; - private SparseBooleanArray mRestoreGesturalNavBarMode = new SparseBooleanArray(); - private ArrayList<ModeChangedListener> mListeners = new ArrayList<>(); private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - switch (intent.getAction()) { - case ACTION_OVERLAY_CHANGED: if (DEBUG) { Log.d(TAG, "ACTION_OVERLAY_CHANGED"); } updateCurrentInteractionMode(true /* notify */); - break; - } } }; - private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedCallback = - new DeviceProvisionedController.DeviceProvisionedListener() { - @Override - public void onDeviceProvisionedChanged() { - if (DEBUG) { - Log.d(TAG, "onDeviceProvisionedChanged: " - + mDeviceProvisionedController.isDeviceProvisioned()); - } - // Once the device has been provisioned, check if we can restore gestural nav - restoreGesturalNavOverlayIfNecessary(); - } - - @Override - public void onUserSetupChanged() { - if (DEBUG) { - Log.d(TAG, "onUserSetupChanged: " - + mDeviceProvisionedController.isCurrentUserSetup()); - } - // Once the user has been setup, check if we can restore gestural nav - restoreGesturalNavOverlayIfNecessary(); - } - - @Override - public void onUserSwitched() { - if (DEBUG) { - Log.d(TAG, "onUserSwitched: " - + ActivityManagerWrapper.getInstance().getCurrentUserId()); - } - - // Update the nav mode for the current user - updateCurrentInteractionMode(true /* notify */); - - // When switching users, defer enabling the gestural nav overlay until the user - // is all set up - deferGesturalNavOverlayIfNecessary(); - } - }; - @Inject - public NavigationModeController(Context context, - DeviceProvisionedController deviceProvisionedController, - @UiBackground Executor uiBgExecutor) { + public NavigationModeController(Context context, @UiBackground Executor uiBgExecutor) { mContext = context; mCurrentUserContext = context; mOverlayManager = IOverlayManager.Stub.asInterface( ServiceManager.getService(Context.OVERLAY_SERVICE)); mUiBgExecutor = uiBgExecutor; - mDeviceProvisionedController = deviceProvisionedController; - mDeviceProvisionedController.addCallback(mDeviceProvisionedCallback); IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); overlayFilter.addDataScheme("package"); overlayFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, overlayFilter, null, null); - IntentFilter preferredActivityFilter = new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED); - mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, preferredActivityFilter, null, - null); - updateCurrentInteractionMode(false /* notify */); - - // Check if we need to defer enabling gestural nav - deferGesturalNavOverlayIfNecessary(); - } - - private boolean setGestureModeOverlayForMainLauncher() { - if (getCurrentInteractionMode(mCurrentUserContext) == NAV_BAR_MODE_GESTURAL) { - // Already in gesture mode - return true; - } - - Log.d(TAG, "Switching system navigation to full-gesture mode:" - + " contextUser=" - + mCurrentUserContext.getUserId()); - - setModeOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY, USER_CURRENT); - return true; } public void updateCurrentInteractionMode(boolean notify) { @@ -176,10 +100,9 @@ public class NavigationModeController implements Dumpable { if (mode == NAV_BAR_MODE_GESTURAL) { switchToDefaultGestureNavOverlayIfNecessary(); } - mUiBgExecutor.execute(() -> { + mUiBgExecutor.execute(() -> Settings.Secure.putString(mCurrentUserContext.getContentResolver(), - Secure.NAVIGATION_MODE, String.valueOf(mode)); - }); + Secure.NAVIGATION_MODE, String.valueOf(mode))); if (DEBUG) { Log.e(TAG, "updateCurrentInteractionMode: mode=" + mode); dumpAssetPaths(mCurrentUserContext); @@ -230,61 +153,11 @@ public class NavigationModeController implements Dumpable { } } - private void deferGesturalNavOverlayIfNecessary() { - final int userId = mDeviceProvisionedController.getCurrentUser(); - mRestoreGesturalNavBarMode.put(userId, false); - if (mDeviceProvisionedController.isDeviceProvisioned() - && mDeviceProvisionedController.isCurrentUserSetup()) { - // User is already setup and device is provisioned, nothing to do - if (DEBUG) { - Log.d(TAG, "deferGesturalNavOverlayIfNecessary: device is provisioned and user is " - + "setup"); - } - return; - } - - ArrayList<String> defaultOverlays = new ArrayList<>(); - try { - defaultOverlays.addAll(Arrays.asList(mOverlayManager.getDefaultOverlayPackages())); - } catch (RemoteException e) { - Log.e(TAG, "deferGesturalNavOverlayIfNecessary: failed to fetch default overlays"); - } - if (!defaultOverlays.contains(NAV_BAR_MODE_GESTURAL_OVERLAY)) { - // No default gesture nav overlay - if (DEBUG) { - Log.d(TAG, "deferGesturalNavOverlayIfNecessary: no default gestural overlay, " - + "default=" + defaultOverlays); - } - return; - } - - // If the default is gestural, force-enable three button mode until the device is - // provisioned - setModeOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY, USER_CURRENT); - mRestoreGesturalNavBarMode.put(userId, true); - - if (DEBUG) { - Log.d(TAG, "deferGesturalNavOverlayIfNecessary: setting to 3 button mode"); - } - } - - private void restoreGesturalNavOverlayIfNecessary() { - if (DEBUG) { - Log.d(TAG, "restoreGesturalNavOverlayIfNecessary: needs restore=" - + mRestoreGesturalNavBarMode); - } - final int userId = mDeviceProvisionedController.getCurrentUser(); - if (mRestoreGesturalNavBarMode.get(userId)) { - // Restore the gestural state if necessary - setGestureModeOverlayForMainLauncher(); - mRestoreGesturalNavBarMode.put(userId, false); - } - } - private void switchToDefaultGestureNavOverlayIfNecessary() { final int userId = mCurrentUserContext.getUserId(); try { - final IOverlayManager om = mOverlayManager; + final IOverlayManager om = IOverlayManager.Stub.asInterface( + ServiceManager.getService(Context.OVERLAY_SERVICE)); final OverlayInfo info = om.getOverlayInfo(NAV_BAR_MODE_GESTURAL_OVERLAY, userId); if (info != null && !info.isEnabled()) { // Enable the default gesture nav overlay, and move the back gesture inset scale to @@ -309,20 +182,6 @@ public class NavigationModeController implements Dumpable { } } - public void setModeOverlay(String overlayPkg, int userId) { - mUiBgExecutor.execute(() -> { - try { - mOverlayManager.setEnabledExclusiveInCategory(overlayPkg, userId); - if (DEBUG) { - Log.d(TAG, "setModeOverlay: overlayPackage=" + overlayPkg - + " userId=" + userId); - } - } catch (SecurityException | IllegalStateException | RemoteException e) { - Log.e(TAG, "Failed to enable overlay " + overlayPkg + " for user " + userId); - } - }); - } - @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("NavigationModeController:"); @@ -334,11 +193,6 @@ public class NavigationModeController implements Dumpable { defaultOverlays = "failed_to_fetch"; } pw.println(" defaultOverlays=" + defaultOverlays); - pw.println(" restoreGesturalNavMode:"); - for (int i = 0; i < mRestoreGesturalNavBarMode.size(); i++) { - pw.println(" userId=" + mRestoreGesturalNavBarMode.keyAt(i) - + " shouldRestore=" + mRestoreGesturalNavBarMode.valueAt(i)); - } dumpAssetPaths(mCurrentUserContext); } 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 31797d1faa61..c9716d39590e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -1780,7 +1780,13 @@ public class NotificationPanelViewController extends PanelViewController { }); animator.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + notifyExpandingStarted(); + } + + @Override public void onAnimationEnd(Animator animation) { + notifyExpandingFinished(); mNotificationStackScroller.resetCheckSnoozeLeavebehind(); mQsExpansionAnimator = null; if (onFinishRunnable != null) { 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 f7d403f667cb..81dc9e1cf0e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java @@ -157,7 +157,7 @@ public abstract class PanelViewController { protected void onExpandingStarted() { } - private void notifyExpandingStarted() { + protected void notifyExpandingStarted() { if (!mExpanding) { mExpanding = true; onExpandingStarted(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java index 1a6b415f87db..bf52a7ae2bf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java @@ -148,11 +148,6 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, updateSamplingRect(); } - private void postUpdateSamplingListener() { - mHandler.removeCallbacks(mUpdateSamplingListener); - mHandler.post(mUpdateSamplingListener); - } - private void updateSamplingListener() { boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty() 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 900b3ea97010..ac5557b571d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -116,6 +116,9 @@ import android.widget.DateTimeView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.RegisterStatusBarResult; @@ -140,6 +143,7 @@ import com.android.systemui.bubbles.BubbleController; import com.android.systemui.charging.WirelessChargingAnimation; import com.android.systemui.classifier.FalsingLog; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.fragments.ExtensionFragmentListener; import com.android.systemui.fragments.FragmentHostManager; @@ -193,7 +197,6 @@ import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.interruption.BypassHeadsUpNotifier; -import com.android.systemui.statusbar.notification.interruption.NotificationAlertingManager; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -301,6 +304,8 @@ public class StatusBar extends SystemUI implements DemoMode, /** If true, the lockscreen will show a distinct wallpaper */ public static final boolean ENABLE_LOCKSCREEN_WALLPAPER = true; + private static final UiEventLogger sUiEventLogger = new UiEventLoggerImpl(); + static { boolean onlyCoreApps; try { @@ -459,6 +464,44 @@ public class StatusBar extends SystemUI implements DemoMode, } }; + @VisibleForTesting + public enum StatusBarUiEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Secured lockscreen is opened.") + LOCKSCREEN_OPEN_SECURE(405), + + @UiEvent(doc = "Lockscreen without security is opened.") + LOCKSCREEN_OPEN_INSECURE(406), + + @UiEvent(doc = "Secured lockscreen is closed.") + LOCKSCREEN_CLOSE_SECURE(407), + + @UiEvent(doc = "Lockscreen without security is closed.") + LOCKSCREEN_CLOSE_INSECURE(408), + + @UiEvent(doc = "Secured bouncer is opened.") + BOUNCER_OPEN_SECURE(409), + + @UiEvent(doc = "Bouncer without security is opened.") + BOUNCER_OPEN_INSECURE(410), + + @UiEvent(doc = "Secured bouncer is closed.") + BOUNCER_CLOSE_SECURE(411), + + @UiEvent(doc = "Bouncer without security is closed.") + BOUNCER_CLOSE_INSECURE(412); + + private final int mId; + + StatusBarUiEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + protected final H mHandler = createHandler(); private int mInteractingWindows; @@ -468,6 +511,7 @@ public class StatusBar extends SystemUI implements DemoMode, private final ScrimController mScrimController; protected DozeScrimController mDozeScrimController; private final Executor mUiBgExecutor; + private final Executor mMainExecutor; protected boolean mDozing; @@ -623,10 +667,10 @@ public class StatusBar extends SystemUI implements DemoMode, NotificationInterruptStateProvider notificationInterruptStateProvider, NotificationViewHierarchyManager notificationViewHierarchyManager, KeyguardViewMediator keyguardViewMediator, - NotificationAlertingManager notificationAlertingManager, // need to inject for now DisplayMetrics displayMetrics, MetricsLogger metricsLogger, @UiBackground Executor uiBgExecutor, + @Main Executor mainExecutor, NotificationMediaManager notificationMediaManager, NotificationLockscreenUserManager lockScreenUserManager, NotificationRemoteInputManager remoteInputManager, @@ -707,6 +751,7 @@ public class StatusBar extends SystemUI implements DemoMode, mDisplayMetrics = displayMetrics; mMetricsLogger = metricsLogger; mUiBgExecutor = uiBgExecutor; + mMainExecutor = mainExecutor; mMediaManager = notificationMediaManager; mLockscreenUserManager = lockScreenUserManager; mRemoteInputManager = remoteInputManager; @@ -1234,7 +1279,8 @@ public class StatusBar extends SystemUI implements DemoMode, mActivityLaunchAnimator = new ActivityLaunchAnimator( mNotificationShadeWindowViewController, this, mNotificationPanelViewController, mNotificationShadeDepthControllerLazy.get(), - (NotificationListContainer) mStackScroller); + (NotificationListContainer) mStackScroller, + mMainExecutor); // TODO: inject this. mPresenter = new StatusBarNotificationPresenter(mContext, mNotificationPanelViewController, @@ -1255,6 +1301,9 @@ public class StatusBar extends SystemUI implements DemoMode, .setNotificationPanelViewController(mNotificationPanelViewController) .build(); + ((NotificationListContainer) mStackScroller) + .setNotificationActivityStarter(mNotificationActivityStarter); + mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter); mNotificationsController.initialize( @@ -2906,6 +2955,12 @@ public class StatusBar extends SystemUI implements DemoMode, isSecure ? 1 : 0, unlocked ? 1 : 0); mLastLoggedStateFingerprint = stateFingerprint; + + StringBuilder uiEventValueBuilder = new StringBuilder(); + uiEventValueBuilder.append(isBouncerShowing ? "BOUNCER" : "LOCKSCREEN"); + uiEventValueBuilder.append(isShowing ? "_OPEN" : "_CLOSE"); + uiEventValueBuilder.append(isSecure ? "_SECURE" : "_INSECURE"); + sUiEventLogger.log(StatusBarUiEvent.valueOf(uiEventValueBuilder.toString())); } } 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 ed25db40fea6..bc94cdeba37f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -152,6 +152,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb protected boolean mShowing; protected boolean mOccluded; protected boolean mRemoteInputActive; + private boolean mGlobalActionsVisible = false; + private boolean mLastGlobalActionsVisible = false; private boolean mDozing; private boolean mPulsing; private boolean mGesturalNav; @@ -293,6 +295,14 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb updateLockIcon(); } + /** + * Update the global actions visibility state in order to show the navBar when active. + */ + public void setGlobalActionsVisible(boolean isVisible) { + mGlobalActionsVisible = isVisible; + updateStates(); + } + private void updateLockIcon() { // Not all form factors have a lock icon if (mLockIconContainer == null) { @@ -820,6 +830,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mFirstUpdate = false; mLastShowing = showing; + mLastGlobalActionsVisible = mGlobalActionsVisible; mLastOccluded = occluded; mLastBouncerShowing = bouncerShowing; mLastBouncerDismissible = bouncerDismissible; @@ -864,7 +875,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb boolean keyguardWithGestureNav = (keyguardShowing && !mDozing || mPulsing && !mIsDocked) && mGesturalNav; return (!keyguardShowing && !hideWhileDozing || mBouncer.isShowing() - || mRemoteInputActive || keyguardWithGestureNav); + || mRemoteInputActive || keyguardWithGestureNav + || mGlobalActionsVisible); } /** @@ -876,7 +888,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb boolean keyguardWithGestureNav = (keyguardShowing && !mLastDozing || mLastPulsing && !mLastIsDocked) && mLastGesturalNav; return (!keyguardShowing && !hideWhileDozing || mLastBouncerShowing - || mLastRemoteInputActive || keyguardWithGestureNav); + || mLastRemoteInputActive || keyguardWithGestureNav + || mLastGlobalActionsVisible); } public boolean shouldDismissOnMenuPressed() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 53fa2630a9c3..d40b5f9728dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -35,12 +35,12 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.Settings; import android.service.dreams.IDreamManager; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.EventLog; -import android.util.Log; import android.view.RemoteAnimationAdapter; import android.view.View; @@ -91,92 +91,119 @@ import dagger.Lazy; */ public class StatusBarNotificationActivityStarter implements NotificationActivityStarter { - private static final String TAG = "NotifActivityStarter"; - protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Context mContext; + private final CommandQueue mCommandQueue; + private final Handler mMainThreadHandler; + private final Handler mBackgroundHandler; + private final Executor mUiBgExecutor; + + private final NotificationEntryManager mEntryManager; + private final NotifPipeline mNotifPipeline; + private final NotifCollection mNotifCollection; + private final HeadsUpManagerPhone mHeadsUpManager; + private final ActivityStarter mActivityStarter; + private final IStatusBarService mBarService; + private final StatusBarStateController mStatusBarStateController; + private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private final KeyguardManager mKeyguardManager; + private final IDreamManager mDreamManager; + private final BubbleController mBubbleController; private final Lazy<AssistManager> mAssistManagerLazy; - private final NotificationGroupManager mGroupManager; - private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback; private final NotificationRemoteInputManager mRemoteInputManager; + private final NotificationGroupManager mGroupManager; private final NotificationLockscreenUserManager mLockscreenUserManager; private final ShadeController mShadeController; - private final StatusBar mStatusBar; private final KeyguardStateController mKeyguardStateController; - private final ActivityStarter mActivityStarter; - private final NotificationEntryManager mEntryManager; - private final NotifPipeline mNotifPipeline; - private final NotifCollection mNotifCollection; - private final FeatureFlags mFeatureFlags; - private final StatusBarStateController mStatusBarStateController; private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; + private final LockPatternUtils mLockPatternUtils; + private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback; + private final ActivityIntentHelper mActivityIntentHelper; + + private final FeatureFlags mFeatureFlags; private final MetricsLogger mMetricsLogger; - private final Context mContext; - private final NotificationPanelViewController mNotificationPanel; + private final StatusBarNotificationActivityStarterLogger mLogger; + + private final StatusBar mStatusBar; private final NotificationPresenter mPresenter; - private final LockPatternUtils mLockPatternUtils; - private final HeadsUpManagerPhone mHeadsUpManager; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private final KeyguardManager mKeyguardManager; + private final NotificationPanelViewController mNotificationPanel; private final ActivityLaunchAnimator mActivityLaunchAnimator; - private final IStatusBarService mBarService; - private final CommandQueue mCommandQueue; - private final IDreamManager mDreamManager; - private final Handler mMainThreadHandler; - private final Handler mBackgroundHandler; - private final ActivityIntentHelper mActivityIntentHelper; - private final BubbleController mBubbleController; - private final Executor mUiBgExecutor; private boolean mIsCollapsingToShowActivityOverLockscreen; - private StatusBarNotificationActivityStarter(Context context, CommandQueue commandQueue, - Lazy<AssistManager> assistManagerLazy, NotificationPanelViewController panel, - NotificationPresenter presenter, NotificationEntryManager entryManager, - HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter, - ActivityLaunchAnimator activityLaunchAnimator, IStatusBarService statusBarService, + private StatusBarNotificationActivityStarter( + Context context, + CommandQueue commandQueue, + Handler mainThreadHandler, + Handler backgroundHandler, + Executor uiBgExecutor, + NotificationEntryManager entryManager, + NotifPipeline notifPipeline, + NotifCollection notifCollection, + HeadsUpManagerPhone headsUpManager, + ActivityStarter activityStarter, + IStatusBarService statusBarService, StatusBarStateController statusBarStateController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, - IDreamManager dreamManager, NotificationRemoteInputManager remoteInputManager, - StatusBarRemoteInputCallback remoteInputCallback, NotificationGroupManager groupManager, + IDreamManager dreamManager, + BubbleController bubbleController, + Lazy<AssistManager> assistManagerLazy, + NotificationRemoteInputManager remoteInputManager, + NotificationGroupManager groupManager, NotificationLockscreenUserManager lockscreenUserManager, - ShadeController shadeController, StatusBar statusBar, + ShadeController shadeController, KeyguardStateController keyguardStateController, NotificationInterruptStateProvider notificationInterruptStateProvider, - MetricsLogger metricsLogger, LockPatternUtils lockPatternUtils, - Handler mainThreadHandler, Handler backgroundHandler, Executor uiBgExecutor, - ActivityIntentHelper activityIntentHelper, BubbleController bubbleController, - FeatureFlags featureFlags, NotifPipeline notifPipeline, - NotifCollection notifCollection) { + LockPatternUtils lockPatternUtils, + StatusBarRemoteInputCallback remoteInputCallback, + ActivityIntentHelper activityIntentHelper, + + FeatureFlags featureFlags, + MetricsLogger metricsLogger, + StatusBarNotificationActivityStarterLogger logger, + + StatusBar statusBar, + NotificationPresenter presenter, + NotificationPanelViewController panel, + ActivityLaunchAnimator activityLaunchAnimator) { mContext = context; - mNotificationPanel = panel; - mPresenter = presenter; + mCommandQueue = commandQueue; + mMainThreadHandler = mainThreadHandler; + mBackgroundHandler = backgroundHandler; + mUiBgExecutor = uiBgExecutor; + mEntryManager = entryManager; + mNotifPipeline = notifPipeline; + mNotifCollection = notifCollection; mHeadsUpManager = headsUpManager; - mActivityLaunchAnimator = activityLaunchAnimator; + mActivityStarter = activityStarter; mBarService = statusBarService; - mCommandQueue = commandQueue; + mStatusBarStateController = statusBarStateController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; + mBubbleController = bubbleController; + mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; + mGroupManager = groupManager; mLockscreenUserManager = lockscreenUserManager; mShadeController = shadeController; - // TODO: use KeyguardStateController#isOccluded to remove this dependency - mStatusBar = statusBar; mKeyguardStateController = keyguardStateController; - mActivityStarter = activityStarter; - mEntryManager = entryManager; - mStatusBarStateController = statusBarStateController; mNotificationInterruptStateProvider = notificationInterruptStateProvider; - mMetricsLogger = metricsLogger; - mAssistManagerLazy = assistManagerLazy; - mGroupManager = groupManager; mLockPatternUtils = lockPatternUtils; - mBackgroundHandler = backgroundHandler; - mUiBgExecutor = uiBgExecutor; + mStatusBarRemoteInputCallback = remoteInputCallback; + mActivityIntentHelper = activityIntentHelper; + mFeatureFlags = featureFlags; - mNotifPipeline = notifPipeline; - mNotifCollection = notifCollection; + mMetricsLogger = metricsLogger; + mLogger = logger; + + // TODO: use KeyguardStateController#isOccluded to remove this dependency + mStatusBar = statusBar; + mPresenter = presenter; + mNotificationPanel = panel; + mActivityLaunchAnimator = activityLaunchAnimator; + if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @Override @@ -192,11 +219,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } }); } - - mStatusBarRemoteInputCallback = remoteInputCallback; - mMainThreadHandler = mainThreadHandler; - mActivityIntentHelper = activityIntentHelper; - mBubbleController = bubbleController; } /** @@ -207,6 +229,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit */ @Override public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) { + mLogger.logStartingActivityFromClick(sbn.getKey()); + RemoteInputController controller = mRemoteInputManager.getController(); if (controller.isRemoteInputActive(row.getEntry()) && !TextUtils.isEmpty(row.getActiveRemoteInputText())) { @@ -225,7 +249,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit // The only valid case is Bubble notifications. Guard against other cases // entering here. if (intent == null && !isBubble) { - Log.e(TAG, "onNotificationClicked called for non-clickable notification!"); + mLogger.logNonClickableNotification(sbn.getKey()); return; } @@ -258,6 +282,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean isActivityIntent, boolean wasOccluded, boolean showOverLockscreen) { + mLogger.logHandleClickAfterKeyguardDismissed(sbn.getKey()); + // TODO: Some of this code may be able to move to NotificationEntryManager. if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(sbn.getKey())) { // Release the HUN notification to the shade. @@ -304,6 +330,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean isActivityIntent, boolean wasOccluded, NotificationEntry parentToCancelFinal) { + mLogger.logHandleClickAfterPanelCollapsed(sbn.getKey()); + String notificationKey = sbn.getKey(); try { // The intent we are sending is for the application, which @@ -343,9 +371,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit remoteInputText.toString()); } if (isBubble) { + mLogger.logExpandingBubble(notificationKey); expandBubbleStackOnMainThread(notificationKey); } else { - startNotificationIntent(intent, fillInIntent, row, wasOccluded, isActivityIntent); + startNotificationIntent( + intent, fillInIntent, entry, row, wasOccluded, isActivityIntent); } if (isActivityIntent || isBubble) { mAssistManagerLazy.get().hideAssist(); @@ -392,10 +422,16 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } } - private void startNotificationIntent(PendingIntent intent, Intent fillInIntent, - View row, boolean wasOccluded, boolean isActivityIntent) { + private void startNotificationIntent( + PendingIntent intent, + Intent fillInIntent, + NotificationEntry entry, + View row, + boolean wasOccluded, + boolean isActivityIntent) { RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(row, wasOccluded); + mLogger.logStartNotificationIntent(entry.getKey(), intent); try { if (adapter != null) { ActivityTaskManager.getService() @@ -408,7 +444,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } catch (RemoteException | PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. // Just log the exception message. - Log.w(TAG, "Sending contentIntent failed: " + e); + mLogger.logSendingIntentFailed(e); // TODO: Dismiss Keyguard. } } @@ -435,16 +471,35 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit }, null, false /* afterKeyguardGone */); } + @Override + public void startHistoryIntent(boolean showHistory) { + mActivityStarter.dismissKeyguardThenExecute(() -> { + AsyncTask.execute(() -> { + Intent intent = showHistory ? new Intent( + Settings.ACTION_NOTIFICATION_HISTORY) : new Intent( + Settings.ACTION_NOTIFICATION_SETTINGS); + TaskStackBuilder tsb = TaskStackBuilder.create(mContext) + .addNextIntent(new Intent(Settings.ACTION_NOTIFICATION_SETTINGS)); + if (showHistory) { + tsb.addNextIntent(intent); + } + tsb.startActivities(); + if (shouldCollapse()) { + // Putting it back on the main thread, since we're touching views + mMainThreadHandler.post(() -> mCommandQueue.animateCollapsePanels( + CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */)); + } + }); + return true; + }, null, false /* afterKeyguardGone */); + } + private void handleFullScreenIntent(NotificationEntry entry) { if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) { if (shouldSuppressFullScreenIntent(entry)) { - if (DEBUG) { - Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + entry.getKey()); - } + mLogger.logFullScreenIntentSuppressedByDnD(entry.getKey()); } else if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) { - if (DEBUG) { - Log.d(TAG, "No Fullscreen intent: not important enough: " + entry.getKey()); - } + mLogger.logFullScreenIntentNotImportantEnough(entry.getKey()); } else { // Stop screensaver if the notification has a fullscreen intent. // (like an incoming phone call) @@ -457,13 +512,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit }); // not immersive & a fullscreen alert should be shown - if (DEBUG) { - Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); - } + final PendingIntent fullscreenIntent = + entry.getSbn().getNotification().fullScreenIntent; + mLogger.logSendingFullScreenIntent(entry.getKey(), fullscreenIntent); try { EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, entry.getKey()); - entry.getSbn().getNotification().fullScreenIntent.send(); + fullscreenIntent.send(); entry.notifyFullScreenIntentLaunched(); mMetricsLogger.count("note_fullscreen", 1); } catch (PendingIntent.CanceledException e) { @@ -578,9 +633,10 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit public static class Builder { private final Context mContext; private final CommandQueue mCommandQueue; - private final Lazy<AssistManager> mAssistManagerLazy; + private final Handler mMainThreadHandler; + private final Handler mBackgroundHandler; + private final Executor mUiBgExecutor; private final NotificationEntryManager mEntryManager; - private final FeatureFlags mFeatureFlags; private final NotifPipeline mNotifPipeline; private final NotifCollection mNotifCollection; private final HeadsUpManagerPhone mHeadsUpManager; @@ -590,30 +646,37 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final KeyguardManager mKeyguardManager; private final IDreamManager mDreamManager; + private final BubbleController mBubbleController; + private final Lazy<AssistManager> mAssistManagerLazy; private final NotificationRemoteInputManager mRemoteInputManager; - private final StatusBarRemoteInputCallback mRemoteInputCallback; private final NotificationGroupManager mGroupManager; private final NotificationLockscreenUserManager mLockscreenUserManager; + private final ShadeController mShadeController; private final KeyguardStateController mKeyguardStateController; - private final MetricsLogger mMetricsLogger; + private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final LockPatternUtils mLockPatternUtils; - private final Handler mMainThreadHandler; - private final Handler mBackgroundHandler; - private final Executor mUiBgExecutor; + private final StatusBarRemoteInputCallback mRemoteInputCallback; private final ActivityIntentHelper mActivityIntentHelper; - private final BubbleController mBubbleController; - private NotificationPanelViewController mNotificationPanelViewController; - private NotificationInterruptStateProvider mNotificationInterruptStateProvider; - private final ShadeController mShadeController; + + private final FeatureFlags mFeatureFlags; + private final MetricsLogger mMetricsLogger; + private final StatusBarNotificationActivityStarterLogger mLogger; + + private StatusBar mStatusBar; private NotificationPresenter mNotificationPresenter; + private NotificationPanelViewController mNotificationPanelViewController; private ActivityLaunchAnimator mActivityLaunchAnimator; - private StatusBar mStatusBar; @Inject - public Builder(Context context, + public Builder( + Context context, CommandQueue commandQueue, - Lazy<AssistManager> assistManagerLazy, + @Main Handler mainThreadHandler, + @Background Handler backgroundHandler, + @UiBackground Executor uiBgExecutor, NotificationEntryManager entryManager, + NotifPipeline notifPipeline, + NotifCollection notifCollection, HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter, IStatusBarService statusBarService, @@ -621,27 +684,30 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, IDreamManager dreamManager, + BubbleController bubbleController, + Lazy<AssistManager> assistManagerLazy, NotificationRemoteInputManager remoteInputManager, - StatusBarRemoteInputCallback remoteInputCallback, NotificationGroupManager groupManager, NotificationLockscreenUserManager lockscreenUserManager, + ShadeController shadeController, KeyguardStateController keyguardStateController, NotificationInterruptStateProvider notificationInterruptStateProvider, - MetricsLogger metricsLogger, LockPatternUtils lockPatternUtils, - @Main Handler mainThreadHandler, - @Background Handler backgroundHandler, - @UiBackground Executor uiBgExecutor, + StatusBarRemoteInputCallback remoteInputCallback, ActivityIntentHelper activityIntentHelper, - BubbleController bubbleController, - ShadeController shadeController, + FeatureFlags featureFlags, - NotifPipeline notifPipeline, - NotifCollection notifCollection) { + MetricsLogger metricsLogger, + StatusBarNotificationActivityStarterLogger logger) { + mContext = context; mCommandQueue = commandQueue; - mAssistManagerLazy = assistManagerLazy; + mMainThreadHandler = mainThreadHandler; + mBackgroundHandler = backgroundHandler; + mUiBgExecutor = uiBgExecutor; mEntryManager = entryManager; + mNotifPipeline = notifPipeline; + mNotifCollection = notifCollection; mHeadsUpManager = headsUpManager; mActivityStarter = activityStarter; mStatusBarService = statusBarService; @@ -649,23 +715,21 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; + mBubbleController = bubbleController; + mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; - mRemoteInputCallback = remoteInputCallback; mGroupManager = groupManager; mLockscreenUserManager = lockscreenUserManager; + mShadeController = shadeController; mKeyguardStateController = keyguardStateController; mNotificationInterruptStateProvider = notificationInterruptStateProvider; - mMetricsLogger = metricsLogger; mLockPatternUtils = lockPatternUtils; - mMainThreadHandler = mainThreadHandler; - mBackgroundHandler = backgroundHandler; - mUiBgExecutor = uiBgExecutor; + mRemoteInputCallback = remoteInputCallback; mActivityIntentHelper = activityIntentHelper; - mBubbleController = bubbleController; - mShadeController = shadeController; + mFeatureFlags = featureFlags; - mNotifPipeline = notifPipeline; - mNotifCollection = notifCollection; + mMetricsLogger = metricsLogger; + mLogger = logger; } /** Sets the status bar to use as {@link StatusBar}. */ @@ -692,37 +756,42 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } public StatusBarNotificationActivityStarter build() { - return new StatusBarNotificationActivityStarter(mContext, - mCommandQueue, mAssistManagerLazy, - mNotificationPanelViewController, - mNotificationPresenter, + return new StatusBarNotificationActivityStarter( + mContext, + mCommandQueue, + mMainThreadHandler, + mBackgroundHandler, + mUiBgExecutor, mEntryManager, + mNotifPipeline, + mNotifCollection, mHeadsUpManager, mActivityStarter, - mActivityLaunchAnimator, mStatusBarService, mStatusBarStateController, mStatusBarKeyguardViewManager, mKeyguardManager, mDreamManager, + mBubbleController, + mAssistManagerLazy, mRemoteInputManager, - mRemoteInputCallback, mGroupManager, mLockscreenUserManager, mShadeController, - mStatusBar, mKeyguardStateController, mNotificationInterruptStateProvider, - mMetricsLogger, mLockPatternUtils, - mMainThreadHandler, - mBackgroundHandler, - mUiBgExecutor, + mRemoteInputCallback, mActivityIntentHelper, - mBubbleController, + mFeatureFlags, - mNotifPipeline, - mNotifCollection); + mMetricsLogger, + mLogger, + + mStatusBar, + mNotificationPresenter, + mNotificationPanelViewController, + mActivityLaunchAnimator); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt new file mode 100644 index 000000000000..d118747a0365 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.app.PendingIntent +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel.DEBUG +import com.android.systemui.log.LogLevel.ERROR +import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.log.dagger.NotifInteractionLog +import javax.inject.Inject + +class StatusBarNotificationActivityStarterLogger @Inject constructor( + @NotifInteractionLog private val buffer: LogBuffer +) { + fun logStartingActivityFromClick(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "(1/4) onNotificationClicked: $str1" + }) + } + + fun logHandleClickAfterKeyguardDismissed(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "(2/4) handleNotificationClickAfterKeyguardDismissed: $str1" + }) + } + + fun logHandleClickAfterPanelCollapsed(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "(3/4) handleNotificationClickAfterPanelCollapsed: $str1" + }) + } + + fun logStartNotificationIntent(key: String, pendingIntent: PendingIntent) { + buffer.log(TAG, INFO, { + str1 = key + str2 = pendingIntent.intent.toString() + }, { + "(4/4) Starting $str2 for notification $str1" + }) + } + + fun logExpandingBubble(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "Expanding bubble for $str1 (rather than firing intent)" + }) + } + + fun logSendingIntentFailed(e: Exception) { + buffer.log(TAG, WARNING, { + str1 = e.toString() + }, { + "Sending contentIntentFailed: $str1" + }) + } + + fun logNonClickableNotification(key: String) { + buffer.log(TAG, ERROR, { + str1 = key + }, { + "onNotificationClicked called for non-clickable notification! $str1" + }) + } + + fun logFullScreenIntentSuppressedByDnD(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "No Fullscreen intent: suppressed by DND: $str1" + }) + } + + fun logFullScreenIntentNotImportantEnough(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "No Fullscreen intent: not important enough: $str1" + }) + } + + fun logSendingFullScreenIntent(key: String, pendingIntent: PendingIntent) { + buffer.log(TAG, INFO, { + str1 = key + str2 = pendingIntent.intent.toString() + }, { + "Notification $str1 has fullScreenIntent; sending fullScreenIntent $str2" + }) + } +} + +private const val TAG = "NotifActivityStarter" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 6193a8e9005b..428de9e9adbb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -30,6 +30,7 @@ import android.content.IntentSender; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; +import android.util.Log; import android.view.View; import android.view.ViewParent; @@ -47,6 +48,8 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.concurrent.atomic.AtomicReference; + import javax.inject.Inject; import javax.inject.Singleton; @@ -54,7 +57,8 @@ import javax.inject.Singleton; */ @Singleton public class StatusBarRemoteInputCallback implements Callback, Callbacks, - StatusBarStateController.StateListener { + StatusBarStateController.StateListener, KeyguardStateController.Callback { + private static final String TAG = StatusBarRemoteInputCallback.class.getSimpleName(); private final KeyguardStateController mKeyguardStateController; private final SysuiStatusBarStateController mStatusBarStateController; @@ -72,6 +76,7 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, private int mDisabled2; protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver(); private Handler mMainHandler = new Handler(); + private final AtomicReference<Intent> mPendingConfirmCredentialIntent = new AtomicReference(); /** */ @@ -98,6 +103,9 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, mCommandQueue.addCallback(this); mActivityIntentHelper = new ActivityIntentHelper(mContext); mGroupManager = groupManager; + // Listen to onKeyguardShowingChanged in case a managed profile needs to be unlocked + // once the primary profile's keyguard is no longer shown. + mKeyguardStateController.addCallback(this); } @Override @@ -201,12 +209,39 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, // Clear pending remote view, as we do not want to trigger pending remote input view when // it's called by other code mPendingWorkRemoteInputView = null; - // Begin old BaseStatusBar.startWorkChallengeIfNecessary. + + final Intent newIntent = createConfirmDeviceCredentialIntent( + userId, intendSender, notificationKey); + if (newIntent == null) { + Log.w(TAG, String.format("Cannot create intent to unlock user %d", userId)); + return false; + } + + mPendingConfirmCredentialIntent.set(newIntent); + + // If the Keyguard is currently showing, starting the ConfirmDeviceCredentialActivity + // would cause it to pause, not letting the user actually unlock the managed profile. + // Instead, wait until we receive a callback indicating it is no longer showing and + // then start the pending intent. + if (mKeyguardStateController.isShowing()) { + // Do nothing, since the callback will get the pending intent and start it. + Log.w(TAG, String.format("Keyguard is showing, waiting until it's not")); + } else { + startPendingConfirmDeviceCredentialIntent(); + } + + return true; + } + + private Intent createConfirmDeviceCredentialIntent( + int userId, IntentSender intendSender, String notificationKey) { final Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null, userId); + if (newIntent == null) { - return false; + return null; } + final Intent callBackIntent = new Intent(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION); callBackIntent.putExtra(Intent.EXTRA_INTENT, intendSender); callBackIntent.putExtra(Intent.EXTRA_INDEX, notificationKey); @@ -222,14 +257,40 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, newIntent.putExtra( Intent.EXTRA_INTENT, callBackPendingIntent.getIntentSender()); + + return newIntent; + } + + private void startPendingConfirmDeviceCredentialIntent() { + final Intent pendingIntent = mPendingConfirmCredentialIntent.getAndSet(null); + if (pendingIntent == null) { + return; + } + try { - ActivityManager.getService().startConfirmDeviceCredentialIntent(newIntent, + if (mKeyguardStateController.isShowing()) { + Log.w(TAG, "Keyguard is showing while starting confirm device credential intent."); + } + ActivityManager.getService().startConfirmDeviceCredentialIntent(pendingIntent, null /*options*/); } catch (RemoteException ex) { // ignore } - return true; - // End old BaseStatusBar.startWorkChallengeIfNecessary. + } + + @Override + public void onKeyguardShowingChanged() { + if (mKeyguardStateController.isShowing()) { + // In order to avoid jarring UX where/ the managed profile challenge is shown and + // immediately dismissed, do not attempt to start the confirm device credential + // activity if the keyguard is still showing. + if (mPendingConfirmCredentialIntent.get() != null) { + Log.w(TAG, "There's a pending unlock intent but keyguard is still showing, abort."); + } + return; + } + + startPendingConfirmDeviceCredentialIntent(); } @Override 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 b81a5198b498..62a3cf040d7e 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 @@ -33,6 +33,7 @@ import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -62,7 +63,6 @@ import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.interruption.BypassHeadsUpNotifier; -import com.android.systemui.statusbar.notification.interruption.NotificationAlertingManager; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -143,10 +143,10 @@ public interface StatusBarPhoneModule { NotificationInterruptStateProvider notificationInterruptStateProvider, NotificationViewHierarchyManager notificationViewHierarchyManager, KeyguardViewMediator keyguardViewMediator, - NotificationAlertingManager notificationAlertingManager, DisplayMetrics displayMetrics, MetricsLogger metricsLogger, @UiBackground Executor uiBgExecutor, + @Main Executor mainExecutor, NotificationMediaManager notificationMediaManager, NotificationLockscreenUserManager lockScreenUserManager, NotificationRemoteInputManager remoteInputManager, @@ -223,10 +223,10 @@ public interface StatusBarPhoneModule { notificationInterruptStateProvider, notificationViewHierarchyManager, keyguardViewMediator, - notificationAlertingManager, displayMetrics, metricsLogger, uiBgExecutor, + mainExecutor, notificationMediaManager, lockScreenUserManager, remoteInputManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index 17cd98ff7f2c..e65b6fe7c3f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -197,8 +197,13 @@ public class MobileSignalController extends SignalController< TelephonyIcons.THREE_G); mNetworkToIconLookup.put(toIconKey(TelephonyManager.NETWORK_TYPE_EHRPD), TelephonyIcons.THREE_G); - mNetworkToIconLookup.put(toIconKey(TelephonyManager.NETWORK_TYPE_UMTS), + if (mConfig.show4gFor3g) { + mNetworkToIconLookup.put(toIconKey(TelephonyManager.NETWORK_TYPE_UMTS), + TelephonyIcons.FOUR_G); + } else { + mNetworkToIconLookup.put(toIconKey(TelephonyManager.NETWORK_TYPE_UMTS), TelephonyIcons.THREE_G); + } mNetworkToIconLookup.put(toIconKey(TelephonyManager.NETWORK_TYPE_TD_SCDMA), TelephonyIcons.THREE_G); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 6e5f8a0ae5e9..a284335c972e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -357,7 +357,18 @@ public class NetworkControllerImpl extends BroadcastReceiver mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mReceiverHandler); mListening = true; + // Initial setup of connectivity. Handled as if we had received a sticky broadcast of + // ConnectivityManager.CONNECTIVITY_ACTION or ConnectivityManager.INET_CONDITION_ACTION. + mReceiverHandler.post(this::updateConnectivity); + + // Initial setup of WifiSignalController. Handled as if we had received a sticky broadcast + // of WifiManager.WIFI_STATE_CHANGED_ACTION or WifiManager.NETWORK_STATE_CHANGED_ACTION + mReceiverHandler.post(mWifiSignalController::fetchInitialState); updateMobileControllers(); + + // Initial setup of emergency information. Handled as if we had received a sticky broadcast + // of TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED. + mReceiverHandler.post(this::recalculateEmergency); } private void unregisterListeners() { @@ -367,7 +378,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mobileSignalController.unregisterListener(); } mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener); - mContext.unregisterReceiver(this); + mBroadcastDispatcher.unregisterReceiver(this); } public int getConnectedWifiLevel() { @@ -859,6 +870,7 @@ public class NetworkControllerImpl extends BroadcastReceiver pw.println(" - telephony ------"); pw.print(" hasVoiceCallingFeature()="); pw.println(hasVoiceCallingFeature()); + pw.println(" mListening=" + mListening); pw.println(" - connectivity ------"); pw.print(" mConnectedTransports="); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 408b3a619ff1..53ac65700a05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -57,7 +57,6 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; -import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputContentInfoCompat; @@ -656,9 +655,10 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - String[] allowedDataTypes = mRemoteInputView.mRemoteInput.getAllowedDataTypes() - .toArray(new String[0]); - EditorInfoCompat.setContentMimeTypes(outAttrs, allowedDataTypes); + // TODO: Pass RemoteInput data types to allow image insertion. + // String[] allowedDataTypes = mRemoteInputView.mRemoteInput.getAllowedDataTypes() + // .toArray(new String[0]); + // EditorInfoCompat.setContentMimeTypes(outAttrs, allowedDataTypes); final InputConnection inputConnection = super.onCreateInputConnection(outAttrs); final InputConnectionCompat.OnCommitContentListener callback = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index bb0b5e00ff67..412962cc797a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -48,6 +48,7 @@ import android.view.ViewGroup; import android.widget.BaseAdapter; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.UserIcons; import com.android.settingslib.RestrictedLockUtilsInternal; @@ -61,6 +62,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; +import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.tiles.UserDetailView; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -108,13 +110,15 @@ public class UserSwitcherController implements Dumpable { private int mSecondaryUser = UserHandle.USER_NULL; private Intent mSecondaryUserServiceIntent; private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2); + private final UiEventLogger mUiEventLogger; @Inject public UserSwitcherController(Context context, KeyguardStateController keyguardStateController, @Main Handler handler, ActivityStarter activityStarter, - BroadcastDispatcher broadcastDispatcher) { + BroadcastDispatcher broadcastDispatcher, UiEventLogger uiEventLogger) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; + mUiEventLogger = uiEventLogger; if (!UserManager.isGuestUserEphemeral()) { mGuestResumeSessionReceiver.register(mBroadcastDispatcher); } @@ -801,7 +805,7 @@ public class UserSwitcherController implements Dumpable { UserDetailView v; if (!(convertView instanceof UserDetailView)) { v = UserDetailView.inflate(context, parent, false); - v.createAndSetAdapter(UserSwitcherController.this); + v.createAndSetAdapter(UserSwitcherController.this, mUiEventLogger); } else { v = (UserDetailView) convertView; } @@ -827,6 +831,21 @@ public class UserSwitcherController implements Dumpable { public int getMetricsCategory() { return MetricsEvent.QS_USERDETAIL; } + + @Override + public UiEventLogger.UiEventEnum openDetailEvent() { + return QSUserSwitcherEvent.QS_USER_DETAIL_OPEN; + } + + @Override + public UiEventLogger.UiEventEnum closeDetailEvent() { + return QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE; + } + + @Override + public UiEventLogger.UiEventEnum moreSettingsEvent() { + return QSUserSwitcherEvent.QS_USER_MORE_SETTINGS; + } }; private final KeyguardStateController.Callback mCallback = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java index b258fd47871a..5257ce4c6bd9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java @@ -102,6 +102,20 @@ public class WifiSignalController extends } /** + * Fetches wifi initial state replacing the initial sticky broadcast. + */ + public void fetchInitialState() { + mWifiTracker.fetchInitialState(); + mCurrentState.enabled = mWifiTracker.enabled; + mCurrentState.connected = mWifiTracker.connected; + mCurrentState.ssid = mWifiTracker.ssid; + mCurrentState.rssi = mWifiTracker.rssi; + mCurrentState.level = mWifiTracker.level; + mCurrentState.statusLabel = mWifiTracker.statusLabel; + notifyListenersIfNecessary(); + } + + /** * Extract wifi state directly from broadcasts about changes in wifi state. */ public void handleBroadcast(Intent intent) { diff --git a/packages/SystemUI/src/com/android/systemui/util/FloatingContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/util/FloatingContentCoordinator.kt index ca4b67db0d46..242f7cde9d3b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/FloatingContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/util/FloatingContentCoordinator.kt @@ -187,16 +187,23 @@ class FloatingContentCoordinator @Inject constructor() { // Tell that content to get out of the way, and save the bounds it says it's moving // (or animating) to. .forEach { (content, bounds) -> - content.moveToBounds( - content.calculateNewBoundsOnOverlap( - conflictingNewBounds, - // Pass all of the content bounds except the bounds of the - // content we're asking to move, and the conflicting new bounds - // (since those are passed separately). - otherContentBounds = allContentBounds.values - .minus(bounds) - .minus(conflictingNewBounds))) - allContentBounds[content] = content.getFloatingBoundsOnScreen() + val newBounds = content.calculateNewBoundsOnOverlap( + conflictingNewBounds, + // Pass all of the content bounds except the bounds of the + // content we're asking to move, and the conflicting new bounds + // (since those are passed separately). + otherContentBounds = allContentBounds.values + .minus(bounds) + .minus(conflictingNewBounds)) + + // If the new bounds are empty, it means there's no non-overlapping position + // that is in bounds. Just leave the content where it is. This should normally + // not happen, but sometimes content like PIP reports incorrect bounds + // temporarily. + if (!newBounds.isEmpty) { + content.moveToBounds(newBounds) + allContentBounds[content] = content.getFloatingBoundsOnScreen() + } } currentlyResolvingConflicts = false @@ -229,8 +236,8 @@ class FloatingContentCoordinator @Inject constructor() { * @param allowedBounds The area within which we're allowed to find new bounds for the * content. * @return New bounds for the content that don't intersect the exclusion rects or the - * newly overlapping rect, and that is within bounds unless no possible in-bounds position - * exists. + * newly overlapping rect, and that is within bounds - or an empty Rect if no in-bounds + * position exists. */ @JvmStatic fun findAreaForContentVertically( @@ -274,7 +281,13 @@ class FloatingContentCoordinator @Inject constructor() { !overlappingContentPushingDown && !positionAboveInBounds // Return the content rect, but offset to reflect the new position. - return if (usePositionBelow) newContentBoundsBelow else newContentBoundsAbove + val newBounds = if (usePositionBelow) newContentBoundsBelow else newContentBoundsAbove + + // If the new bounds are within the allowed bounds, return them. If not, it means that + // there are no legal new bounds. This can happen if the new content's bounds are too + // large (for example, full-screen PIP). Since there is no reasonable action to take + // here, return an empty Rect and we will just not move the content. + return if (allowedBounds.contains(newBounds)) newBounds else Rect() } /** diff --git a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt index d65b285adb0c..8880df9959c1 100644 --- a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt +++ b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt @@ -115,7 +115,9 @@ abstract class RelativeTouchListener : View.OnTouchListener { performedLongClick = false handler.postDelayed({ - performedLongClick = v.performLongClick() + if (v.isLongClickable) { + performedLongClick = v.performLongClick() + } }, ViewConfiguration.getLongPressTimeout().toLong()) } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt index 8625d63a3c7e..db08d64acc10 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt @@ -61,14 +61,17 @@ internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>() /** * Default spring configuration to use for animations where stiffness and/or damping ratio - * were not provided. + * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. */ -private val defaultSpring = PhysicsAnimator.SpringConfig( +private val globalDefaultSpring = PhysicsAnimator.SpringConfig( SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) -/** Default fling configuration to use for animations where friction was not provided. */ -private val defaultFling = PhysicsAnimator.FlingConfig( +/** + * Default fling configuration to use for animations where friction was not provided, and a default + * fling config was not set via [PhysicsAnimator.setDefaultFlingConfig]. + */ +private val globalDefaultFling = PhysicsAnimator.FlingConfig( friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE) /** Whether to log helpful debug information about animations. */ @@ -111,6 +114,12 @@ class PhysicsAnimator<T> private constructor (val target: T) { /** End actions to run when all animations have completed. */ private val endActions = ArrayList<EndAction>() + /** SpringConfig to use by default for properties whose springs were not provided. */ + private var defaultSpring: SpringConfig = globalDefaultSpring + + /** FlingConfig to use by default for properties whose fling configs were not provided. */ + private var defaultFling: FlingConfig = globalDefaultFling + /** * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add @@ -204,6 +213,19 @@ class PhysicsAnimator<T> private constructor (val target: T) { } /** + * Springs a property to a given value using the provided configuration options, and a start + * velocity of 0f. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float + ): PhysicsAnimator<T> { + return spring(property, toPosition, 0f) + } + + /** * Flings a property using the given start velocity, using a [FlingAnimation] configured using * the provided configuration settings. * @@ -392,6 +414,14 @@ class PhysicsAnimator<T> private constructor (val target: T) { return this } + fun setDefaultSpringConfig(defaultSpring: SpringConfig) { + this.defaultSpring = defaultSpring + } + + fun setDefaultFlingConfig(defaultFling: FlingConfig) { + this.defaultFling = defaultFling + } + /** Starts the animations! */ fun start() { startAction() @@ -752,7 +782,7 @@ class PhysicsAnimator<T> private constructor (val target: T) { ) { constructor() : - this(defaultSpring.stiffness, defaultSpring.dampingRatio) + this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio) constructor(stiffness: Float, dampingRatio: Float) : this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f) @@ -782,10 +812,10 @@ class PhysicsAnimator<T> private constructor (val target: T) { internal var startVelocity: Float ) { - constructor() : this(defaultFling.friction) + constructor() : this(globalDefaultFling.friction) constructor(friction: Float) : - this(friction, defaultFling.min, defaultFling.max) + this(friction, globalDefaultFling.min, globalDefaultFling.max) constructor(friction: Float, min: Float, max: Float) : this(friction, min, max, startVelocity = 0f) diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java index cc6d607a60cf..8acfbf2b6996 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java @@ -137,6 +137,36 @@ public abstract class ConcurrencyModule { } /** + * Provide a Background-Thread Executor by default. + */ + @Provides + @Singleton + public static RepeatableExecutor provideRepeatableExecutor(@Background DelayableExecutor exec) { + return new RepeatableExecutorImpl(exec); + } + + /** + * Provide a Background-Thread Executor. + */ + @Provides + @Singleton + @Background + public static RepeatableExecutor provideBackgroundRepeatableExecutor( + @Background DelayableExecutor exec) { + return new RepeatableExecutorImpl(exec); + } + + /** + * Provide a Main-Thread Executor. + */ + @Provides + @Singleton + @Main + public static RepeatableExecutor provideMainRepeatableExecutor(@Main DelayableExecutor exec) { + return new RepeatableExecutorImpl(exec); + } + + /** * Provide an Executor specifically for running UI operations on a separate thread. * * Keep submitted runnables short and to the point, just as with any other UI code. diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java new file mode 100644 index 000000000000..aefdc992e831 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * A sub-class of {@link Executor} that allows scheduling commands to execute periodically. + */ +public interface RepeatableExecutor extends Executor { + + /** + * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with + * the given delay between the termination of one execution and the commencement of the next. + * + * Each invocation of the supplied Runnable will be scheduled after the previous invocation + * completes. For example, if you schedule the Runnable with a 60 second delay, and the Runnable + * itself takes 1 second, the effective delay will be 61 seconds between each invocation. + * + * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable, + * long, long)} + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + default Runnable executeRepeatedly(Runnable r, long initialDelayMillis, long delayMillis) { + return executeRepeatedly(r, initialDelayMillis, delayMillis, TimeUnit.MILLISECONDS); + } + + /** + * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with + * the given delay between the termination of one execution and the commencement of the next.. + * + * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable, + * long, long)} + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + Runnable executeRepeatedly(Runnable r, long initialDelay, long delay, TimeUnit unit); +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java new file mode 100644 index 000000000000..c03e10e5c981 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import java.util.concurrent.TimeUnit; + +/** + * Implementation of {@link RepeatableExecutor} for SystemUI. + */ +class RepeatableExecutorImpl implements RepeatableExecutor { + + private final DelayableExecutor mExecutor; + + RepeatableExecutorImpl(DelayableExecutor executor) { + mExecutor = executor; + } + + @Override + public void execute(Runnable command) { + mExecutor.execute(command); + } + + @Override + public Runnable executeRepeatedly(Runnable r, long initDelay, long delay, TimeUnit unit) { + ExecutionToken token = new ExecutionToken(r, delay, unit); + token.start(initDelay, unit); + return token::cancel; + } + + private class ExecutionToken implements Runnable { + private final Runnable mCommand; + private final long mDelay; + private final TimeUnit mUnit; + private final Object mLock = new Object(); + private Runnable mCancel; + + ExecutionToken(Runnable r, long delay, TimeUnit unit) { + mCommand = r; + mDelay = delay; + mUnit = unit; + } + + @Override + public void run() { + mCommand.run(); + synchronized (mLock) { + if (mCancel != null) { + mCancel = mExecutor.executeDelayed(this, mDelay, mUnit); + } + } + } + + /** Starts execution that will repeat the command until {@link cancel}. */ + public void start(long startDelay, TimeUnit unit) { + synchronized (mLock) { + mCancel = mExecutor.executeDelayed(this, startDelay, unit); + } + } + + /** Cancel repeated execution of command. */ + public void cancel() { + synchronized (mLock) { + if (mCancel != null) { + mCancel.run(); + mCancel = null; + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/magnetictarget/MagnetizedObject.kt b/packages/SystemUI/src/com/android/systemui/util/magnetictarget/MagnetizedObject.kt index f27bdbfbeda0..e905e6772074 100644 --- a/packages/SystemUI/src/com/android/systemui/util/magnetictarget/MagnetizedObject.kt +++ b/packages/SystemUI/src/com/android/systemui/util/magnetictarget/MagnetizedObject.kt @@ -27,6 +27,7 @@ import android.provider.Settings import android.view.MotionEvent import android.view.VelocityTracker import android.view.View +import android.view.ViewConfiguration import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce @@ -146,6 +147,10 @@ abstract class MagnetizedObject<T : Any>( private val velocityTracker: VelocityTracker = VelocityTracker.obtain() private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private var touchDown = PointF() + private var touchSlop = 0 + private var movedBeyondSlop = false + /** Whether touch events are presently occurring within the magnetic field area of a target. */ val objectStuckToTarget: Boolean get() = targetObjectIsStuckTo != null @@ -324,15 +329,32 @@ abstract class MagnetizedObject<T : Any>( // When a gesture begins, recalculate target views' positions on the screen in case they // have changed. Also, clear state. if (ev.action == MotionEvent.ACTION_DOWN) { - updateTargetViewLocations() + updateTargetViews() - // Clear the velocity tracker and assume we're not stuck to a target yet. + // Clear the velocity tracker and stuck target. velocityTracker.clear() targetObjectIsStuckTo = null + + // Set the touch down coordinates and reset movedBeyondSlop. + touchDown.set(ev.rawX, ev.rawY) + movedBeyondSlop = false } + // Always pass events to the VelocityTracker. addMovement(ev) + // If we haven't yet moved beyond the slop distance, check if we have. + if (!movedBeyondSlop) { + val dragDistance = hypot(ev.rawX - touchDown.x, ev.rawY - touchDown.y) + if (dragDistance > touchSlop) { + // If we're beyond the slop distance, save that and continue. + movedBeyondSlop = true + } else { + // Otherwise, don't do anything yet. + return false + } + } + val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target -> val distanceFromTargetCenter = hypot( ev.rawX - target.centerOnScreen.x, @@ -559,8 +581,14 @@ abstract class MagnetizedObject<T : Any>( } /** Updates the locations on screen of all of the [associatedTargets]. */ - internal fun updateTargetViewLocations() { + internal fun updateTargetViews() { associatedTargets.forEach { it.updateLocationOnScreen() } + + // Update the touch slop, since the configuration may have changed. + if (associatedTargets.size > 0) { + touchSlop = + ViewConfiguration.get(associatedTargets[0].targetView.context).scaledTouchSlop + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java index 7bb987ca7cf0..0cd4fb9578ff 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java +++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java @@ -56,9 +56,12 @@ import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Prefs; import com.android.systemui.R; +import com.android.systemui.qs.QSDndEvent; +import com.android.systemui.qs.QSEvents; import com.android.systemui.statusbar.policy.ZenModeController; import java.io.FileDescriptor; @@ -103,6 +106,7 @@ public class ZenModePanel extends FrameLayout { private final TransitionHelper mTransitionHelper = new TransitionHelper(); private final Uri mForeverId; private final ConfigurableTexts mConfigurableTexts; + private final UiEventLogger mUiEventLogger = QSEvents.INSTANCE.getQsUiEventsLogger(); private String mTag = TAG + "/" + Integer.toHexString(System.identityHashCode(this)); @@ -662,6 +666,7 @@ public class ZenModePanel extends FrameLayout { tag.rb.setChecked(true); if (DEBUG) Log.d(mTag, "onCheckedChanged " + conditionId); MetricsLogger.action(mContext, MetricsEvent.QS_DND_CONDITION_SELECT); + mUiEventLogger.log(QSDndEvent.QS_DND_CONDITION_SELECT); select(tag.condition); announceConditionSelection(tag); } @@ -767,6 +772,7 @@ public class ZenModePanel extends FrameLayout { private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) { MetricsLogger.action(mContext, MetricsEvent.QS_DND_TIME, up); + mUiEventLogger.log(up ? QSDndEvent.QS_DND_TIME_UP : QSDndEvent.QS_DND_TIME_DOWN); Condition newCondition = null; final int N = MINUTE_BUCKETS.length; if (mBucketIndex == -1) { diff --git a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java index e5da603321cd..0b6e4b2ab598 100644 --- a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java +++ b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java @@ -32,6 +32,7 @@ import android.util.SparseArray; import android.view.Display; import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.IScrollCaptureController; import android.view.IWindow; import android.view.IWindowManager; import android.view.IWindowSession; @@ -200,6 +201,14 @@ public class SystemWindows { attrs.flags |= FLAG_HARDWARE_ACCELERATED; viewRoot.setView(view, attrs); mViewRoots.put(view, viewRoot); + + try { + mWmService.setShellRootAccessibilityWindow(mDisplayId, windowType, + viewRoot.getWindowToken()); + } catch (RemoteException e) { + Slog.e(TAG, "Error setting accessibility window for " + mDisplayId + ":" + + windowType, e); + } } SysUiWindowManager addRoot(int windowType) { @@ -352,5 +361,14 @@ public class SystemWindows { @Override public void dispatchPointerCaptureChanged(boolean hasCapture) {} + + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + try { + controller.onClientUnavailable(); + } catch (RemoteException ex) { + // ignore + } + } } } |