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:
25
packages/SystemUI/res-keyguard/drawable/ic_palette.xml
Normal file
25
packages/SystemUI/res-keyguard/drawable/ic_palette.xml
Normal 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>
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user