Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5a9f8801 authored by Beverly Tai's avatar Beverly Tai Committed by Android (Google) Code Review
Browse files

Merge "Add interactors for KeyEvents" into udc-qpr-dev

parents e75a45ec ce5fbaed
Loading
Loading
Loading
Loading
+67 −0
Original line number Diff line number Diff line
/*
 * 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.keyevent.domain.interactor

import android.view.KeyEvent
import com.android.systemui.back.domain.interactor.BackActionInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor
import javax.inject.Inject

/**
 * Sends key events to the appropriate interactors and then acts upon key events that haven't
 * already been handled but should be handled by SystemUI.
 */
@SysUISingleton
class KeyEventInteractor
@Inject
constructor(
    private val backActionInteractor: BackActionInteractor,
    private val keyguardKeyEventInteractor: KeyguardKeyEventInteractor,
) {
    fun dispatchKeyEvent(event: KeyEvent): Boolean {
        if (keyguardKeyEventInteractor.dispatchKeyEvent(event)) {
            return true
        }

        when (event.keyCode) {
            KeyEvent.KEYCODE_BACK -> {
                if (event.handleAction()) {
                    backActionInteractor.onBackRequested()
                }
                return true
            }
        }
        return false
    }

    fun interceptMediaKey(event: KeyEvent): Boolean {
        return keyguardKeyEventInteractor.interceptMediaKey(event)
    }

    fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
        return keyguardKeyEventInteractor.dispatchKeyEventPreIme(event)
    }

    companion object {
        // Most actions shouldn't be handled on the down event and instead handled on subsequent
        // key events like ACTION_UP.
        fun KeyEvent.handleAction(): Boolean {
            return action != KeyEvent.ACTION_DOWN
        }
    }
}
+117 −0
Original line number Diff line number Diff line
/*
 * 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.keyguard.domain.interactor

import android.content.Context
import android.media.AudioManager
import android.view.KeyEvent
import com.android.systemui.back.domain.interactor.BackActionInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor.Companion.handleAction
import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import javax.inject.Inject

/** Handles key events arriving when the keyguard is showing or device is dozing. */
@SysUISingleton
class KeyguardKeyEventInteractor
@Inject
constructor(
    private val context: Context,
    private val statusBarStateController: StatusBarStateController,
    private val keyguardInteractor: KeyguardInteractor,
    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
    private val shadeController: ShadeController,
    private val mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper,
    private val backActionInteractor: BackActionInteractor,
) {

    fun dispatchKeyEvent(event: KeyEvent): Boolean {
        if (statusBarStateController.isDozing) {
            when (event.keyCode) {
                KeyEvent.KEYCODE_VOLUME_DOWN,
                KeyEvent.KEYCODE_VOLUME_UP -> return dispatchVolumeKeyEvent(event)
            }
        }

        if (event.handleAction()) {
            when (event.keyCode) {
                KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent()
                KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent()
            }
        }
        return false
    }

    /**
     * While IME is active and a BACK event is detected, check with {@link
     * StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event should be
     * handled before routing to IME, in order to prevent the user from having to hit back twice to
     * exit bouncer.
     */
    fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
        when (event.keyCode) {
            KeyEvent.KEYCODE_BACK ->
                if (
                    statusBarStateController.state == StatusBarState.KEYGUARD &&
                        statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()
                ) {
                    return backActionInteractor.onBackRequested()
                }
        }
        return false
    }

    fun interceptMediaKey(event: KeyEvent): Boolean {
        return statusBarStateController.state == StatusBarState.KEYGUARD &&
            statusBarKeyguardViewManager.interceptMediaKey(event)
    }

    private fun dispatchMenuKeyEvent(): Boolean {
        val shouldUnlockOnMenuPressed =
            isDeviceInteractive() &&
                (statusBarStateController.state != StatusBarState.SHADE) &&
                statusBarKeyguardViewManager.shouldDismissOnMenuPressed()
        if (shouldUnlockOnMenuPressed) {
            shadeController.animateCollapseShadeForced()
            return true
        }
        return false
    }

    private fun dispatchSpaceEvent(): Boolean {
        if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) {
            shadeController.animateCollapseShadeForced()
            return true
        }
        return false
    }

    private fun dispatchVolumeKeyEvent(event: KeyEvent): Boolean {
        mediaSessionLegacyHelperWrapper
            .getHelper(context)
            .sendVolumeKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE, true)
        return true
    }

    private fun isDeviceInteractive(): Boolean {
        return keyguardInteractor.wakefulnessModel.value.isDeviceInteractive()
    }
}
+30 −0
Original line number Diff line number Diff line
/*
 * 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.media.controls.util

import android.content.Context
import android.media.session.MediaSessionLegacyHelper
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject

/** Injectable wrapper around `MediaSessionLegacyHelper` functions */
@SysUISingleton
class MediaSessionLegacyHelperWrapper @Inject constructor() {
    fun getHelper(context: Context): MediaSessionLegacyHelper {
        return MediaSessionLegacyHelper.getHelper(context)
    }
}
+147 −0
Original line number Diff line number Diff line
/*
 * 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.keyevent.domain.interactor

import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.back.domain.interactor.BackActionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor
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.Rule
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.junit.MockitoJUnit

@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyEventInteractorTest : SysuiTestCase() {
    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()

    private lateinit var keyguardInteractorWithDependencies:
        KeyguardInteractorFactory.WithDependencies
    @Mock private lateinit var keyguardKeyEventInteractor: KeyguardKeyEventInteractor
    @Mock private lateinit var backActionInteractor: BackActionInteractor

    private lateinit var underTest: KeyEventInteractor

    @Before
    fun setup() {
        keyguardInteractorWithDependencies = KeyguardInteractorFactory.create()
        underTest =
            KeyEventInteractor(
                backActionInteractor,
                keyguardKeyEventInteractor,
            )
    }

    @Test
    fun dispatchBackKey_notHandledByKeyguardKeyEventInteractor_handledByBackActionInteractor() {
        val backKeyEventActionDown = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)
        val backKeyEventActionUp = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)

        // GIVEN back key ACTION_DOWN and ACTION_UP aren't handled by the keyguardKeyEventInteractor
        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionDown))
            .thenReturn(false)
        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionUp))
            .thenReturn(false)

        // WHEN back key event ACTION_DOWN, the event is handled even though back isn't requested
        assertThat(underTest.dispatchKeyEvent(backKeyEventActionDown)).isTrue()
        // THEN back event isn't handled on ACTION_DOWN
        verify(backActionInteractor, never()).onBackRequested()

        // WHEN back key event ACTION_UP
        assertThat(underTest.dispatchKeyEvent(backKeyEventActionUp)).isTrue()
        // THEN back event is handled on ACTION_UP
        verify(backActionInteractor).onBackRequested()
    }

    @Test
    fun dispatchKeyEvent_isNotHandledByKeyguardKeyEventInteractor() {
        val keyEvent =
            KeyEvent(
                KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_SPACE,
            )
        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(false)
        assertThat(underTest.dispatchKeyEvent(keyEvent)).isFalse()
    }

    @Test
    fun dispatchKeyEvent_handledByKeyguardKeyEventInteractor() {
        val keyEvent =
            KeyEvent(
                KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_SPACE,
            )
        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(true)
        assertThat(underTest.dispatchKeyEvent(keyEvent)).isTrue()
    }

    @Test
    fun interceptMediaKey_isNotHandledByKeyguardKeyEventInteractor() {
        val keyEvent =
            KeyEvent(
                KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_SPACE,
            )
        whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(false)
        assertThat(underTest.interceptMediaKey(keyEvent)).isFalse()
    }

    @Test
    fun interceptMediaKey_handledByKeyguardKeyEventInteractor() {
        val keyEvent =
            KeyEvent(
                KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_SPACE,
            )
        whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(true)
        assertThat(underTest.interceptMediaKey(keyEvent)).isTrue()
    }

    @Test
    fun dispatchKeyEventPreIme_isNotHandledByKeyguardKeyEventInteractor() {
        val keyEvent =
            KeyEvent(
                KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_SPACE,
            )
        whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(false)
        assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isFalse()
    }

    @Test
    fun dispatchKeyEventPreIme_handledByKeyguardKeyEventInteractor() {
        val keyEvent =
            KeyEvent(
                KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_SPACE,
            )
        whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(true)
        assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isTrue()
    }
}
+255 −0

File added.

Preview size limit exceeded, changes collapsed.