Merge "Log UI events for all note-taking entry points" into tm-qpr-dev am: f24155da26

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21141400

Change-Id: I948fc4ed8ceb8658b6756dfd7477478452433bd1
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Anna Zappone
2023-02-04 00:06:27 +00:00
committed by Automerger Merge Worker
12 changed files with 424 additions and 195 deletions

View File

@@ -20,9 +20,12 @@ import android.app.KeyguardManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.UserManager import android.os.UserManager
import android.util.Log import android.util.Log
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.kotlin.getOrNull
@@ -42,11 +45,12 @@ internal class NoteTaskController
@Inject @Inject
constructor( constructor(
private val context: Context, private val context: Context,
private val intentResolver: NoteTaskIntentResolver, private val resolver: NoteTaskInfoResolver,
private val optionalBubbles: Optional<Bubbles>, private val optionalBubbles: Optional<Bubbles>,
private val optionalKeyguardManager: Optional<KeyguardManager>, private val optionalKeyguardManager: Optional<KeyguardManager>,
private val optionalUserManager: Optional<UserManager>, private val optionalUserManager: Optional<UserManager>,
@NoteTaskEnabledKey private val isEnabled: Boolean, @NoteTaskEnabledKey private val isEnabled: Boolean,
private val uiEventLogger: UiEventLogger,
) { ) {
/** /**
@@ -64,7 +68,9 @@ constructor(
* *
* That will let users open other apps in full screen, and take contextual notes. * That will let users open other apps in full screen, and take contextual notes.
*/ */
fun showNoteTask(isInMultiWindowMode: Boolean = false) { @JvmOverloads
fun showNoteTask(isInMultiWindowMode: Boolean = false, uiEvent: ShowNoteTaskUiEvent? = null) {
if (!isEnabled) return if (!isEnabled) return
val bubbles = optionalBubbles.getOrNull() ?: return val bubbles = optionalBubbles.getOrNull() ?: return
@@ -74,9 +80,12 @@ constructor(
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return if (!userManager.isUserUnlocked) return
val intent = intentResolver.resolveIntent() ?: return val noteTaskInfo = resolver.resolveInfo() ?: return
uiEvent?.let { uiEventLogger.log(it, noteTaskInfo.uid, noteTaskInfo.packageName) }
// TODO(b/266686199): We should handle when app not available. For now, we log. // TODO(b/266686199): We should handle when app not available. For now, we log.
val intent = noteTaskInfo.toCreateNoteIntent()
try { try {
if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) { if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
context.startActivity(intent) context.startActivity(intent)
@@ -84,9 +93,7 @@ constructor(
bubbles.showOrHideAppBubble(intent) bubbles.showOrHideAppBubble(intent)
} }
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
val message = Log.e(TAG, "Activity not found for action: $ACTION_CREATE_NOTE.", e)
"Activity not found for action: ${NoteTaskIntentResolver.ACTION_CREATE_NOTE}."
Log.e(TAG, message, e)
} }
} }
@@ -114,10 +121,47 @@ constructor(
) )
} }
/** IDs of UI events accepted by [showNoteTask]. */
enum class ShowNoteTaskUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "User opened a note by tapping on the lockscreen shortcut.")
NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE(1294),
/* ktlint-disable max-line-length */
@UiEvent(
doc =
"User opened a note by pressing the stylus tail button while the screen was unlocked."
)
NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON(1295),
@UiEvent(
doc =
"User opened a note by pressing the stylus tail button while the screen was locked."
)
NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1296),
@UiEvent(doc = "User opened a note by tapping on an app shortcut.")
NOTE_OPENED_VIA_SHORTCUT(1297);
override fun getId() = _id
}
companion object { companion object {
private val TAG = NoteTaskController::class.simpleName.orEmpty() private val TAG = NoteTaskController::class.simpleName.orEmpty()
private fun NoteTaskInfoResolver.NoteTaskInfo.toCreateNoteIntent(): Intent {
return Intent(ACTION_CREATE_NOTE)
.setPackage(packageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
// was used to start it.
.putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
}
// TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead. // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
const val NOTE_TASK_KEY_EVENT = 311 const val NOTE_TASK_KEY_EVENT = 311
// TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
// TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
} }
} }

View File

@@ -0,0 +1,69 @@
/*
* 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.role.RoleManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.UserHandle
import android.util.Log
import javax.inject.Inject
internal class NoteTaskInfoResolver
@Inject
constructor(
private val context: Context,
private val roleManager: RoleManager,
private val packageManager: PackageManager,
) {
fun resolveInfo(): NoteTaskInfo? {
// TODO(b/267634412): Select UserHandle depending on where the user initiated note-taking.
val user = context.user
val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, user).firstOrNull()
if (packageName.isNullOrEmpty()) return null
return NoteTaskInfo(packageName, packageManager.getUidOf(packageName, user))
}
/** Package name and kernel user-ID of a note-taking app. */
data class NoteTaskInfo(val packageName: String, val uid: Int)
companion object {
private val TAG = NoteTaskInfoResolver::class.simpleName.orEmpty()
private val EMPTY_APPLICATION_INFO_FLAGS = PackageManager.ApplicationInfoFlags.of(0)!!
/**
* Returns the kernel user-ID of [packageName] for a [user]. Returns zero if the app cannot
* be found.
*/
private fun PackageManager.getUidOf(packageName: String, user: UserHandle): Int {
val applicationInfo =
try {
getApplicationInfoAsUser(packageName, EMPTY_APPLICATION_INFO_FLAGS, user)
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Couldn't find notes app UID", e)
return 0
}
return applicationInfo.uid
}
// TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
const val ROLE_NOTES = "android.app.role.NOTES"
}
}

View File

@@ -16,8 +16,10 @@
package com.android.systemui.notetask package com.android.systemui.notetask
import android.app.KeyguardManager
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.util.kotlin.getOrNull
import com.android.wm.shell.bubbles.Bubbles import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
@@ -30,6 +32,7 @@ constructor(
private val noteTaskController: NoteTaskController, private val noteTaskController: NoteTaskController,
private val commandQueue: CommandQueue, private val commandQueue: CommandQueue,
@NoteTaskEnabledKey private val isEnabled: Boolean, @NoteTaskEnabledKey private val isEnabled: Boolean,
private val optionalKeyguardManager: Optional<KeyguardManager>,
) { ) {
@VisibleForTesting @VisibleForTesting
@@ -37,11 +40,21 @@ constructor(
object : CommandQueue.Callbacks { object : CommandQueue.Callbacks {
override fun handleSystemKey(keyCode: Int) { override fun handleSystemKey(keyCode: Int) {
if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) { if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
noteTaskController.showNoteTask() showNoteTask()
} }
} }
} }
private fun showNoteTask() {
val uiEvent =
if (optionalKeyguardManager.isKeyguardLocked) {
NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED
} else {
NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
}
noteTaskController.showNoteTask(uiEvent = uiEvent)
}
fun initialize() { fun initialize() {
if (isEnabled && optionalBubbles.isPresent) { if (isEnabled && optionalBubbles.isPresent) {
commandQueue.addCallback(callbacks) commandQueue.addCallback(callbacks)
@@ -49,3 +62,7 @@ constructor(
noteTaskController.setNoteTaskShortcutEnabled(isEnabled) noteTaskController.setNoteTaskShortcutEnabled(isEnabled)
} }
} }
private val Optional<KeyguardManager>.isKeyguardLocked: Boolean
// If there's no KeyguardManager, assume that the keyguard is not locked.
get() = getOrNull()?.isKeyguardLocked ?: false

View File

@@ -1,54 +0,0 @@
/*
* Copyright (C) 2022 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.role.RoleManager
import android.content.Context
import android.content.Intent
import javax.inject.Inject
internal class NoteTaskIntentResolver
@Inject
constructor(
private val context: Context,
private val roleManager: RoleManager,
) {
fun resolveIntent(): Intent? {
val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, context.user).firstOrNull()
if (packageName.isNullOrEmpty()) return null
return Intent(ACTION_CREATE_NOTE)
.setPackage(packageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint was
// used to start it.
.putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
}
companion object {
// TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
// TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
const val ROLE_NOTES = "android.app.role.NOTES"
// TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
}
}

View File

@@ -51,7 +51,7 @@ internal interface NoteTaskModule {
featureFlags: FeatureFlags, featureFlags: FeatureFlags,
roleManager: RoleManager, roleManager: RoleManager,
): Boolean { ): Boolean {
val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.ROLE_NOTES) val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskInfoResolver.ROLE_NOTES)
val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS) val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS)
return isRoleAvailable && isFeatureEnabled return isRoleAvailable && isFeatureEnabled
} }

View File

@@ -27,6 +27,7 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState
import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskController
import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.notetask.NoteTaskEnabledKey import com.android.systemui.notetask.NoteTaskEnabledKey
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@@ -64,7 +65,9 @@ constructor(
} }
override fun onTriggered(expandable: Expandable?): OnTriggeredResult { override fun onTriggered(expandable: Expandable?): OnTriggeredResult {
noteTaskController.showNoteTask() noteTaskController.showNoteTask(
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE
)
return OnTriggeredResult.Handled return OnTriggeredResult.Handled
} }
} }

View File

@@ -21,7 +21,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskController
import com.android.systemui.notetask.NoteTaskIntentResolver import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import javax.inject.Inject import javax.inject.Inject
/** Activity responsible for launching the note experience, and finish. */ /** Activity responsible for launching the note experience, and finish. */
@@ -34,7 +34,10 @@ constructor(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
noteTaskController.showNoteTask(isInMultiWindowMode) noteTaskController.showNoteTask(
isInMultiWindowMode = isInMultiWindowMode,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
)
finish() finish()
} }
@@ -46,7 +49,7 @@ constructor(
return Intent(context, LaunchNoteTaskActivity::class.java).apply { return Intent(context, LaunchNoteTaskActivity::class.java).apply {
// Intent's action must be set in shortcuts, or an exception will be thrown. // Intent's action must be set in shortcuts, or an exception will be thrown.
// TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead. // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
action = NoteTaskIntentResolver.ACTION_CREATE_NOTE action = NoteTaskController.ACTION_CREATE_NOTE
} }
} }
} }

View File

@@ -23,10 +23,14 @@ import android.content.pm.PackageManager
import android.os.UserManager import android.os.UserManager
import android.test.suitebuilder.annotation.SmallTest import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import com.android.systemui.notetask.NoteTaskController.Companion.INTENT_EXTRA_USE_STYLUS_MODE
import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.notetask.NoteTaskInfoResolver.NoteTaskInfo
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles import com.android.wm.shell.bubbles.Bubbles
@@ -36,8 +40,8 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
/** /**
@@ -50,24 +54,23 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() { internal class NoteTaskControllerTest : SysuiTestCase() {
private val notesIntent = Intent(ACTION_CREATE_NOTE)
@Mock lateinit var context: Context @Mock lateinit var context: Context
@Mock lateinit var packageManager: PackageManager @Mock lateinit var packageManager: PackageManager
@Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver @Mock lateinit var resolver: NoteTaskInfoResolver
@Mock lateinit var bubbles: Bubbles @Mock lateinit var bubbles: Bubbles
@Mock lateinit var optionalBubbles: Optional<Bubbles> @Mock lateinit var optionalBubbles: Optional<Bubbles>
@Mock lateinit var keyguardManager: KeyguardManager @Mock lateinit var keyguardManager: KeyguardManager
@Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager> @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
@Mock lateinit var optionalUserManager: Optional<UserManager> @Mock lateinit var optionalUserManager: Optional<UserManager>
@Mock lateinit var userManager: UserManager @Mock lateinit var userManager: UserManager
@Mock lateinit var uiEventLogger: UiEventLogger
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(context.packageManager).thenReturn(packageManager) whenever(context.packageManager).thenReturn(packageManager)
whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent) whenever(resolver.resolveInfo()).thenReturn(NoteTaskInfo(NOTES_PACKAGE_NAME, NOTES_UID))
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles) whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager) whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
whenever(optionalUserManager.orElse(null)).thenReturn(userManager) whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
@@ -77,101 +80,182 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController { private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
return NoteTaskController( return NoteTaskController(
context = context, context = context,
intentResolver = noteTaskIntentResolver, resolver = resolver,
optionalBubbles = optionalBubbles, optionalBubbles = optionalBubbles,
optionalKeyguardManager = optionalKeyguardManager, optionalKeyguardManager = optionalKeyguardManager,
optionalUserManager = optionalUserManager, optionalUserManager = optionalUserManager,
isEnabled = isEnabled, isEnabled = isEnabled,
uiEventLogger = uiEventLogger,
) )
} }
// region showNoteTask // region showNoteTask
@Test @Test
fun showNoteTask_keyguardIsLocked_shouldStartActivity() { fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(true) whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
createNoteTaskController().showNoteTask(isInMultiWindowMode = false) createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = false,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
)
verify(context).startActivity(notesIntent) val intentCaptor = argumentCaptor<Intent>()
verify(bubbles, never()).showOrHideAppBubble(notesIntent) verify(context).startActivity(capture(intentCaptor))
intentCaptor.value.let { intent ->
assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
}
verifyZeroInteractions(bubbles)
verify(uiEventLogger)
.log(
ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
NOTES_UID,
NOTES_PACKAGE_NAME
)
} }
@Test @Test
fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() { fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false) whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
createNoteTaskController().showNoteTask(isInMultiWindowMode = false) createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = false,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
)
verify(bubbles).showOrHideAppBubble(notesIntent) verifyZeroInteractions(context)
verify(context, never()).startActivity(notesIntent) val intentCaptor = argumentCaptor<Intent>()
verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
intentCaptor.value.let { intent ->
assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
}
verify(uiEventLogger)
.log(
ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
NOTES_UID,
NOTES_PACKAGE_NAME
)
} }
@Test @Test
fun showNoteTask_isInMultiWindowMode_shouldStartActivity() { fun showNoteTask_keyguardIsUnlocked_uiEventIsNull_shouldStartBubblesWithoutLoggingUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false) whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
createNoteTaskController().showNoteTask(isInMultiWindowMode = true) createNoteTaskController().showNoteTask(isInMultiWindowMode = false, uiEvent = null)
verify(context).startActivity(notesIntent) verifyZeroInteractions(context)
verify(bubbles, never()).showOrHideAppBubble(notesIntent) val intentCaptor = argumentCaptor<Intent>()
verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
intentCaptor.value.let { intent ->
assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
}
verifyZeroInteractions(uiEventLogger)
}
@Test
fun showNoteTask_isInMultiWindowMode_shouldStartActivityAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = true,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
)
val intentCaptor = argumentCaptor<Intent>()
verify(context).startActivity(capture(intentCaptor))
intentCaptor.value.let { intent ->
assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
}
verifyZeroInteractions(bubbles)
verify(uiEventLogger)
.log(ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT, NOTES_UID, NOTES_PACKAGE_NAME)
} }
@Test @Test
fun showNoteTask_bubblesIsNull_shouldDoNothing() { fun showNoteTask_bubblesIsNull_shouldDoNothing() {
whenever(optionalBubbles.orElse(null)).thenReturn(null) whenever(optionalBubbles.orElse(null)).thenReturn(null)
createNoteTaskController().showNoteTask(isInMultiWindowMode = false) createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = false,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
)
verify(context, never()).startActivity(notesIntent) verifyZeroInteractions(context, bubbles, uiEventLogger)
verify(bubbles, never()).showOrHideAppBubble(notesIntent)
} }
@Test @Test
fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() { fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() {
whenever(optionalKeyguardManager.orElse(null)).thenReturn(null) whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
createNoteTaskController().showNoteTask(isInMultiWindowMode = false) createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = false,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
)
verify(context, never()).startActivity(notesIntent) verifyZeroInteractions(context, bubbles, uiEventLogger)
verify(bubbles, never()).showOrHideAppBubble(notesIntent)
} }
@Test @Test
fun showNoteTask_userManagerIsNull_shouldDoNothing() { fun showNoteTask_userManagerIsNull_shouldDoNothing() {
whenever(optionalUserManager.orElse(null)).thenReturn(null) whenever(optionalUserManager.orElse(null)).thenReturn(null)
createNoteTaskController().showNoteTask(isInMultiWindowMode = false) createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = false,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
)
verify(context, never()).startActivity(notesIntent) verifyZeroInteractions(context, bubbles, uiEventLogger)
verify(bubbles, never()).showOrHideAppBubble(notesIntent)
} }
@Test @Test
fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() { fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() {
whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null) whenever(resolver.resolveInfo()).thenReturn(null)
createNoteTaskController().showNoteTask(isInMultiWindowMode = false) createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = false,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
)
verify(context, never()).startActivity(notesIntent) verifyZeroInteractions(context, bubbles, uiEventLogger)
verify(bubbles, never()).showOrHideAppBubble(notesIntent)
} }
@Test @Test
fun showNoteTask_flagDisabled_shouldDoNothing() { fun showNoteTask_flagDisabled_shouldDoNothing() {
createNoteTaskController(isEnabled = false).showNoteTask() createNoteTaskController(isEnabled = false)
.showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
verify(context, never()).startActivity(notesIntent) verifyZeroInteractions(context, bubbles, uiEventLogger)
verify(bubbles, never()).showOrHideAppBubble(notesIntent)
} }
@Test @Test
fun showNoteTask_userIsLocked_shouldDoNothing() { fun showNoteTask_userIsLocked_shouldDoNothing() {
whenever(userManager.isUserUnlocked).thenReturn(false) whenever(userManager.isUserUnlocked).thenReturn(false)
createNoteTaskController().showNoteTask(isInMultiWindowMode = false) createNoteTaskController()
.showNoteTask(
isInMultiWindowMode = false,
uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
)
verify(context, never()).startActivity(notesIntent) verifyZeroInteractions(context, bubbles, uiEventLogger)
verify(bubbles, never()).showOrHideAppBubble(notesIntent)
} }
// endregion // endregion
@@ -206,4 +290,9 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
} }
// endregion // endregion
private companion object {
const val NOTES_PACKAGE_NAME = "com.android.note.app"
const val NOTES_UID = 123456
}
} }

View File

@@ -0,0 +1,108 @@
/*
* 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.role.RoleManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.MockitoAnnotations
/**
* Tests for [NoteTaskInfoResolver].
*
* Build/Install/Run:
* - atest SystemUITests:NoteTaskInfoResolverTest
*/
@SmallTest
@RunWith(AndroidJUnit4::class)
internal class NoteTaskInfoResolverTest : SysuiTestCase() {
@Mock lateinit var packageManager: PackageManager
@Mock lateinit var roleManager: RoleManager
private lateinit var underTest: NoteTaskInfoResolver
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
underTest = NoteTaskInfoResolver(context, roleManager, packageManager)
}
@Test
fun resolveInfo_shouldReturnInfo() {
val packageName = "com.android.note.app"
val uid = 123456
whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
.then { listOf(packageName) }
whenever(
packageManager.getApplicationInfoAsUser(
eq(packageName),
any<PackageManager.ApplicationInfoFlags>(),
eq(context.user)
)
)
.thenReturn(ApplicationInfo().apply { this.uid = uid })
val actual = underTest.resolveInfo()
requireNotNull(actual) { "Note task info must not be null" }
assertThat(actual.packageName).isEqualTo(packageName)
assertThat(actual.uid).isEqualTo(uid)
}
@Test
fun resolveInfo_packageManagerThrowsException_shouldReturnInfoWithZeroUid() {
val packageName = "com.android.note.app"
whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
.then { listOf(packageName) }
whenever(
packageManager.getApplicationInfoAsUser(
eq(packageName),
any<PackageManager.ApplicationInfoFlags>(),
eq(context.user)
)
)
.thenThrow(PackageManager.NameNotFoundException(packageName))
val actual = underTest.resolveInfo()
requireNotNull(actual) { "Note task info must not be null" }
assertThat(actual.packageName).isEqualTo(packageName)
assertThat(actual.uid).isEqualTo(0)
}
@Test
fun resolveInfo_noRoleHolderIsSet_shouldReturnNull() {
whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskInfoResolver.ROLE_NOTES), any()))
.then { listOf<String>() }
val actual = underTest.resolveInfo()
assertThat(actual).isNull()
}
}

View File

@@ -15,11 +15,14 @@
*/ */
package com.android.systemui.notetask package com.android.systemui.notetask
import android.app.KeyguardManager
import android.test.suitebuilder.annotation.SmallTest import android.test.suitebuilder.annotation.SmallTest
import android.view.KeyEvent import android.view.KeyEvent
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional import java.util.Optional
@@ -30,6 +33,7 @@ import org.mockito.ArgumentMatchers.any
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
/** /**
@@ -55,12 +59,16 @@ internal class NoteTaskInitializerTest : SysuiTestCase() {
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles) whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
} }
private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer { private fun createNoteTaskInitializer(
isEnabled: Boolean = true,
optionalKeyguardManager: Optional<KeyguardManager> = Optional.empty(),
): NoteTaskInitializer {
return NoteTaskInitializer( return NoteTaskInitializer(
optionalBubbles = optionalBubbles, optionalBubbles = optionalBubbles,
noteTaskController = noteTaskController, noteTaskController = noteTaskController,
commandQueue = commandQueue, commandQueue = commandQueue,
isEnabled = isEnabled, isEnabled = isEnabled,
optionalKeyguardManager = optionalKeyguardManager,
) )
} }
@@ -105,19 +113,44 @@ internal class NoteTaskInitializerTest : SysuiTestCase() {
// region handleSystemKey // region handleSystemKey
@Test @Test
fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { fun handleSystemKey_receiveValidSystemKey_keyguardNotLocked_shouldShowNoteTaskWithUnlocked() {
createNoteTaskInitializer() val keyguardManager =
mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(false) }
createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
.callbacks .callbacks
.handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT) .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
verify(noteTaskController).showNoteTask() verify(noteTaskController)
.showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
}
@Test
fun handleSystemKey_receiveValidSystemKey_keyguardLocked_shouldShowNoteTaskWithLocked() {
val keyguardManager =
mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(true) }
createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
.callbacks
.handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
verify(noteTaskController)
.showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED)
}
@Test
fun handleSystemKey_receiveValidSystemKey_nullKeyguardManager_shouldShowNoteTaskWithUnlocked() {
createNoteTaskInitializer(optionalKeyguardManager = Optional.empty())
.callbacks
.handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
verify(noteTaskController)
.showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
} }
@Test @Test
fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
verify(noteTaskController, never()).showNoteTask() verifyZeroInteractions(noteTaskController)
} }
// endregion // endregion
} }

View File

@@ -1,83 +0,0 @@
/*
* Copyright (C) 2022 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.role.RoleManager
import android.content.Intent
import android.content.pm.PackageManager
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.MockitoAnnotations
/**
* Tests for [NoteTaskIntentResolver].
*
* Build/Install/Run:
* - atest SystemUITests:NoteTaskIntentResolverTest
*/
@SmallTest
@RunWith(AndroidJUnit4::class)
internal class NoteTaskIntentResolverTest : SysuiTestCase() {
@Mock lateinit var packageManager: PackageManager
@Mock lateinit var roleManager: RoleManager
private lateinit var underTest: NoteTaskIntentResolver
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
underTest = NoteTaskIntentResolver(context, roleManager)
}
@Test
fun resolveIntent_shouldReturnIntentInStylusMode() {
val packageName = "com.android.note.app"
whenever(roleManager.getRoleHoldersAsUser(NoteTaskIntentResolver.ROLE_NOTES, context.user))
.then { listOf(packageName) }
val actual = underTest.resolveIntent()
requireNotNull(actual) { "Intent must not be null" }
assertThat(actual.action).isEqualTo(ACTION_CREATE_NOTE)
assertThat(actual.`package`).isEqualTo(packageName)
val expectedExtra = actual.getExtra(NoteTaskIntentResolver.INTENT_EXTRA_USE_STYLUS_MODE)
assertThat(expectedExtra).isEqualTo(true)
val expectedFlag = actual.flags and Intent.FLAG_ACTIVITY_NEW_TASK
assertThat(expectedFlag).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
}
@Test
fun resolveIntent_noRoleHolderIsSet_shouldReturnNull() {
whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskIntentResolver.ROLE_NOTES), any()))
.then { listOf<String>() }
val actual = underTest.resolveIntent()
assertThat(actual).isNull()
}
}

View File

@@ -27,7 +27,7 @@ import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
import com.android.systemui.notetask.NoteTaskController import com.android.systemui.notetask.NoteTaskController
import com.android.systemui.util.mockito.whenever import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -53,7 +53,6 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(noteTaskController.showNoteTask()).then {}
} }
private fun createUnderTest(isEnabled: Boolean) = private fun createUnderTest(isEnabled: Boolean) =
@@ -96,6 +95,7 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
underTest.onTriggered(expandable = null) underTest.onTriggered(expandable = null)
verify(noteTaskController).showNoteTask() verify(noteTaskController)
.showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE)
} }
} }