Merge "Update AppClips to handle multi-user." into udc-qpr-dev

This commit is contained in:
Ajinkya Chalke
2023-05-19 02:35:09 +00:00
committed by Android (Google) Code Review
20 changed files with 611 additions and 666 deletions

View File

@@ -23,4 +23,6 @@ package com.android.internal.statusbar;
*/
interface IAppClipsService {
boolean canLaunchCaptureContentActivityForNote(in int taskId);
}
int canLaunchCaptureContentActivityForNoteInternal(in int taskId);
}

View File

@@ -451,12 +451,14 @@
android:noHistory="true" />
<service android:name=".screenshot.appclips.AppClipsScreenshotHelperService"
android:permission="com.android.systemui.permission.SELF"
android:exported="false" />
android:exported="false"
android:singleUser="true"
android:permission="com.android.systemui.permission.SELF" />
<service android:name=".screenshot.appclips.AppClipsService"
android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
android:exported="true" />
android:exported="true"
android:singleUser="true"
android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" />
<service android:name=".screenrecord.RecordingService"
android:foregroundServiceType="systemExempted"/>
@@ -990,6 +992,11 @@
<service android:name=".notetask.NoteTaskControllerUpdateService" />
<service android:name=".notetask.NoteTaskBubblesController$NoteTaskBubblesService"
android:exported="false"
android:singleUser="true"
android:permission="com.android.systemui.permission.SELF" />
<activity
android:name=".notetask.shortcut.LaunchNoteTaskActivity"
android:exported="true"
@@ -1003,16 +1010,6 @@
</intent-filter>
</activity>
<!-- LaunchNoteTaskManagedProfileProxyActivity MUST NOT be exported because it allows caller
to specify an Android user when launching the default notes app. -->
<activity
android:name=".notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity"
android:exported="false"
android:enabled="true"
android:excludeFromRecents="true"
android:resizeableActivity="false"
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".notetask.LaunchNotesRoleSettingsTrampolineActivity"
android:exported="true"

View File

@@ -0,0 +1,29 @@
/*
* 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;
/** A service to help with controlling the state of notes app bubble through the system user. */
interface INoteTaskBubblesService {
boolean areBubblesAvailable();
void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon);
}

View File

@@ -0,0 +1,138 @@
/*
* 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.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.IBinder
import android.os.UserHandle
import com.android.internal.infra.ServiceConnector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.DebugLogger.debugLog
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
/**
* A utility class to help interact with [Bubbles] as system user. The SysUI instance running as
* system user is the only instance that has the instance of [Bubbles] that manages the notes app
* bubble for all users.
*
* <p>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<INoteTaskBubblesService> =
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.
*
* <p>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<Bubbles>) : 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."
}
}
)
}
}
}
}
}

View File

@@ -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<Bubbles>,
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<NoteTaskInfo?>()
@@ -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" }
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*
* <p>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 {

View File

@@ -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.
*
* <p>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<Bubbles> mOptionalBubbles;
private final DevicePolicyManager mDevicePolicyManager;
private final UserManager mUserManager;
private final boolean mAreTaskAndTimeIndependentPrerequisitesMet;
@VisibleForTesting()
@Nullable ServiceConnector<IAppClipsService> mProxyConnectorToMainProfile;
@Inject
public AppClipsService(@Application Context context, FeatureFlags featureFlags,
Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager,
UserManager userManager) {
Optional<Bubbles> 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<Boolean> future = mProxyConnectorToMainProfile.postForResult(
service -> service.canLaunchCaptureContentActivityForNote(taskId));
return future.get();
} catch (ExecutionException | InterruptedException e) {
Log.d(TAG, "Exception from service\n" + e);
}
return false;
}
}

View File

@@ -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<Bubbles> 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<IAppClipsService> mAppClipsServiceConnector;
private UserHandle mUserHandle;
private Intent mKillAppClipsBroadcastIntent;
private UserHandle mNotesAppUser;
@Inject
public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags,
Optional<Bubbles> 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<IAppClipsService> 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<Integer> 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<UserHandle> 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<IAppClipsService> 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() {

View File

@@ -191,12 +191,6 @@
android:permission="com.android.systemui.permission.SELF"
android:excludeFromRecents="true" />
<activity
android:name="com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity"
android:exported="false"
android:permission="com.android.systemui.permission.SELF"
android:excludeFromRecents="true" />
<activity
android:name="com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity"
android:exported="false"

View File

@@ -0,0 +1,46 @@
/*
* 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.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.UserHandle
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
import kotlinx.coroutines.CoroutineDispatcher
/**
* Fake for [NoteTaskBubblesController] as mocking suspending functions is not supported in the
* Android tree's version of mockito. Ideally the [NoteTaskBubblesController] should be implemented
* using an interface for effectively providing multiple implementations but as this fake primarily
* for dealing with old version of mockito there isn't any benefit in adding complexity.
*/
class FakeNoteTaskBubbleController(
unUsed1: Context,
unsUsed2: CoroutineDispatcher,
private val optionalBubbles: Optional<Bubbles>
) : 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() }
)
}
}

View File

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

View File

@@ -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<Intent>()
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() {

View File

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

View File

@@ -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<LaunchNoteTaskManagedProfileProxyActivity>(
/* activityFactory= */ object :
SingleActivityFactory<LaunchNoteTaskManagedProfileProxyActivity>(
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)
}
}

View File

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

View File

@@ -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<Bubbles> mOptionalBubbles;
@Mock
private Bubbles mBubbles;
@Mock private ServiceConnector<IAppClipsService> 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<Bubbles> optionalBubbles,
NoteTaskController noteTaskController,
PackageManager packageManager,
UserTracker userTracker,
UiEventLogger uiEventLogger,
UserManager userManager,
public AppClipsTrampolineActivityTestable(
ServiceConnector<IAppClipsService> 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