Work profile first run updates
- Show until the user dismisses it.
- Fetch the app icon based upon config ComponentName.
- Fetch the app name based upon config.
- Extract relevant logic to WorkProfileMessageController
- Some no-op cleanups in SaveIMageInBackgroundTask to prevent sysui
studio from complaining.
Flag: The calls into the new message controller are protected by
SCREENSHOT_WORK_PROFILE_POLICY (in teamfood).
Bug: 254245929
Test: atest WorkProfileMessageControllerTest
Test: Manually testing dismissal and UI.
Change-Id: I943303992fdc2caeba76a06f88c1b91419d02be8
This commit is contained in:
@@ -437,6 +437,11 @@
|
||||
This name is in the ComponentName flattened format (package/class) -->
|
||||
<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.
|
||||
This name is in the ComponentName flattened format (package/class) -->
|
||||
<string name="config_remoteCopyPackage" translatable="false"></string>
|
||||
|
||||
@@ -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] -->
|
||||
<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] -->
|
||||
<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]-->
|
||||
<string name="screenrecord_name">Screen Recorder</string>
|
||||
|
||||
@@ -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_STORAGE;
|
||||
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.REGULAR_SMART_ACTIONS;
|
||||
import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType;
|
||||
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.app.Notification;
|
||||
@@ -155,7 +154,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
CompletableFuture<List<Notification.Action>> smartActionsFuture =
|
||||
mScreenshotSmartActions.getSmartActionsFuture(
|
||||
mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS,
|
||||
mScreenshotId, uri, image, mSmartActionsProvider,
|
||||
ScreenshotSmartActionType.REGULAR_SMART_ACTIONS,
|
||||
smartActionsEnabled, user);
|
||||
List<Notification.Action> smartActions = new ArrayList<>();
|
||||
if (smartActionsEnabled) {
|
||||
@@ -166,7 +166,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
|
||||
smartActions.addAll(buildSmartActions(
|
||||
mScreenshotSmartActions.getSmartActions(
|
||||
mScreenshotId, smartActionsFuture, timeoutMs,
|
||||
mSmartActionsProvider, REGULAR_SMART_ACTIONS),
|
||||
mSmartActionsProvider,
|
||||
ScreenshotSmartActionType.REGULAR_SMART_ACTIONS),
|
||||
mContext));
|
||||
}
|
||||
|
||||
@@ -476,7 +477,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
|
||||
CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
|
||||
mScreenshotSmartActions.getSmartActionsFuture(
|
||||
mScreenshotId, null, image, mSmartActionsProvider,
|
||||
QUICK_SHARE_ACTION,
|
||||
ScreenshotSmartActionType.QUICK_SHARE_ACTION,
|
||||
true /* smartActionsEnabled */, user);
|
||||
int timeoutMs = DeviceConfig.getInt(
|
||||
DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||
@@ -485,7 +486,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
|
||||
List<Notification.Action> quickShareActions =
|
||||
mScreenshotSmartActions.getSmartActions(
|
||||
mScreenshotId, quickShareActionsFuture, timeoutMs,
|
||||
mSmartActionsProvider, QUICK_SHARE_ACTION);
|
||||
mSmartActionsProvider,
|
||||
ScreenshotSmartActionType.QUICK_SHARE_ACTION);
|
||||
if (!quickShareActions.isEmpty()) {
|
||||
mQuickShareData.quickShareAction = quickShareActions.get(0);
|
||||
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
|
||||
|
||||
@@ -280,6 +280,7 @@ public class ScreenshotController {
|
||||
private final TimeoutHandler mScreenshotHandler;
|
||||
private final ActionIntentExecutor mActionExecutor;
|
||||
private final UserManager mUserManager;
|
||||
private final WorkProfileMessageController mWorkProfileMessageController;
|
||||
|
||||
private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
|
||||
if (DEBUG_INPUT) {
|
||||
@@ -326,7 +327,8 @@ public class ScreenshotController {
|
||||
BroadcastSender broadcastSender,
|
||||
ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
|
||||
ActionIntentExecutor actionExecutor,
|
||||
UserManager userManager
|
||||
UserManager userManager,
|
||||
WorkProfileMessageController workProfileMessageController
|
||||
) {
|
||||
mScreenshotSmartActions = screenshotSmartActions;
|
||||
mNotificationsController = screenshotNotificationsController;
|
||||
@@ -358,6 +360,7 @@ public class ScreenshotController {
|
||||
mFlags = flags;
|
||||
mActionExecutor = actionExecutor;
|
||||
mUserManager = userManager;
|
||||
mWorkProfileMessageController = workProfileMessageController;
|
||||
|
||||
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
|
||||
|
||||
@@ -679,7 +682,6 @@ public class ScreenshotController {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
|
||||
mScreenshotView.badgeScreenshot(
|
||||
mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
|
||||
@@ -780,9 +782,9 @@ public class ScreenshotController {
|
||||
mLongScreenshotHolder.setLongScreenshot(longScreenshot);
|
||||
mLongScreenshotHolder.setTransitionDestinationCallback(
|
||||
(transitionDestination, onTransitionEnd) -> {
|
||||
mScreenshotView.startLongScreenshotTransition(
|
||||
transitionDestination, onTransitionEnd,
|
||||
longScreenshot);
|
||||
mScreenshotView.startLongScreenshotTransition(
|
||||
transitionDestination, onTransitionEnd,
|
||||
longScreenshot);
|
||||
// TODO: Do this via ActionIntentExecutor instead.
|
||||
mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
@@ -1033,10 +1035,8 @@ public class ScreenshotController {
|
||||
|
||||
private void doPostAnimation(ScreenshotController.SavedImageData imageData) {
|
||||
mScreenshotView.setChipIntents(imageData);
|
||||
if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
|
||||
&& mUserManager.isManagedProfile(imageData.owner.getIdentifier())) {
|
||||
// TODO: Read app from configuration
|
||||
mScreenshotView.showWorkProfileMessage("Files");
|
||||
if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
|
||||
mWorkProfileMessageController.onScreenshotTaken(imageData.owner, mScreenshotView);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
@@ -100,7 +101,8 @@ import java.util.ArrayList;
|
||||
* Handles the visual elements and animations for the screenshot flow.
|
||||
*/
|
||||
public class ScreenshotView extends FrameLayout implements
|
||||
ViewTreeObserver.OnComputeInternalInsetsListener {
|
||||
ViewTreeObserver.OnComputeInternalInsetsListener,
|
||||
WorkProfileMessageController.WorkProfileMessageDisplay {
|
||||
|
||||
interface ScreenshotViewCallback {
|
||||
void onUserInteraction();
|
||||
@@ -351,13 +353,23 @@ public class ScreenshotView extends FrameLayout implements
|
||||
* 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
|
||||
*/
|
||||
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(
|
||||
mContext.getString(R.string.screenshot_work_profile_notification, appName));
|
||||
mMessageContainer.setVisibility(VISIBLE);
|
||||
mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> {
|
||||
mMessageContainer.setVisibility(View.GONE);
|
||||
onDismiss.run();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user