Add a proxy activity for launch bubble for another user in primary user
This CL is required for work profile notes app shortcuts to be able to
launch in a bubble.
Test: atest SystemUITests:com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivityTest
atest SystemUITests:com.android.systemui.notetask.shortcut.LaunchNoteTaskActivityTest
atest SystemUITests:com.android.systemui.notetask.NoteTaskControllerTest
Bug: 259952057
Change-Id: I8c5cce46c5b7cf6d0c9e6031628ebe3735171723
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 941697b..ac75cc8 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -354,6 +354,7 @@
"androidx.test.uiautomator_uiautomator",
"mockito-target-extended-minus-junit4",
"androidx.test.ext.junit",
+ "androidx.test.ext.truth",
],
libs: [
"android.test.runner",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 9b3f1a8..d37a4aa 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -983,6 +983,16 @@
android:excludeFromRecents="true"
android:resizeableActivity="false"
android:theme="@android:style/Theme.NoDisplay" />
+
+ <!-- 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" />
<!-- endregion -->
<!-- started from ControlsRequestReceiver -->
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 93ed859..6387c65 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -39,6 +39,7 @@
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.util.kotlin.getOrNull
import com.android.wm.shell.bubbles.Bubble
@@ -94,6 +95,18 @@
}
}
+ /** 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
+ )
+ }
+
/**
* Shows a note task. How the task is shown will depend on when the method is invoked.
*
@@ -146,7 +159,7 @@
when (info.launchMode) {
is NoteTaskLaunchMode.AppBubble -> {
// TODO: provide app bubble icon
- bubbles.showOrHideAppBubble(intent, userTracker.userHandle, null /* icon */)
+ bubbles.showOrHideAppBubble(intent, user, null /* icon */)
// App bubble logging happens on `onBubbleExpandChanged`.
logDebug { "onShowNoteTask - opened as app bubble: $info" }
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 6278c69..1839dfd 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -23,6 +23,7 @@
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
@@ -36,6 +37,9 @@
@[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity
+ @[Binds IntoMap ClassKey(LaunchNoteTaskManagedProfileProxyActivity::class)]
+ fun LaunchNoteTaskManagedProfileProxyActivity.bindNoteTaskLauncherProxyActivity(): Activity
+
@[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)]
fun CreateNoteTaskShortcutActivity.bindNoteTaskShortcutActivity(): Activity
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index 14b0779..44855fb 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -18,10 +18,13 @@
import android.content.Context
import android.content.Intent
+import android.content.pm.UserInfo
import android.os.Bundle
+import android.os.UserManager
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
/** Activity responsible for launching the note experience, and finish. */
@@ -29,11 +32,43 @@
@Inject
constructor(
private val controller: NoteTaskController,
+ private val userManager: UserManager,
+ private val userTracker: UserTracker,
) : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- controller.showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT)
+
+ // 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: UserInfo? = userTracker.userProfiles.firstOrNull { it.isMain }
+ if (userManager.isManagedProfile && mainUser != null) {
+ controller.startNoteTaskProxyActivityForUser(mainUser.userHandle)
+ } else {
+ controller.showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT)
+ }
finish()
}
@@ -43,7 +78,6 @@
fun newIntent(context: Context): Intent {
return Intent(context, LaunchNoteTaskActivity::class.java).apply {
// Intent's action must be set in shortcuts, or an exception will be thrown.
- // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
action = Intent.ACTION_CREATE_NOTE
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt
new file mode 100644
index 0000000..3259b0d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask.shortcut
+
+import android.os.Build
+import android.os.Bundle
+import android.os.UserManager
+import android.util.Log
+import androidx.activity.ComponentActivity
+import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskEntryPoint
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+
+/**
+ * An internal proxy activity that starts notes app in the work profile.
+ *
+ * If there is no work profile, this activity finishes gracefully.
+ *
+ * This activity MUST NOT be exported because that would expose the INTERACT_ACROSS_USER privilege
+ * to any apps.
+ */
+class LaunchNoteTaskManagedProfileProxyActivity
+@Inject
+constructor(
+ private val controller: NoteTaskController,
+ private val userTracker: UserTracker,
+ private val userManager: UserManager,
+) : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val managedProfileUser =
+ userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }
+
+ if (managedProfileUser == null) {
+ logDebug { "Fail to find the work profile user." }
+ } else {
+ controller.showNoteTaskAsUser(
+ entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT,
+ user = managedProfileUser.userHandle
+ )
+ }
+ finish()
+ }
+}
+
+private inline fun logDebug(message: () -> String) {
+ if (Build.IS_DEBUGGABLE) {
+ Log.d(NoteTaskController.TAG, message())
+ }
+}
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index ce2d15f..5344c0d 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -171,6 +171,18 @@
android:exported="false"
android:permission="com.android.systemui.permission.SELF"
android:excludeFromRecents="true" />
+
+ <activity
+ android:name="com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity"
+ android:exported="false"
+ 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" />
</application>
<instrumentation android:name="android.testing.TestableInstrumentation"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index e640946..b0f1fc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -34,6 +34,7 @@
import android.content.pm.ShortcutManager
import android.os.UserHandle
import android.os.UserManager
+import androidx.test.ext.truth.content.IntentSubject.assertThat
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.R
@@ -42,6 +43,7 @@
import com.android.systemui.notetask.NoteTaskController.Companion.SHORTCUT_ID
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.settings.UserTracker
import com.android.systemui.util.mockito.any
@@ -527,6 +529,24 @@
}
// 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))
+ intentCaptor.value.let { intent ->
+ assertThat(intent)
+ .hasComponent(
+ ComponentName(context, LaunchNoteTaskManagedProfileProxyActivity::class.java)
+ )
+ assertThat(intent).hasFlags(FLAG_ACTIVITY_NEW_TASK)
+ }
+ }
+ // endregion
+
private companion object {
const val NOTES_SHORT_LABEL = "Notetaking"
const val NOTES_PACKAGE_NAME = "com.android.note.app"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
new file mode 100644
index 0000000..c96853d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.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.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 LaunchNoteTaskActivityTest : SysuiTestCase() {
+
+ @Mock lateinit var noteTaskController: NoteTaskController
+ @Mock lateinit var userManager: UserManager
+ private val userTracker: FakeUserTracker = FakeUserTracker()
+
+ @Rule
+ @JvmField
+ val activityRule =
+ ActivityTestRule<LaunchNoteTaskActivity>(
+ /* activityFactory= */ object :
+ SingleActivityFactory<LaunchNoteTaskActivity>(LaunchNoteTaskActivity::class.java) {
+ override fun create(intent: Intent?) =
+ LaunchNoteTaskActivity(
+ 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 startActivityOnNonWorkProfileUser_shouldLaunchNoteTask() {
+ activityRule.launchActivity(/* startIntent= */ null)
+
+ verify(noteTaskController).showNoteTask(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT))
+ }
+
+ @Test
+ fun startActivityOnWorkProfileUser_shouldLaunchProxyActivity() {
+ userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1)
+ whenever(userManager.isManagedProfile).thenReturn(true)
+
+ activityRule.launchActivity(/* startIntent= */ null)
+
+ val mainUserHandle: UserHandle = mainUser.userHandle
+ verify(noteTaskController).startNoteTaskProxyActivityForUser(eq(mainUserHandle))
+ }
+
+ 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)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt
new file mode 100644
index 0000000..6347c34
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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)
+ }
+}