Fix crash when a shortcut update for notes happens before user unlocked
* Ensure NoteTaskController is updated and valid state when the system is initialized (after direct boot user is unlocked). * NoteTaskController now double check if the user is unlocked before triggering a shortcut update (onRoleHolderChanged). * If by any reason an event is lost, we check again in the next boot. Test: atest SystemUITests:NoteTaskControllerTest Test: atest SystemUITests:NoteTaskInitializerTest Flag: not needed Fixes: b/275852881 Change-Id: Ib3c9d66abf9074c50cdc79ef93fa8d162e9608cb
This commit is contained in:
@@ -250,6 +250,11 @@ constructor(
|
||||
* Widget Picker to all users.
|
||||
*/
|
||||
fun setNoteTaskShortcutEnabled(value: Boolean, user: UserHandle) {
|
||||
if (!userManager.isUserUnlocked(user)) {
|
||||
debugLog { "setNoteTaskShortcutEnabled call but user locked: user=$user" }
|
||||
return
|
||||
}
|
||||
|
||||
val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
|
||||
|
||||
val enabledState =
|
||||
@@ -305,6 +310,10 @@ constructor(
|
||||
/** @see OnRoleHoldersChangedListener */
|
||||
fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
|
||||
if (roleName != ROLE_NOTES) return
|
||||
if (!userManager.isUserUnlocked(user)) {
|
||||
debugLog { "onRoleHoldersChanged call but user locked: role=$roleName, user=$user" }
|
||||
return
|
||||
}
|
||||
|
||||
if (user == userTracker.userHandle) {
|
||||
updateNoteTaskAsUser(user)
|
||||
|
||||
@@ -18,8 +18,13 @@ package com.android.systemui.notetask
|
||||
import android.app.role.RoleManager
|
||||
import android.os.UserHandle
|
||||
import android.view.KeyEvent
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import android.view.KeyEvent.KEYCODE_N
|
||||
import android.view.KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL
|
||||
import com.android.keyguard.KeyguardUpdateMonitor
|
||||
import com.android.keyguard.KeyguardUpdateMonitorCallback
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT
|
||||
import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.statusbar.CommandQueue
|
||||
import com.android.wm.shell.bubbles.Bubbles
|
||||
@@ -35,35 +40,82 @@ constructor(
|
||||
private val roleManager: RoleManager,
|
||||
private val commandQueue: CommandQueue,
|
||||
private val optionalBubbles: Optional<Bubbles>,
|
||||
private val userTracker: UserTracker,
|
||||
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
|
||||
@Background private val backgroundExecutor: Executor,
|
||||
@NoteTaskEnabledKey private val isEnabled: Boolean,
|
||||
private val userTracker: UserTracker,
|
||||
) {
|
||||
|
||||
@VisibleForTesting
|
||||
val callbacks =
|
||||
object : CommandQueue.Callbacks {
|
||||
override fun handleSystemKey(key: KeyEvent) {
|
||||
if (key.keyCode == KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL) {
|
||||
controller.showNoteTask(NoteTaskEntryPoint.TAIL_BUTTON)
|
||||
} else if (
|
||||
key.keyCode == KeyEvent.KEYCODE_N && key.isMetaPressed && key.isCtrlPressed
|
||||
) {
|
||||
controller.showNoteTask(NoteTaskEntryPoint.KEYBOARD_SHORTCUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes note task related features and glue it with other parts of the SystemUI. */
|
||||
fun initialize() {
|
||||
// Guard against feature not being enabled or mandatory dependencies aren't available.
|
||||
if (!isEnabled || optionalBubbles.isEmpty) return
|
||||
|
||||
controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
|
||||
initializeHandleSystemKey()
|
||||
initializeOnRoleHoldersChanged()
|
||||
initializeOnUserUnlocked()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a callback for [CommandQueue] which will redirect [KeyEvent] from a Stylus to
|
||||
* [NoteTaskController], ensure custom actions can be triggered (i.e., keyboard shortcut).
|
||||
*/
|
||||
private fun initializeHandleSystemKey() {
|
||||
val callbacks =
|
||||
object : CommandQueue.Callbacks {
|
||||
override fun handleSystemKey(key: KeyEvent) {
|
||||
key.toNoteTaskEntryPointOrNull()?.let(controller::showNoteTask)
|
||||
}
|
||||
}
|
||||
commandQueue.addCallback(callbacks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the [RoleManager] role holder changed listener to ensure [NoteTaskController]
|
||||
* will always update whenever the role holder app changes. Keep in mind that a role may change
|
||||
* by direct user interaction (i.e., user goes to settings and change it) or by indirect
|
||||
* interaction (i.e., the current role holder app is uninstalled).
|
||||
*/
|
||||
private fun initializeOnRoleHoldersChanged() {
|
||||
roleManager.addOnRoleHoldersChangedListenerAsUser(
|
||||
backgroundExecutor,
|
||||
controller::onRoleHoldersChanged,
|
||||
UserHandle.ALL,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a [KeyguardUpdateMonitor] listener that will ensure [NoteTaskController] is in
|
||||
* correct state during system initialization (after a direct boot user unlocked event).
|
||||
*
|
||||
* Once the system is unlocked, we will force trigger [NoteTaskController.onRoleHoldersChanged]
|
||||
* with a hardcoded [RoleManager.ROLE_NOTES] for the current user.
|
||||
*/
|
||||
private fun initializeOnUserUnlocked() {
|
||||
if (keyguardUpdateMonitor.isUserUnlocked(userTracker.userId)) {
|
||||
controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
|
||||
} else {
|
||||
keyguardUpdateMonitor.registerCallback(onUserUnlockedCallback)
|
||||
}
|
||||
}
|
||||
|
||||
// KeyguardUpdateMonitor.registerCallback uses a weak reference, so we need a hard reference.
|
||||
private val onUserUnlockedCallback =
|
||||
object : KeyguardUpdateMonitorCallback() {
|
||||
override fun onUserUnlocked() {
|
||||
controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
|
||||
keyguardUpdateMonitor.removeCallback(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a [KeyEvent] to a [NoteTaskEntryPoint]. If the [KeyEvent] does not represent a
|
||||
* [NoteTaskEntryPoint], returns null.
|
||||
*/
|
||||
private fun KeyEvent.toNoteTaskEntryPointOrNull(): NoteTaskEntryPoint? =
|
||||
when {
|
||||
keyCode == KEYCODE_STYLUS_BUTTON_TAIL -> TAIL_BUTTON
|
||||
keyCode == KEYCODE_N && isMetaPressed && isCtrlPressed -> KEYBOARD_SHORTCUT
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
|
||||
whenever(context.packageManager).thenReturn(packageManager)
|
||||
whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO)
|
||||
whenever(userManager.isUserUnlocked).thenReturn(true)
|
||||
whenever(userManager.isUserUnlocked(any<Int>())).thenReturn(true)
|
||||
whenever(userManager.isUserUnlocked(any<UserHandle>())).thenReturn(true)
|
||||
whenever(
|
||||
devicePolicyManager.getKeyguardDisabledFeatures(
|
||||
/* admin= */ eq(null),
|
||||
|
||||
@@ -16,124 +16,169 @@
|
||||
package com.android.systemui.notetask
|
||||
|
||||
import android.app.role.RoleManager
|
||||
import android.test.suitebuilder.annotation.SmallTest
|
||||
import android.app.role.RoleManager.ROLE_NOTES
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.view.KeyEvent
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import android.view.KeyEvent.ACTION_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.keyguard.KeyguardUpdateMonitor
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.settings.FakeUserTracker
|
||||
import com.android.systemui.statusbar.CommandQueue
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.android.wm.shell.bubbles.Bubbles
|
||||
import java.util.Optional
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyZeroInteractions
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.MockitoAnnotations.initMocks
|
||||
|
||||
/** atest SystemUITests:NoteTaskInitializerTest */
|
||||
@OptIn(ExperimentalCoroutinesApi::class, InternalNoteTaskApi::class)
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
internal class NoteTaskInitializerTest : SysuiTestCase() {
|
||||
|
||||
@Mock lateinit var commandQueue: CommandQueue
|
||||
@Mock lateinit var bubbles: Bubbles
|
||||
@Mock lateinit var controller: NoteTaskController
|
||||
@Mock lateinit var roleManager: RoleManager
|
||||
private val clock = FakeSystemClock()
|
||||
private val executor = FakeExecutor(clock)
|
||||
@Mock lateinit var userManager: UserManager
|
||||
@Mock lateinit var keyguardMonitor: KeyguardUpdateMonitor
|
||||
|
||||
private val executor = FakeExecutor(FakeSystemClock())
|
||||
private val userTracker = FakeUserTracker()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
initMocks(this)
|
||||
whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true)
|
||||
}
|
||||
|
||||
private fun createNoteTaskInitializer(
|
||||
isEnabled: Boolean = true,
|
||||
bubbles: Bubbles? = this.bubbles,
|
||||
): NoteTaskInitializer {
|
||||
return NoteTaskInitializer(
|
||||
private fun createUnderTest(
|
||||
isEnabled: Boolean,
|
||||
bubbles: Bubbles?,
|
||||
): NoteTaskInitializer =
|
||||
NoteTaskInitializer(
|
||||
controller = controller,
|
||||
commandQueue = commandQueue,
|
||||
optionalBubbles = Optional.ofNullable(bubbles),
|
||||
isEnabled = isEnabled,
|
||||
roleManager = roleManager,
|
||||
backgroundExecutor = executor,
|
||||
userTracker = userTracker,
|
||||
keyguardUpdateMonitor = keyguardMonitor,
|
||||
backgroundExecutor = executor,
|
||||
)
|
||||
}
|
||||
|
||||
// region initializer
|
||||
@Test
|
||||
fun initialize() {
|
||||
createNoteTaskInitializer().initialize()
|
||||
fun initialize_withUserUnlocked() {
|
||||
whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true)
|
||||
|
||||
createUnderTest(isEnabled = true, bubbles = bubbles).initialize()
|
||||
|
||||
verify(controller).setNoteTaskShortcutEnabled(eq(true), eq(userTracker.userHandle))
|
||||
verify(commandQueue).addCallback(any())
|
||||
verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
|
||||
verify(controller).setNoteTaskShortcutEnabled(any(), any())
|
||||
verify(keyguardMonitor, never()).registerCallback(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialize_withUserLocked() {
|
||||
whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false)
|
||||
|
||||
createUnderTest(isEnabled = true, bubbles = bubbles).initialize()
|
||||
|
||||
verify(commandQueue).addCallback(any())
|
||||
verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
|
||||
verify(controller, never()).setNoteTaskShortcutEnabled(any(), any())
|
||||
verify(keyguardMonitor).registerCallback(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialize_flagDisabled() {
|
||||
createNoteTaskInitializer(isEnabled = false).initialize()
|
||||
val underTest = createUnderTest(isEnabled = false, bubbles = bubbles)
|
||||
|
||||
verify(controller, never()).setNoteTaskShortcutEnabled(any(), any())
|
||||
verify(commandQueue, never()).addCallback(any())
|
||||
verify(roleManager, never()).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
|
||||
underTest.initialize()
|
||||
|
||||
verifyZeroInteractions(
|
||||
commandQueue,
|
||||
bubbles,
|
||||
controller,
|
||||
roleManager,
|
||||
userManager,
|
||||
keyguardMonitor,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialize_bubblesNotPresent() {
|
||||
createNoteTaskInitializer(bubbles = null).initialize()
|
||||
val underTest = createUnderTest(isEnabled = true, bubbles = null)
|
||||
|
||||
verify(controller, never()).setNoteTaskShortcutEnabled(any(), any())
|
||||
verify(commandQueue, never()).addCallback(any())
|
||||
verify(roleManager, never()).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
|
||||
}
|
||||
// endregion
|
||||
underTest.initialize()
|
||||
|
||||
// region handleSystemKey
|
||||
@Test
|
||||
fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
|
||||
createNoteTaskInitializer()
|
||||
.callbacks
|
||||
.handleSystemKey(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL))
|
||||
|
||||
verify(controller).showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON)
|
||||
verifyZeroInteractions(
|
||||
commandQueue,
|
||||
bubbles,
|
||||
controller,
|
||||
roleManager,
|
||||
userManager,
|
||||
keyguardMonitor,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemKey_receiveKeyboardShortcut_shouldShowNoteTask() {
|
||||
createNoteTaskInitializer()
|
||||
.callbacks
|
||||
.handleSystemKey(
|
||||
KeyEvent(
|
||||
0,
|
||||
0,
|
||||
KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_N,
|
||||
0,
|
||||
KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON
|
||||
)
|
||||
)
|
||||
fun initialize_handleSystemKey() {
|
||||
val expectedKeyEvent = KeyEvent(ACTION_DOWN, KEYCODE_STYLUS_BUTTON_TAIL)
|
||||
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
|
||||
underTest.initialize()
|
||||
val callback = captureArgument { verify(commandQueue).addCallback(capture()) }
|
||||
|
||||
verify(controller).showNoteTask(entryPoint = NoteTaskEntryPoint.KEYBOARD_SHORTCUT)
|
||||
callback.handleSystemKey(expectedKeyEvent)
|
||||
|
||||
verify(controller).showNoteTask(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
|
||||
createNoteTaskInitializer()
|
||||
.callbacks
|
||||
.handleSystemKey(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN))
|
||||
fun initialize_userUnlocked() {
|
||||
whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false)
|
||||
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
|
||||
underTest.initialize()
|
||||
val callback = captureArgument { verify(keyguardMonitor).registerCallback(capture()) }
|
||||
whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true)
|
||||
|
||||
verifyZeroInteractions(controller)
|
||||
callback.onUserUnlocked()
|
||||
verify(controller).setNoteTaskShortcutEnabled(any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialize_onRoleHoldersChanged() {
|
||||
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
|
||||
underTest.initialize()
|
||||
val callback = captureArgument {
|
||||
verify(roleManager)
|
||||
.addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL))
|
||||
}
|
||||
|
||||
callback.onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
|
||||
|
||||
verify(controller).onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
private inline fun <reified T : Any> captureArgument(block: ArgumentCaptor<T>.() -> Unit) =
|
||||
argumentCaptor<T>().apply(block).value
|
||||
|
||||
Reference in New Issue
Block a user