Merge "Structure for screenshot detection notification." into tm-qpr-dev

This commit is contained in:
Matt Casey
2023-01-30 21:31:12 +00:00
committed by Android (Google) Code Review
12 changed files with 450 additions and 142 deletions

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/screenshot_detection_notice"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="12dp"
android:visibility="gone">
<TextView
android:id="@+id/screenshot_detection_notice_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineHeight="24sp"
android:textSize="18sp" />
</FrameLayout>

View File

@@ -134,7 +134,7 @@
android:orientation="horizontal"
app:layout_constraintGuide_end="0dp" />
<androidx.constraintlayout.widget.ConstraintLayout
<FrameLayout
android:id="@+id/screenshot_message_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -145,10 +145,12 @@
android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
android:elevation="4dp"
android:background="@drawable/action_chip_container_background"
android:visibility="invisible"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/screenshot_work_profile_first_run" />
<include layout="@layout/screenshot_detection_notice" />
</FrameLayout>
</com.android.systemui.screenshot.DraggableConstraintLayout>

View File

@@ -1,36 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
android:id="@+id/work_profile_first_run"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:visibility="gone">
<ImageView
android:id="@+id/screenshot_message_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:paddingEnd="4dp"
android:src="@drawable/ic_work_app_badge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/screenshot_message_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
android:src="@drawable/ic_work_app_badge"/>
<TextView
android:id="@+id/screenshot_message_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
app:layout_constraintEnd_toStartOf="@id/message_dismiss_button"/>
android:layout_weight="1"
android:layout_gravity="start"/>
<FrameLayout
android:id="@+id/message_dismiss_button"
android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
app:layout_constraintStart_toEndOf="@id/screenshot_message_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
android:layout_width="match_parent"
@@ -38,4 +30,4 @@
android:layout_margin="@dimen/overlay_dismiss_button_margin"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
</merge>
</LinearLayout>

View File

@@ -243,6 +243,10 @@
<string name="screenshot_work_profile_notification">Work screenshots are saved in the <xliff:g id="app" example="Work Files">%1$s</xliff:g> app</string>
<!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] -->
<string name="screenshot_default_files_app_name">Files</string>
<!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
<string name="screenshot_detected_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> detected this screenshot.</string>
<!-- A notice shown to the user to indicate that multiple apps have detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
<string name="screenshot_detected_multiple_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> and other open apps detected this screenshot.</string>
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
<string name="screenrecord_name">Screen Recorder</string>

View File

@@ -512,6 +512,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")

View File

@@ -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<ImageView>(R.id.screenshot_message_icon)
imageView.setImageDrawable(appIcon)
}
val messageContent =
messageContainer.requireViewById<TextView>(R.id.screenshot_message_content)
messageContent.text =
messageContainer.context.getString(
R.string.screenshot_work_profile_notification,
appName
)
messageContainer.requireViewById<View>(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<CharSequence> = 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

View File

@@ -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);
}
}
/**

View File

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

View File

@@ -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<CharSequence> {
// TODO: actually ask the window manager once API is available.
return listOf()
}
fun populateView(view: ViewGroup, appNames: List<CharSequence>) {
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]
)
}
}
}

View File

@@ -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<ImageView>(R.id.screenshot_message_icon)
imageView.setImageDrawable(data.icon)
}
val messageContent = view.requireViewById<TextView>(R.id.screenshot_message_content)
messageContent.text =
view.context.getString(R.string.screenshot_work_profile_notification, data.appName)
view.requireViewById<View>(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"

View File

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

View File

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