Merge "Add a proxy activity for launch bubble for another user in primary user" into udc-dev

This commit is contained in:
TreeHugger Robot
2023-03-27 11:31:52 +00:00
committed by Android (Google) Code Review
10 changed files with 377 additions and 2 deletions

View File

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

View File

@@ -992,6 +992,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 -->

View File

@@ -40,6 +40,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.shared.system.ActivityManagerKt.isInForeground
import com.android.systemui.util.kotlin.getOrNull
@@ -97,6 +98,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.
*

View File

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

View File

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

View File

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

View File

@@ -184,6 +184,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"

View File

@@ -36,6 +36,7 @@ import android.content.pm.ShortcutManager
import android.content.pm.UserInfo
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
@@ -44,6 +45,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.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -591,6 +593,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 NOTE_TASK_SHORT_LABEL = "Notetaking"
const val NOTE_TASK_ACTIVITY_NAME = "NoteTaskActivity"

View File

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

View File

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