Merge "Work profile first run updates" into tm-qpr-dev

This commit is contained in:
Matt Casey
2023-01-09 22:03:51 +00:00
committed by Android (Google) Code Review
7 changed files with 305 additions and 18 deletions

View File

@@ -437,6 +437,11 @@
This name is in the ComponentName flattened format (package/class) --> This name is in the ComponentName flattened format (package/class) -->
<string name="config_screenshotEditor" translatable="false"></string> <string name="config_screenshotEditor" translatable="false"></string>
<!-- ComponentName for the file browsing app that the system would expect to be used in work
profile. The icon for this app will be shown to the user when informing them that a
screenshot has been saved to work profile. If blank, a default icon will be shown. -->
<string name="config_sceenshotWorkProfileFilesApp" translatable="false"></string>
<!-- Remote copy default activity. Must handle REMOTE_COPY_ACTION intents. <!-- Remote copy default activity. Must handle REMOTE_COPY_ACTION intents.
This name is in the ComponentName flattened format (package/class) --> This name is in the ComponentName flattened format (package/class) -->
<string name="config_remoteCopyPackage" translatable="false"></string> <string name="config_remoteCopyPackage" translatable="false"></string>

View File

@@ -240,7 +240,9 @@
<!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] --> <!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string> <string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] --> <!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] -->
<string name="screenshot_work_profile_notification" translatable="false">Work screenshots are saved in the work <xliff:g id="app" example="Files">%1$s</xliff:g> app</string> <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>
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]--> <!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
<string name="screenrecord_name">Screen Recorder</string> <string name="screenrecord_name">Screen Recorder</string>

View File

@@ -20,8 +20,7 @@ import static com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE; import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE;
import static com.android.systemui.screenshot.LogConfig.logTag; import static com.android.systemui.screenshot.LogConfig.logTag;
import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION; import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType;
import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS;
import android.app.ActivityTaskManager; import android.app.ActivityTaskManager;
import android.app.Notification; import android.app.Notification;
@@ -155,7 +154,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
CompletableFuture<List<Notification.Action>> smartActionsFuture = CompletableFuture<List<Notification.Action>> smartActionsFuture =
mScreenshotSmartActions.getSmartActionsFuture( mScreenshotSmartActions.getSmartActionsFuture(
mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS, mScreenshotId, uri, image, mSmartActionsProvider,
ScreenshotSmartActionType.REGULAR_SMART_ACTIONS,
smartActionsEnabled, user); smartActionsEnabled, user);
List<Notification.Action> smartActions = new ArrayList<>(); List<Notification.Action> smartActions = new ArrayList<>();
if (smartActionsEnabled) { if (smartActionsEnabled) {
@@ -166,7 +166,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
smartActions.addAll(buildSmartActions( smartActions.addAll(buildSmartActions(
mScreenshotSmartActions.getSmartActions( mScreenshotSmartActions.getSmartActions(
mScreenshotId, smartActionsFuture, timeoutMs, mScreenshotId, smartActionsFuture, timeoutMs,
mSmartActionsProvider, REGULAR_SMART_ACTIONS), mSmartActionsProvider,
ScreenshotSmartActionType.REGULAR_SMART_ACTIONS),
mContext)); mContext));
} }
@@ -476,7 +477,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
CompletableFuture<List<Notification.Action>> quickShareActionsFuture = CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
mScreenshotSmartActions.getSmartActionsFuture( mScreenshotSmartActions.getSmartActionsFuture(
mScreenshotId, null, image, mSmartActionsProvider, mScreenshotId, null, image, mSmartActionsProvider,
QUICK_SHARE_ACTION, ScreenshotSmartActionType.QUICK_SHARE_ACTION,
true /* smartActionsEnabled */, user); true /* smartActionsEnabled */, user);
int timeoutMs = DeviceConfig.getInt( int timeoutMs = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_SYSTEMUI, DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -485,7 +486,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
List<Notification.Action> quickShareActions = List<Notification.Action> quickShareActions =
mScreenshotSmartActions.getSmartActions( mScreenshotSmartActions.getSmartActions(
mScreenshotId, quickShareActionsFuture, timeoutMs, mScreenshotId, quickShareActionsFuture, timeoutMs,
mSmartActionsProvider, QUICK_SHARE_ACTION); mSmartActionsProvider,
ScreenshotSmartActionType.QUICK_SHARE_ACTION);
if (!quickShareActions.isEmpty()) { if (!quickShareActions.isEmpty()) {
mQuickShareData.quickShareAction = quickShareActions.get(0); mQuickShareData.quickShareAction = quickShareActions.get(0);
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);

View File

@@ -280,6 +280,7 @@ public class ScreenshotController {
private final TimeoutHandler mScreenshotHandler; private final TimeoutHandler mScreenshotHandler;
private final ActionIntentExecutor mActionExecutor; private final ActionIntentExecutor mActionExecutor;
private final UserManager mUserManager; private final UserManager mUserManager;
private final WorkProfileMessageController mWorkProfileMessageController;
private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
if (DEBUG_INPUT) { if (DEBUG_INPUT) {
@@ -326,7 +327,8 @@ public class ScreenshotController {
BroadcastSender broadcastSender, BroadcastSender broadcastSender,
ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
ActionIntentExecutor actionExecutor, ActionIntentExecutor actionExecutor,
UserManager userManager UserManager userManager,
WorkProfileMessageController workProfileMessageController
) { ) {
mScreenshotSmartActions = screenshotSmartActions; mScreenshotSmartActions = screenshotSmartActions;
mNotificationsController = screenshotNotificationsController; mNotificationsController = screenshotNotificationsController;
@@ -358,6 +360,7 @@ public class ScreenshotController {
mFlags = flags; mFlags = flags;
mActionExecutor = actionExecutor; mActionExecutor = actionExecutor;
mUserManager = userManager; mUserManager = userManager;
mWorkProfileMessageController = workProfileMessageController;
mAccessibilityManager = AccessibilityManager.getInstance(mContext); mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -683,7 +686,6 @@ public class ScreenshotController {
return true; return true;
} }
}); });
if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
mScreenshotView.badgeScreenshot( mScreenshotView.badgeScreenshot(
mContext.getPackageManager().getUserBadgeForDensity(owner, 0)); mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
@@ -1037,10 +1039,8 @@ public class ScreenshotController {
private void doPostAnimation(ScreenshotController.SavedImageData imageData) { private void doPostAnimation(ScreenshotController.SavedImageData imageData) {
mScreenshotView.setChipIntents(imageData); mScreenshotView.setChipIntents(imageData);
if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY) if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
&& mUserManager.isManagedProfile(imageData.owner.getIdentifier())) { mWorkProfileMessageController.onScreenshotTaken(imageData.owner, mScreenshotView);
// TODO: Read app from configuration
mScreenshotView.showWorkProfileMessage("Files");
} }
} }

View File

@@ -33,6 +33,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.Notification; import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
@@ -100,7 +101,8 @@ import java.util.ArrayList;
* Handles the visual elements and animations for the screenshot flow. * Handles the visual elements and animations for the screenshot flow.
*/ */
public class ScreenshotView extends FrameLayout implements public class ScreenshotView extends FrameLayout implements
ViewTreeObserver.OnComputeInternalInsetsListener { ViewTreeObserver.OnComputeInternalInsetsListener,
WorkProfileMessageController.WorkProfileMessageDisplay {
interface ScreenshotViewCallback { interface ScreenshotViewCallback {
void onUserInteraction(); void onUserInteraction();
@@ -351,13 +353,23 @@ public class ScreenshotView extends FrameLayout implements
* been taken and which app can be used to view it. * been taken and which app can be used to view it.
* *
* @param appName The name of the app to use to view screenshots * @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
*/ */
void showWorkProfileMessage(String appName) { @Override
public void showWorkProfileMessage(CharSequence appName, @Nullable Drawable appIcon,
Runnable onDismiss) {
if (appIcon != null) {
// Replace the default icon if one is provided.
ImageView imageView = mMessageContainer.findViewById(R.id.screenshot_message_icon);
imageView.setImageDrawable(appIcon);
}
mMessageContent.setText( mMessageContent.setText(
mContext.getString(R.string.screenshot_work_profile_notification, appName)); mContext.getString(R.string.screenshot_work_profile_notification, appName));
mMessageContainer.setVisibility(VISIBLE); mMessageContainer.setVisibility(VISIBLE);
mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> { mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> {
mMessageContainer.setVisibility(View.GONE); mMessageContainer.setVisibility(View.GONE);
onDismiss.run();
}); });
} }

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2022 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.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
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.
*/
class WorkProfileMessageController
@Inject
constructor(
private val context: Context,
private val userManager: UserManager,
private val packageManager: PackageManager,
) {
/**
* Determine if a message should be shown to the user, send message details to messageDisplay if
* appropriate.
*/
fun onScreenshotTaken(userHandle: UserHandle, messageDisplay: WorkProfileMessageDisplay) {
if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) {
var badgedIcon: Drawable? = null
var label: CharSequence? = null
val fileManager = fileManagerComponentName()
try {
val info =
packageManager.getActivityInfo(
fileManager,
PackageManager.ComponentInfoFlags.of(0)
)
val icon = packageManager.getActivityIcon(fileManager)
badgedIcon = packageManager.getUserBadgedIcon(icon, userHandle)
label = info.loadLabel(packageManager)
} catch (e: PackageManager.NameNotFoundException) {
Log.w(TAG, "Component $fileManager not found")
}
// If label wasn't loaded, use a default
val badgedLabel =
packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle)
messageDisplay.showWorkProfileMessage(badgedLabel, badgedIcon) { onMessageDismissed() }
}
}
private fun messageAlreadyDismissed(): Boolean {
return sharedPreference().getBoolean(PREFERENCE_KEY, false)
}
private fun onMessageDismissed() {
val editor = sharedPreference().edit()
editor.putBoolean(PREFERENCE_KEY, true)
editor.apply()
}
private fun sharedPreference() =
context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
private fun fileManagerComponentName() =
ComponentName.unflattenFromString(
context.getString(R.string.config_sceenshotWorkProfileFilesApp)
)
private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name)
/** UI that can show work profile messages (ScreenshotView in practice) */
interface WorkProfileMessageDisplay {
/**
* Show the given message and icon, calling onDismiss if the user explicitly dismisses the
* message.
*/
fun showWorkProfileMessage(text: CharSequence, icon: Drawable?, onDismiss: Runnable)
}
companion object {
const val TAG = "WorkProfileMessageCtrl"
const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot"
const val PREFERENCE_KEY = "work_profile_first_run"
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.screenshot;
import static org.junit.Assert.assertFalse;
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;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.os.UserManager;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
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;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class WorkProfileMessageControllerTest {
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";
private static final String BADGED_APP_LABEL = "badged app label";
private static final UserHandle NON_WORK_USER = UserHandle.of(0);
private static final UserHandle WORK_USER = UserHandle.of(10);
@Mock
private UserManager mUserManager;
@Mock
private PackageManager mPackageManager;
@Mock
private Context mContext;
@Mock
private WorkProfileMessageController.WorkProfileMessageDisplay mMessageDisplay;
@Mock
private Drawable mActivityIcon;
@Mock
private Drawable mBadgedActivityIcon;
@Mock
private ActivityInfo mActivityInfo;
@Captor
private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
private FakeSharedPreferences mSharedPreferences = new FakeSharedPreferences();
private WorkProfileMessageController mMessageController;
@Before
public void setup() throws PackageManager.NameNotFoundException {
MockitoAnnotations.initMocks(this);
when(mUserManager.isManagedProfile(eq(WORK_USER.getIdentifier()))).thenReturn(true);
when(mContext.getSharedPreferences(
eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME),
eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences);
when(mContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
when(mPackageManager.getUserBadgedLabel(eq(DEFAULT_LABEL), any()))
.thenReturn(BADGED_DEFAULT_LABEL);
when(mPackageManager.getUserBadgedLabel(eq(APP_LABEL), any()))
.thenReturn(BADGED_APP_LABEL);
when(mPackageManager.getActivityIcon(any(ComponentName.class)))
.thenReturn(mActivityIcon);
when(mPackageManager.getUserBadgedIcon(
any(), any())).thenReturn(mBadgedActivityIcon);
when(mPackageManager.getActivityInfo(any(),
any(PackageManager.ComponentInfoFlags.class))).thenReturn(mActivityInfo);
when(mActivityInfo.loadLabel(eq(mPackageManager))).thenReturn(APP_LABEL);
mSharedPreferences.edit().putBoolean(
WorkProfileMessageController.PREFERENCE_KEY, false).apply();
mMessageController = new WorkProfileMessageController(mContext, mUserManager,
mPackageManager);
}
@Test
public void testOnScreenshotTaken_notManaged() {
mMessageController.onScreenshotTaken(NON_WORK_USER, mMessageDisplay);
verify(mMessageDisplay, never())
.showWorkProfileMessage(any(), nullable(Drawable.class), any());
}
@Test
public void testOnScreenshotTaken_alreadyDismissed() {
mSharedPreferences.edit().putBoolean(
WorkProfileMessageController.PREFERENCE_KEY, true).apply();
mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay);
verify(mMessageDisplay, never())
.showWorkProfileMessage(any(), nullable(Drawable.class), any());
}
@Test
public void testOnScreenshotTaken_packageNotFound()
throws PackageManager.NameNotFoundException {
when(mPackageManager.getActivityInfo(any(),
any(PackageManager.ComponentInfoFlags.class))).thenThrow(
new PackageManager.NameNotFoundException());
mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay);
verify(mMessageDisplay).showWorkProfileMessage(
eq(BADGED_DEFAULT_LABEL), eq(null), any());
}
@Test
public void testOnScreenshotTaken() {
mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay);
verify(mMessageDisplay).showWorkProfileMessage(
eq(BADGED_APP_LABEL), eq(mBadgedActivityIcon), mRunnableArgumentCaptor.capture());
// Dismiss hasn't been tapped, preference untouched.
assertFalse(
mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false));
mRunnableArgumentCaptor.getValue().run();
// After dismiss has been tapped, the setting should be updated.
assertTrue(
mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false));
}
}