diff options
12 files changed, 424 insertions, 195 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 6bfe1a099c51..be615d63a3d7 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -20,9 +20,12 @@ import android.app.KeyguardManager import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.os.UserManager 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.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.util.kotlin.getOrNull @@ -42,11 +45,12 @@ internal class NoteTaskController @Inject constructor( private val context: Context, - private val intentResolver: NoteTaskIntentResolver, + private val resolver: NoteTaskInfoResolver, private val optionalBubbles: Optional<Bubbles>, private val optionalKeyguardManager: Optional<KeyguardManager>, private val optionalUserManager: Optional<UserManager>, @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. */ - fun showNoteTask(isInMultiWindowMode: Boolean = false) { + @JvmOverloads + fun showNoteTask(isInMultiWindowMode: Boolean = false, uiEvent: ShowNoteTaskUiEvent? = null) { + if (!isEnabled) 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. 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. + val intent = noteTaskInfo.toCreateNoteIntent() try { if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) { context.startActivity(intent) @@ -84,9 +93,7 @@ constructor( bubbles.showOrHideAppBubble(intent) } } catch (e: ActivityNotFoundException) { - val message = - "Activity not found for action: ${NoteTaskIntentResolver.ACTION_CREATE_NOTE}." - Log.e(TAG, message, e) + Log.e(TAG, "Activity not found for action: $ACTION_CREATE_NOTE.", 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 { 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. 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" } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt new file mode 100644 index 000000000000..bd822d40b950 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt @@ -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" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index d5f4a5a5d351..d40bf2b49975 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -16,8 +16,10 @@ package com.android.systemui.notetask +import android.app.KeyguardManager import androidx.annotation.VisibleForTesting import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.kotlin.getOrNull import com.android.wm.shell.bubbles.Bubbles import java.util.Optional import javax.inject.Inject @@ -30,6 +32,7 @@ constructor( private val noteTaskController: NoteTaskController, private val commandQueue: CommandQueue, @NoteTaskEnabledKey private val isEnabled: Boolean, + private val optionalKeyguardManager: Optional<KeyguardManager>, ) { @VisibleForTesting @@ -37,11 +40,21 @@ constructor( object : CommandQueue.Callbacks { override fun handleSystemKey(keyCode: Int) { 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() { if (isEnabled && optionalBubbles.isPresent) { commandQueue.addCallback(callbacks) @@ -49,3 +62,7 @@ constructor( 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 diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt deleted file mode 100644 index 11dc1d7eb804..000000000000 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt +++ /dev/null @@ -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" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index ec6a16accc4d..b8800a242d06 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -51,7 +51,7 @@ internal interface NoteTaskModule { featureFlags: FeatureFlags, roleManager: RoleManager, ): Boolean { - val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.ROLE_NOTES) + val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskInfoResolver.ROLE_NOTES) val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS) return isRoleAvailable && isFeatureEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt index cfbaa48a4fa4..43869ccda2b1 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt @@ -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.PickerScreenState import com.android.systemui.notetask.NoteTaskController +import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent import com.android.systemui.notetask.NoteTaskEnabledKey import javax.inject.Inject import kotlinx.coroutines.flow.flowOf @@ -64,7 +65,9 @@ constructor( } override fun onTriggered(expandable: Expandable?): OnTriggeredResult { - noteTaskController.showNoteTask() + noteTaskController.showNoteTask( + uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE + ) return OnTriggeredResult.Handled } } 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 f203e7a51643..3ac5bfa09aaa 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -21,7 +21,7 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import com.android.systemui.notetask.NoteTaskController -import com.android.systemui.notetask.NoteTaskIntentResolver +import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent import javax.inject.Inject /** Activity responsible for launching the note experience, and finish. */ @@ -34,7 +34,10 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - noteTaskController.showNoteTask(isInMultiWindowMode) + noteTaskController.showNoteTask( + isInMultiWindowMode = isInMultiWindowMode, + uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT, + ) finish() } @@ -46,7 +49,7 @@ constructor( 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 = NoteTaskIntentResolver.ACTION_CREATE_NOTE + action = NoteTaskController.ACTION_CREATE_NOTE } } } 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 8440455127bd..39c4e06ff0bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -23,10 +23,14 @@ import android.content.pm.PackageManager import android.os.UserManager import android.test.suitebuilder.annotation.SmallTest import androidx.test.runner.AndroidJUnit4 +import com.android.internal.logging.UiEventLogger 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.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.android.wm.shell.bubbles.Bubbles @@ -36,8 +40,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations /** @@ -50,24 +54,23 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) internal class NoteTaskControllerTest : SysuiTestCase() { - private val notesIntent = Intent(ACTION_CREATE_NOTE) - @Mock lateinit var context: Context @Mock lateinit var packageManager: PackageManager - @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver + @Mock lateinit var resolver: NoteTaskInfoResolver @Mock lateinit var bubbles: Bubbles @Mock lateinit var optionalBubbles: Optional<Bubbles> @Mock lateinit var keyguardManager: KeyguardManager @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager> @Mock lateinit var optionalUserManager: Optional<UserManager> @Mock lateinit var userManager: UserManager + @Mock lateinit var uiEventLogger: UiEventLogger @Before fun setUp() { MockitoAnnotations.initMocks(this) 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(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager) whenever(optionalUserManager.orElse(null)).thenReturn(userManager) @@ -77,101 +80,182 @@ internal class NoteTaskControllerTest : SysuiTestCase() { private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController { return NoteTaskController( context = context, - intentResolver = noteTaskIntentResolver, + resolver = resolver, optionalBubbles = optionalBubbles, optionalKeyguardManager = optionalKeyguardManager, optionalUserManager = optionalUserManager, isEnabled = isEnabled, + uiEventLogger = uiEventLogger, ) } // region showNoteTask @Test - fun showNoteTask_keyguardIsLocked_shouldStartActivity() { + fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() { 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) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + 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_KEYGUARD_QUICK_AFFORDANCE, + NOTES_UID, + NOTES_PACKAGE_NAME + ) } @Test - fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() { + fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesAndLogUiEvent() { whenever(keyguardManager.isKeyguardLocked).thenReturn(false) - createNoteTaskController().showNoteTask(isInMultiWindowMode = false) + createNoteTaskController() + .showNoteTask( + isInMultiWindowMode = false, + uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON, + ) + + verifyZeroInteractions(context) + 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 + fun showNoteTask_keyguardIsUnlocked_uiEventIsNull_shouldStartBubblesWithoutLoggingUiEvent() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(false) - verify(bubbles).showOrHideAppBubble(notesIntent) - verify(context, never()).startActivity(notesIntent) + createNoteTaskController().showNoteTask(isInMultiWindowMode = false, uiEvent = null) + + verifyZeroInteractions(context) + 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_shouldStartActivity() { + fun showNoteTask_isInMultiWindowMode_shouldStartActivityAndLogUiEvent() { whenever(keyguardManager.isKeyguardLocked).thenReturn(false) - createNoteTaskController().showNoteTask(isInMultiWindowMode = true) + createNoteTaskController() + .showNoteTask( + isInMultiWindowMode = true, + uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT, + ) - verify(context).startActivity(notesIntent) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + 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 fun showNoteTask_bubblesIsNull_shouldDoNothing() { 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) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + verifyZeroInteractions(context, bubbles, uiEventLogger) } @Test fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() { 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) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + verifyZeroInteractions(context, bubbles, uiEventLogger) } @Test fun showNoteTask_userManagerIsNull_shouldDoNothing() { 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) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + verifyZeroInteractions(context, bubbles, uiEventLogger) } @Test 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) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + verifyZeroInteractions(context, bubbles, uiEventLogger) } @Test 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) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + verifyZeroInteractions(context, bubbles, uiEventLogger) } @Test fun showNoteTask_userIsLocked_shouldDoNothing() { 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) - verify(bubbles, never()).showOrHideAppBubble(notesIntent) + verifyZeroInteractions(context, bubbles, uiEventLogger) } // endregion @@ -206,4 +290,9 @@ internal class NoteTaskControllerTest : SysuiTestCase() { assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) } // endregion + + private companion object { + const val NOTES_PACKAGE_NAME = "com.android.note.app" + const val NOTES_UID = 123456 + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt new file mode 100644 index 000000000000..d6495d8fe1b7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt @@ -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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 010ac5bbb2d9..53720ffdff94 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -15,11 +15,14 @@ */ package com.android.systemui.notetask +import android.app.KeyguardManager import android.test.suitebuilder.annotation.SmallTest import android.view.KeyEvent import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.wm.shell.bubbles.Bubbles import java.util.Optional @@ -30,6 +33,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations /** @@ -55,12 +59,16 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { 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( optionalBubbles = optionalBubbles, noteTaskController = noteTaskController, commandQueue = commandQueue, isEnabled = isEnabled, + optionalKeyguardManager = optionalKeyguardManager, ) } @@ -105,19 +113,44 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { // region handleSystemKey @Test - fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { - createNoteTaskInitializer() + fun handleSystemKey_receiveValidSystemKey_keyguardNotLocked_shouldShowNoteTaskWithUnlocked() { + val keyguardManager = + mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(false) } + createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager)) .callbacks .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 fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) - verify(noteTaskController, never()).showNoteTask() + verifyZeroInteractions(noteTaskController) } // endregion } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt deleted file mode 100644 index 18be92ba27cf..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt +++ /dev/null @@ -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() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt index a1d42a0ce505..cdc683f8f8f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt @@ -27,7 +27,7 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState 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 kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -53,7 +53,6 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever(noteTaskController.showNoteTask()).then {} } private fun createUnderTest(isEnabled: Boolean) = @@ -96,6 +95,7 @@ internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() { underTest.onTriggered(expandable = null) - verify(noteTaskController).showNoteTask() + verify(noteTaskController) + .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE) } } |