Merge changes I60d63d65,Id41e82b9 into udc-dev

* changes:
  UX polish for long-press lock screen popup.
  Adds flag for opening WPP directly from keyguard long-press.
This commit is contained in:
Ale Nijamkin
2023-04-07 00:11:07 +00:00
committed by Android (Google) Code Review
29 changed files with 1153 additions and 714 deletions

View File

@@ -0,0 +1,25 @@
<?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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,395 112,322Q144,249 199.5,195Q255,141 329.5,110.5Q404,80 489,80Q568,80 639,106.5Q710,133 763.5,180Q817,227 848.5,291.5Q880,356 880,433Q880,541 817,603.5Q754,666 650,666L575,666Q557,666 544,680Q531,694 531,711Q531,738 545.5,757Q560,776 560,801Q560,839 539,859.5Q518,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM247,506Q267,506 282,491Q297,476 297,456Q297,436 282,421Q267,406 247,406Q227,406 212,421Q197,436 197,456Q197,476 212,491Q227,506 247,506ZM373,336Q393,336 408,321Q423,306 423,286Q423,266 408,251Q393,236 373,236Q353,236 338,251Q323,266 323,286Q323,306 338,321Q353,336 373,336ZM587,336Q607,336 622,321Q637,306 637,286Q637,266 622,251Q607,236 587,236Q567,236 552,251Q537,266 537,286Q537,306 552,321Q567,336 587,336ZM718,506Q738,506 753,491Q768,476 768,456Q768,436 753,421Q738,406 718,406Q698,406 683,421Q668,436 668,456Q668,476 683,491Q698,506 718,506ZM480,820Q491,820 495.5,815.5Q500,811 500,801Q500,787 485.5,775Q471,763 471,722Q471,676 501,641Q531,606 577,606L650,606Q726,606 773,561.5Q820,517 820,433Q820,301 720,220.5Q620,140 489,140Q343,140 241.5,238.5Q140,337 140,480Q140,621 239.5,720.5Q339,820 480,820Z"/>
</vector>

View File

@@ -0,0 +1,22 @@
<?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.
-->
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
android:controlX1="0.30"
android:controlY1="0.00"
android:controlX2="0.33"
android:controlY2="1.00" />

View File

@@ -0,0 +1,49 @@
<?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.
-->
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<scale
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:duration="200"
android:fromXScale="0.5"
android:fromYScale="0.5"
android:toXScale="1.02"
android:toYScale="1.02"
android:pivotX="50%"
android:pivotY="50%" />
<scale
android:interpolator="@anim/keyguard_settings_popup_ease_out_interpolator"
android:startOffset="200"
android:duration="200"
android:fromXScale="1"
android:fromYScale="1"
android:toXScale="0.98"
android:toYScale="0.98"
android:pivotX="50%"
android:pivotY="50%" />
<alpha
android:interpolator="@android:anim/linear_interpolator"
android:duration="83"
android:fromAlpha="0"
android:toAlpha="1" />
</set>

View File

@@ -0,0 +1,39 @@
<?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.
-->
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<scale
android:interpolator="@android:anim/accelerate_interpolator"
android:duration="233"
android:fromXScale="1"
android:fromYScale="1"
android:toXScale="0.5"
android:toYScale="0.5"
android:pivotX="50%"
android:pivotY="50%" />
<alpha
android:interpolator="@android:anim/linear_interpolator"
android:delay="150"
android:duration="83"
android:fromAlpha="1"
android:toAlpha="0" />
</set>

View File

@@ -17,17 +17,17 @@
<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">
android:color="#4d000000">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@android:color/white"/>
<corners android:radius="28dp" />
<corners android:radius="@dimen/keyguard_affordance_fixed_radius" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="?androidprv:attr/colorSurface" />
<corners android:radius="28dp" />
<solid android:color="?androidprv:attr/materialColorOnBackground" />
<corners android:radius="@dimen/keyguard_affordance_fixed_radius" />
</shape>
</item>
</ripple>

View File

@@ -20,7 +20,7 @@
android:id="@+id/keyguard_bottom_area"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:outlineProvider="none" > <!-- Put it above the status bar header -->
android:outlineProvider="none" >
<LinearLayout
android:id="@+id/keyguard_indication_area"
@@ -59,33 +59,58 @@
</LinearLayout>
<com.android.systemui.animation.view.LaunchableImageView
android:id="@+id/start_button"
android:layout_height="@dimen/keyguard_affordance_fixed_height"
android:layout_width="@dimen/keyguard_affordance_fixed_width"
android:layout_gravity="bottom|start"
android:scaleType="fitCenter"
android:padding="@dimen/keyguard_affordance_fixed_padding"
android:tint="?android:attr/textColorPrimary"
android:background="@drawable/keyguard_bottom_affordance_bg"
android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset"
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="bottom"
>
<com.android.systemui.animation.view.LaunchableImageView
android:id="@+id/end_button"
android:layout_height="@dimen/keyguard_affordance_fixed_height"
android:layout_width="@dimen/keyguard_affordance_fixed_width"
android:layout_gravity="bottom|end"
android:scaleType="fitCenter"
android:padding="@dimen/keyguard_affordance_fixed_padding"
android:tint="?android:attr/textColorPrimary"
android:background="@drawable/keyguard_bottom_affordance_bg"
android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset"
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
android:visibility="gone" />
<com.android.systemui.animation.view.LaunchableImageView
android:id="@+id/start_button"
android:layout_height="@dimen/keyguard_affordance_fixed_height"
android:layout_width="@dimen/keyguard_affordance_fixed_width"
android:layout_gravity="bottom|start"
android:scaleType="fitCenter"
android:padding="@dimen/keyguard_affordance_fixed_padding"
android:tint="?android:attr/textColorPrimary"
android:background="@drawable/keyguard_bottom_affordance_bg"
android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset"
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
android:visibility="invisible" />
<FrameLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
>
<include
android:id="@+id/keyguard_settings_button"
layout="@layout/keyguard_settings_popup_menu"
android:layout_width="wrap_content"
android:layout_height="@dimen/keyguard_affordance_fixed_height"
android:layout_gravity="center"
android:visibility="gone"
/>
</FrameLayout>
<com.android.systemui.animation.view.LaunchableImageView
android:id="@+id/end_button"
android:layout_height="@dimen/keyguard_affordance_fixed_height"
android:layout_width="@dimen/keyguard_affordance_fixed_width"
android:layout_gravity="bottom|end"
android:scaleType="fitCenter"
android:padding="@dimen/keyguard_affordance_fixed_padding"
android:tint="?android:attr/textColorPrimary"
android:background="@drawable/keyguard_bottom_affordance_bg"
android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset"
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
android:visibility="invisible" />
</LinearLayout>
<FrameLayout
android:id="@+id/overlay_container"

View File

@@ -15,25 +15,23 @@
~
-->
<LinearLayout
<com.android.systemui.animation.view.LaunchableLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="52dp"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@drawable/keyguard_settings_popup_menu_background"
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:paddingVertical="16dp">
android:padding="12dp">
<ImageView
android:id="@+id/icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="16dp"
android:tint="?android:attr/textColorPrimary"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:tint="?androidprv:attr/materialColorOnSecondaryFixed"
android:importantForAccessibility="no"
tools:ignore="UseAppTint" />
@@ -42,9 +40,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textColor="?androidprv:attr/materialColorOnSecondaryFixed"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
</com.android.systemui.animation.view.LaunchableLinearLayout>

View File

@@ -120,10 +120,6 @@
/>
</com.android.systemui.shade.NotificationsQuickSettingsContainer>
<include
layout="@layout/keyguard_bottom_area"
android:visibility="gone" />
<ViewStub
android:id="@+id/keyguard_user_switcher_stub"
android:layout="@layout/keyguard_user_switcher"
@@ -153,6 +149,10 @@
</com.android.keyguard.LockIconView>
<include
layout="@layout/keyguard_bottom_area"
android:visibility="gone" />
<FrameLayout
android:id="@+id/preview_container"
android:layout_width="match_parent"

View File

@@ -1774,13 +1774,6 @@
<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>
<!-- Bouncer user switcher margins -->
<dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
<dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>

View File

@@ -3057,13 +3057,17 @@
<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.
Label for a menu item in a menu that is shown when the user wishes to customize the lock screen.
Clicking on this menu item takes the user to a screen where they can modify the settings of the
lock screen.
It is critical that this text is as short as possible. If needed, translators should feel free
to drop "lock screen" from their translation and keep just "Customize" or "Customization", in
cases when that verb is not available in their target language.
[CHAR LIMIT=32]
-->
<string name="lock_screen_settings">Lock screen settings</string>
<string name="lock_screen_settings">Customize lock screen</string>
<!-- Content description for Wi-Fi not available icon on dream [CHAR LIMIT=NONE]-->
<string name="wifi_unavailable_dream_overlay_content_description">Wi-Fi not available</string>

View File

@@ -1399,4 +1399,9 @@
<style name="ShortcutItemBackground">
<item name="android:background">@color/ksh_key_item_new_background</item>
</style>
<style name="LongPressLockScreenAnimation">
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
</style>
</resources>

View File

@@ -5,26 +5,24 @@
* 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
* 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
package com.android.systemui.common.ui.view
import com.android.systemui.common.shared.model.Position
import android.util.MathUtils
import android.view.MotionEvent
/** 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,
)
/** Returns the distance from the position of this [MotionEvent] and the given coordinates. */
fun MotionEvent.distanceFrom(
x: Float,
y: Float,
): Float {
return MathUtils.dist(this.x, this.y, x, y)
}

View File

@@ -230,6 +230,12 @@ object Flags {
@JvmField
val REFACTOR_KEYGUARD_DISMISS_INTENT = unreleasedFlag(231, "refactor_keyguard_dismiss_intent")
/** Whether to allow long-press on the lock screen to directly open wallpaper picker. */
// TODO(b/277220285): Tracking bug.
@JvmField
val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP =
unreleasedFlag(232, "lock_screen_long_press_directly_opens_wallpaper_picker")
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")

View File

@@ -17,29 +17,29 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.view.accessibility.AccessibilityManager
import androidx.annotation.VisibleForTesting
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 com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Business logic for use-cases related to the keyguard long-press feature. */
@OptIn(ExperimentalCoroutinesApi::class)
@@ -54,18 +55,16 @@ import kotlinx.coroutines.flow.stateIn
class KeyguardLongPressInteractor
@Inject
constructor(
@Application unsafeContext: Context,
@Application scope: CoroutineScope,
@Application private val scope: CoroutineScope,
transitionInteractor: KeyguardTransitionInteractor,
repository: KeyguardRepository,
private val activityStarter: ActivityStarter,
private val logger: UiEventLogger,
private val featureFlags: FeatureFlags,
broadcastDispatcher: BroadcastDispatcher,
private val accessibilityManager: AccessibilityManagerWrapper,
) {
private val appContext = unsafeContext.applicationContext
private val _isLongPressHandlingEnabled: StateFlow<Boolean> =
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: StateFlow<Boolean> =
if (isFeatureEnabled()) {
combine(
transitionInteractor.finishedKeyguardState.map {
@@ -84,19 +83,35 @@ constructor(
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)
private val _isMenuVisible = MutableStateFlow(false)
/** Model for whether the menu should be shown. */
val isMenuVisible: StateFlow<Boolean> =
isLongPressHandlingEnabled
.flatMapLatest { isEnabled ->
if (isEnabled) {
_isMenuVisible.asStateFlow()
} else {
// Reset the state so we don't see a menu when long-press handling is enabled
// again in the future.
_isMenuVisible.value = false
flowOf(false)
}
}
}
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
private val _shouldOpenSettings = MutableStateFlow(false)
/**
* Whether the long-press accessible "settings" flow should be opened.
*
* Note that [onSettingsShown] must be invoked to consume this, once the settings are opened.
*/
val shouldOpenSettings = _shouldOpenSettings.asStateFlow()
private var delayedHideMenuJob: Job? = null
init {
if (isFeatureEnabled()) {
@@ -110,15 +125,46 @@ constructor(
}
/** Notifies that the user has long-pressed on the lock screen. */
fun onLongPress(x: Int, y: Int) {
if (!_isLongPressHandlingEnabled.value) {
fun onLongPress() {
if (!isLongPressHandlingEnabled.value) {
return
}
showMenu(
x = x,
y = y,
)
if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) {
showSettings()
} else {
showMenu()
}
}
/** Notifies that the user has touched outside of the pop-up. */
fun onTouchedOutside() {
hideMenu()
}
/** Notifies that the user has started a touch gesture on the menu. */
fun onMenuTouchGestureStarted() {
cancelAutomaticMenuHiding()
}
/** Notifies that the user has started a touch gesture on the menu. */
fun onMenuTouchGestureEnded(isClick: Boolean) {
if (isClick) {
hideMenu()
logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
showSettings()
} else {
scheduleAutomaticMenuHiding()
}
}
/** Notifies that the settings UI has been shown, consuming the event to show it. */
fun onSettingsShown() {
_shouldOpenSettings.value = false
}
private fun showSettings() {
_shouldOpenSettings.value = true
}
private fun isFeatureEnabled(): Boolean {
@@ -126,51 +172,40 @@ constructor(
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() },
)
/** Updates application state to ask to show the menu. */
private fun showMenu() {
_isMenuVisible.value = true
scheduleAutomaticMenuHiding()
logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
}
private fun scheduleAutomaticMenuHiding() {
cancelAutomaticMenuHiding()
delayedHideMenuJob =
scope.launch {
delay(timeOutMs())
hideMenu()
}
}
/** Updates application state to ask to hide the menu. */
private fun hideMenu() {
_menu.value = null
cancelAutomaticMenuHiding()
_isMenuVisible.value = false
}
/** 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,
)
private fun cancelAutomaticMenuHiding() {
delayedHideMenuJob?.cancel()
delayedHideMenuJob = null
}
private fun timeOutMs(): Long {
return accessibilityManager
.getRecommendedTimeoutMillis(
DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS.toInt(),
AccessibilityManager.FLAG_CONTENT_ICONS or AccessibilityManager.FLAG_CONTENT_TEXT,
)
.toLong()
}
enum class LogEvents(
@@ -184,4 +219,8 @@ constructor(
override fun getId() = _id
}
companion object {
@VisibleForTesting const val DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS = 5000L
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.os.VibrationEffect
import kotlin.time.Duration.Companion.milliseconds
object KeyguardBottomAreaVibrations {
val ShakeAnimationDuration = 300.milliseconds
const val ShakeAnimationCycles = 5f
private const val SmallVibrationScale = 0.3f
private const val BigVibrationScale = 0.6f
val Shake =
VibrationEffect.startComposition()
.apply {
val vibrationDelayMs =
(ShakeAnimationDuration.inWholeMilliseconds / ShakeAnimationCycles * 2).toInt()
val vibrationCount = ShakeAnimationCycles.toInt() * 2
repeat(vibrationCount) {
addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
SmallVibrationScale,
vibrationDelayMs,
)
}
}
.compose()
val Activated =
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
BigVibrationScale,
0,
)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
0.1f,
0,
)
.compose()
val Deactivated =
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
BigVibrationScale,
0,
)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
0.1f,
0,
)
.compose()
}

View File

@@ -17,41 +17,42 @@
package com.android.systemui.keyguard.ui.binder
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.graphics.drawable.Animatable2
import android.os.VibrationEffect
import android.util.Size
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.widget.ImageView
import android.widget.TextView
import androidx.core.animation.CycleInterpolator
import androidx.core.animation.ObjectAnimator
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.common.ui.binder.TextViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -91,15 +92,20 @@ object KeyguardBottomAreaViewBinder {
* icon
*/
fun shouldConstrainToTopOfLockIcon(): Boolean
/** Destroys this binding, releases resources, and cancels any coroutines. */
fun destroy()
}
/** Binds the view to the view-model, continuing to update the former based on the latter. */
@SuppressLint("ClickableViewAccessibility")
@JvmStatic
fun bind(
view: ViewGroup,
viewModel: KeyguardBottomAreaViewModel,
falsingManager: FalsingManager?,
vibratorHelper: VibratorHelper?,
activityStarter: ActivityStarter?,
messageDisplayer: (Int) -> Unit,
): Binding {
val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
@@ -110,137 +116,192 @@ object KeyguardBottomAreaViewBinder {
val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
val indicationTextBottom: TextView =
view.requireViewById(R.id.keyguard_indication_text_bottom)
val settingsMenu: LaunchableLinearLayout =
view.requireViewById(R.id.keyguard_settings_button)
view.clipChildren = false
view.clipToPadding = false
view.setOnTouchListener { _, event ->
if (settingsMenu.isVisible) {
val hitRect = Rect()
settingsMenu.getHitRect(hitRect)
if (!hitRect.contains(event.x.toInt(), event.y.toInt())) {
viewModel.onTouchedOutsideLockScreenSettingsMenu()
}
}
false
}
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.startButton.collect { buttonModel ->
updateButton(
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.startButton.collect { buttonModel ->
updateButton(
view = startButton,
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
vibratorHelper = vibratorHelper,
)
}
}
launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
view = endButton,
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
vibratorHelper = vibratorHelper,
)
}
}
launch {
viewModel.isOverlayContainerVisible.collect { isVisible ->
overlayContainer.visibility =
if (isVisible) {
View.VISIBLE
} else {
View.INVISIBLE
}
}
}
launch {
viewModel.alpha.collect { alpha ->
view.importantForAccessibility =
if (alpha == 0f) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
ambientIndicationArea?.alpha = alpha
indicationArea.alpha = alpha
}
}
launch {
updateButtonAlpha(
view = startButton,
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
vibratorHelper = vibratorHelper,
viewModel = viewModel.startButton,
alphaFlow = viewModel.alpha,
)
}
}
launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
launch {
updateButtonAlpha(
view = endButton,
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
vibratorHelper = vibratorHelper,
viewModel = viewModel.endButton,
alphaFlow = viewModel.alpha,
)
}
}
launch {
viewModel.isOverlayContainerVisible.collect { isVisible ->
overlayContainer.visibility =
launch {
viewModel.indicationAreaTranslationX.collect { translationX ->
indicationArea.translationX = translationX
ambientIndicationArea?.translationX = translationX
}
}
launch {
combine(
viewModel.isIndicationAreaPadded,
configurationBasedDimensions.map { it.indicationAreaPaddingPx },
) { isPadded, paddingIfPaddedPx ->
if (isPadded) {
paddingIfPaddedPx
} else {
0
}
}
.collect { paddingPx ->
indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
}
}
launch {
configurationBasedDimensions
.map { it.defaultBurnInPreventionYOffsetPx }
.flatMapLatest { defaultBurnInOffsetY ->
viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
}
.collect { translationY ->
indicationArea.translationY = translationY
ambientIndicationArea?.translationY = translationY
}
}
launch {
configurationBasedDimensions.collect { dimensions ->
indicationText.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
dimensions.indicationTextSizePx.toFloat(),
)
indicationTextBottom.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
dimensions.indicationTextSizePx.toFloat(),
)
startButton.updateLayoutParams<ViewGroup.LayoutParams> {
width = dimensions.buttonSizePx.width
height = dimensions.buttonSizePx.height
}
endButton.updateLayoutParams<ViewGroup.LayoutParams> {
width = dimensions.buttonSizePx.width
height = dimensions.buttonSizePx.height
}
}
}
launch {
viewModel.settingsMenuViewModel.isVisible.distinctUntilChanged().collect {
isVisible ->
settingsMenu.animateVisibility(visible = isVisible)
if (isVisible) {
View.VISIBLE
} else {
View.INVISIBLE
}
}
}
launch {
viewModel.alpha.collect { alpha ->
view.importantForAccessibility =
if (alpha == 0f) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
ambientIndicationArea?.alpha = alpha
indicationArea.alpha = alpha
}
}
launch {
updateButtonAlpha(
view = startButton,
viewModel = viewModel.startButton,
alphaFlow = viewModel.alpha,
)
}
launch {
updateButtonAlpha(
view = endButton,
viewModel = viewModel.endButton,
alphaFlow = viewModel.alpha,
)
}
launch {
viewModel.indicationAreaTranslationX.collect { translationX ->
indicationArea.translationX = translationX
ambientIndicationArea?.translationX = translationX
}
}
launch {
combine(
viewModel.isIndicationAreaPadded,
configurationBasedDimensions.map { it.indicationAreaPaddingPx },
) { isPadded, paddingIfPaddedPx ->
if (isPadded) {
paddingIfPaddedPx
} else {
0
vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Activated)
settingsMenu.setOnTouchListener(
KeyguardSettingsButtonOnTouchListener(
view = settingsMenu,
viewModel = viewModel.settingsMenuViewModel,
)
)
IconViewBinder.bind(
icon = viewModel.settingsMenuViewModel.icon,
view = settingsMenu.requireViewById(R.id.icon),
)
TextViewBinder.bind(
view = settingsMenu.requireViewById(R.id.text),
viewModel = viewModel.settingsMenuViewModel.text,
)
}
}
.collect { paddingPx ->
indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
}
}
}
launch {
configurationBasedDimensions
.map { it.defaultBurnInPreventionYOffsetPx }
.flatMapLatest { defaultBurnInOffsetY ->
viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
}
.collect { translationY ->
indicationArea.translationY = translationY
ambientIndicationArea?.translationY = translationY
}
}
launch {
configurationBasedDimensions.collect { dimensions ->
indicationText.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
dimensions.indicationTextSizePx.toFloat(),
)
indicationTextBottom.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
dimensions.indicationTextSizePx.toFloat(),
)
startButton.updateLayoutParams<ViewGroup.LayoutParams> {
width = dimensions.buttonSizePx.width
height = dimensions.buttonSizePx.height
}
endButton.updateLayoutParams<ViewGroup.LayoutParams> {
width = dimensions.buttonSizePx.width
height = dimensions.buttonSizePx.height
// activityStarter will only be null when rendering the preview that
// shows up in the Wallpaper Picker app. If we do that, then the
// settings menu should never be visible.
if (activityStarter != null) {
launch {
viewModel.settingsMenuViewModel.shouldOpenSettings
.filter { it }
.collect {
navigateToLockScreenSettings(
activityStarter = activityStarter,
view = settingsMenu,
)
viewModel.settingsMenuViewModel.onSettingsShown()
}
}
}
}
}
}
return object : Binding {
override fun getIndicationAreaAnimators(): List<ViewPropertyAnimator> {
@@ -253,6 +314,10 @@ object KeyguardBottomAreaViewBinder {
override fun shouldConstrainToTopOfLockIcon(): Boolean =
viewModel.shouldConstrainToTopOfLockIcon()
override fun destroy() {
disposableHandle.dispose()
}
}
}
@@ -265,7 +330,7 @@ object KeyguardBottomAreaViewBinder {
vibratorHelper: VibratorHelper?,
) {
if (!viewModel.isVisible) {
view.isVisible = false
view.isInvisible = true
return
}
@@ -342,7 +407,7 @@ object KeyguardBottomAreaViewBinder {
if (viewModel.isClickable) {
if (viewModel.useLongPress) {
view.setOnTouchListener(
OnTouchListener(
KeyguardQuickAffordanceOnTouchListener(
view,
viewModel,
messageDisplayer,
@@ -372,187 +437,21 @@ object KeyguardBottomAreaViewBinder {
.collect { view.alpha = it }
}
private class OnTouchListener(
private val view: View,
private val viewModel: KeyguardQuickAffordanceViewModel,
private val messageDisplayer: (Int) -> Unit,
private val vibratorHelper: VibratorHelper?,
private val falsingManager: FalsingManager?,
) : View.OnTouchListener {
private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
private var longPressAnimator: ViewPropertyAnimator? = null
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return when (event?.actionMasked) {
MotionEvent.ACTION_DOWN ->
if (viewModel.configKey != null) {
if (isUsingAccurateTool(event)) {
// For accurate tool types (stylus, mouse, etc.), we don't require a
// long-press.
} else {
// When not using a stylus, we require a long-press to activate the
// quick affordance, mostly to do "falsing" (e.g. protect from false
// clicks in the pocket/bag).
longPressAnimator =
view
.animate()
.scaleX(PRESSED_SCALE)
.scaleY(PRESSED_SCALE)
.setDuration(longPressDurationMs)
.withEndAction {
if (
falsingManager
?.isFalseLongTap(
FalsingManager.MODERATE_PENALTY
) == false
) {
dispatchClick(viewModel.configKey)
}
cancel()
}
}
true
} else {
false
}
MotionEvent.ACTION_MOVE -> {
if (!isUsingAccurateTool(event)) {
// Moving too far while performing a long-press gesture cancels that
// gesture.
val distanceMoved = distanceMoved(event)
if (distanceMoved > ViewConfiguration.getTouchSlop()) {
cancel()
}
}
true
}
MotionEvent.ACTION_UP -> {
if (isUsingAccurateTool(event)) {
// When using an accurate tool type (stylus, mouse, etc.), we don't require
// a long-press gesture to activate the quick affordance. Therefore, lifting
// the pointer performs a click.
if (
viewModel.configKey != null &&
distanceMoved(event) <= ViewConfiguration.getTouchSlop() &&
falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false
) {
dispatchClick(viewModel.configKey)
}
} else {
// When not using a stylus, lifting the finger/pointer will actually cancel
// the long-press gesture. Calling cancel after the quick affordance was
// already long-press activated is a no-op, so it's safe to call from here.
cancel(
onAnimationEnd =
if (event.eventTime - event.downTime < longPressDurationMs) {
Runnable {
messageDisplayer.invoke(
R.string.keyguard_affordance_press_too_short
)
val amplitude =
view.context.resources
.getDimensionPixelSize(
R.dimen.keyguard_affordance_shake_amplitude
)
.toFloat()
val shakeAnimator =
ObjectAnimator.ofFloat(
view,
"translationX",
-amplitude / 2,
amplitude / 2,
)
shakeAnimator.duration =
ShakeAnimationDuration.inWholeMilliseconds
shakeAnimator.interpolator =
CycleInterpolator(ShakeAnimationCycles)
shakeAnimator.start()
vibratorHelper?.vibrate(Vibrations.Shake)
}
} else {
null
}
)
}
true
}
MotionEvent.ACTION_CANCEL -> {
cancel()
true
}
else -> false
}
}
private fun dispatchClick(
configKey: String,
) {
view.setOnClickListener {
vibratorHelper?.vibrate(
if (viewModel.isActivated) {
Vibrations.Activated
} else {
Vibrations.Deactivated
}
)
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = configKey,
expandable = Expandable.fromView(view),
slotId = viewModel.slotId,
)
)
}
view.performClick()
view.setOnClickListener(null)
}
private fun cancel(onAnimationEnd: Runnable? = null) {
longPressAnimator?.cancel()
longPressAnimator = null
view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
}
companion object {
private const val PRESSED_SCALE = 1.5f
/**
* Returns `true` if the tool type at the given pointer index is an accurate tool (like
* stylus or mouse), which means we can trust it to not be a false click; `false`
* otherwise.
*/
private fun isUsingAccurateTool(
event: MotionEvent,
pointerIndex: Int = 0,
): Boolean {
return when (event.getToolType(pointerIndex)) {
MotionEvent.TOOL_TYPE_STYLUS -> true
MotionEvent.TOOL_TYPE_MOUSE -> true
else -> false
private fun View.animateVisibility(visible: Boolean) {
animate()
.withStartAction {
if (visible) {
alpha = 0f
isVisible = true
}
}
/**
* Returns the amount of distance the pointer moved since the historical record at the
* [since] index.
*/
private fun distanceMoved(
event: MotionEvent,
since: Int = 0,
): Float {
return if (event.historySize > 0) {
sqrt(
(event.y - event.getHistoricalY(since)).pow(2) +
(event.x - event.getHistoricalX(since)).pow(2)
)
} else {
0f
.alpha(if (visible) 1f else 0f)
.withEndAction {
if (!visible) {
isVisible = false
}
}
}
.start()
}
private class OnClickListener(
@@ -594,64 +493,28 @@ object KeyguardBottomAreaViewBinder {
)
}
/** Opens the wallpaper picker screen after the device is unlocked by the user. */
private fun navigateToLockScreenSettings(
activityStarter: ActivityStarter,
view: View,
) {
activityStarter.startActivity(
Intent(Intent.ACTION_SET_WALLPAPER).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
view.context
.getString(R.string.config_wallpaperPickerPackage)
.takeIf { it.isNotEmpty() }
?.let { packageName -> setPackage(packageName) }
},
/* dismissShade= */ true,
ActivityLaunchAnimator.Controller.fromView(view),
)
}
private data class ConfigurationBasedDimensions(
val defaultBurnInPreventionYOffsetPx: Int,
val indicationAreaPaddingPx: Int,
val indicationTextSizePx: Int,
val buttonSizePx: Size,
)
private val ShakeAnimationDuration = 300.milliseconds
private val ShakeAnimationCycles = 5f
object Vibrations {
private const val SmallVibrationScale = 0.3f
private const val BigVibrationScale = 0.6f
val Shake =
VibrationEffect.startComposition()
.apply {
val vibrationDelayMs =
(ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2))
.toInt()
val vibrationCount = ShakeAnimationCycles.toInt() * 2
repeat(vibrationCount) {
addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
SmallVibrationScale,
vibrationDelayMs,
)
}
}
.compose()
val Activated =
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
BigVibrationScale,
0,
)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
0.1f,
0,
)
.compose()
val Deactivated =
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
BigVibrationScale,
0,
)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
0.1f,
0,
)
.compose()
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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

@@ -50,10 +50,7 @@ object KeyguardLongPressViewBinder {
return
}
viewModel.onLongPress(
x = x,
y = y,
)
viewModel.onLongPress()
}
override fun onSingleTapDetected(view: View) {
@@ -72,23 +69,6 @@ object KeyguardLongPressViewBinder {
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,200 @@
/*
* 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.graphics.PointF
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewPropertyAnimator
import androidx.core.animation.CycleInterpolator
import androidx.core.animation.ObjectAnimator
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.ui.view.distanceFrom
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
class KeyguardQuickAffordanceOnTouchListener(
private val view: View,
private val viewModel: KeyguardQuickAffordanceViewModel,
private val messageDisplayer: (Int) -> Unit,
private val vibratorHelper: VibratorHelper?,
private val falsingManager: FalsingManager?,
) : View.OnTouchListener {
private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
private var longPressAnimator: ViewPropertyAnimator? = null
private val down: PointF by lazy { PointF() }
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN ->
if (viewModel.configKey != null) {
down.set(event.x, event.y)
if (isUsingAccurateTool(event)) {
// For accurate tool types (stylus, mouse, etc.), we don't require a
// long-press.
} else {
// When not using a stylus, we require a long-press to activate the
// quick affordance, mostly to do "falsing" (e.g. protect from false
// clicks in the pocket/bag).
longPressAnimator =
view
.animate()
.scaleX(PRESSED_SCALE)
.scaleY(PRESSED_SCALE)
.setDuration(longPressDurationMs)
.withEndAction {
if (
falsingManager?.isFalseLongTap(
FalsingManager.MODERATE_PENALTY
) == false
) {
dispatchClick(viewModel.configKey)
}
cancel()
}
}
true
} else {
false
}
MotionEvent.ACTION_MOVE -> {
if (!isUsingAccurateTool(event)) {
// Moving too far while performing a long-press gesture cancels that
// gesture.
if (event.distanceFrom(down.x, down.y) > ViewConfiguration.getTouchSlop()) {
cancel()
}
}
true
}
MotionEvent.ACTION_UP -> {
if (isUsingAccurateTool(event)) {
// When using an accurate tool type (stylus, mouse, etc.), we don't require
// a long-press gesture to activate the quick affordance. Therefore, lifting
// the pointer performs a click.
if (
viewModel.configKey != null &&
event.distanceFrom(down.x, down.y) <=
ViewConfiguration.getTouchSlop() &&
falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false
) {
dispatchClick(viewModel.configKey)
}
} else {
// When not using a stylus, lifting the finger/pointer will actually cancel
// the long-press gesture. Calling cancel after the quick affordance was
// already long-press activated is a no-op, so it's safe to call from here.
cancel(
onAnimationEnd =
if (event.eventTime - event.downTime < longPressDurationMs) {
Runnable {
messageDisplayer.invoke(
R.string.keyguard_affordance_press_too_short
)
val amplitude =
view.context.resources
.getDimensionPixelSize(
R.dimen.keyguard_affordance_shake_amplitude
)
.toFloat()
val shakeAnimator =
ObjectAnimator.ofFloat(
view,
"translationX",
-amplitude / 2,
amplitude / 2,
)
shakeAnimator.duration =
KeyguardBottomAreaVibrations.ShakeAnimationDuration
.inWholeMilliseconds
shakeAnimator.interpolator =
CycleInterpolator(
KeyguardBottomAreaVibrations.ShakeAnimationCycles
)
shakeAnimator.start()
vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
}
} else {
null
}
)
}
true
}
MotionEvent.ACTION_CANCEL -> {
cancel()
true
}
else -> false
}
}
private fun dispatchClick(
configKey: String,
) {
view.setOnClickListener {
vibratorHelper?.vibrate(
if (viewModel.isActivated) {
KeyguardBottomAreaVibrations.Activated
} else {
KeyguardBottomAreaVibrations.Deactivated
}
)
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = configKey,
expandable = Expandable.fromView(view),
slotId = viewModel.slotId,
)
)
}
view.performClick()
view.setOnClickListener(null)
}
private fun cancel(onAnimationEnd: Runnable? = null) {
longPressAnimator?.cancel()
longPressAnimator = null
view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
}
companion object {
private const val PRESSED_SCALE = 1.5f
/**
* Returns `true` if the tool type at the given pointer index is an accurate tool (like
* stylus or mouse), which means we can trust it to not be a false click; `false` otherwise.
*/
private fun isUsingAccurateTool(
event: MotionEvent,
pointerIndex: Int = 0,
): Boolean {
return when (event.getToolType(pointerIndex)) {
MotionEvent.TOOL_TYPE_STYLUS -> true
MotionEvent.TOOL_TYPE_MOUSE -> true
else -> false
}
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.graphics.PointF
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.common.ui.view.distanceFrom
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
class KeyguardSettingsButtonOnTouchListener(
private val view: LaunchableLinearLayout,
private val viewModel: KeyguardSettingsMenuViewModel,
) : View.OnTouchListener {
private val downPosition = PointF()
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
view.isPressed = true
downPosition.set(motionEvent.x, motionEvent.y)
viewModel.onTouchGestureStarted()
}
MotionEvent.ACTION_UP -> {
view.isPressed = false
val distanceMoved = motionEvent.distanceFrom(downPosition.x, downPosition.y)
val isClick = distanceMoved < ViewConfiguration.getTouchSlop()
viewModel.onTouchGestureEnded(isClick)
if (isClick) {
view.performClick()
}
}
MotionEvent.ACTION_CANCEL -> {
view.isPressed = false
viewModel.onTouchGestureEnded(/* isClick= */ false)
}
}
return true
}
}

View File

@@ -44,6 +44,8 @@ constructor(
private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
private val burnInHelperWrapper: BurnInHelperWrapper,
private val longPressViewModel: KeyguardLongPressViewModel,
val settingsMenuViewModel: KeyguardSettingsMenuViewModel,
) {
data class PreviewMode(
val isInPreviewMode: Boolean = false,
@@ -161,6 +163,14 @@ constructor(
selectedPreviewSlotId.value = slotId
}
/**
* Notifies that some input gesture has started somewhere in the bottom area that's outside of
* the lock screen settings menu item pop-up.
*/
fun onTouchedOutsideLockScreenSettingsMenu() {
longPressViewModel.onTouchedOutside()
}
private fun button(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceViewModel> {
@@ -225,9 +235,10 @@ constructor(
isDimmed = isDimmed,
slotId = slotId,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel(
slotId = slotId,
)
is KeyguardQuickAffordanceModel.Hidden ->
KeyguardQuickAffordanceViewModel(
slotId = slotId,
)
}
}

View File

@@ -17,15 +17,13 @@
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.dagger.SysUISingleton
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. */
@SysUISingleton
class KeyguardLongPressViewModel
@Inject
constructor(
@@ -35,35 +33,16 @@ constructor(
/** 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,
)
fun onLongPress() {
interactor.onLongPress()
}
/**
* Notifies that some input gesture has started somewhere outside of the lock screen settings
* menu item pop-up.
*/
fun onTouchedOutside() {
interactor.onTouchedOutside()
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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
/** Models the UI state of a keyguard settings popup menu. */
class KeyguardSettingsMenuViewModel
@Inject
constructor(
private val interactor: KeyguardLongPressInteractor,
) {
val isVisible: Flow<Boolean> = interactor.isMenuVisible
val shouldOpenSettings: Flow<Boolean> = interactor.shouldOpenSettings
val icon: Icon =
Icon.Resource(
res = R.drawable.ic_palette,
contentDescription = null,
)
val text: Text =
Text.Resource(
res = R.string.lock_screen_settings,
)
fun onTouchGestureStarted() {
interactor.onMenuTouchGestureStarted()
}
fun onTouchGestureEnded(isClick: Boolean) {
interactor.onMenuTouchGestureEnded(
isClick = isClick,
)
}
fun onSettingsShown() {
interactor.onSettingsShown()
}
}

View File

@@ -1,34 +0,0 @@
/*
* 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

@@ -162,6 +162,8 @@ import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.plugins.ClockController;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.FalsingManager.FalsingTapListener;
@@ -694,23 +696,29 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
}
};
private final ActivityStarter mActivityStarter;
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@Main Handler handler,
LayoutInflater layoutInflater,
FeatureFlags featureFlags,
NotificationWakeUpCoordinator coordinator, PulseExpansionHandler pulseExpansionHandler,
NotificationWakeUpCoordinator coordinator,
PulseExpansionHandler pulseExpansionHandler,
DynamicPrivacyController dynamicPrivacyController,
KeyguardBypassController bypassController, FalsingManager falsingManager,
KeyguardBypassController bypassController,
FalsingManager falsingManager,
FalsingCollector falsingCollector,
KeyguardStateController keyguardStateController,
StatusBarStateController statusBarStateController,
StatusBarWindowStateController statusBarWindowStateController,
NotificationShadeWindowController notificationShadeWindowController,
DozeLog dozeLog,
DozeParameters dozeParameters, CommandQueue commandQueue, VibratorHelper vibratorHelper,
LatencyTracker latencyTracker, PowerManager powerManager,
DozeParameters dozeParameters,
CommandQueue commandQueue,
VibratorHelper vibratorHelper,
LatencyTracker latencyTracker,
PowerManager powerManager,
AccessibilityManager accessibilityManager, @DisplayId int displayId,
KeyguardUpdateMonitor keyguardUpdateMonitor,
MetricsLogger metricsLogger,
@@ -771,7 +779,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
Provider<MultiShadeInteractor> multiShadeInteractorProvider,
DumpManager dumpManager,
KeyguardLongPressViewModel keyguardLongPressViewModel,
KeyguardInteractor keyguardInteractor) {
KeyguardInteractor keyguardInteractor,
ActivityStarter activityStarter) {
mInteractionJankMonitor = interactionJankMonitor;
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
@@ -952,6 +961,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
return Unit.INSTANCE;
},
mFalsingManager);
mActivityStarter = activityStarter;
onFinishInflate();
keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -1394,7 +1404,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mLockIconViewController,
stringResourceId ->
mKeyguardIndicationController.showTransientIndication(stringResourceId),
mVibratorHelper);
mVibratorHelper,
mActivityStarter);
}
@VisibleForTesting

View File

@@ -29,6 +29,7 @@ import com.android.systemui.R
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
@@ -57,7 +58,7 @@ constructor(
}
private var ambientIndicationArea: View? = null
private lateinit var binding: KeyguardBottomAreaViewBinder.Binding
private var binding: KeyguardBottomAreaViewBinder.Binding? = null
private var lockIconViewController: LockIconViewController? = null
/** Initializes the view. */
@@ -67,13 +68,16 @@ constructor(
lockIconViewController: LockIconViewController? = null,
messageDisplayer: MessageDisplayer? = null,
vibratorHelper: VibratorHelper? = null,
activityStarter: ActivityStarter? = null,
) {
binding?.destroy()
binding =
bind(
this,
viewModel,
falsingManager,
vibratorHelper,
activityStarter,
) {
messageDisplayer?.display(it)
}
@@ -114,12 +118,12 @@ constructor(
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
binding.onConfigurationChanged()
binding?.onConfigurationChanged()
}
/** Returns a list of animators to use to animate the indication areas. */
val indicationAreaAnimators: List<ViewPropertyAnimator>
get() = binding.getIndicationAreaAnimators()
get() = checkNotNull(binding).getIndicationAreaAnimators()
override fun hasOverlappingRendering(): Boolean {
return false
@@ -139,7 +143,7 @@ constructor(
super.onLayout(changed, left, top, right, bottom)
findViewById<View>(R.id.ambient_indication_container)?.let {
val (ambientLeft, ambientTop) = it.locationOnScreen
if (binding.shouldConstrainToTopOfLockIcon()) {
if (binding?.shouldConstrainToTopOfLockIcon() == true) {
// make top of ambient indication view the bottom of the lock icon
it.layout(
ambientLeft,

View File

@@ -29,20 +29,20 @@ 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.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
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.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -51,8 +51,8 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class)
class KeyguardLongPressInteractorTest : SysuiTestCase() {
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var logger: UiEventLogger
@Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper
private lateinit var underTest: KeyguardLongPressInteractor
@@ -63,6 +63,14 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(accessibilityManager.getRecommendedTimeoutMillis(anyInt(), anyInt())).thenAnswer {
it.arguments[0]
}
testScope = TestScope()
keyguardRepository = FakeKeyguardRepository()
keyguardTransitionRepository = FakeKeyguardTransitionRepository()
runBlocking { createUnderTest() }
}
@@ -98,60 +106,117 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
}
@Test
fun `long-pressed - pop-up clicked - starts activity`() =
fun longPressed_menuClicked_showsSettings() =
testScope.runTest {
val menu = collectLastValue(underTest.menu)
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
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)
underTest.onLongPress()
assertThat(isMenuVisible).isTrue()
menu()?.onClicked?.invoke()
underTest.onMenuTouchGestureEnded(/* isClick= */ true)
assertThat(menu()).isNull()
verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean())
assertThat(isMenuVisible).isFalse()
assertThat(shouldOpenSettings).isTrue()
}
@Test
fun `long-pressed - pop-up dismissed - never starts activity`() =
fun onSettingsShown_consumesSettingsShowEvent() =
testScope.runTest {
val menu = collectLastValue(underTest.menu)
val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
runCurrent()
menu()?.onDismissed?.invoke()
underTest.onLongPress()
underTest.onMenuTouchGestureEnded(/* isClick= */ true)
assertThat(shouldOpenSettings).isTrue()
assertThat(menu()).isNull()
verify(activityStarter, never()).dismissKeyguardThenExecute(any(), any(), anyBoolean())
underTest.onSettingsShown()
assertThat(shouldOpenSettings).isFalse()
}
@Test
fun onTouchedOutside_neverShowsSettings() =
testScope.runTest {
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
runCurrent()
underTest.onTouchedOutside()
assertThat(isMenuVisible).isFalse()
assertThat(shouldOpenSettings).isFalse()
}
@Test
fun longPressed_openWppDirectlyEnabled_doesNotShowMenu_opensSettings() =
testScope.runTest {
createUnderTest(isOpenWppDirectlyEnabled = true)
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
runCurrent()
underTest.onLongPress()
assertThat(isMenuVisible).isFalse()
assertThat(shouldOpenSettings).isTrue()
}
@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)
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
runCurrent()
underTest.onLongPress(123, 456)
assertThat(menu()).isNotNull()
underTest.onLongPress()
assertThat(isMenuVisible).isTrue()
fakeBroadcastDispatcher.registeredReceivers.forEach { broadcastReceiver ->
broadcastReceiver.onReceive(context, Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
assertThat(menu()).isNull()
assertThat(isMenuVisible).isFalse()
}
@Test
fun closesDialogAfterTimeout() =
testScope.runTest {
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
runCurrent()
underTest.onLongPress()
assertThat(isMenuVisible).isTrue()
advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
assertThat(isMenuVisible).isFalse()
}
@Test
fun closesDialogAfterTimeout_onlyAfterTouchGestureEnded() =
testScope.runTest {
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
runCurrent()
underTest.onLongPress()
assertThat(isMenuVisible).isTrue()
underTest.onMenuTouchGestureStarted()
advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
assertThat(isMenuVisible).isTrue()
underTest.onMenuTouchGestureEnded(/* isClick= */ false)
advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
assertThat(isMenuVisible).isFalse()
}
@Test
fun `logs when menu is shown`() =
testScope.runTest {
collectLastValue(underTest.menu)
collectLastValue(underTest.isMenuVisible)
runCurrent()
underTest.onLongPress(100, 123)
underTest.onLongPress()
verify(logger)
.log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
@@ -160,41 +225,61 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
@Test
fun `logs when menu is clicked`() =
testScope.runTest {
val menu = collectLastValue(underTest.menu)
collectLastValue(underTest.isMenuVisible)
runCurrent()
underTest.onLongPress(100, 123)
menu()?.onClicked?.invoke()
underTest.onLongPress()
underTest.onMenuTouchGestureEnded(/* isClick= */ true)
verify(logger)
.log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
}
@Test
fun showMenu_leaveLockscreen_returnToLockscreen_menuNotVisible() =
testScope.runTest {
val isMenuVisible by collectLastValue(underTest.isMenuVisible)
runCurrent()
underTest.onLongPress()
assertThat(isMenuVisible).isTrue()
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
to = KeyguardState.GONE,
),
)
assertThat(isMenuVisible).isFalse()
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
to = KeyguardState.LOCKSCREEN,
),
)
assertThat(isMenuVisible).isFalse()
}
private suspend fun createUnderTest(
isLongPressFeatureEnabled: Boolean = true,
isRevampedWppFeatureEnabled: Boolean = true,
isOpenWppDirectlyEnabled: Boolean = false,
) {
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)
set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled)
},
broadcastDispatcher = fakeBroadcastDispatcher,
accessibilityManager = accessibilityManager
)
setUpState()
}

View File

@@ -20,10 +20,12 @@ import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.os.UserHandle
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.doze.util.BurnInHelperWrapper
@@ -38,10 +40,13 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
@@ -51,6 +56,7 @@ import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any
@@ -91,6 +97,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
@Mock private lateinit var commandQueue: CommandQueue
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper
private lateinit var underTest: KeyguardBottomAreaViewModel
@@ -134,6 +142,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
FakeFeatureFlags().apply {
set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
set(Flags.FACE_AUTH_REFACTOR, true)
set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false)
}
val keyguardInteractor =
@@ -196,6 +206,19 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
dumpManager = mock(),
userHandle = UserHandle.SYSTEM,
)
val keyguardLongPressInteractor =
KeyguardLongPressInteractor(
scope = testScope.backgroundScope,
transitionInteractor =
KeyguardTransitionInteractor(
repository = FakeKeyguardTransitionRepository(),
),
repository = repository,
logger = UiEventLoggerFake(),
featureFlags = featureFlags,
broadcastDispatcher = broadcastDispatcher,
accessibilityManager = accessibilityManager,
)
underTest =
KeyguardBottomAreaViewModel(
keyguardInteractor = keyguardInteractor,
@@ -216,6 +239,14 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
),
bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
burnInHelperWrapper = burnInHelperWrapper,
longPressViewModel =
KeyguardLongPressViewModel(
interactor = keyguardLongPressInteractor,
),
settingsMenuViewModel =
KeyguardSettingsMenuViewModel(
interactor = keyguardLongPressInteractor,
),
)
}

View File

@@ -106,6 +106,7 @@ import com.android.systemui.model.SysUiState;
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.QSFragment;
@@ -295,6 +296,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
@Captor
protected ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
mEmptySpaceClickListenerCaptor;
@Mock protected ActivityStarter mActivityStarter;
protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
protected KeyguardInteractor mKeyguardInteractor;
@@ -575,7 +577,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
() -> mMultiShadeInteractor,
mDumpManager,
mKeyuardLongPressViewModel,
mKeyguardInteractor);
mKeyguardInteractor,
mActivityStarter);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
null,