Lock screen long-press to open wallpaper picker.

Implements a long-press gesture on the lock screen that opens the
wallpaper picker (WPP) screen (after properly unlocking the device).

- Respects device state
  - Only works on the lock screen - does not work when unlocked, quick
    or quick-quick settings is shown, etc.
  - Lets other lock screen UI components receive and handle touch
    (including smart space, unlock/UDFPS, shortcuts, shade, and
    bouncer)
- Flag gated - this is a brand new flag as we're not sure whether we
  want to roll this out and it can be rolled out berore the WPP revamp
  project
- Logging included - logs long-press popup impressions and clicks, to
  help inform launch/no-launch decisions and identify falsing challenges

Fix: 265987364
Test: manually verified that it matches the UX spec: does not work
unless locked and showing the lock screen un-occluded (tested occlusion
with the camera app). Does not work if quick settings or quick-quick
settings is visible. Does not work if unlocked with or without the shade
being shown.
Test: manually verified that smart space clicks, notification clicks and
long presses, UDFPS, and shortcut buttons all still work as advertised.
Test: unit tests included

Change-Id: I600f3e60a03b58691ffd66cddb1be6ff0c80b3ca
This commit is contained in:
Alejandro Nijamkin
2023-01-20 16:40:42 -08:00
parent 8027f4608d
commit 73260bd178
23 changed files with 1315 additions and 8 deletions

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
~
-->
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:color="?android:attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@android:color/white"/>
<corners android:radius="28dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="?androidprv:attr/colorSurface" />
<corners android:radius="28dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
~
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="52dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@drawable/keyguard_settings_popup_menu_background"
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:paddingVertical="16dp">
<ImageView
android:id="@+id/icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="16dp"
android:tint="?android:attr/textColorPrimary"
android:importantForAccessibility="no"
tools:ignore="UseAppTint" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>

View File

@@ -26,6 +26,11 @@
android:layout_height="match_parent"
android:background="@android:color/transparent">
<com.android.systemui.common.ui.view.LongPressHandlingView
android:id="@+id/keyguard_long_press"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
android:id="@+id/keyguard_qs_user_switch_stub"
android:layout="@layout/keyguard_qs_user_switch"

View File

@@ -824,4 +824,8 @@
<item>bottom_end:wallet</item>
</string-array>
<!-- Package name for the app that implements the wallpaper picker. -->
<string name="config_wallpaperPickerPackage" translatable="false">
com.android.wallpaper
</string>
</resources>

View File

@@ -1648,4 +1648,10 @@
<dimen name="rear_display_animation_height">200dp</dimen>
<dimen name="rear_display_title_top_padding">24dp</dimen>
<dimen name="rear_display_title_bottom_padding">16dp</dimen>
<!--
Vertical distance between the pointer and the popup menu that shows up on the lock screen when
it is long-pressed.
-->
<dimen name="keyguard_long_press_settings_popup_vertical_offset">96dp</dimen>
</resources>

View File

@@ -2798,4 +2798,13 @@
<!-- Label for the close button on switch to work profile dialog. Switch to work profile dialog guide users to make call from work
profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] -->
<string name="call_from_work_profile_close">Close</string>
<!--
Label for a menu item in a menu that is shown when the user wishes to configure the lock screen.
Clicking on this menu item takes the user to a screen where they can modify the settings of the
lock screen.
[CHAR LIMIT=32]
-->
<string name="lock_screen_settings">Lock screen settings</string>
</resources>

View File

@@ -0,0 +1,123 @@
/*
* 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.common.ui.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.pow
import kotlin.math.sqrt
import kotlinx.coroutines.DisposableHandle
/**
* View designed to handle long-presses.
*
* The view will not handle any long pressed by default. To set it up, set up a listener and, when
* ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
*/
class LongPressHandlingView(
context: Context,
attrs: AttributeSet?,
) :
View(
context,
attrs,
) {
interface Listener {
/** Notifies that a long-press has been detected by the given view. */
fun onLongPressDetected(
view: View,
x: Int,
y: Int,
)
/** Notifies that the gesture was too short for a long press, it is actually a click. */
fun onSingleTapDetected(view: View) = Unit
}
var listener: Listener? = null
private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
LongPressHandlingViewInteractionHandler(
postDelayed = { block, timeoutMs ->
val dispatchToken = Any()
handler.postDelayed(
block,
dispatchToken,
timeoutMs,
)
DisposableHandle { handler.removeCallbacksAndMessages(dispatchToken) }
},
isAttachedToWindow = ::isAttachedToWindow,
onLongPressDetected = { x, y ->
listener?.onLongPressDetected(
view = this,
x = x,
y = y,
)
},
onSingleTapDetected = { listener?.onSingleTapDetected(this@LongPressHandlingView) },
)
}
fun setLongPressHandlingEnabled(isEnabled: Boolean) {
interactionHandler.isLongPressHandlingEnabled = isEnabled
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
return interactionHandler.onTouchEvent(event?.toModel())
}
}
private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel {
return when (actionMasked) {
MotionEvent.ACTION_DOWN ->
LongPressHandlingViewInteractionHandler.MotionEventModel.Down(
x = x.toInt(),
y = y.toInt(),
)
MotionEvent.ACTION_MOVE ->
LongPressHandlingViewInteractionHandler.MotionEventModel.Move(
distanceMoved = distanceMoved(),
)
MotionEvent.ACTION_UP ->
LongPressHandlingViewInteractionHandler.MotionEventModel.Up(
distanceMoved = distanceMoved(),
gestureDuration = gestureDuration(),
)
MotionEvent.ACTION_CANCEL -> LongPressHandlingViewInteractionHandler.MotionEventModel.Cancel
else -> LongPressHandlingViewInteractionHandler.MotionEventModel.Other
}
}
private fun MotionEvent.distanceMoved(): Float {
return if (historySize > 0) {
sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
} else {
0f
}
}
private fun MotionEvent.gestureDuration(): Long {
return eventTime - downTime
}

View File

@@ -0,0 +1,132 @@
/*
* 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.common.ui.view
import android.view.ViewConfiguration
import kotlinx.coroutines.DisposableHandle
/** Encapsulates logic to handle complex touch interactions with a [LongPressHandlingView]. */
class LongPressHandlingViewInteractionHandler(
/**
* Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
* allowing the delayed runnable to be canceled before it is run.
*/
private val postDelayed: (block: Runnable, delayMs: Long) -> DisposableHandle,
/** Callback to be queried to check if the view is attached to its window. */
private val isAttachedToWindow: () -> Boolean,
/** Callback reporting the a long-press gesture was detected at the given coordinates. */
private val onLongPressDetected: (x: Int, y: Int) -> Unit,
/** Callback reporting the a single tap gesture was detected at the given coordinates. */
private val onSingleTapDetected: () -> Unit,
) {
sealed class MotionEventModel {
object Other : MotionEventModel()
data class Down(
val x: Int,
val y: Int,
) : MotionEventModel()
data class Move(
val distanceMoved: Float,
) : MotionEventModel()
data class Up(
val distanceMoved: Float,
val gestureDuration: Long,
) : MotionEventModel()
object Cancel : MotionEventModel()
}
var isLongPressHandlingEnabled: Boolean = false
var scheduledLongPressHandle: DisposableHandle? = null
fun onTouchEvent(event: MotionEventModel?): Boolean {
if (!isLongPressHandlingEnabled) {
return false
}
return when (event) {
is MotionEventModel.Down -> {
scheduleLongPress(event.x, event.y)
true
}
is MotionEventModel.Move -> {
if (event.distanceMoved > ViewConfiguration.getTouchSlop()) {
cancelScheduledLongPress()
}
false
}
is MotionEventModel.Up -> {
cancelScheduledLongPress()
if (
event.distanceMoved <= ViewConfiguration.getTouchSlop() &&
event.gestureDuration < ViewConfiguration.getLongPressTimeout()
) {
dispatchSingleTap()
}
false
}
is MotionEventModel.Cancel -> {
cancelScheduledLongPress()
false
}
else -> false
}
}
private fun scheduleLongPress(
x: Int,
y: Int,
) {
scheduledLongPressHandle =
postDelayed(
{
dispatchLongPress(
x = x,
y = y,
)
},
ViewConfiguration.getLongPressTimeout().toLong(),
)
}
private fun dispatchLongPress(
x: Int,
y: Int,
) {
if (!isAttachedToWindow()) {
return
}
onLongPressDetected(x, y)
}
private fun cancelScheduledLongPress() {
scheduledLongPressHandle?.dispose()
}
private fun dispatchSingleTap() {
if (!isAttachedToWindow()) {
return
}
onSingleTapDetected()
}
}

View File

@@ -65,8 +65,7 @@ object Flags {
val FSI_ON_DND_UPDATE = unreleasedFlag(259130119, "fsi_on_dnd_update", teamfood = true)
// TODO(b/265804648): Tracking Bug
@JvmField
val DISABLE_FSI = unreleasedFlag(265804648, "disable_fsi")
@JvmField val DISABLE_FSI = unreleasedFlag(265804648, "disable_fsi")
// TODO(b/254512538): Tracking Bug
val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply", teamfood = true)
@@ -221,6 +220,15 @@ object Flags {
val WALLPAPER_FULLSCREEN_PREVIEW =
unreleasedFlag(227, "wallpaper_fullscreen_preview", teamfood = true)
/** Whether the long-press gesture to open wallpaper picker is enabled. */
// TODO(b/266242192): Tracking Bug
@JvmField
val LOCK_SCREEN_LONG_PRESS_ENABLED =
unreleasedFlag(
228,
"lock_screen_long_press_enabled",
)
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -570,6 +578,5 @@ object Flags {
// 2600 - keyboard shortcut
// TODO(b/259352579): Tracking Bug
@JvmField
val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout")
@JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout")
}

View File

@@ -148,6 +148,9 @@ interface KeyguardRepository {
/** Source of the most recent biometric unlock, such as fingerprint or face. */
val biometricUnlockSource: Flow<BiometricUnlockSource?>
/** Whether quick settings or quick-quick settings is visible. */
val isQuickSettingsVisible: Flow<Boolean>
/**
* Returns `true` if the keyguard is showing; `false` otherwise.
*
@@ -172,6 +175,9 @@ interface KeyguardRepository {
* Returns whether the keyguard bottom area should be constrained to the top of the lock icon
*/
fun isUdfpsSupported(): Boolean
/** Sets whether quick settings or quick-quick settings is visible. */
fun setQuickSettingsVisible(isVisible: Boolean)
}
/** Encapsulates application state for the keyguard. */
@@ -581,6 +587,9 @@ constructor(
awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
}
private val _isQuickSettingsVisible = MutableStateFlow(false)
override val isQuickSettingsVisible: Flow<Boolean> = _isQuickSettingsVisible.asStateFlow()
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
@@ -595,6 +604,10 @@ constructor(
override fun isUdfpsSupported(): Boolean = keyguardUpdateMonitor.isUdfpsSupported
override fun setQuickSettingsVisible(isVisible: Boolean) {
_isQuickSettingsVisible.value = isVisible
}
private fun statusBarStateIntToObject(value: Int): StatusBarState {
return when (value) {
0 -> StatusBarState.SHADE

View File

@@ -155,6 +155,11 @@ constructor(
}
}
/** Sets whether quick settings or quick-quick settings is visible. */
fun setQuickSettingsVisible(isVisible: Boolean) {
repository.setQuickSettingsVisible(isVisible)
}
companion object {
private const val TAG = "KeyguardInteractor"
}

View File

@@ -0,0 +1,187 @@
/*
* 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.content.Intent
import android.content.IntentFilter
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.shared.model.Position
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.domain.model.KeyguardSettingsPopupMenuModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/** Business logic for use-cases related to the keyguard long-press feature. */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardLongPressInteractor
@Inject
constructor(
@Application unsafeContext: Context,
@Application scope: CoroutineScope,
transitionInteractor: KeyguardTransitionInteractor,
repository: KeyguardRepository,
private val activityStarter: ActivityStarter,
private val logger: UiEventLogger,
private val featureFlags: FeatureFlags,
broadcastDispatcher: BroadcastDispatcher,
) {
private val appContext = unsafeContext.applicationContext
private val _isLongPressHandlingEnabled: StateFlow<Boolean> =
if (isFeatureEnabled()) {
combine(
transitionInteractor.finishedKeyguardState.map {
it == KeyguardState.LOCKSCREEN
},
repository.isQuickSettingsVisible,
) { isFullyTransitionedToLockScreen, isQuickSettingsVisible ->
isFullyTransitionedToLockScreen && !isQuickSettingsVisible
}
} else {
flowOf(false)
}
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: Flow<Boolean> = _isLongPressHandlingEnabled
private val _menu = MutableStateFlow<KeyguardSettingsPopupMenuModel?>(null)
/** Model for a menu that should be shown; `null` when no menu should be shown. */
val menu: Flow<KeyguardSettingsPopupMenuModel?> =
isLongPressHandlingEnabled.flatMapLatest { isEnabled ->
if (isEnabled) {
_menu
} else {
flowOf(null)
}
}
init {
if (isFeatureEnabled()) {
broadcastDispatcher
.broadcastFlow(
IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
)
.onEach { hideMenu() }
.launchIn(scope)
}
}
/** Notifies that the user has long-pressed on the lock screen. */
fun onLongPress(x: Int, y: Int) {
if (!_isLongPressHandlingEnabled.value) {
return
}
showMenu(
x = x,
y = y,
)
}
private fun isFeatureEnabled(): Boolean {
return featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED) &&
featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI)
}
/** Updates application state to ask to show the menu at the given coordinates. */
private fun showMenu(
x: Int,
y: Int,
) {
_menu.value =
KeyguardSettingsPopupMenuModel(
position =
Position(
x = x,
y = y,
),
onClicked = {
hideMenu()
navigateToLockScreenSettings()
},
onDismissed = { hideMenu() },
)
logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
}
/** Updates application state to ask to hide the menu. */
private fun hideMenu() {
_menu.value = null
}
/** Opens the wallpaper picker screen after the device is unlocked by the user. */
private fun navigateToLockScreenSettings() {
logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
activityStarter.dismissKeyguardThenExecute(
/* action= */ {
appContext.startActivity(
Intent(Intent.ACTION_SET_WALLPAPER).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
appContext
.getString(R.string.config_wallpaperPickerPackage)
.takeIf { it.isNotEmpty() }
?.let { packageName -> setPackage(packageName) }
}
)
true
},
/* cancel= */ {},
/* afterKeyguardGone= */ true,
)
}
enum class LogEvents(
private val _id: Int,
) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "The lock screen was long-pressed and we showed the settings popup menu.")
LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN(1292),
@UiEvent(doc = "The lock screen long-press popup menu was clicked.")
LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED(1293),
;
override fun getId() = _id
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.model
import com.android.systemui.common.shared.model.Position
/** Models a settings popup menu for the lock screen. */
data class KeyguardSettingsPopupMenuModel(
/** Where the menu should be anchored, roughly in screen space. */
val position: Position,
/** Callback to invoke when the menu gets clicked by the user. */
val onClicked: () -> Unit,
/** Callback to invoke when the menu gets dismissed by the user. */
val onDismissed: () -> Unit,
)

View File

@@ -0,0 +1,88 @@
/*
* 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.ui.binder
import android.annotation.SuppressLint
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.PopupWindow
import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.common.ui.binder.TextViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsPopupMenuViewModel
object KeyguardLongPressPopupViewBinder {
@SuppressLint("InflateParams") // We don't care that the parent is null.
fun createAndShow(
container: View,
viewModel: KeyguardSettingsPopupMenuViewModel,
onDismissed: () -> Unit,
): () -> Unit {
val contentView: View =
LayoutInflater.from(container.context)
.inflate(
R.layout.keyguard_settings_popup_menu,
null,
)
contentView.setOnClickListener { viewModel.onClicked() }
IconViewBinder.bind(
icon = viewModel.icon,
view = contentView.requireViewById(R.id.icon),
)
TextViewBinder.bind(
view = contentView.requireViewById(R.id.text),
viewModel = viewModel.text,
)
val popupWindow =
PopupWindow(container.context).apply {
windowLayoutType = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
setBackgroundDrawable(null)
animationStyle = com.android.internal.R.style.Animation_Dialog
isOutsideTouchable = true
isFocusable = true
setContentView(contentView)
setOnDismissListener { onDismissed() }
contentView.measure(
View.MeasureSpec.makeMeasureSpec(
0,
View.MeasureSpec.UNSPECIFIED,
),
View.MeasureSpec.makeMeasureSpec(
0,
View.MeasureSpec.UNSPECIFIED,
),
)
showAtLocation(
container,
Gravity.NO_GRAVITY,
viewModel.position.x - contentView.measuredWidth / 2,
viewModel.position.y -
contentView.measuredHeight -
container.context.resources.getDimensionPixelSize(
R.dimen.keyguard_long_press_settings_popup_vertical_offset
),
)
}
return { popupWindow.dismiss() }
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.ui.binder
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.common.ui.view.LongPressHandlingView
import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import kotlinx.coroutines.launch
object KeyguardLongPressViewBinder {
/**
* Drives UI for the lock screen long-press feature.
*
* @param view The view that listens for long-presses.
* @param viewModel The view-model that models the UI state.
* @param onSingleTap A callback to invoke when the system decides that there was a single tap.
* @param falsingManager [FalsingManager] for making sure the long-press didn't just happen in
* the user's pocket.
*/
@JvmStatic
fun bind(
view: LongPressHandlingView,
viewModel: KeyguardLongPressViewModel,
onSingleTap: () -> Unit,
falsingManager: FalsingManager,
) {
view.listener =
object : LongPressHandlingView.Listener {
override fun onLongPressDetected(view: View, x: Int, y: Int) {
if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
return
}
viewModel.onLongPress(
x = x,
y = y,
)
}
override fun onSingleTapDetected(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return
}
onSingleTap()
}
}
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.isLongPressHandlingEnabled.collect { isEnabled ->
view.setLongPressHandlingEnabled(isEnabled)
}
}
launch {
var dismissMenu: (() -> Unit)? = null
viewModel.menu.collect { menuOrNull ->
if (menuOrNull != null) {
dismissMenu =
KeyguardLongPressPopupViewBinder.createAndShow(
container = view,
viewModel = menuOrNull,
onDismissed = menuOrNull.onDismissed,
)
} else {
dismissMenu?.invoke()
}
}
}
}
}
}
}

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.keyguard.ui.viewmodel
import com.android.systemui.R
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Text
import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/** Models UI state to support the lock screen long-press feature. */
class KeyguardLongPressViewModel
@Inject
constructor(
private val interactor: KeyguardLongPressInteractor,
) {
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
/** View-model for a menu that should be shown; `null` when no menu should be shown. */
val menu: Flow<KeyguardSettingsPopupMenuViewModel?> =
interactor.menu.map { model ->
model?.let {
KeyguardSettingsPopupMenuViewModel(
icon =
Icon.Resource(
res = R.drawable.ic_settings,
contentDescription = null,
),
text =
Text.Resource(
res = R.string.lock_screen_settings,
),
position = model.position,
onClicked = model.onClicked,
onDismissed = model.onDismissed,
)
}
}
/** Notifies that the user has long-pressed on the lock screen. */
fun onLongPress(
x: Int,
y: Int,
) {
interactor.onLongPress(
x = x,
y = y,
)
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.ui.viewmodel
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Position
import com.android.systemui.common.shared.model.Text
/** Models the UI state of a keyguard settings popup menu. */
data class KeyguardSettingsPopupMenuViewModel(
val icon: Icon,
val text: Text,
/** Where the menu should be anchored, roughly in screen space. */
val position: Position,
/** Callback to invoke when the menu gets clicked by the user. */
val onClicked: () -> Unit,
/** Callback to invoke when the menu gets dismissed by the user. */
val onDismissed: () -> Unit,
)

View File

@@ -140,12 +140,15 @@ import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
@@ -243,6 +246,7 @@ import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Provider;
import kotlin.Unit;
import kotlinx.coroutines.CoroutineDispatcher;
@CentralSurfacesComponent.CentralSurfacesScope
@@ -698,6 +702,7 @@ public final class NotificationPanelViewController implements Dumpable {
private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final KeyguardInteractor mKeyguardInteractor;
private CoroutineDispatcher mMainDispatcher;
private boolean mIsOcclusionTransitionRunning = false;
private int mDreamingToLockscreenTransitionTranslationY;
@@ -827,7 +832,9 @@ public final class NotificationPanelViewController implements Dumpable {
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
@Main CoroutineDispatcher mainDispatcher,
KeyguardTransitionInteractor keyguardTransitionInteractor,
DumpManager dumpManager) {
DumpManager dumpManager,
KeyguardLongPressViewModel keyguardLongPressViewModel,
KeyguardInteractor keyguardInteractor) {
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
public void onKeyguardFadingAwayChanged() {
@@ -848,6 +855,7 @@ public final class NotificationPanelViewController implements Dumpable {
mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel;
mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mKeyguardInteractor = keyguardInteractor;
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
@@ -1000,6 +1008,14 @@ public final class NotificationPanelViewController implements Dumpable {
updateUserSwitcherFlags();
mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
KeyguardLongPressViewBinder.bind(
mView.requireViewById(R.id.keyguard_long_press),
keyguardLongPressViewModel,
() -> {
onEmptySpaceClick();
return Unit.INSTANCE;
},
mFalsingManager);
onFinishInflate();
keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -3129,6 +3145,7 @@ public final class NotificationPanelViewController implements Dumpable {
mQsClipBottom,
radius,
qsVisible && !mSplitShadeEnabled);
mKeyguardInteractor.setQuickSettingsVisible(mQsVisible);
}
// The padding on this area is large enough that we can use a cheaper clipping strategy
mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);

View File

@@ -0,0 +1,163 @@
/*
* 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.common.ui.view
import android.view.ViewConfiguration
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel
import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Down
import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Move
import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Up
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class LongPressHandlingViewInteractionHandlerTest : SysuiTestCase() {
@Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle
@Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit
@Mock private lateinit var onSingleTapDetected: () -> Unit
private lateinit var underTest: LongPressHandlingViewInteractionHandler
private var isAttachedToWindow: Boolean = true
private var delayedRunnable: Runnable? = null
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(postDelayed.invoke(any(), any())).thenAnswer { invocation ->
delayedRunnable = invocation.arguments[0] as Runnable
DisposableHandle { delayedRunnable = null }
}
underTest =
LongPressHandlingViewInteractionHandler(
postDelayed = postDelayed,
isAttachedToWindow = { isAttachedToWindow },
onLongPressDetected = onLongPressDetected,
onSingleTapDetected = onSingleTapDetected,
)
underTest.isLongPressHandlingEnabled = true
}
@Test
fun `long-press`() = runTest {
val downX = 123
val downY = 456
dispatchTouchEvents(
Down(
x = downX,
y = downY,
),
Move(
distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f,
),
)
delayedRunnable?.run()
verify(onLongPressDetected).invoke(downX, downY)
verify(onSingleTapDetected, never()).invoke()
}
@Test
fun `long-press but feature not enabled`() = runTest {
underTest.isLongPressHandlingEnabled = false
dispatchTouchEvents(
Down(
x = 123,
y = 456,
),
)
assertThat(delayedRunnable).isNull()
verify(onLongPressDetected, never()).invoke(any(), any())
verify(onSingleTapDetected, never()).invoke()
}
@Test
fun `long-press but view not attached`() = runTest {
isAttachedToWindow = false
dispatchTouchEvents(
Down(
x = 123,
y = 456,
),
)
delayedRunnable?.run()
verify(onLongPressDetected, never()).invoke(any(), any())
verify(onSingleTapDetected, never()).invoke()
}
@Test
fun `dragged too far to be considered a long-press`() = runTest {
dispatchTouchEvents(
Down(
x = 123,
y = 456,
),
Move(
distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f,
),
)
assertThat(delayedRunnable).isNull()
verify(onLongPressDetected, never()).invoke(any(), any())
verify(onSingleTapDetected, never()).invoke()
}
@Test
fun `held down too briefly to be considered a long-press`() = runTest {
dispatchTouchEvents(
Down(
x = 123,
y = 456,
),
Up(
distanceMoved = ViewConfiguration.getTouchSlop().toFloat(),
gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L,
),
)
assertThat(delayedRunnable).isNull()
verify(onLongPressDetected, never()).invoke(any(), any())
verify(onSingleTapDetected).invoke()
}
private fun dispatchTouchEvents(
vararg models: MotionEventModel,
) {
models.forEach { model -> underTest.onTouchEvent(model) }
}
}

View File

@@ -0,0 +1,213 @@
/*
* 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.Intent
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class KeyguardLongPressInteractorTest : SysuiTestCase() {
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var logger: UiEventLogger
private lateinit var underTest: KeyguardLongPressInteractor
private lateinit var testScope: TestScope
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
runBlocking { createUnderTest() }
}
@Test
fun isEnabled() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
setUpState(
keyguardState = keyguardState,
)
if (keyguardState == KeyguardState.LOCKSCREEN) {
assertThat(isEnabled()).isTrue()
} else {
assertThat(isEnabled()).isFalse()
}
}
}
@Test
fun `isEnabled - always false when quick settings are visible`() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
setUpState(
keyguardState = keyguardState,
isQuickSettingsVisible = true,
)
assertThat(isEnabled()).isFalse()
}
}
@Test
fun `long-pressed - pop-up clicked - starts activity`() =
testScope.runTest {
val menu = collectLastValue(underTest.menu)
runCurrent()
val x = 100
val y = 123
underTest.onLongPress(x, y)
assertThat(menu()).isNotNull()
assertThat(menu()?.position?.x).isEqualTo(x)
assertThat(menu()?.position?.y).isEqualTo(y)
menu()?.onClicked?.invoke()
assertThat(menu()).isNull()
verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean())
}
@Test
fun `long-pressed - pop-up dismissed - never starts activity`() =
testScope.runTest {
val menu = collectLastValue(underTest.menu)
runCurrent()
menu()?.onDismissed?.invoke()
assertThat(menu()).isNull()
verify(activityStarter, never()).dismissKeyguardThenExecute(any(), any(), anyBoolean())
}
@Suppress("DEPRECATION") // We're okay using ACTION_CLOSE_SYSTEM_DIALOGS on system UI.
@Test
fun `long pressed - close dialogs broadcast received - popup dismissed`() =
testScope.runTest {
val menu = collectLastValue(underTest.menu)
runCurrent()
underTest.onLongPress(123, 456)
assertThat(menu()).isNotNull()
fakeBroadcastDispatcher.registeredReceivers.forEach { broadcastReceiver ->
broadcastReceiver.onReceive(context, Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
assertThat(menu()).isNull()
}
@Test
fun `logs when menu is shown`() =
testScope.runTest {
collectLastValue(underTest.menu)
runCurrent()
underTest.onLongPress(100, 123)
verify(logger)
.log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
}
@Test
fun `logs when menu is clicked`() =
testScope.runTest {
val menu = collectLastValue(underTest.menu)
runCurrent()
underTest.onLongPress(100, 123)
menu()?.onClicked?.invoke()
verify(logger)
.log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
}
private suspend fun createUnderTest(
isLongPressFeatureEnabled: Boolean = true,
isRevampedWppFeatureEnabled: Boolean = true,
) {
testScope = TestScope()
keyguardRepository = FakeKeyguardRepository()
keyguardTransitionRepository = FakeKeyguardTransitionRepository()
underTest =
KeyguardLongPressInteractor(
unsafeContext = context,
scope = testScope.backgroundScope,
transitionInteractor =
KeyguardTransitionInteractor(
repository = keyguardTransitionRepository,
),
repository = keyguardRepository,
activityStarter = activityStarter,
logger = logger,
featureFlags =
FakeFeatureFlags().apply {
set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled)
set(Flags.REVAMPED_WALLPAPER_UI, isRevampedWppFeatureEnabled)
},
broadcastDispatcher = fakeBroadcastDispatcher,
)
setUpState()
}
private suspend fun setUpState(
keyguardState: KeyguardState = KeyguardState.LOCKSCREEN,
isQuickSettingsVisible: Boolean = false,
) {
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
to = keyguardState,
),
)
keyguardRepository.setQuickSettingsVisible(isVisible = isQuickSettingsVisible)
}
}

View File

@@ -97,6 +97,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.common.ui.view.LongPressHandlingView;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
@@ -105,10 +106,12 @@ import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
@@ -197,7 +200,7 @@ import kotlinx.coroutines.CoroutineDispatcher;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class NotificationPanelViewControllerTest extends SysuiTestCase {
private static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
@@ -302,6 +305,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
@Mock private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
@Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Mock private KeyguardInteractor mKeyguardInteractor;
@Mock private KeyguardLongPressViewModel mKeyuardLongPressViewModel;
@Mock private CoroutineDispatcher mMainDispatcher;
@Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock private MotionEvent mDownMotionEvent;
@@ -459,6 +464,9 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
mMainHandler = new Handler(Looper.getMainLooper());
when(mView.requireViewById(R.id.keyguard_long_press))
.thenReturn(mock(LongPressHandlingView.class));
mNotificationPanelViewController = new NotificationPanelViewController(
mView,
mMainHandler,
@@ -528,7 +536,9 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
mLockscreenToOccludedTransitionViewModel,
mMainDispatcher,
mKeyguardTransitionInteractor,
mDumpManager);
mDumpManager,
mKeyuardLongPressViewModel,
mKeyguardInteractor);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
null,

View File

@@ -29,6 +29,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** Fake implementation of [KeyguardRepository] */
class FakeKeyguardRepository : KeyguardRepository {
@@ -101,6 +102,13 @@ class FakeKeyguardRepository : KeyguardRepository {
private val _biometricUnlockSource = MutableStateFlow<BiometricUnlockSource?>(null)
override val biometricUnlockSource: Flow<BiometricUnlockSource?> = _biometricUnlockSource
private val _isQuickSettingsVisible = MutableStateFlow(false)
override val isQuickSettingsVisible: Flow<Boolean> = _isQuickSettingsVisible.asStateFlow()
override fun setQuickSettingsVisible(isVisible: Boolean) {
_isQuickSettingsVisible.value = isVisible
}
override fun isKeyguardShowing(): Boolean {
return _isKeyguardShowing.value
}
@@ -169,6 +177,10 @@ class FakeKeyguardRepository : KeyguardRepository {
_dozeTransitionModel.value = model
}
fun setStatusBarState(state: StatusBarState) {
_statusBarState.value = state
}
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}

View File

@@ -22,13 +22,15 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import java.util.UUID
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
/** Fake implementation of [KeyguardTransitionRepository] */
class FakeKeyguardTransitionRepository : KeyguardTransitionRepository {
private val _transitions = MutableSharedFlow<TransitionStep>()
private val _transitions =
MutableSharedFlow<TransitionStep>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val transitions: SharedFlow<TransitionStep> = _transitions
suspend fun sendTransitionStep(step: TransitionStep) {