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
This commit is contained in:
@@ -23,4 +23,6 @@ package com.android.internal.statusbar;
|
||||
*/
|
||||
interface IAppClipsService {
|
||||
boolean canLaunchCaptureContentActivityForNote(in int taskId);
|
||||
}
|
||||
|
||||
int canLaunchCaptureContentActivityForNoteInternal(in int taskId);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user