diff options
10 files changed, 378 insertions, 3 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 941697b2392f..ac75cc88fdf5 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -354,6 +354,7 @@ android_library { "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 9b3f1a8f0734..d37a4aa7ad03 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 93ed8591e738..6387c657ca7d 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.devicepolicy.areKeyguardShortcutsDisabled 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 @@ 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 + ) + } + /** * Shows a note task. How the task is shown will depend on when the method is invoked. * @@ -146,7 +159,7 @@ constructor( 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 6278c699498c..1839dfd3fe32 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.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 @@ -36,6 +37,9 @@ interface NoteTaskModule { @[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 14b0779ab162..44855fb7c8cc 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 @@ package com.android.systemui.notetask.shortcut 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 @@ class LaunchNoteTaskActivity @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 @@ constructor( 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 000000000000..3259b0dcc53d --- /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 ce2d15f97cd8..5344c0d016b0 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 e64094675ff5..b0f1fc1842e5 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.ShortcutInfo 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.EXTRA_SHORTCUT 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 @@ 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)) + 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 000000000000..c96853d1a406 --- /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 000000000000..6347c3404348 --- /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) + } +} |