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); } }