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