diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index 496eb6e6130eb..7e8bc2c1577e2 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -31,7 +31,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" app:layout_constraintEnd_toEndOf="@+id/actions_container" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"/> + app:layout_constraintBottom_toTopOf="@id/guideline"/> + + - - - - - - - - - + > diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml new file mode 100644 index 0000000000000..c794d91c02bed --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index dfc01501a9997..f3bef920d3113 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -337,7 +337,7 @@ 8dp 8dp - 4dp + 6dp 242dp 18dp 4dp diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt new file mode 100644 index 0000000000000..1e531ba7a6e03 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt @@ -0,0 +1,113 @@ +package com.android.systemui.screenshot + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.drawable.Drawable +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.view.ViewTreeObserver +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.Guideline +import com.android.systemui.R + +/** + * MessageContainerController controls the display of content in the screenshot message container. + */ +class MessageContainerController +constructor( + parent: ViewGroup, +) { + private val guideline: Guideline = parent.requireViewById(R.id.guideline) + private val messageContainer: ViewGroup = + parent.requireViewById(R.id.screenshot_message_container) + + /** + * Show a notification under the screenshot view indicating that a work profile screenshot has + * been taken and which app can be used to view it. + * + * @param appName The name of the app to use to view screenshots + * @param appIcon Optional icon for the relevant files app + * @param onDismiss Runnable to be run when the user dismisses this message + */ + fun showWorkProfileMessage(appName: CharSequence, appIcon: Drawable?, onDismiss: Runnable) { + // Eventually this container will support multiple notification types, but for now just make + // sure we don't double inflate. + if (messageContainer.childCount == 0) { + View.inflate( + messageContainer.context, + R.layout.screenshot_work_profile_first_run, + messageContainer + ) + } + if (appIcon != null) { + // Replace the default icon if one is provided. + val imageView: ImageView = + messageContainer.requireViewById(R.id.screenshot_message_icon) + imageView.setImageDrawable(appIcon) + } + val messageContent = + messageContainer.requireViewById(R.id.screenshot_message_content) + messageContent.text = + messageContainer.context.getString( + R.string.screenshot_work_profile_notification, + appName + ) + messageContainer.requireViewById(R.id.message_dismiss_button).setOnClickListener { + animateOutMessageContainer() + onDismiss.run() + } + + // Need the container to be fully measured before animating in (to know animation offset + // destination) + messageContainer.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + messageContainer.viewTreeObserver.removeOnPreDrawListener(this) + animateInMessageContainer() + return false + } + } + ) + } + + private fun animateInMessageContainer() { + if (messageContainer.visibility == View.VISIBLE) return + + messageContainer.visibility = View.VISIBLE + getAnimator(true).start() + } + + private fun animateOutMessageContainer() { + getAnimator(false).apply { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + messageContainer.visibility = View.INVISIBLE + } + } + ) + start() + } + } + + private fun getAnimator(animateIn: Boolean): Animator { + val params = messageContainer.layoutParams as MarginLayoutParams + val offset = messageContainer.height + params.topMargin + params.bottomMargin + val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f) + with(anim) { + duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS + interpolator = AccelerateDecelerateInterpolator() + addUpdateListener { valueAnimator: ValueAnimator -> + val interpolation = valueAnimator.animatedValue as Float + guideline.setGuidelineEnd((interpolation * offset).toInt()) + messageContainer.alpha = interpolation + } + } + return anim + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 9f7d4f08b42d2..ab13962b405db 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -293,6 +293,7 @@ public class ScreenshotController { }; private ScreenshotView mScreenshotView; + private MessageContainerController mMessageContainerController; private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; @@ -631,6 +632,7 @@ public class ScreenshotController { // Inflate the screenshot layout mScreenshotView = (ScreenshotView) LayoutInflater.from(mContext).inflate(R.layout.screenshot, null); + mMessageContainerController = new MessageContainerController(mScreenshotView); mScreenshotView.addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { @Override @@ -1193,7 +1195,8 @@ public class ScreenshotController { private void doPostAnimation(ScreenshotController.SavedImageData imageData) { mScreenshotView.setChipIntents(imageData); if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { - mWorkProfileMessageController.onScreenshotTaken(imageData.owner, mScreenshotView); + mWorkProfileMessageController.onScreenshotTaken(imageData.owner, + mMessageContainerController); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 8b73a57ffa46a..afba7ad24692e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -33,7 +33,6 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; @@ -82,7 +81,6 @@ import android.widget.FrameLayout; import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.TextView; import androidx.constraintlayout.widget.ConstraintLayout; @@ -102,8 +100,7 @@ import java.util.ArrayList; * Handles the visual elements and animations for the screenshot flow. */ public class ScreenshotView extends FrameLayout implements - ViewTreeObserver.OnComputeInternalInsetsListener, - WorkProfileMessageController.WorkProfileMessageDisplay { + ViewTreeObserver.OnComputeInternalInsetsListener { interface ScreenshotViewCallback { void onUserInteraction(); @@ -123,7 +120,7 @@ public class ScreenshotView extends FrameLayout implements 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; + public static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe @@ -142,8 +139,6 @@ public class ScreenshotView extends FrameLayout implements private ImageView mScrollingScrim; private DraggableConstraintLayout mScreenshotStatic; - private ViewGroup mMessageContainer; - private TextView mMessageContent; private ImageView mScreenshotPreview; private ImageView mScreenshotBadge; private View mScreenshotPreviewBorder; @@ -295,8 +290,11 @@ public class ScreenshotView extends FrameLayout implements mDismissButton.getBoundsOnScreen(tmpRect); swipeRegion.op(tmpRect, Region.Op.UNION); - mMessageContainer.findViewById(R.id.message_dismiss_button).getBoundsOnScreen(tmpRect); - swipeRegion.op(tmpRect, Region.Op.UNION); + View messageDismiss = findViewById(R.id.message_dismiss_button); + if (messageDismiss != null) { + messageDismiss.getBoundsOnScreen(tmpRect); + swipeRegion.op(tmpRect, Region.Op.UNION); + } return swipeRegion; } @@ -352,39 +350,11 @@ public class ScreenshotView extends FrameLayout implements } } - /** - * Show a notification under the screenshot view indicating that a work profile screenshot has - * been taken and which app can be used to view it. - * - * @param appName The name of the app to use to view screenshots - * @param appIcon Optional icon for the relevant files app - * @param onDismiss Runnable to be run when the user dismisses this message - */ - @Override - public void showWorkProfileMessage(CharSequence appName, @Nullable Drawable appIcon, - Runnable onDismiss) { - if (appIcon != null) { - // Replace the default icon if one is provided. - ImageView imageView = mMessageContainer.findViewById(R.id.screenshot_message_icon); - imageView.setImageDrawable(appIcon); - } - mMessageContent.setText( - mContext.getString(R.string.screenshot_work_profile_notification, appName)); - mMessageContainer.setVisibility(VISIBLE); - mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> { - mMessageContainer.setVisibility(View.GONE); - onDismiss.run(); - }); - } - @Override // View protected void onFinishInflate() { + super.onFinishInflate(); mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim)); mScreenshotStatic = requireNonNull(findViewById(R.id.screenshot_static)); - mMessageContainer = - requireNonNull(mScreenshotStatic.findViewById(R.id.screenshot_message_container)); - mMessageContent = - requireNonNull(mMessageContainer.findViewById(R.id.screenshot_message_content)); mScreenshotPreview = requireNonNull(findViewById(R.id.screenshot_preview)); mScreenshotPreviewBorder = requireNonNull( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt index 5d7e56f6c98a3..b4a07d4321a93 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt @@ -40,10 +40,10 @@ constructor( ) { /** - * Determine if a message should be shown to the user, send message details to messageDisplay if - * appropriate. + * Determine if a message should be shown to the user, send message details to + * MessageContainerController if appropriate. */ - fun onScreenshotTaken(userHandle: UserHandle, messageDisplay: WorkProfileMessageDisplay) { + fun onScreenshotTaken(userHandle: UserHandle, messageContainer: MessageContainerController) { if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) { var badgedIcon: Drawable? = null var label: CharSequence? = null @@ -65,7 +65,9 @@ constructor( val badgedLabel = packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle) - messageDisplay.showWorkProfileMessage(badgedLabel, badgedIcon) { onMessageDismissed() } + messageContainer.showWorkProfileMessage(badgedLabel, badgedIcon) { + onMessageDismissed() + } } } @@ -89,15 +91,6 @@ constructor( private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name) - /** UI that can show work profile messages (ScreenshotView in practice) */ - interface WorkProfileMessageDisplay { - /** - * Show the given message and icon, calling onDismiss if the user explicitly dismisses the - * message. - */ - fun showWorkProfileMessage(text: CharSequence, icon: Drawable?, onDismiss: Runnable) - } - companion object { const val TAG = "WorkProfileMessageCtrl" const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot" diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java index bd04b3ccc039c..e8905ab22bc0d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java @@ -65,7 +65,7 @@ public class WorkProfileMessageControllerTest { @Mock private Context mContext; @Mock - private WorkProfileMessageController.WorkProfileMessageDisplay mMessageDisplay; + private MessageContainerController mMessageDisplay; @Mock private Drawable mActivityIcon; @Mock