From b505e775d085f6606a29258cdcbdcee6f29105c1 Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Fri, 27 Jan 2023 16:52:19 +0000 Subject: [PATCH] Structure for screenshot detection notification. The notification itself won't be hooked up until android U. - The message container now has two (View.GONE) children, one for work profile first run, the other for screenshot detection. - All interactions with this space go through MessageContainerController. - Populating the message space has two phases: figuring out which messages want to be shown, then showing no more than one. These are implemented as separate methods on each of ScreenshotDetectionController and WorkProfileMessageController. MessageContainerController no longer knows the details of these views. - ScreenshotDetectionController doesn't do the actual detection yet, but the UI bits have been manually tested. - Moved the MessageContainerController.onScreenshotTaken calls to right after the entrance animation completes in ScreenshotController. Flags: Added a new SCREENSHOT_DETECTION flag. The detection and relevant display code are protected by this. All other changes to the message space are protected by the work profile flag. Test: atest com.android.systemui.screenshot Bug: 265937777 Change-Id: Id1e51ffcb322839badf4f1d81a5aafceb0b67a06 --- .../layout/screenshot_detection_notice.xml | 16 ++ .../SystemUI/res/layout/screenshot_static.xml | 8 +- .../screenshot_work_profile_first_run.xml | 26 +-- packages/SystemUI/res/values/strings.xml | 4 + .../src/com/android/systemui/flags/Flags.kt | 3 + .../screenshot/MessageContainerController.kt | 158 +++++++++++------- .../screenshot/ScreenshotController.java | 43 +++-- .../systemui/screenshot/ScreenshotData.kt | 6 + .../ScreenshotDetectionController.kt | 56 +++++++ .../WorkProfileMessageController.kt | 43 ++++- .../MessageContainerControllerTest.kt | 143 ++++++++++++++++ .../WorkProfileMessageControllerTest.java | 86 ++++++---- 12 files changed, 450 insertions(+), 142 deletions(-) create mode 100644 packages/SystemUI/res/layout/screenshot_detection_notice.xml create mode 100644 packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt diff --git a/packages/SystemUI/res/layout/screenshot_detection_notice.xml b/packages/SystemUI/res/layout/screenshot_detection_notice.xml new file mode 100644 index 0000000000000..fc936c01227e6 --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_detection_notice.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index 7e8bc2c1577e2..a748e29ee0413 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -134,7 +134,7 @@ android:orientation="horizontal" app:layout_constraintGuide_end="0dp" /> - - + + + diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml index c794d91c02bed..c1817242aa01e 100644 --- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml +++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml @@ -1,36 +1,28 @@ - + android:id="@+id/work_profile_first_run" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:visibility="gone"> + android:src="@drawable/ic_work_app_badge"/> + android:layout_weight="1" + android:layout_gravity="start"/> - + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e60835cc5ea42..4c123ec87d400 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -243,6 +243,10 @@ Work screenshots are saved in the %1$s app Files + + %1$s detected this screenshot. + + %1$s and other open apps detected this screenshot. Screen Recorder diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 47c41fe1e3ba2..4d3217c6ec48c 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -516,6 +516,9 @@ object Flags { // TODO(b/264916608): Tracking Bug @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata") + // TODO(b/266955521): Tracking bug + @JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection") + // 1400 - columbus // TODO(b/254512756): Tracking Bug val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc") diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt index 1e531ba7a6e03..ad66514c689c3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt @@ -3,101 +3,133 @@ package com.android.systemui.screenshot import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator -import android.graphics.drawable.Drawable +import android.os.UserHandle 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 +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import javax.inject.Inject /** * MessageContainerController controls the display of content in the screenshot message container. */ class MessageContainerController +@Inject constructor( - parent: ViewGroup, + private val workProfileMessageController: WorkProfileMessageController, + private val screenshotDetectionController: ScreenshotDetectionController, + private val featureFlags: FeatureFlags, ) { - private val guideline: Guideline = parent.requireViewById(R.id.guideline) - private val messageContainer: ViewGroup = - parent.requireViewById(R.id.screenshot_message_container) + private lateinit var container: ViewGroup + private lateinit var guideline: Guideline + private lateinit var workProfileFirstRunView: ViewGroup + private lateinit var detectionNoticeView: ViewGroup + private var animateOut: Animator? = null - /** - * 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 - ) + fun setView(screenshotView: ViewGroup) { + container = screenshotView.requireViewById(R.id.screenshot_message_container) + guideline = screenshotView.requireViewById(R.id.guideline) + + workProfileFirstRunView = container.requireViewById(R.id.work_profile_first_run) + detectionNoticeView = container.requireViewById(R.id.screenshot_detection_notice) + + // Restore to starting state. + container.visibility = View.GONE + guideline.setGuidelineEnd(0) + workProfileFirstRunView.visibility = View.GONE + detectionNoticeView.visibility = View.GONE + } + + // Minimal implementation for use when Flags.SCREENSHOT_METADATA isn't turned on. + fun onScreenshotTaken(userHandle: UserHandle) { + if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { + val workProfileData = workProfileMessageController.onScreenshotTaken(userHandle) + if (workProfileData != null) { + workProfileFirstRunView.visibility = View.VISIBLE + detectionNoticeView.visibility = View.GONE + + workProfileMessageController.populateView( + workProfileFirstRunView, + workProfileData, + this::animateOutMessageContainer + ) + animateInMessageContainer() + } } - 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() + } + + fun onScreenshotTaken(screenshot: ScreenshotData) { + if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { + val workProfileData = + workProfileMessageController.onScreenshotTaken(screenshot.userHandle) + var notifiedApps: List = listOf() + if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) { + notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) + } + + // If work profile first run needs to show, bias towards that, otherwise show screenshot + // detection notification if needed. + if (workProfileData != null) { + workProfileFirstRunView.visibility = View.VISIBLE + detectionNoticeView.visibility = View.GONE + workProfileMessageController.populateView( + workProfileFirstRunView, + workProfileData, + this::animateOutMessageContainer + ) + animateInMessageContainer() + } else if (notifiedApps.isNotEmpty()) { + detectionNoticeView.visibility = View.VISIBLE + workProfileFirstRunView.visibility = View.GONE + screenshotDetectionController.populateView(detectionNoticeView, notifiedApps) + animateInMessageContainer() + } } + } + + private fun animateInMessageContainer() { + if (container.visibility == View.VISIBLE) return // Need the container to be fully measured before animating in (to know animation offset // destination) - messageContainer.viewTreeObserver.addOnPreDrawListener( + container.visibility = View.VISIBLE + container.viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { - messageContainer.viewTreeObserver.removeOnPreDrawListener(this) - animateInMessageContainer() + container.viewTreeObserver.removeOnPreDrawListener(this) + getAnimator(true).start() 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 + if (animateOut != null) return + + animateOut = + getAnimator(false).apply { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + container.visibility = View.GONE + animateOut = null + } } - } - ) - start() - } + ) + start() + } } private fun getAnimator(animateIn: Boolean): Animator { - val params = messageContainer.layoutParams as MarginLayoutParams - val offset = messageContainer.height + params.topMargin + params.bottomMargin + val params = container.layoutParams as MarginLayoutParams + val offset = container.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 @@ -105,7 +137,7 @@ constructor( addUpdateListener { valueAnimator: ValueAnimator -> val interpolation = valueAnimator.animatedValue as Float guideline.setGuidelineEnd((interpolation * offset).toInt()) - messageContainer.alpha = interpolation + container.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 ab13962b405db..72a8e23fbff51 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -282,7 +282,6 @@ public class ScreenshotController { private final TimeoutHandler mScreenshotHandler; private final ActionIntentExecutor mActionExecutor; private final UserManager mUserManager; - private final WorkProfileMessageController mWorkProfileMessageController; private final AssistContentRequester mAssistContentRequester; private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { @@ -293,7 +292,7 @@ public class ScreenshotController { }; private ScreenshotView mScreenshotView; - private MessageContainerController mMessageContainerController; + private final MessageContainerController mMessageContainerController; private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; @@ -332,8 +331,8 @@ public class ScreenshotController { ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, ActionIntentExecutor actionExecutor, UserManager userManager, - WorkProfileMessageController workProfileMessageController, AssistContentRequester assistContentRequester, + MessageContainerController messageContainerController, DisplayTracker displayTracker ) { mScreenshotSmartActions = screenshotSmartActions; @@ -367,7 +366,7 @@ public class ScreenshotController { mFlags = flags; mActionExecutor = actionExecutor; mUserManager = userManager; - mWorkProfileMessageController = workProfileMessageController; + mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; mAccessibilityManager = AccessibilityManager.getInstance(mContext); @@ -468,7 +467,11 @@ public class ScreenshotController { } } - prepareAnimation(screenshot.getScreenBounds(), showFlash); + prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> { + if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { + mMessageContainerController.onScreenshotTaken(screenshot); + } + }); if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( @@ -632,7 +635,9 @@ public class ScreenshotController { // Inflate the screenshot layout mScreenshotView = (ScreenshotView) LayoutInflater.from(mContext).inflate(R.layout.screenshot, null); - mMessageContainerController = new MessageContainerController(mScreenshotView); + if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { + mMessageContainerController.setView(mScreenshotView); + } mScreenshotView.addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { @Override @@ -782,7 +787,11 @@ public class ScreenshotController { enqueueScrollCaptureRequest(owner); attachWindow(); - prepareAnimation(screenRect, showFlash); + prepareAnimation(screenRect, showFlash, () -> { + if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { + mMessageContainerController.onScreenshotTaken(owner); + } + }); if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( @@ -799,7 +808,8 @@ public class ScreenshotController { mScreenshotHandler.cancelTimeout(); // restarted after animation } - private void prepareAnimation(Rect screenRect, boolean showFlash) { + private void prepareAnimation(Rect screenRect, boolean showFlash, + Runnable onAnimationComplete) { mScreenshotView.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override @@ -808,7 +818,7 @@ public class ScreenshotController { Log.d(TAG, "onPreDraw: startAnimation"); } mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this); - startAnimation(screenRect, showFlash); + startAnimation(screenRect, showFlash, onAnimationComplete); return true; } }); @@ -1089,13 +1099,22 @@ public class ScreenshotController { /** * Starts the animation after taking the screenshot */ - private void startAnimation(Rect screenRect, boolean showFlash) { + private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) { if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { mScreenshotAnimation.cancel(); } mScreenshotAnimation = mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash); + if (onAnimationComplete != null) { + mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + onAnimationComplete.run(); + } + }); + } // Play the shutter sound to notify that we've taken a screenshot playCameraSound(); @@ -1194,10 +1213,6 @@ public class ScreenshotController { private void doPostAnimation(ScreenshotController.SavedImageData imageData) { mScreenshotView.setChipIntents(imageData); - if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { - mWorkProfileMessageController.onScreenshotTaken(imageData.owner, - mMessageContainerController); - } } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt index c43e4b4e3f3a3..e9be88a599909 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.os.UserHandle import android.view.WindowManager.ScreenshotSource import android.view.WindowManager.ScreenshotType +import androidx.annotation.VisibleForTesting import com.android.internal.util.ScreenshotRequest /** ScreenshotData represents the current state of a single screenshot being acquired. */ @@ -42,5 +43,10 @@ data class ScreenshotData( request.bitmap, ) } + + @VisibleForTesting + fun forTesting(): ScreenshotData { + return ScreenshotData(0, 0, null, null, null, 0, Insets.NONE, null) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt new file mode 100644 index 0000000000000..70ea2b5b9507b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.pm.PackageManager +import android.view.IWindowManager +import android.view.ViewGroup +import android.widget.TextView +import com.android.systemui.R +import javax.inject.Inject + +class ScreenshotDetectionController +@Inject +constructor( + private val windowManager: IWindowManager, + private val packageManager: PackageManager, +) { + /** + * Notify potentially listening apps of the screenshot. Return a list of the names of the apps + * notified. + */ + fun maybeNotifyOfScreenshot(data: ScreenshotData): List { + // TODO: actually ask the window manager once API is available. + return listOf() + } + + fun populateView(view: ViewGroup, appNames: List) { + assert(appNames.isNotEmpty()) + + val textView: TextView = view.requireViewById(R.id.screenshot_detection_notice_text) + if (appNames.size == 1) { + textView.text = + view.resources.getString(R.string.screenshot_detected_template, appNames[0]) + } else { + textView.text = + view.resources.getString( + R.string.screenshot_detected_multiple_template, + appNames[0] + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt index b4a07d4321a93..66b78420146b7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt @@ -23,13 +23,16 @@ import android.graphics.drawable.Drawable import android.os.UserHandle import android.os.UserManager import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView import com.android.systemui.R import javax.inject.Inject /** - * Handles all the non-UI portions of the work profile first run: - * - Track whether the user has already dismissed it. - * - Load the proper icon and app name. + * Handles work profile first run, determining whether a first run UI should be shown and populating + * that UI if needed. */ class WorkProfileMessageController @Inject @@ -40,10 +43,12 @@ constructor( ) { /** - * Determine if a message should be shown to the user, send message details to - * MessageContainerController if appropriate. + * @return a populated WorkProfileFirstRunData object if a work profile first run message should + * be shown */ - fun onScreenshotTaken(userHandle: UserHandle, messageContainer: MessageContainerController) { + fun onScreenshotTaken(userHandle: UserHandle?): WorkProfileFirstRunData? { + if (userHandle == null) return null + if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) { var badgedIcon: Drawable? = null var label: CharSequence? = null @@ -65,9 +70,27 @@ constructor( val badgedLabel = packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle) - messageContainer.showWorkProfileMessage(badgedLabel, badgedIcon) { - onMessageDismissed() - } + return WorkProfileFirstRunData(badgedLabel, badgedIcon) + } + return null + } + + /** + * Use the provided WorkProfileFirstRunData to populate the work profile first run UI in the + * given view. + */ + fun populateView(view: ViewGroup, data: WorkProfileFirstRunData, animateOut: () -> Unit) { + if (data.icon != null) { + // Replace the default icon if one is provided. + val imageView: ImageView = view.requireViewById(R.id.screenshot_message_icon) + imageView.setImageDrawable(data.icon) + } + val messageContent = view.requireViewById(R.id.screenshot_message_content) + messageContent.text = + view.context.getString(R.string.screenshot_work_profile_notification, data.appName) + view.requireViewById(R.id.message_dismiss_button).setOnClickListener { + animateOut() + onMessageDismissed() } } @@ -91,6 +114,8 @@ constructor( private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name) + data class WorkProfileFirstRunData constructor(val appName: CharSequence, val icon: Drawable?) + 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/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt new file mode 100644 index 0000000000000..9f0a803fac400 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt @@ -0,0 +1,143 @@ +package com.android.systemui.screenshot + +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.Guideline +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class MessageContainerControllerTest : SysuiTestCase() { + lateinit var messageContainer: MessageContainerController + + @Mock lateinit var workProfileMessageController: WorkProfileMessageController + + @Mock lateinit var screenshotDetectionController: ScreenshotDetectionController + + @Mock lateinit var icon: Drawable + + lateinit var workProfileFirstRunView: ViewGroup + lateinit var detectionNoticeView: ViewGroup + lateinit var container: FrameLayout + + var featureFlags = FakeFeatureFlags() + lateinit var screenshotView: ViewGroup + + val userHandle = UserHandle.of(5) + val screenshotData = ScreenshotData.forTesting() + + val appName = "app name" + lateinit var workProfileData: WorkProfileMessageController.WorkProfileFirstRunData + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + messageContainer = + MessageContainerController( + workProfileMessageController, + screenshotDetectionController, + featureFlags + ) + screenshotView = ConstraintLayout(mContext) + workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon) + + val guideline = Guideline(mContext) + guideline.id = com.android.systemui.R.id.guideline + screenshotView.addView(guideline) + + container = FrameLayout(mContext) + container.id = com.android.systemui.R.id.screenshot_message_container + screenshotView.addView(container) + + workProfileFirstRunView = FrameLayout(mContext) + workProfileFirstRunView.id = com.android.systemui.R.id.work_profile_first_run + container.addView(workProfileFirstRunView) + + detectionNoticeView = FrameLayout(mContext) + detectionNoticeView.id = com.android.systemui.R.id.screenshot_detection_notice + container.addView(detectionNoticeView) + + messageContainer.setView(screenshotView) + + screenshotData.userHandle = userHandle + } + + @Test + fun testOnScreenshotTakenUserHandle_noWorkProfileFirstRun() { + featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true) + // (just being explicit here) + whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))).thenReturn(null) + + messageContainer.onScreenshotTaken(userHandle) + + verify(workProfileMessageController, never()).populateView(any(), any(), any()) + } + + @Test + fun testOnScreenshotTakenUserHandle_noWorkProfileFlag() { + featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false) + + messageContainer.onScreenshotTaken(userHandle) + + verify(workProfileMessageController, never()).onScreenshotTaken(any()) + verify(workProfileMessageController, never()).populateView(any(), any(), any()) + } + + @Test + fun testOnScreenshotTakenUserHandle_withWorkProfileFirstRun() { + featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true) + whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))) + .thenReturn(workProfileData) + messageContainer.onScreenshotTaken(userHandle) + + verify(workProfileMessageController) + .populateView(eq(workProfileFirstRunView), eq(workProfileData), any()) + assertEquals(View.VISIBLE, workProfileFirstRunView.visibility) + assertEquals(View.GONE, detectionNoticeView.visibility) + } + + @Test + fun testOnScreenshotTakenScreenshotData_flagsOff() { + featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false) + featureFlags.set(Flags.SCREENSHOT_DETECTION, false) + + messageContainer.onScreenshotTaken(screenshotData) + + verify(workProfileMessageController, never()).onScreenshotTaken(any()) + verify(screenshotDetectionController, never()).maybeNotifyOfScreenshot(any()) + + assertEquals(View.GONE, container.visibility) + } + + @Test + fun testOnScreenshotTakenScreenshotData_nothingToShow() { + featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true) + featureFlags.set(Flags.SCREENSHOT_DETECTION, true) + + messageContainer.onScreenshotTaken(screenshotData) + + verify(workProfileMessageController, never()).populateView(any(), any(), any()) + verify(screenshotDetectionController, never()).populateView(any(), any()) + + assertEquals(View.GONE, container.visibility) + } +} 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 e8905ab22bc0d..576652f4dbdbf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java @@ -16,13 +16,11 @@ package com.android.systemui.screenshot; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ComponentName; @@ -33,24 +31,33 @@ import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; import android.testing.AndroidTestingRunner; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; import androidx.test.filters.SmallTest; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; import com.android.systemui.util.FakeSharedPreferences; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import kotlin.Unit; + @SmallTest @RunWith(AndroidTestingRunner.class) -public class WorkProfileMessageControllerTest { +public class WorkProfileMessageControllerTest extends SysuiTestCase { private static final String DEFAULT_LABEL = "default label"; private static final String BADGED_DEFAULT_LABEL = "badged default label"; private static final String APP_LABEL = "app label"; @@ -63,17 +70,13 @@ public class WorkProfileMessageControllerTest { @Mock private PackageManager mPackageManager; @Mock - private Context mContext; - @Mock - private MessageContainerController mMessageDisplay; + private Context mMockContext; @Mock private Drawable mActivityIcon; @Mock private Drawable mBadgedActivityIcon; @Mock private ActivityInfo mActivityInfo; - @Captor - private ArgumentCaptor mRunnableArgumentCaptor; private FakeSharedPreferences mSharedPreferences = new FakeSharedPreferences(); @@ -84,10 +87,10 @@ public class WorkProfileMessageControllerTest { MockitoAnnotations.initMocks(this); when(mUserManager.isManagedProfile(eq(WORK_USER.getIdentifier()))).thenReturn(true); - when(mContext.getSharedPreferences( + when(mMockContext.getSharedPreferences( eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME), eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences); - when(mContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL); + when(mMockContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL); when(mPackageManager.getUserBadgedLabel(eq(DEFAULT_LABEL), any())) .thenReturn(BADGED_DEFAULT_LABEL); when(mPackageManager.getUserBadgedLabel(eq(APP_LABEL), any())) @@ -103,16 +106,13 @@ public class WorkProfileMessageControllerTest { mSharedPreferences.edit().putBoolean( WorkProfileMessageController.PREFERENCE_KEY, false).apply(); - mMessageController = new WorkProfileMessageController(mContext, mUserManager, + mMessageController = new WorkProfileMessageController(mMockContext, mUserManager, mPackageManager); } @Test public void testOnScreenshotTaken_notManaged() { - mMessageController.onScreenshotTaken(NON_WORK_USER, mMessageDisplay); - - verify(mMessageDisplay, never()) - .showWorkProfileMessage(any(), nullable(Drawable.class), any()); + assertNull(mMessageController.onScreenshotTaken(NON_WORK_USER)); } @Test @@ -120,10 +120,7 @@ public class WorkProfileMessageControllerTest { mSharedPreferences.edit().putBoolean( WorkProfileMessageController.PREFERENCE_KEY, true).apply(); - mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay); - - verify(mMessageDisplay, never()) - .showWorkProfileMessage(any(), nullable(Drawable.class), any()); + assertNull(mMessageController.onScreenshotTaken(WORK_USER)); } @Test @@ -133,28 +130,45 @@ public class WorkProfileMessageControllerTest { any(PackageManager.ComponentInfoFlags.class))).thenThrow( new PackageManager.NameNotFoundException()); - mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay); + WorkProfileMessageController.WorkProfileFirstRunData data = + mMessageController.onScreenshotTaken(WORK_USER); - verify(mMessageDisplay).showWorkProfileMessage( - eq(BADGED_DEFAULT_LABEL), eq(null), any()); + assertEquals(BADGED_DEFAULT_LABEL, data.getAppName()); + assertNull(data.getIcon()); } @Test public void testOnScreenshotTaken() { - mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay); + WorkProfileMessageController.WorkProfileFirstRunData data = + mMessageController.onScreenshotTaken(WORK_USER); - verify(mMessageDisplay).showWorkProfileMessage( - eq(BADGED_APP_LABEL), eq(mBadgedActivityIcon), mRunnableArgumentCaptor.capture()); + assertEquals(BADGED_APP_LABEL, data.getAppName()); + assertEquals(mBadgedActivityIcon, data.getIcon()); + } - // Dismiss hasn't been tapped, preference untouched. - assertFalse( - mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false)); + @Test + public void testPopulateView() throws InterruptedException { + ViewGroup layout = (ViewGroup) LayoutInflater.from(mContext).inflate( + R.layout.screenshot_work_profile_first_run, null); + WorkProfileMessageController.WorkProfileFirstRunData data = + new WorkProfileMessageController.WorkProfileFirstRunData(BADGED_APP_LABEL, + mBadgedActivityIcon); + final CountDownLatch countdown = new CountDownLatch(1); + mMessageController.populateView(layout, data, () -> { + countdown.countDown(); + return Unit.INSTANCE; + }); - mRunnableArgumentCaptor.getValue().run(); + ImageView image = layout.findViewById(R.id.screenshot_message_icon); + assertEquals(mBadgedActivityIcon, image.getDrawable()); + TextView text = layout.findViewById(R.id.screenshot_message_content); + // The app name is used in a template, but at least validate that it was inserted. + assertTrue(text.getText().toString().contains(BADGED_APP_LABEL)); - // After dismiss has been tapped, the setting should be updated. - assertTrue( - mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false)); + // Validate that clicking the dismiss button calls back properly. + assertEquals(1, countdown.getCount()); + layout.findViewById(R.id.message_dismiss_button).callOnClick(); + countdown.await(1000, TimeUnit.MILLISECONDS); } }