From 240e4cde69ebd10432d011e7f904f05807d6f77a Mon Sep 17 00:00:00 2001 From: Ajinkya Chalke Date: Mon, 15 May 2023 20:07:10 +0100 Subject: [PATCH] Update AppClips to handle multi-user. - All NoteTask components needing to interact with the Bubbles APIs should use the new NoteTaskBubbleService if there is a chance that the component can be launched as a non system user. - As a side effect of this change, the shortcut/widget code is also simplified. - Used the singleUser manifest attribute to always only start the services under the correct SysUI user as an extra safeguard. Bug: 282763432 Test: atest SystemUITests Change-Id: I520c1881e550e1aa8444b9177248913843e56720 --- .../internal/statusbar/IAppClipsService.aidl | 4 +- packages/SystemUI/AndroidManifest.xml | 25 +-- .../notetask/INoteTaskBubblesService.aidl | 29 +++ .../notetask/NoteTaskBubblesController.kt | 138 ++++++++++++ .../systemui/notetask/NoteTaskController.kt | 37 ++-- .../systemui/notetask/NoteTaskModule.kt | 7 +- .../shortcut/LaunchNoteTaskActivity.kt | 49 +---- ...unchNoteTaskManagedProfileProxyActivity.kt | 67 ------ .../appclips/AppClipsCrossProcessHelper.java | 7 +- .../AppClipsScreenshotHelperService.java | 5 + .../screenshot/appclips/AppClipsService.java | 102 +++------ .../appclips/AppClipsTrampolineActivity.java | 198 +++++++++--------- packages/SystemUI/tests/AndroidManifest.xml | 6 - .../notetask/FakeNoteTaskBubbleController.kt | 46 ++++ .../notetask/NoteTaskBubblesServiceTest.kt | 73 +++++++ .../notetask/NoteTaskControllerTest.kt | 55 +++-- .../shortcut/LaunchNoteTaskActivityTest.kt | 44 +--- ...NoteTaskManagedProfileProxyActivityTest.kt | 111 ---------- .../appclips/AppClipsServiceTest.java | 132 ++++++++---- .../AppClipsTrampolineActivityTest.java | 142 ++++--------- 20 files changed, 611 insertions(+), 666 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl create mode 100644 packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt diff --git a/core/java/com/android/internal/statusbar/IAppClipsService.aidl b/core/java/com/android/internal/statusbar/IAppClipsService.aidl index 013d0d32e7a2a..d6ab8bcdde20d 100644 --- a/core/java/com/android/internal/statusbar/IAppClipsService.aidl +++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl @@ -23,4 +23,6 @@ package com.android.internal.statusbar; */ interface IAppClipsService { boolean canLaunchCaptureContentActivityForNote(in int taskId); -} \ No newline at end of file + + int canLaunchCaptureContentActivityForNoteInternal(in int taskId); +} diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index fe90caf2646c8..b661ba485e73a 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -451,12 +451,14 @@ android:noHistory="true" /> + android:exported="false" + android:singleUser="true" + android:permission="com.android.systemui.permission.SELF" /> + android:exported="true" + android:singleUser="true" + android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" /> @@ -990,6 +992,11 @@ + + - - - Note: This class is made overridable so that a fake can be created for as mocking suspending + * functions is not supported by the Android tree's version of mockito. + */ +@SysUISingleton +open class NoteTaskBubblesController +@Inject +constructor( + @Application private val context: Context, + @Background private val bgDispatcher: CoroutineDispatcher +) { + + private val serviceConnector: ServiceConnector = + ServiceConnector.Impl( + context, + Intent(context, NoteTaskBubblesService::class.java), + Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE, + UserHandle.USER_SYSTEM, + INoteTaskBubblesService.Stub::asInterface + ) + + /** Returns whether notes app bubble is supported. */ + open suspend fun areBubblesAvailable(): Boolean = + withContext(bgDispatcher) { + suspendCoroutine { continuation -> + serviceConnector + .postForResult { it.areBubblesAvailable() } + .whenComplete { available, error -> + if (error != null) { + debugLog(error = error) { "Failed to query Bubbles as system user." } + } + continuation.resume(available ?: false) + } + } + } + + /** Calls the [Bubbles.showOrHideAppBubble] API as [UserHandle.USER_SYSTEM]. */ + open suspend fun showOrHideAppBubble( + intent: Intent, + userHandle: UserHandle, + icon: Icon + ) { + withContext(bgDispatcher) { + serviceConnector + .post { it.showOrHideAppBubble(intent, userHandle, icon) } + .whenComplete { _, error -> + if (error != null) { + debugLog(error = error) { + "Failed to show notes app bubble for intent $intent, " + + "user $userHandle, and icon $icon." + } + } else { + debugLog { + "Call to show notes app bubble for intent $intent, " + + "user $userHandle, and icon $icon successful." + } + } + } + } + } + + /** + * A helper service to call [Bubbles] APIs that should always be called from the system user + * instance of SysUI. + * + *

Note: This service always runs in the SysUI process running on the system user + * irrespective of which user started the service. This is required so that the correct instance + * of {@link Bubbles} is injected. This is set via attribute {@code android:singleUser=”true”} + * in AndroidManifest. + */ + class NoteTaskBubblesService + @Inject + constructor(private val mOptionalBubbles: Optional) : Service() { + + override fun onBind(intent: Intent): IBinder { + return object : INoteTaskBubblesService.Stub() { + override fun areBubblesAvailable() = mOptionalBubbles.isPresent + + override fun showOrHideAppBubble( + intent: Intent, + userHandle: UserHandle, + icon: Icon + ) { + mOptionalBubbles.ifPresentOrElse( + { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) }, + { + debugLog { + "Failed to show or hide bubble for intent $intent," + + "user $user, and icon $icon as bubble is empty." + } + } + ) + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index c84e0067c2e84..7627c0c22d1ef 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -38,22 +38,22 @@ import android.widget.Toast import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity -import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity import com.android.systemui.settings.UserTracker import com.android.systemui.shared.system.ActivityManagerKt.isInForeground import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.settings.SecureSettings import com.android.wm.shell.bubbles.Bubble -import com.android.wm.shell.bubbles.Bubbles import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener -import java.util.Optional import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Entry point for creating and managing note. @@ -71,7 +71,7 @@ constructor( private val shortcutManager: ShortcutManager, private val resolver: NoteTaskInfoResolver, private val eventLogger: NoteTaskEventLogger, - private val optionalBubbles: Optional, + private val noteTaskBubblesController: NoteTaskBubblesController, private val userManager: UserManager, private val keyguardManager: KeyguardManager, private val activityManager: ActivityManager, @@ -79,6 +79,7 @@ constructor( private val devicePolicyManager: DevicePolicyManager, private val userTracker: UserTracker, private val secureSettings: SecureSettings, + @Application private val applicationScope: CoroutineScope ) { @VisibleForTesting val infoReference = AtomicReference() @@ -103,18 +104,6 @@ constructor( } } - /** Starts [LaunchNoteTaskProxyActivity] on the given [user]. */ - fun startNoteTaskProxyActivityForUser(user: UserHandle) { - context.startActivityAsUser( - Intent().apply { - component = - ComponentName(context, LaunchNoteTaskManagedProfileProxyActivity::class.java) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }, - user - ) - } - /** Starts the notes role setting. */ fun startNotesRoleSetting(activityContext: Context, entryPoint: NoteTaskEntryPoint?) { val user = @@ -178,7 +167,19 @@ constructor( ) { if (!isEnabled) return - val bubbles = optionalBubbles.getOrNull() ?: return + applicationScope.launch { awaitShowNoteTaskAsUser(entryPoint, user) } + } + + private suspend fun awaitShowNoteTaskAsUser( + entryPoint: NoteTaskEntryPoint, + user: UserHandle, + ) { + if (!isEnabled) return + + if (!noteTaskBubblesController.areBubblesAvailable()) { + debugLog { "Bubbles not available in the system user SysUI instance" } + return + } // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. if (!userManager.isUserUnlocked) return @@ -213,7 +214,7 @@ constructor( val intent = createNoteTaskIntent(info) val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) - bubbles.showOrHideAppBubble(intent, user, icon) + noteTaskBubblesController.showOrHideAppBubble(intent, user, icon) // App bubble logging happens on `onBubbleExpandChanged`. debugLog { "onShowNoteTask - opened as app bubble: $info" } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index 109cfeec07233..c0e688f0f82fa 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -26,7 +26,6 @@ import com.android.systemui.flags.Flags import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity -import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity import dagger.Binds import dagger.Module import dagger.Provides @@ -40,12 +39,12 @@ interface NoteTaskModule { @[Binds IntoMap ClassKey(NoteTaskControllerUpdateService::class)] fun NoteTaskControllerUpdateService.bindNoteTaskControllerUpdateService(): Service + @[Binds IntoMap ClassKey(NoteTaskBubblesController.NoteTaskBubblesService::class)] + fun NoteTaskBubblesController.NoteTaskBubblesService.bindNoteTaskBubblesService(): Service + @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)] fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity - @[Binds IntoMap ClassKey(LaunchNoteTaskManagedProfileProxyActivity::class)] - fun LaunchNoteTaskManagedProfileProxyActivity.bindNoteTaskLauncherProxyActivity(): Activity - @[Binds IntoMap ClassKey(LaunchNotesRoleSettingsTrampolineActivity::class)] fun LaunchNotesRoleSettingsTrampolineActivity.bindLaunchNotesRoleSettingsTrampolineActivity(): Activity diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt index 8ca13b9776bb8..493330af56ce8 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -19,61 +19,18 @@ package com.android.systemui.notetask.shortcut import android.content.Context import android.content.Intent import android.os.Bundle -import android.os.UserHandle -import android.os.UserManager import androidx.activity.ComponentActivity -import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.UserTracker import javax.inject.Inject /** Activity responsible for launching the note experience, and finish. */ -class LaunchNoteTaskActivity -@Inject -constructor( - private val controller: NoteTaskController, - private val userManager: UserManager, - private val userTracker: UserTracker, -) : ComponentActivity() { +class LaunchNoteTaskActivity @Inject constructor(private val controller: NoteTaskController) : + ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - // Under the hood, notes app shortcuts are shown in a floating window, called Bubble. - // Bubble API is only available in the main user but not work profile. - // - // On devices with work profile (WP), SystemUI provides both personal notes app shortcuts & - // work profile notes app shortcuts. In order to make work profile notes app shortcuts to - // show in Bubble, a few redirections across users are required: - // 1. When `LaunchNoteTaskActivity` is started in the work profile user, we launch - // `LaunchNoteTaskManagedProfileProxyActivity` on the main user, which has access to the - // Bubble API. - // 2. `LaunchNoteTaskManagedProfileProxyActivity` calls `Bubble#showOrHideAppBubble` with - // the work profile user ID. - // 3. Bubble renders the work profile notes app activity in a floating window, which is - // hosted in the main user. - // - // WP main user - // ------------------------ ------------------------------------------- - // | LaunchNoteTaskActivity | -> | LaunchNoteTaskManagedProfileProxyActivity | - // ------------------------ ------------------------------------------- - // | - // main user | - // ---------------------------- | - // | Bubble#showOrHideAppBubble | <-------------- - // | (with WP user ID) | - // ---------------------------- - val mainUser: UserHandle? = userManager.mainUser - if (userManager.isManagedProfile) { - if (mainUser == null) { - debugLog { "Can't find the main user. Skipping the notes app launch." } - } else { - controller.startNoteTaskProxyActivityForUser(mainUser) - } - } else { - controller.showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT) - } + controller.showNoteTaskAsUser(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT, user) finish() } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt deleted file mode 100644 index 3259b0dcc53dd..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.notetask.shortcut - -import android.os.Build -import android.os.Bundle -import android.os.UserManager -import android.util.Log -import androidx.activity.ComponentActivity -import com.android.systemui.notetask.NoteTaskController -import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.UserTracker -import javax.inject.Inject - -/** - * An internal proxy activity that starts notes app in the work profile. - * - * If there is no work profile, this activity finishes gracefully. - * - * This activity MUST NOT be exported because that would expose the INTERACT_ACROSS_USER privilege - * to any apps. - */ -class LaunchNoteTaskManagedProfileProxyActivity -@Inject -constructor( - private val controller: NoteTaskController, - private val userTracker: UserTracker, - private val userManager: UserManager, -) : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val managedProfileUser = - userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) } - - if (managedProfileUser == null) { - logDebug { "Fail to find the work profile user." } - } else { - controller.showNoteTaskAsUser( - entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT, - user = managedProfileUser.userHandle - ) - } - finish() - } -} - -private inline fun logDebug(message: () -> String) { - if (Build.IS_DEBUGGABLE) { - Log.d(NoteTaskController.TAG, message()) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java index afc8bff91766b..7de22b1a9c775 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java @@ -19,7 +19,7 @@ package com.android.systemui.screenshot.appclips; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.os.UserManager; +import android.os.UserHandle; import androidx.annotation.Nullable; @@ -39,15 +39,14 @@ class AppClipsCrossProcessHelper { private final DisplayTracker mDisplayTracker; @Inject - AppClipsCrossProcessHelper(@Application Context context, UserManager userManager, - DisplayTracker displayTracker) { + AppClipsCrossProcessHelper(@Application Context context, DisplayTracker displayTracker) { // Start a service as main user so that even if the app clips activity is running as work // profile user the service is able to use correct instance of Bubbles to grab a screenshot // excluding the bubble layer. mProxyConnector = new ServiceConnector.Impl<>(context, new Intent(context, AppClipsScreenshotHelperService.class), Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_VISIBLE, userManager.getMainUser().getIdentifier(), + | Context.BIND_NOT_VISIBLE, UserHandle.USER_SYSTEM, IAppClipsScreenshotHelperService.Stub::asInterface); mDisplayTracker = displayTracker; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java index 83ff020362f1a..e0b9f9b7ad936 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java @@ -33,6 +33,11 @@ import javax.inject.Inject; /** * A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its * own separate process take a screenshot. + * + *

Note: This service always runs in the SysUI process running on the system user irrespective of + * which user started the service. This is required so that the correct instance of {@link Bubbles} + * instance is injected. This is set via attribute {@code android:singleUser=”true”} in + * AndroidManifest. */ public class AppClipsScreenshotHelperService extends Service { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java index 394949297d6d6..dce8c81c34626 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java @@ -16,6 +16,11 @@ package com.android.systemui.screenshot.appclips; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; + import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import android.app.Activity; @@ -25,17 +30,12 @@ import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.Intent.CaptureContentForNoteStatusCodes; import android.content.res.Resources; import android.os.IBinder; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.android.internal.infra.AndroidFuture; -import com.android.internal.infra.ServiceConnector; import com.android.internal.statusbar.IAppClipsService; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Application; @@ -43,73 +43,36 @@ import com.android.systemui.flags.FeatureFlags; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; -import java.util.concurrent.ExecutionException; import javax.inject.Inject; /** * A service that communicates with {@link StatusBarManager} to support the - * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API. + * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API. Also used by + * {@link AppClipsTrampolineActivity} to query if an app should be allowed to user App Clips. + * + *

Note: This service always runs in the SysUI process running on the system user irrespective of + * which user started the service. This is required so that the correct instance of {@link Bubbles} + * instance is injected. This is set via attribute {@code android:singleUser=”true”} in + * AndroidManifest. */ public class AppClipsService extends Service { - private static final String TAG = AppClipsService.class.getSimpleName(); - @Application private final Context mContext; private final FeatureFlags mFeatureFlags; private final Optional mOptionalBubbles; private final DevicePolicyManager mDevicePolicyManager; - private final UserManager mUserManager; - private final boolean mAreTaskAndTimeIndependentPrerequisitesMet; - @VisibleForTesting() - @Nullable ServiceConnector mProxyConnectorToMainProfile; - @Inject public AppClipsService(@Application Context context, FeatureFlags featureFlags, - Optional optionalBubbles, DevicePolicyManager devicePolicyManager, - UserManager userManager) { + Optional optionalBubbles, DevicePolicyManager devicePolicyManager) { mContext = context; mFeatureFlags = featureFlags; mOptionalBubbles = optionalBubbles; mDevicePolicyManager = devicePolicyManager; - mUserManager = userManager; - - // The consumer of this service are apps that call through StatusBarManager API to query if - // it can use app clips API. Since these apps can be launched as work profile users, this - // service will start as work profile user. SysUI doesn't share injected instances for - // different users. This is why the bubbles instance injected will be incorrect. As the apps - // don't generally have permission to connect to a service running as different user, we - // start a proxy connection to communicate with the main user's version of this service. - if (mUserManager.isManagedProfile()) { - // No need to check for prerequisites in this case as those are incorrect for work - // profile user instance of the service and the main user version of the service will - // take care of this check. - mAreTaskAndTimeIndependentPrerequisitesMet = false; - - // Get the main user so that we can connect to the main user's version of the service. - UserHandle mainUser = mUserManager.getMainUser(); - if (mainUser == null) { - // If main user is not available there isn't much we can do, no apps can use app - // clips. - return; - } - - // Set up the connection to be used later during onBind callback. - mProxyConnectorToMainProfile = - new ServiceConnector.Impl<>( - context, - new Intent(context, AppClipsService.class), - Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_VISIBLE, - mainUser.getIdentifier(), - IAppClipsService.Stub::asInterface); - return; - } mAreTaskAndTimeIndependentPrerequisitesMet = checkIndependentVariables(); - mProxyConnectorToMainProfile = null; } private boolean checkIndependentVariables() { @@ -144,40 +107,25 @@ public class AppClipsService extends Service { return new IAppClipsService.Stub() { @Override public boolean canLaunchCaptureContentActivityForNote(int taskId) { - // In case of managed profile, use the main user's instance of the service. Callers - // cannot directly connect to the main user's instance as they may not have the - // permission to interact across users. - if (mUserManager.isManagedProfile()) { - return canLaunchCaptureContentActivityForNoteFromMainUser(taskId); - } + return canLaunchCaptureContentActivityForNoteInternal(taskId) + == CAPTURE_CONTENT_FOR_NOTE_SUCCESS; + } + @Override + @CaptureContentForNoteStatusCodes + public int canLaunchCaptureContentActivityForNoteInternal(int taskId) { if (!mAreTaskAndTimeIndependentPrerequisitesMet) { - return false; + return CAPTURE_CONTENT_FOR_NOTE_FAILED; } if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) { - return false; + return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; } - return !mDevicePolicyManager.getScreenCaptureDisabled(null); + return mDevicePolicyManager.getScreenCaptureDisabled(null) + ? CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN + : CAPTURE_CONTENT_FOR_NOTE_SUCCESS; } }; } - - /** Returns whether the app clips API can be used by querying the service as the main user. */ - private boolean canLaunchCaptureContentActivityForNoteFromMainUser(int taskId) { - if (mProxyConnectorToMainProfile == null) { - return false; - } - - try { - AndroidFuture future = mProxyConnectorToMainProfile.postForResult( - service -> service.canLaunchCaptureContentActivityForNote(taskId)); - return future.get(); - } catch (ExecutionException | InterruptedException e) { - Log.d(TAG, "Exception from service\n" + e); - } - - return false; - } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index f00803c6d64bc..6e5cef47400fe 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -22,41 +22,41 @@ import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; -import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import android.app.Activity; -import android.app.admin.DevicePolicyManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.Intent.CaptureContentForNoteStatusCodes; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.UserInfo; -import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.ResultReceiver; import android.os.UserHandle; -import android.os.UserManager; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.internal.infra.AndroidFuture; +import com.android.internal.infra.ServiceConnector; import com.android.internal.logging.UiEventLogger; +import com.android.internal.statusbar.IAppClipsService; import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.notetask.NoteTaskController; import com.android.systemui.notetask.NoteTaskEntryPoint; -import com.android.systemui.settings.UserTracker; -import com.android.wm.shell.bubbles.Bubbles; -import java.util.Optional; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -82,39 +82,57 @@ public class AppClipsTrampolineActivity extends Activity { private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName(); static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI"; - @VisibleForTesting - static final String EXTRA_USE_WP_USER = TAG + "USE_WP_USER"; static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE"; static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER"; static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME"; private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0); - private final DevicePolicyManager mDevicePolicyManager; - private final FeatureFlags mFeatureFlags; - private final Optional mOptionalBubbles; private final NoteTaskController mNoteTaskController; private final PackageManager mPackageManager; - private final UserTracker mUserTracker; private final UiEventLogger mUiEventLogger; - private final UserManager mUserManager; + private final BroadcastSender mBroadcastSender; + @Background + private final Executor mBgExecutor; + @Main + private final Executor mMainExecutor; private final ResultReceiver mResultReceiver; + private final ServiceConnector mAppClipsServiceConnector; + + private UserHandle mUserHandle; private Intent mKillAppClipsBroadcastIntent; - private UserHandle mNotesAppUser; @Inject - public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags, - Optional optionalBubbles, NoteTaskController noteTaskController, - PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger, - UserManager userManager, @Main Handler mainHandler) { - mDevicePolicyManager = devicePolicyManager; - mFeatureFlags = flags; - mOptionalBubbles = optionalBubbles; + public AppClipsTrampolineActivity(@Application Context context, + NoteTaskController noteTaskController, PackageManager packageManager, + UiEventLogger uiEventLogger, BroadcastSender broadcastSender, + @Background Executor bgExecutor, @Main Executor mainExecutor, + @Main Handler mainHandler) { mNoteTaskController = noteTaskController; mPackageManager = packageManager; - mUserTracker = userTracker; mUiEventLogger = uiEventLogger; - mUserManager = userManager; + mBroadcastSender = broadcastSender; + mBgExecutor = bgExecutor; + mMainExecutor = mainExecutor; + + mResultReceiver = createResultReceiver(mainHandler); + mAppClipsServiceConnector = createServiceConnector(context); + } + + /** A constructor used only for testing to verify interactions with {@link ServiceConnector}. */ + @VisibleForTesting + AppClipsTrampolineActivity(ServiceConnector appClipsServiceConnector, + NoteTaskController noteTaskController, PackageManager packageManager, + UiEventLogger uiEventLogger, BroadcastSender broadcastSender, + @Background Executor bgExecutor, @Main Executor mainExecutor, + @Main Handler mainHandler) { + mAppClipsServiceConnector = appClipsServiceConnector; + mNoteTaskController = noteTaskController; + mPackageManager = packageManager; + mUiEventLogger = uiEventLogger; + mBroadcastSender = broadcastSender; + mBgExecutor = bgExecutor; + mMainExecutor = mainExecutor; mResultReceiver = createResultReceiver(mainHandler); } @@ -127,62 +145,62 @@ public class AppClipsTrampolineActivity extends Activity { return; } - if (mUserManager.isManagedProfile()) { - maybeStartActivityForWPUser(); - finish(); + mUserHandle = getUser(); + + mBgExecutor.execute(() -> { + AndroidFuture statusCodeFuture = mAppClipsServiceConnector.postForResult( + service -> service.canLaunchCaptureContentActivityForNoteInternal(getTaskId())); + statusCodeFuture.whenCompleteAsync(this::handleAppClipsStatusCode, mMainExecutor); + }); + } + + @Override + protected void onDestroy() { + if (isFinishing() && mKillAppClipsBroadcastIntent != null) { + mBroadcastSender.sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF); + } + + super.onDestroy(); + } + + private void handleAppClipsStatusCode(@CaptureContentForNoteStatusCodes int statusCode, + Throwable error) { + if (isFinishing()) { + // It's too late, trampoline activity is finishing or already finished. Return early. return; } - if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) { - finish(); + if (error != null) { + Log.d(TAG, "Error querying app clips service", error); + setErrorResultAndFinish(statusCode); return; } - if (mOptionalBubbles.isEmpty()) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); - return; - } + switch (statusCode) { + case CAPTURE_CONTENT_FOR_NOTE_SUCCESS: + launchAppClipsActivity(); + break; - if (!mOptionalBubbles.get().isAppBubbleTaskId(getTaskId())) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); - return; + case CAPTURE_CONTENT_FOR_NOTE_FAILED: + case CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED: + case CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN: + default: + setErrorResultAndFinish(statusCode); } + } - if (mDevicePolicyManager.getScreenCaptureDisabled(null)) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); - return; - } - - ComponentName componentName; - try { - componentName = ComponentName.unflattenFromString( + private void launchAppClipsActivity() { + ComponentName componentName = ComponentName.unflattenFromString( getString(R.string.config_screenshotAppClipsActivityComponent)); - } catch (Resources.NotFoundException e) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); - return; - } - - if (componentName == null || componentName.getPackageName().isEmpty() - || componentName.getClassName().isEmpty()) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); - return; - } - - mNotesAppUser = getUser(); - if (getIntent().getBooleanExtra(EXTRA_USE_WP_USER, /* defaultValue= */ false)) { - // Get the work profile user internally instead of passing around via intent extras as - // this activity is exported apps could potentially mess around with intent extras. - mNotesAppUser = getWorkProfileUser().orElse(mNotesAppUser); - } - String callingPackageName = getCallingPackage(); - Intent intent = new Intent().setComponent(componentName) + + Intent intent = new Intent() + .setComponent(componentName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver) .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName); try { - // Start the App Clips activity for the user corresponding to the notes app user. - startActivityAsUser(intent, mNotesAppUser); + startActivity(intent); // Set up the broadcast intent that will inform the above App Clips activity to finish // when this trampoline activity is finished. @@ -198,39 +216,6 @@ public class AppClipsTrampolineActivity extends Activity { } } - @Override - protected void onDestroy() { - super.onDestroy(); - - if (isFinishing() && mKillAppClipsBroadcastIntent != null) { - sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF); - } - } - - private Optional getWorkProfileUser() { - return mUserTracker.getUserProfiles().stream() - .filter(profile -> mUserManager.isManagedProfile(profile.id)) - .findFirst() - .map(UserInfo::getUserHandle); - } - - private void maybeStartActivityForWPUser() { - UserHandle mainUser = mUserManager.getMainUser(); - if (mainUser == null) { - setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); - return; - } - - // Start the activity as the main user with activity result forwarding. Set the intent extra - // so that the newly started trampoline activity starts the actual app clips activity as the - // work profile user. Starting the app clips activity as the work profile user is required - // to save the screenshot in work profile user storage and grant read permission to the URI. - startActivityAsUser( - new Intent(this, AppClipsTrampolineActivity.class) - .putExtra(EXTRA_USE_WP_USER, /* value= */ true) - .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser); - } - private void setErrorResultAndFinish(int errorCode) { setResult(RESULT_OK, new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode)); @@ -241,7 +226,7 @@ public class AppClipsTrampolineActivity extends Activity { int callingPackageUid = 0; try { callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName, - APPLICATION_INFO_FLAGS, mNotesAppUser.getIdentifier()).uid; + APPLICATION_INFO_FLAGS, mUserHandle.getIdentifier()).uid; } catch (NameNotFoundException e) { Log.d(TAG, "Couldn't find notes app UID " + e); } @@ -281,7 +266,7 @@ public class AppClipsTrampolineActivity extends Activity { mKillAppClipsBroadcastIntent = null; // Expand the note bubble before returning the result. - mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mNotesAppUser); + mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mUserHandle); setResult(RESULT_OK, convertedData); finish(); } @@ -298,11 +283,18 @@ public class AppClipsTrampolineActivity extends Activity { appClipsResultReceiver.writeToParcel(parcel, 0); parcel.setDataPosition(0); - ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel); + ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel); parcel.recycle(); return resultReceiver; } + private ServiceConnector createServiceConnector( + @Application Context context) { + return new ServiceConnector.Impl<>(context, new Intent(context, AppClipsService.class), + Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | Context.BIND_NOT_VISIBLE, + UserHandle.USER_SYSTEM, IAppClipsService.Stub::asInterface); + } + /** This is a test only API for mocking response from {@link AppClipsActivity}. */ @VisibleForTesting public ResultReceiver getResultReceiverForTest() { diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index 080be6d8cf25a..12da17f6a3012 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -191,12 +191,6 @@ android:permission="com.android.systemui.permission.SELF" android:excludeFromRecents="true" /> - - +) : NoteTaskBubblesController(unUsed1, unsUsed2) { + override suspend fun areBubblesAvailable() = optionalBubbles.isPresent + + override suspend fun showOrHideAppBubble(intent: Intent, userHandle: UserHandle, icon: Icon) { + optionalBubbles.ifPresentOrElse( + { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) }, + { throw IllegalAccessException() } + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt new file mode 100644 index 0000000000000..baac9e0202803 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt @@ -0,0 +1,73 @@ +/* + * 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.notetask + +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.UserHandle +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService +import com.android.wm.shell.bubbles.Bubbles +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** atest SystemUITests:NoteTaskBubblesServiceTest */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskBubblesServiceTest : SysuiTestCase() { + + @Mock private lateinit var bubbles: Bubbles + + private fun createServiceBinder(bubbles: Bubbles? = this.bubbles) = + NoteTaskBubblesService(Optional.ofNullable(bubbles)).onBind(Intent()) + as INoteTaskBubblesService + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun areBubblesAvailable_bubblesNotNull_shouldReturnTrue() { + assertThat(createServiceBinder().areBubblesAvailable()).isTrue() + } + + @Test + fun areBubblesAvailable_bubblesNull_shouldReturnFalse() { + assertThat(createServiceBinder(bubbles = null).areBubblesAvailable()).isFalse() + } + + @Test + fun showOrHideAppBubble() { + val intent = Intent() + val user = UserHandle.SYSTEM + val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) + + createServiceBinder().showOrHideAppBubble(intent, user, icon) + + verify(bubbles).showOrHideAppBubble(intent, user, icon) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 967e44bb6b848..8b070ef875042 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -57,7 +57,6 @@ import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity -import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity import com.android.systemui.settings.FakeUserTracker import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -69,11 +68,15 @@ import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.Bubbles import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.doNothing import org.mockito.Mockito.never import org.mockito.Mockito.spy @@ -82,6 +85,7 @@ import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations /** atest SystemUITests:NoteTaskControllerTest */ +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) internal class NoteTaskControllerTest : SysuiTestCase() { @@ -101,6 +105,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var secureSettings: SecureSettings private val userTracker = FakeUserTracker() + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun setUp() { @@ -136,7 +142,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { context = context, resolver = resolver, eventLogger = eventLogger, - optionalBubbles = Optional.ofNullable(bubbles), userManager = userManager, keyguardManager = keyguardManager, isEnabled = isEnabled, @@ -146,6 +151,9 @@ internal class NoteTaskControllerTest : SysuiTestCase() { shortcutManager = shortcutManager, activityManager = activityManager, secureSettings = secureSettings, + noteTaskBubblesController = + FakeNoteTaskBubbleController(context, testDispatcher, Optional.ofNullable(bubbles)), + applicationScope = testScope, ) // region onBubbleExpandChanged @@ -161,7 +169,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) verify(eventLogger).logNoteTaskOpened(expectedInfo) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager) + verifyZeroInteractions(bubbles, keyguardManager, userManager) } @Test @@ -176,7 +184,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) verify(eventLogger).logNoteTaskClosed(expectedInfo) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager) + verifyZeroInteractions(bubbles, keyguardManager, userManager) } @Test @@ -190,7 +198,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } @Test @@ -204,7 +212,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user), ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } @Test @@ -215,7 +223,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = "any other key", ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } @Test @@ -226,7 +234,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { key = Bubble.getAppBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user), ) - verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger) + verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger) } // endregion @@ -352,7 +360,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) // Context package name used to create bubble icon from drawable resource id - verify(context).packageName + verify(context, atLeastOnce()).packageName verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) verifyZeroInteractions(eventLogger) } @@ -361,7 +369,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun showNoteTask_bubblesIsNull_shouldDoNothing() { createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -373,14 +381,14 @@ internal class NoteTaskControllerTest : SysuiTestCase() { noteTaskController.showNoteTask(entryPoint = TAIL_BUTTON) verify(noteTaskController).showNoDefaultNotesAppToast() - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test fun showNoteTask_flagDisabled_shouldDoNothing() { createNoteTaskController(isEnabled = false).showNoteTask(entryPoint = TAIL_BUTTON) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -389,7 +397,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -509,7 +517,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -525,7 +533,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE) - verifyZeroInteractions(context, bubbles, eventLogger) + verifyZeroInteractions(bubbles, eventLogger) } @Test @@ -637,7 +645,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController(isEnabled = true).onRoleHoldersChanged("NOT_NOTES", user) - verifyZeroInteractions(context) + verify(context, never()).startActivityAsUser(any(), any()) } @Test @@ -733,21 +741,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } // endregion - // startregion startNoteTaskProxyActivityForUser - @Test - fun startNoteTaskProxyActivityForUser_shouldStartLaunchNoteTaskProxyActivityWithExpectedUser() { - val user0 = UserHandle.of(0) - createNoteTaskController().startNoteTaskProxyActivityForUser(user0) - - val intentCaptor = argumentCaptor() - verify(context).startActivityAsUser(intentCaptor.capture(), eq(user0)) - assertThat(intentCaptor.value).run { - hasComponentClass(LaunchNoteTaskManagedProfileProxyActivity::class.java) - hasFlags(FLAG_ACTIVITY_NEW_TASK) - } - } - // endregion - // region getUserForHandlingNotesTaking @Test fun getUserForHandlingNotesTaking_cope_quickAffordance_shouldReturnWorkProfileUser() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt index a0c376ff1a1ca..627c4a80e1ecf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt @@ -17,9 +17,6 @@ package com.android.systemui.notetask.shortcut import android.content.Intent -import android.content.pm.UserInfo -import android.os.UserHandle -import android.os.UserManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest @@ -29,17 +26,14 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.verify import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.FakeUserTracker import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -48,8 +42,6 @@ import org.mockito.MockitoAnnotations class LaunchNoteTaskActivityTest : SysuiTestCase() { @Mock lateinit var noteTaskController: NoteTaskController - @Mock lateinit var userManager: UserManager - private val userTracker: FakeUserTracker = FakeUserTracker() @Rule @JvmField @@ -60,8 +52,6 @@ class LaunchNoteTaskActivityTest : SysuiTestCase() { override fun create(intent: Intent?) = LaunchNoteTaskActivity( controller = noteTaskController, - userManager = userManager, - userTracker = userTracker ) }, /* initialTouchMode= */ false, @@ -71,7 +61,6 @@ class LaunchNoteTaskActivityTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true) } @After @@ -83,36 +72,7 @@ class LaunchNoteTaskActivityTest : SysuiTestCase() { fun startActivityOnNonWorkProfileUser_shouldLaunchNoteTask() { activityRule.launchActivity(/* startIntent= */ null) - verify(noteTaskController).showNoteTask(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT)) - } - - @Test - fun startActivityOnWorkProfileUser_shouldLaunchProxyActivity() { - val mainUserHandle: UserHandle = mainUser.userHandle - userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1) - whenever(userManager.isManagedProfile).thenReturn(true) - whenever(userManager.mainUser).thenReturn(mainUserHandle) - - activityRule.launchActivity(/* startIntent= */ null) - - verify(noteTaskController).startNoteTaskProxyActivityForUser(eq(mainUserHandle)) - } - - @Test - fun startActivityOnWorkProfileUser_noMainUser_shouldNotLaunch() { - userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1) - whenever(userManager.isManagedProfile).thenReturn(true) - whenever(userManager.mainUser).thenReturn(null) - - activityRule.launchActivity(/* startIntent= */ null) - - verify(noteTaskController, never()).showNoteTask(any()) - verify(noteTaskController, never()).startNoteTaskProxyActivityForUser(any()) - } - - private companion object { - val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN) - val workProfileUser = - UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE) + verify(noteTaskController) + .showNoteTaskAsUser(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT), any()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt deleted file mode 100644 index 6347c3404348e..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.notetask.shortcut - -import android.content.Intent -import android.content.pm.UserInfo -import android.os.UserHandle -import android.os.UserManager -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import androidx.test.rule.ActivityTestRule -import androidx.test.runner.intercepting.SingleActivityFactory -import com.android.dx.mockito.inline.extended.ExtendedMockito.never -import com.android.dx.mockito.inline.extended.ExtendedMockito.verify -import com.android.systemui.SysuiTestCase -import com.android.systemui.notetask.NoteTaskController -import com.android.systemui.notetask.NoteTaskEntryPoint -import com.android.systemui.settings.FakeUserTracker -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -@TestableLooper.RunWithLooper -class LaunchNoteTaskManagedProfileProxyActivityTest : SysuiTestCase() { - - @Mock lateinit var noteTaskController: NoteTaskController - @Mock lateinit var userManager: UserManager - private val userTracker = FakeUserTracker() - - @Rule - @JvmField - val activityRule = - ActivityTestRule( - /* activityFactory= */ object : - SingleActivityFactory( - LaunchNoteTaskManagedProfileProxyActivity::class.java - ) { - override fun create(intent: Intent?) = - LaunchNoteTaskManagedProfileProxyActivity( - controller = noteTaskController, - userManager = userManager, - userTracker = userTracker - ) - }, - /* initialTouchMode= */ false, - /* launchActivity= */ false, - ) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true) - } - - @After - fun tearDown() { - activityRule.finishActivity() - } - - @Test - fun startActivity_noWorkProfileUser_shouldNotLaunchNoteTask() { - userTracker.set(listOf(mainUser), selectedUserIndex = 0) - activityRule.launchActivity(/* startIntent= */ null) - - verify(noteTaskController, never()).showNoteTaskAsUser(any(), any()) - } - - @Test - fun startActivity_hasWorkProfileUser_shouldLaunchNoteTaskOnTheWorkProfileUser() { - userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUser)) - activityRule.launchActivity(/* startIntent= */ null) - - val workProfileUserHandle: UserHandle = workProfileUser.userHandle - verify(noteTaskController) - .showNoteTaskAsUser( - eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT), - eq(workProfileUserHandle) - ) - } - - private companion object { - val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN) - val workProfileUser = - UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE) - val mainAndWorkProfileUsers = listOf(mainUser, workProfileUser) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java index 67b1099c1e0a3..d8897e9048c32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java @@ -16,14 +16,17 @@ package com.android.systemui.screenshot.appclips; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; +import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; + import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.admin.DevicePolicyManager; @@ -31,8 +34,6 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; import androidx.test.runner.AndroidJUnit4; @@ -46,7 +47,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -63,7 +63,6 @@ public final class AppClipsServiceTest extends SysuiTestCase { @Mock private Optional mOptionalBubbles; @Mock private Bubbles mBubbles; @Mock private DevicePolicyManager mDevicePolicyManager; - @Mock private UserManager mUserManager; private AppClipsService mAppClipsService; @@ -80,51 +79,84 @@ public final class AppClipsServiceTest extends SysuiTestCase { .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } + @Test + public void flagOff_internal_shouldReturnFailed() throws RemoteException { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + } + @Test public void emptyBubbles_shouldReturnFalse() throws RemoteException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(true); + mockForEmptyBubbles(); assertThat(getInterfaceWithRealContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } + @Test + public void emptyBubbles_internal_shouldReturnFailed() throws RemoteException { + mockForEmptyBubbles(); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + } + @Test public void taskIdNotAppBubble_shouldReturnFalse() throws RemoteException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false); + mockForTaskIdNotAppBubble(); assertThat(getInterfaceWithRealContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } + @Test + public void taskIdNotAppBubble_internal_shouldReturnWindowUnsupported() throws RemoteException { + mockForTaskIdNotAppBubble(); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); + } + @Test public void dpmScreenshotBlocked_shouldReturnFalse() throws RemoteException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true); + mockForScreenshotBlocked(); assertThat(getInterfaceWithRealContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } + @Test + public void dpmScreenshotBlocked_internal_shouldReturnBlockedByAdmin() throws RemoteException { + mockForScreenshotBlocked(); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); + } + @Test public void configComponentNameNotValid_shouldReturnFalse() throws RemoteException { - when(mMockContext.getString(anyInt())).thenReturn(EMPTY); - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); + mockForInvalidConfigComponentName(); assertThat(getInterfaceWithMockContext() .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse(); } + @Test + public void configComponentNameNotValid_internal_shouldReturnFailed() throws RemoteException { + mockForInvalidConfigComponentName(); + + assertThat(getInterfaceWithMockContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + } + + @Test public void allPrerequisitesSatisfy_shouldReturnTrue() throws RemoteException { mockToSatisfyAllPrerequisites(); @@ -134,28 +166,44 @@ public final class AppClipsServiceTest extends SysuiTestCase { } @Test - public void isManagedProfile_shouldUseProxyConnection() throws RemoteException { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM); - IAppClipsService service = getInterfaceWithRealContext(); - mAppClipsService.mProxyConnectorToMainProfile = - Mockito.spy(mAppClipsService.mProxyConnectorToMainProfile); + public void allPrerequisitesSatisfy_internal_shouldReturnSuccess() throws RemoteException { + mockToSatisfyAllPrerequisites(); - service.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID); - - verify(mAppClipsService.mProxyConnectorToMainProfile).postForResult(any()); + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID)) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS); } - @Test - public void isManagedProfile_noMainUser_shouldReturnFalse() { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(null); - - getInterfaceWithRealContext(); - - assertThat(mAppClipsService.mProxyConnectorToMainProfile).isNull(); + private void mockForEmptyBubbles() { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(true); } + private void mockForTaskIdNotAppBubble() { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(false); + when(mOptionalBubbles.get()).thenReturn(mBubbles); + when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false); + } + + private void mockForScreenshotBlocked() { + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(false); + when(mOptionalBubbles.get()).thenReturn(mBubbles); + when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); + when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true); + } + + private void mockForInvalidConfigComponentName() { + when(mMockContext.getString(anyInt())).thenReturn(EMPTY); + when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); + when(mOptionalBubbles.isEmpty()).thenReturn(false); + when(mOptionalBubbles.get()).thenReturn(mBubbles); + when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); + when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); + } + + private void mockToSatisfyAllPrerequisites() { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); @@ -166,13 +214,13 @@ public final class AppClipsServiceTest extends SysuiTestCase { private IAppClipsService getInterfaceWithRealContext() { mAppClipsService = new AppClipsService(getContext(), mFeatureFlags, - mOptionalBubbles, mDevicePolicyManager, mUserManager); + mOptionalBubbles, mDevicePolicyManager); return getInterfaceFromService(mAppClipsService); } private IAppClipsService getInterfaceWithMockContext() { mAppClipsService = new AppClipsService(mMockContext, mFeatureFlags, - mOptionalBubbles, mDevicePolicyManager, mUserManager); + mOptionalBubbles, mDevicePolicyManager); return getInterfaceFromService(mAppClipsService); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java index e9007ff84f13e..7fad972d83fcd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java @@ -24,22 +24,19 @@ import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; -import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; +import static com.android.internal.infra.AndroidFuture.completedFuture; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; -import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_USE_WP_USER; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; -import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -52,20 +49,22 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; -import android.os.UserManager; import android.testing.AndroidTestingRunner; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.intercepting.SingleActivityFactory; +import com.android.internal.infra.ServiceConnector; import com.android.internal.logging.UiEventLogger; +import com.android.internal.statusbar.IAppClipsService; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.notetask.NoteTaskController; -import com.android.systemui.settings.UserTracker; -import com.android.wm.shell.bubbles.Bubbles; + +import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Before; @@ -75,8 +74,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.List; -import java.util.Optional; +import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) public final class AppClipsTrampolineActivityTest extends SysuiTestCase { @@ -86,25 +84,19 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { private static final int TEST_UID = 42; private static final String TEST_CALLING_PACKAGE = "test-calling-package"; - @Mock - private DevicePolicyManager mDevicePolicyManager; - @Mock - private FeatureFlags mFeatureFlags; - @Mock - private Optional mOptionalBubbles; - @Mock - private Bubbles mBubbles; + @Mock private ServiceConnector mServiceConnector; @Mock private NoteTaskController mNoteTaskController; @Mock private PackageManager mPackageManager; @Mock - private UserTracker mUserTracker; - @Mock private UiEventLogger mUiEventLogger; @Mock - private UserManager mUserManager; - + private BroadcastSender mBroadcastSender; + @Background + private Executor mBgExecutor; + @Main + private Executor mMainExecutor; @Main private Handler mMainHandler; @@ -114,9 +106,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { new SingleActivityFactory<>(AppClipsTrampolineActivityTestable.class) { @Override protected AppClipsTrampolineActivityTestable create(Intent unUsed) { - return new AppClipsTrampolineActivityTestable(mDevicePolicyManager, - mFeatureFlags, mOptionalBubbles, mNoteTaskController, mPackageManager, - mUserTracker, mUiEventLogger, mUserManager, mMainHandler); + return new AppClipsTrampolineActivityTestable(mServiceConnector, + mNoteTaskController, mPackageManager, mUiEventLogger, mBroadcastSender, + mBgExecutor, mMainExecutor, mMainHandler); } }; @@ -133,6 +125,8 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)); MockitoAnnotations.initMocks(this); + mBgExecutor = MoreExecutors.directExecutor(); + mMainExecutor = MoreExecutors.directExecutor(); mMainHandler = mContext.getMainThreadHandler(); mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class); @@ -169,19 +163,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { } @Test - public void flagOff_shouldFinishWithResultCancel() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false); - - mActivityRule.launchActivity(mActivityIntent); - - assertThat(mActivityRule.getActivityResult().getResultCode()) - .isEqualTo(Activity.RESULT_CANCELED); - } - - @Test - public void bubblesEmpty_shouldFinishWithFailed() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(true); + public void queryService_returnedFailed_shouldFinishWithFailed() { + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_FAILED)); mActivityRule.launchActivity(mActivityIntent); @@ -189,14 +173,13 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test - public void taskIdNotAppBubble_shouldFinishWithWindowModeUnsupported() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(false); + public void queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported() { + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED)); mActivityRule.launchActivity(mActivityIntent); @@ -204,15 +187,13 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test - public void dpmScreenshotBlocked_shouldFinishWithBlockedByAdmin() { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true); + public void queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin() { + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN)); mActivityRule.launchActivity(mActivityIntent); @@ -220,6 +201,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test @@ -240,6 +222,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test @@ -261,6 +244,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(getStatusCodeExtra(actualResult.getResultData())) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS); assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI); + assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); } @Test @@ -274,48 +258,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE); } - @Test - public void startAppClipsActivity_throughWPUser_shouldStartMainUserActivity() - throws NameNotFoundException { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM); - mockToSatisfyAllPrerequisites(); - - AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent); - waitForIdleSync(); - - Intent actualIntent = activity.mStartedIntent; - assertThat(actualIntent.getComponent()).isEqualTo( - new ComponentName(mContext, AppClipsTrampolineActivity.class)); - assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - assertThat(actualIntent.getBooleanExtra(EXTRA_USE_WP_USER, false)).isTrue(); - assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM); - } - - @Test - public void startAppClipsActivity_throughWPUser_noMainUser_shouldFinishWithFailed() - throws NameNotFoundException { - when(mUserManager.isManagedProfile()).thenReturn(true); - when(mUserManager.getMainUser()).thenReturn(null); - - mockToSatisfyAllPrerequisites(); - - mActivityRule.launchActivity(mActivityIntent); - waitForIdleSync(); - - ActivityResult actualResult = mActivityRule.getActivityResult(); - assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); - assertThat(getStatusCodeExtra(actualResult.getResultData())) - .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); - } - private void mockToSatisfyAllPrerequisites() throws NameNotFoundException { - when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); - when(mOptionalBubbles.isEmpty()).thenReturn(false); - when(mOptionalBubbles.get()).thenReturn(mBubbles); - when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true); - when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); - when(mUserTracker.getUserProfiles()).thenReturn(List.of()); + when(mServiceConnector.postForResult(any())) + .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_SUCCESS)); ApplicationInfo testApplicationInfo = new ApplicationInfo(); testApplicationInfo.uid = TEST_UID; @@ -330,17 +275,14 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { Intent mStartedIntent; UserHandle mStartingUser; - public AppClipsTrampolineActivityTestable(DevicePolicyManager devicePolicyManager, - FeatureFlags flags, - Optional optionalBubbles, - NoteTaskController noteTaskController, - PackageManager packageManager, - UserTracker userTracker, - UiEventLogger uiEventLogger, - UserManager userManager, + public AppClipsTrampolineActivityTestable( + ServiceConnector serviceServiceConnector, + NoteTaskController noteTaskController, PackageManager packageManager, + UiEventLogger uiEventLogger, BroadcastSender broadcastSender, + @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler) { - super(devicePolicyManager, flags, optionalBubbles, noteTaskController, packageManager, - userTracker, uiEventLogger, userManager, mainHandler); + super(serviceServiceConnector, noteTaskController, packageManager, uiEventLogger, + broadcastSender, bgExecutor, mainExecutor, mainHandler); } @Override