diff options
| author | 2022-12-28 20:21:55 +0000 | |
|---|---|---|
| committer | 2022-12-29 20:49:37 +0000 | |
| commit | af2116862dee4b4580990934b3bfc609f62b26d7 (patch) | |
| tree | 18dd428a4143006d4917b63c13785a8a152fba8b | |
| parent | de157676364faaff6143d7f6f28466e8c0c81e14 (diff) | |
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
7 files changed, 305 insertions, 18 deletions
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 2d756ae25b3c..0d8fdfc2a90a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -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> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 7a8ffa1863f6..2f1ea87f015f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -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> diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index b4934cf7b804..bf5fbd223186 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index a6447a5bf500..80ab971ea5e5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -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); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index e8ceb521b6b0..899cdb74274f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -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(); }); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt new file mode 100644 index 000000000000..5d7e56f6c98a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt @@ -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" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java new file mode 100644 index 000000000000..bd04b3ccc039 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java @@ -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)); + } +} + |