Merge changes from topic "lock-screen-preview-in-wpp" into tm-qpr-dev
* changes: Long-press gesture for lock screen affordances. Lock screen preview.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.shared.quickaffordance.shared.model
|
||||
|
||||
object KeyguardQuickAffordancePreviewConstants {
|
||||
const val MESSAGE_ID_SLOT_SELECTED = 1337
|
||||
const val KEY_SLOT_ID = "slot_id"
|
||||
const val KEY_INITIALLY_SELECTED_SLOT_ID = "initially_selected_slot_id"
|
||||
}
|
||||
@@ -16,13 +16,53 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
-->
|
||||
<shape
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?androidprv:attr/colorSurface"/>
|
||||
<size
|
||||
android:width="@dimen/keyguard_affordance_width"
|
||||
android:height="@dimen/keyguard_affordance_height"/>
|
||||
<corners android:radius="@dimen/keyguard_affordance_fixed_radius"/>
|
||||
</shape>
|
||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
|
||||
|
||||
<item android:state_selected="true">
|
||||
<layer-list>
|
||||
<item
|
||||
android:left="3dp"
|
||||
android:top="3dp"
|
||||
android:right="3dp"
|
||||
android:bottom="3dp">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?androidprv:attr/colorSurface"/>
|
||||
<size
|
||||
android:width="@dimen/keyguard_affordance_width"
|
||||
android:height="@dimen/keyguard_affordance_height"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:color="@color/control_primary_text"
|
||||
android:width="2dp"/>
|
||||
<size
|
||||
android:width="@dimen/keyguard_affordance_width"
|
||||
android:height="@dimen/keyguard_affordance_height"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<layer-list>
|
||||
<item
|
||||
android:left="3dp"
|
||||
android:top="3dp"
|
||||
android:right="3dp"
|
||||
android:bottom="3dp">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?androidprv:attr/colorSurface"/>
|
||||
<size
|
||||
android:width="@dimen/keyguard_affordance_width"
|
||||
android:height="@dimen/keyguard_affordance_height"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
|
||||
</selector>
|
||||
|
||||
@@ -758,6 +758,8 @@
|
||||
<dimen name="keyguard_affordance_fixed_height">48dp</dimen>
|
||||
<dimen name="keyguard_affordance_fixed_width">48dp</dimen>
|
||||
<dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
|
||||
<!-- Amount the button should shake when it's not long-pressed for long enough. -->
|
||||
<dimen name="keyguard_affordance_shake_amplitude">8dp</dimen>
|
||||
|
||||
<dimen name="keyguard_affordance_horizontal_offset">32dp</dimen>
|
||||
<dimen name="keyguard_affordance_vertical_offset">32dp</dimen>
|
||||
|
||||
@@ -2740,6 +2740,12 @@
|
||||
-->
|
||||
<string name="keyguard_affordance_enablement_dialog_home_instruction_2">• At least one device is available</string>
|
||||
|
||||
<!--
|
||||
Error message shown when a button should be pressed and held to activate it, usually shown when
|
||||
the user attempted to tap the button or held it for too short a time. [CHAR LIMIT=32].
|
||||
-->
|
||||
<string name="keyguard_affordance_press_too_short">Press and hold to activate</string>
|
||||
|
||||
<!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
|
||||
<string name="rear_display_bottom_sheet_cancel">Cancel</string>
|
||||
<!-- Text for the user to confirm they flipped the device around. [CHAR_LIMIT=NONE] -->
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
@@ -43,6 +44,21 @@ public class KeyguardClockSwitch extends RelativeLayout {
|
||||
public static final int LARGE = 0;
|
||||
public static final int SMALL = 1;
|
||||
|
||||
/** Returns a region for the large clock to position itself, based on the given parent. */
|
||||
public static Rect getLargeClockRegion(ViewGroup parent) {
|
||||
int largeClockTopMargin = parent.getResources()
|
||||
.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
|
||||
int targetHeight = parent.getResources()
|
||||
.getDimensionPixelSize(R.dimen.large_clock_text_size) * 2;
|
||||
int top = parent.getHeight() / 2 - targetHeight / 2
|
||||
+ largeClockTopMargin / 2;
|
||||
return new Rect(
|
||||
parent.getLeft(),
|
||||
top,
|
||||
parent.getRight(),
|
||||
top + targetHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame for small/large clocks
|
||||
*/
|
||||
@@ -129,17 +145,8 @@ public class KeyguardClockSwitch extends RelativeLayout {
|
||||
}
|
||||
|
||||
if (mLargeClockFrame.isLaidOut()) {
|
||||
int largeClockTopMargin = getResources()
|
||||
.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
|
||||
int targetHeight = getResources()
|
||||
.getDimensionPixelSize(R.dimen.large_clock_text_size) * 2;
|
||||
int top = mLargeClockFrame.getHeight() / 2 - targetHeight / 2
|
||||
+ largeClockTopMargin / 2;
|
||||
mClock.getLargeClock().getEvents().onTargetRegionChanged(new Rect(
|
||||
mLargeClockFrame.getLeft(),
|
||||
top,
|
||||
mLargeClockFrame.getRight(),
|
||||
top + targetHeight));
|
||||
mClock.getLargeClock().getEvents().onTargetRegionChanged(
|
||||
getLargeClockRegion(mLargeClockFrame));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,18 @@ import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.UriMatcher
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.android.systemui.SystemUIAppComponentFactoryBase
|
||||
import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
|
||||
import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
|
||||
import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -37,6 +41,7 @@ class KeyguardQuickAffordanceProvider :
|
||||
ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
|
||||
|
||||
@Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
|
||||
@Inject lateinit var previewManager: KeyguardRemotePreviewManager
|
||||
|
||||
private lateinit var contextAvailableCallback: ContextAvailableCallback
|
||||
|
||||
@@ -149,6 +154,21 @@ class KeyguardQuickAffordanceProvider :
|
||||
return deleteSelection(uri, selectionArgs)
|
||||
}
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
return if (
|
||||
requireContext()
|
||||
.checkPermission(
|
||||
android.Manifest.permission.BIND_WALLPAPER,
|
||||
Binder.getCallingPid(),
|
||||
Binder.getCallingUid(),
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
previewManager.preview(extras)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertSelection(values: ContentValues?): Uri? {
|
||||
if (values == null) {
|
||||
throw IllegalArgumentException("Cannot insert selection, no values passed in!")
|
||||
|
||||
@@ -34,7 +34,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentati
|
||||
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
|
||||
import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract
|
||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||
import dagger.Lazy
|
||||
@@ -62,12 +61,20 @@ constructor(
|
||||
private val isUsingRepository: Boolean
|
||||
get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
|
||||
|
||||
/**
|
||||
* Whether the UI should use the long press gesture to activate quick affordances.
|
||||
*
|
||||
* If `false`, the UI goes back to using single taps.
|
||||
*/
|
||||
val useLongPress: Boolean
|
||||
get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
|
||||
|
||||
/** Returns an observable for the quick affordance at the given position. */
|
||||
fun quickAffordance(
|
||||
position: KeyguardQuickAffordancePosition
|
||||
): Flow<KeyguardQuickAffordanceModel> {
|
||||
return combine(
|
||||
quickAffordanceInternal(position),
|
||||
quickAffordanceAlwaysVisible(position),
|
||||
keyguardInteractor.isDozing,
|
||||
keyguardInteractor.isKeyguardShowing,
|
||||
) { affordance, isDozing, isKeyguardShowing ->
|
||||
@@ -79,6 +86,19 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable for the quick affordance at the given position but always visible,
|
||||
* regardless of lock screen state.
|
||||
*
|
||||
* This is useful for experiences like the lock screen preview mode, where the affordances must
|
||||
* always be visible.
|
||||
*/
|
||||
fun quickAffordanceAlwaysVisible(
|
||||
position: KeyguardQuickAffordancePosition,
|
||||
): Flow<KeyguardQuickAffordanceModel> {
|
||||
return quickAffordanceInternal(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that a quick affordance has been "triggered" (clicked) by the user.
|
||||
*
|
||||
@@ -290,15 +310,6 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun KeyguardQuickAffordancePosition.toSlotId(): String {
|
||||
return when (this) {
|
||||
KeyguardQuickAffordancePosition.BOTTOM_START ->
|
||||
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
|
||||
KeyguardQuickAffordancePosition.BOTTOM_END ->
|
||||
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.encode(slotId: String): String {
|
||||
return "$slotId$DELIMITER$this"
|
||||
}
|
||||
|
||||
@@ -16,8 +16,17 @@
|
||||
|
||||
package com.android.systemui.keyguard.shared.quickaffordance
|
||||
|
||||
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
|
||||
|
||||
/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
|
||||
enum class KeyguardQuickAffordancePosition {
|
||||
BOTTOM_START,
|
||||
BOTTOM_END,
|
||||
BOTTOM_END;
|
||||
|
||||
fun toSlotId(): String {
|
||||
return when (this) {
|
||||
BOTTOM_START -> KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
|
||||
BOTTOM_END -> KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,19 @@
|
||||
|
||||
package com.android.systemui.keyguard.ui.binder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.Animatable2
|
||||
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.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -38,8 +43,10 @@ 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.FalsingManager
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -52,6 +59,7 @@ import kotlinx.coroutines.launch
|
||||
* view-binding, binding each view only once. It is okay and expected for the same instance of the
|
||||
* view-model to be reused for multiple view/view-binder bindings.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
object KeyguardBottomAreaViewBinder {
|
||||
|
||||
private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
|
||||
@@ -84,7 +92,8 @@ object KeyguardBottomAreaViewBinder {
|
||||
fun bind(
|
||||
view: ViewGroup,
|
||||
viewModel: KeyguardBottomAreaViewModel,
|
||||
falsingManager: FalsingManager,
|
||||
falsingManager: FalsingManager?,
|
||||
messageDisplayer: (Int) -> Unit,
|
||||
): Binding {
|
||||
val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
|
||||
val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
|
||||
@@ -108,6 +117,7 @@ object KeyguardBottomAreaViewBinder {
|
||||
view = startButton,
|
||||
viewModel = buttonModel,
|
||||
falsingManager = falsingManager,
|
||||
messageDisplayer = messageDisplayer,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -118,6 +128,7 @@ object KeyguardBottomAreaViewBinder {
|
||||
view = endButton,
|
||||
viewModel = buttonModel,
|
||||
falsingManager = falsingManager,
|
||||
messageDisplayer = messageDisplayer,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -222,10 +233,12 @@ object KeyguardBottomAreaViewBinder {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun updateButton(
|
||||
view: ImageView,
|
||||
viewModel: KeyguardQuickAffordanceViewModel,
|
||||
falsingManager: FalsingManager,
|
||||
falsingManager: FalsingManager?,
|
||||
messageDisplayer: (Int) -> Unit,
|
||||
) {
|
||||
if (!viewModel.isVisible) {
|
||||
view.isVisible = false
|
||||
@@ -281,21 +294,126 @@ object KeyguardBottomAreaViewBinder {
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
view.backgroundTintList =
|
||||
Utils.getColorAttr(
|
||||
view.context,
|
||||
if (viewModel.isActivated) {
|
||||
com.android.internal.R.attr.colorAccentPrimary
|
||||
} else {
|
||||
com.android.internal.R.attr.colorSurface
|
||||
}
|
||||
)
|
||||
if (!viewModel.isSelected) {
|
||||
Utils.getColorAttr(
|
||||
view.context,
|
||||
if (viewModel.isActivated) {
|
||||
com.android.internal.R.attr.colorAccentPrimary
|
||||
} else {
|
||||
com.android.internal.R.attr.colorSurface
|
||||
}
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
view.isClickable = viewModel.isClickable
|
||||
if (viewModel.isClickable) {
|
||||
view.setOnClickListener(OnClickListener(viewModel, falsingManager))
|
||||
if (viewModel.useLongPress) {
|
||||
view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer))
|
||||
} else {
|
||||
view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
|
||||
}
|
||||
} else {
|
||||
view.setOnClickListener(null)
|
||||
view.setOnTouchListener(null)
|
||||
}
|
||||
|
||||
view.isSelected = viewModel.isSelected
|
||||
}
|
||||
|
||||
private class OnTouchListener(
|
||||
private val view: View,
|
||||
private val viewModel: KeyguardQuickAffordanceViewModel,
|
||||
private val messageDisplayer: (Int) -> Unit,
|
||||
) : View.OnTouchListener {
|
||||
|
||||
private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
|
||||
private var longPressAnimator: ViewPropertyAnimator? = null
|
||||
private var downTimestamp = 0L
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
return when (event?.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN ->
|
||||
if (viewModel.configKey != null) {
|
||||
downTimestamp = System.currentTimeMillis()
|
||||
longPressAnimator =
|
||||
view
|
||||
.animate()
|
||||
.scaleX(PRESSED_SCALE)
|
||||
.scaleY(PRESSED_SCALE)
|
||||
.setDuration(longPressDurationMs)
|
||||
.withEndAction {
|
||||
view.setOnClickListener {
|
||||
viewModel.onClicked(
|
||||
KeyguardQuickAffordanceViewModel.OnClickedParameters(
|
||||
configKey = viewModel.configKey,
|
||||
expandable = Expandable.fromView(view),
|
||||
)
|
||||
)
|
||||
}
|
||||
view.performClick()
|
||||
view.setOnClickListener(null)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (event.historySize > 0) {
|
||||
val distance =
|
||||
sqrt(
|
||||
(event.y - event.getHistoricalY(0)).pow(2) +
|
||||
(event.x - event.getHistoricalX(0)).pow(2)
|
||||
)
|
||||
if (distance > ViewConfiguration.getTouchSlop()) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
|
||||
messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
|
||||
val shakeAnimator =
|
||||
ObjectAnimator.ofFloat(
|
||||
view,
|
||||
"translationX",
|
||||
0f,
|
||||
view.context.resources
|
||||
.getDimensionPixelSize(
|
||||
R.dimen.keyguard_affordance_shake_amplitude
|
||||
)
|
||||
.toFloat(),
|
||||
0f,
|
||||
)
|
||||
shakeAnimator.duration = 300
|
||||
shakeAnimator.interpolator = CycleInterpolator(5f)
|
||||
shakeAnimator.start()
|
||||
}
|
||||
cancel()
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
cancel()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancel() {
|
||||
downTimestamp = 0L
|
||||
longPressAnimator?.cancel()
|
||||
longPressAnimator = null
|
||||
view.animate().scaleX(1f).scaleY(1f)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PRESSED_SCALE = 1.5f
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.preview
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.SurfaceControlViewHost
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import com.android.keyguard.ClockEventController
|
||||
import com.android.keyguard.KeyguardClockSwitch
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
|
||||
import com.android.systemui.shared.clocks.ClockRegistry
|
||||
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
|
||||
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.DisposableHandle
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/** Renders the preview of the lock screen. */
|
||||
class KeyguardPreviewRenderer
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Application private val context: Context,
|
||||
@Main private val mainDispatcher: CoroutineDispatcher,
|
||||
private val bottomAreaViewModel: KeyguardBottomAreaViewModel,
|
||||
displayManager: DisplayManager,
|
||||
private val windowManager: WindowManager,
|
||||
private val clockController: ClockEventController,
|
||||
private val clockRegistry: ClockRegistry,
|
||||
private val broadcastDispatcher: BroadcastDispatcher,
|
||||
@Assisted bundle: Bundle,
|
||||
) {
|
||||
|
||||
val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
|
||||
private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
|
||||
private val height: Int = bundle.getInt(KEY_VIEW_HEIGHT)
|
||||
|
||||
private var host: SurfaceControlViewHost
|
||||
|
||||
val surfacePackage: SurfaceControlViewHost.SurfacePackage
|
||||
get() = host.surfacePackage
|
||||
|
||||
private var clockView: View? = null
|
||||
|
||||
private val disposables = mutableSetOf<DisposableHandle>()
|
||||
private var isDestroyed = false
|
||||
|
||||
init {
|
||||
bottomAreaViewModel.enablePreviewMode(
|
||||
initiallySelectedSlotId =
|
||||
bundle.getString(
|
||||
KeyguardQuickAffordancePreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
|
||||
),
|
||||
)
|
||||
runBlocking(mainDispatcher) {
|
||||
host =
|
||||
SurfaceControlViewHost(
|
||||
context,
|
||||
displayManager.getDisplay(bundle.getInt(KEY_DISPLAY_ID)),
|
||||
hostToken,
|
||||
)
|
||||
disposables.add(DisposableHandle { host.release() })
|
||||
}
|
||||
}
|
||||
|
||||
fun render() {
|
||||
runBlocking(mainDispatcher) {
|
||||
val rootView = FrameLayout(context)
|
||||
|
||||
setUpBottomArea(rootView)
|
||||
setUpClock(rootView)
|
||||
|
||||
rootView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(
|
||||
windowManager.currentWindowMetrics.bounds.width(),
|
||||
View.MeasureSpec.EXACTLY
|
||||
),
|
||||
View.MeasureSpec.makeMeasureSpec(
|
||||
windowManager.currentWindowMetrics.bounds.height(),
|
||||
View.MeasureSpec.EXACTLY
|
||||
),
|
||||
)
|
||||
rootView.layout(0, 0, rootView.measuredWidth, rootView.measuredHeight)
|
||||
|
||||
// This aspect scales the view to fit in the surface and centers it
|
||||
val scale: Float =
|
||||
(width / rootView.measuredWidth.toFloat()).coerceAtMost(
|
||||
height / rootView.measuredHeight.toFloat()
|
||||
)
|
||||
|
||||
rootView.scaleX = scale
|
||||
rootView.scaleY = scale
|
||||
rootView.pivotX = 0f
|
||||
rootView.pivotY = 0f
|
||||
rootView.translationX = (width - scale * rootView.width) / 2
|
||||
rootView.translationY = (height - scale * rootView.height) / 2
|
||||
|
||||
host.setView(rootView, rootView.measuredWidth, rootView.measuredHeight)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSlotSelected(slotId: String) {
|
||||
bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
isDestroyed = true
|
||||
disposables.forEach { it.dispose() }
|
||||
}
|
||||
|
||||
private fun setUpBottomArea(parentView: ViewGroup) {
|
||||
val bottomAreaView =
|
||||
LayoutInflater.from(context)
|
||||
.inflate(
|
||||
R.layout.keyguard_bottom_area,
|
||||
parentView,
|
||||
false,
|
||||
) as KeyguardBottomAreaView
|
||||
bottomAreaView.init(
|
||||
viewModel = bottomAreaViewModel,
|
||||
)
|
||||
parentView.addView(
|
||||
bottomAreaView,
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.BOTTOM,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun setUpClock(parentView: ViewGroup) {
|
||||
val clockChangeListener = ClockRegistry.ClockChangeListener { onClockChanged(parentView) }
|
||||
clockRegistry.registerClockChangeListener(clockChangeListener)
|
||||
disposables.add(
|
||||
DisposableHandle { clockRegistry.unregisterClockChangeListener(clockChangeListener) }
|
||||
)
|
||||
|
||||
clockController.registerListeners(parentView)
|
||||
disposables.add(DisposableHandle { clockController.unregisterListeners() })
|
||||
|
||||
val receiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
clockController.clock?.events?.onTimeTick()
|
||||
}
|
||||
}
|
||||
broadcastDispatcher.registerReceiver(
|
||||
receiver,
|
||||
IntentFilter().apply {
|
||||
addAction(Intent.ACTION_TIME_TICK)
|
||||
addAction(Intent.ACTION_TIME_CHANGED)
|
||||
},
|
||||
)
|
||||
disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
|
||||
|
||||
onClockChanged(parentView)
|
||||
}
|
||||
|
||||
private fun onClockChanged(parentView: ViewGroup) {
|
||||
clockController.clock = clockRegistry.createCurrentClock()
|
||||
clockController.clock
|
||||
?.largeClock
|
||||
?.events
|
||||
?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView))
|
||||
clockView?.let { parentView.removeView(it) }
|
||||
clockView = clockController.clock?.largeClock?.view?.apply { parentView.addView(this) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_HOST_TOKEN = "host_token"
|
||||
private const val KEY_VIEW_WIDTH = "width"
|
||||
private const val KEY_VIEW_HEIGHT = "height"
|
||||
private const val KEY_DISPLAY_ID = "display_id"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.preview
|
||||
|
||||
import android.os.Bundle
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
||||
@AssistedFactory
|
||||
interface KeyguardPreviewRendererFactory {
|
||||
fun create(bundle: Bundle): KeyguardPreviewRenderer
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.preview
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import android.util.ArrayMap
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@SysUISingleton
|
||||
class KeyguardRemotePreviewManager
|
||||
@Inject
|
||||
constructor(
|
||||
private val previewRendererFactory: KeyguardPreviewRendererFactory,
|
||||
@Main private val mainDispatcher: CoroutineDispatcher,
|
||||
@Background private val backgroundHandler: Handler,
|
||||
) {
|
||||
private val activePreviews: ArrayMap<IBinder, PreviewLifecycleObserver> =
|
||||
ArrayMap<IBinder, PreviewLifecycleObserver>()
|
||||
|
||||
fun preview(request: Bundle?): Bundle? {
|
||||
if (request == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var observer: PreviewLifecycleObserver? = null
|
||||
return try {
|
||||
val renderer = previewRendererFactory.create(request)
|
||||
|
||||
// Destroy any previous renderer associated with this token.
|
||||
activePreviews[renderer.hostToken]?.let { destroyObserver(it) }
|
||||
observer = PreviewLifecycleObserver(renderer, mainDispatcher, ::destroyObserver)
|
||||
activePreviews[renderer.hostToken] = observer
|
||||
renderer.render()
|
||||
renderer.hostToken?.linkToDeath(observer, 0)
|
||||
val result = Bundle()
|
||||
result.putParcelable(
|
||||
KEY_PREVIEW_SURFACE_PACKAGE,
|
||||
renderer.surfacePackage,
|
||||
)
|
||||
val messenger =
|
||||
Messenger(
|
||||
Handler(
|
||||
backgroundHandler.looper,
|
||||
observer,
|
||||
)
|
||||
)
|
||||
val msg = Message.obtain()
|
||||
msg.replyTo = messenger
|
||||
result.putParcelable(KEY_PREVIEW_CALLBACK, msg)
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to generate preview", e)
|
||||
observer?.let { destroyObserver(it) }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun destroyObserver(observer: PreviewLifecycleObserver) {
|
||||
observer.onDestroy()?.let { hostToken ->
|
||||
if (activePreviews[hostToken] === observer) {
|
||||
activePreviews.remove(hostToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewLifecycleObserver(
|
||||
private val renderer: KeyguardPreviewRenderer,
|
||||
private val mainDispatcher: CoroutineDispatcher,
|
||||
private val requestDestruction: (PreviewLifecycleObserver) -> Unit,
|
||||
) : Handler.Callback, IBinder.DeathRecipient {
|
||||
|
||||
private var isDestroyed = false
|
||||
|
||||
override fun handleMessage(message: Message): Boolean {
|
||||
when (message.what) {
|
||||
KeyguardQuickAffordancePreviewConstants.MESSAGE_ID_SLOT_SELECTED -> {
|
||||
message.data
|
||||
.getString(
|
||||
KeyguardQuickAffordancePreviewConstants.KEY_SLOT_ID,
|
||||
)
|
||||
?.let { slotId -> renderer.onSlotSelected(slotId = slotId) }
|
||||
}
|
||||
else -> requestDestruction(this)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun binderDied() {
|
||||
requestDestruction(this)
|
||||
}
|
||||
|
||||
fun onDestroy(): IBinder? {
|
||||
if (isDestroyed) {
|
||||
return null
|
||||
}
|
||||
|
||||
isDestroyed = true
|
||||
val hostToken = renderer.hostToken
|
||||
hostToken?.unlinkToDeath(this, 0)
|
||||
runBlocking(mainDispatcher) { renderer.destroy() }
|
||||
return hostToken
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "KeyguardRemotePreviewManager"
|
||||
@VisibleForTesting const val KEY_PREVIEW_SURFACE_PACKAGE = "surface_package"
|
||||
@VisibleForTesting const val KEY_PREVIEW_CALLBACK = "callback"
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,19 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceIn
|
||||
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
|
||||
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
|
||||
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
|
||||
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
|
||||
import javax.inject.Inject
|
||||
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.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** View-model for the keyguard bottom area view */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class KeyguardBottomAreaViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@@ -39,6 +45,20 @@ constructor(
|
||||
private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
|
||||
private val burnInHelperWrapper: BurnInHelperWrapper,
|
||||
) {
|
||||
/**
|
||||
* Whether this view-model instance is powering the preview experience that renders exclusively
|
||||
* in the wallpaper picker application. This should _always_ be `false` for the real lock screen
|
||||
* experience.
|
||||
*/
|
||||
private val isInPreviewMode = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* ID of the slot that's currently selected in the preview that renders exclusively in the
|
||||
* wallpaper picker application. This is ignored for the actual, real lock screen experience.
|
||||
*/
|
||||
private val selectedPreviewSlotId =
|
||||
MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
|
||||
|
||||
/**
|
||||
* Whether quick affordances are "opaque enough" to be considered visible to and interactive by
|
||||
* the user. If they are not interactive, user input should not be allowed on them.
|
||||
@@ -66,7 +86,14 @@ constructor(
|
||||
val isOverlayContainerVisible: Flow<Boolean> =
|
||||
keyguardInteractor.isDozing.map { !it }.distinctUntilChanged()
|
||||
/** An observable for the alpha level for the entire bottom area. */
|
||||
val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged()
|
||||
val alpha: Flow<Float> =
|
||||
isInPreviewMode.flatMapLatest { isInPreviewMode ->
|
||||
if (isInPreviewMode) {
|
||||
flowOf(1f)
|
||||
} else {
|
||||
bottomAreaInteractor.alpha.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
/** An observable for whether the indication area should be padded. */
|
||||
val isIndicationAreaPadded: Flow<Boolean> =
|
||||
combine(startButton, endButton) { startButtonModel, endButtonModel ->
|
||||
@@ -94,27 +121,61 @@ constructor(
|
||||
* Returns whether the keyguard bottom area should be constrained to the top of the lock icon
|
||||
*/
|
||||
fun shouldConstrainToTopOfLockIcon(): Boolean =
|
||||
bottomAreaInteractor.shouldConstrainToTopOfLockIcon()
|
||||
bottomAreaInteractor.shouldConstrainToTopOfLockIcon()
|
||||
|
||||
/**
|
||||
* Puts this view-model in "preview mode", which means it's being used for UI that is rendering
|
||||
* the lock screen preview in wallpaper picker / settings and not the real experience on the
|
||||
* lock screen.
|
||||
*
|
||||
* @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
|
||||
*/
|
||||
fun enablePreviewMode(initiallySelectedSlotId: String?) {
|
||||
isInPreviewMode.value = true
|
||||
onPreviewSlotSelected(
|
||||
initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that a slot with the given ID has been selected in the preview experience that is
|
||||
* rendering in the wallpaper picker. This is ignored for the real lock screen experience.
|
||||
*
|
||||
* @see enablePreviewMode
|
||||
*/
|
||||
fun onPreviewSlotSelected(slotId: String) {
|
||||
selectedPreviewSlotId.value = slotId
|
||||
}
|
||||
|
||||
private fun button(
|
||||
position: KeyguardQuickAffordancePosition
|
||||
): Flow<KeyguardQuickAffordanceViewModel> {
|
||||
return combine(
|
||||
quickAffordanceInteractor.quickAffordance(position),
|
||||
bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
|
||||
areQuickAffordancesFullyOpaque,
|
||||
) { model, animateReveal, isFullyOpaque ->
|
||||
model.toViewModel(
|
||||
animateReveal = animateReveal,
|
||||
isClickable = isFullyOpaque,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
return isInPreviewMode.flatMapLatest { isInPreviewMode ->
|
||||
combine(
|
||||
if (isInPreviewMode) {
|
||||
quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
|
||||
} else {
|
||||
quickAffordanceInteractor.quickAffordance(position = position)
|
||||
},
|
||||
bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
|
||||
areQuickAffordancesFullyOpaque,
|
||||
selectedPreviewSlotId,
|
||||
) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId ->
|
||||
model.toViewModel(
|
||||
animateReveal = !isInPreviewMode && animateReveal,
|
||||
isClickable = isFullyOpaque && !isInPreviewMode,
|
||||
isSelected =
|
||||
(isInPreviewMode && selectedPreviewSlotId == position.toSlotId()),
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun KeyguardQuickAffordanceModel.toViewModel(
|
||||
animateReveal: Boolean,
|
||||
isClickable: Boolean,
|
||||
isSelected: Boolean,
|
||||
): KeyguardQuickAffordanceViewModel {
|
||||
return when (this) {
|
||||
is KeyguardQuickAffordanceModel.Visible ->
|
||||
@@ -131,6 +192,8 @@ constructor(
|
||||
},
|
||||
isClickable = isClickable,
|
||||
isActivated = activationState is ActivationState.Active,
|
||||
isSelected = isSelected,
|
||||
useLongPress = quickAffordanceInteractor.useLongPress,
|
||||
)
|
||||
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ data class KeyguardQuickAffordanceViewModel(
|
||||
val onClicked: (OnClickedParameters) -> Unit = {},
|
||||
val isClickable: Boolean = false,
|
||||
val isActivated: Boolean = false,
|
||||
val isSelected: Boolean = false,
|
||||
val useLongPress: Boolean = false,
|
||||
) {
|
||||
data class OnClickedParameters(
|
||||
val configKey: String,
|
||||
|
||||
@@ -1328,7 +1328,9 @@ public final class NotificationPanelViewController implements Dumpable {
|
||||
mKeyguardBottomArea.init(
|
||||
mKeyguardBottomAreaViewModel,
|
||||
mFalsingManager,
|
||||
mLockIconViewController
|
||||
mLockIconViewController,
|
||||
stringResourceId ->
|
||||
mKeyguardIndicationController.showTransientIndication(stringResourceId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.WindowInsets
|
||||
import android.widget.FrameLayout
|
||||
import com.android.keyguard.KeyguardUpdateMonitor
|
||||
import androidx.annotation.StringRes
|
||||
import com.android.keyguard.LockIconViewController
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
|
||||
@@ -51,21 +51,29 @@ constructor(
|
||||
defStyleRes,
|
||||
) {
|
||||
|
||||
interface MessageDisplayer {
|
||||
fun display(@StringRes stringResourceId: Int)
|
||||
}
|
||||
|
||||
private var ambientIndicationArea: View? = null
|
||||
private lateinit var binding: KeyguardBottomAreaViewBinder.Binding
|
||||
private lateinit var lockIconViewController: LockIconViewController
|
||||
private var lockIconViewController: LockIconViewController? = null
|
||||
|
||||
/** Initializes the view. */
|
||||
fun init(
|
||||
viewModel: KeyguardBottomAreaViewModel,
|
||||
falsingManager: FalsingManager,
|
||||
lockIconViewController: LockIconViewController,
|
||||
falsingManager: FalsingManager? = null,
|
||||
lockIconViewController: LockIconViewController? = null,
|
||||
messageDisplayer: MessageDisplayer? = null,
|
||||
) {
|
||||
binding = bind(
|
||||
binding =
|
||||
bind(
|
||||
this,
|
||||
viewModel,
|
||||
falsingManager,
|
||||
)
|
||||
) {
|
||||
messageDisplayer?.display(it)
|
||||
}
|
||||
this.lockIconViewController = lockIconViewController
|
||||
}
|
||||
|
||||
@@ -129,21 +137,21 @@ constructor(
|
||||
findViewById<View>(R.id.ambient_indication_container)?.let {
|
||||
val (ambientLeft, ambientTop) = it.locationOnScreen
|
||||
if (binding.shouldConstrainToTopOfLockIcon()) {
|
||||
//make top of ambient indication view the bottom of the lock icon
|
||||
// make top of ambient indication view the bottom of the lock icon
|
||||
it.layout(
|
||||
ambientLeft,
|
||||
lockIconViewController.bottom.toInt(),
|
||||
right - ambientLeft,
|
||||
ambientTop + it.measuredHeight
|
||||
ambientLeft,
|
||||
lockIconViewController?.bottom?.toInt() ?: 0,
|
||||
right - ambientLeft,
|
||||
ambientTop + it.measuredHeight
|
||||
)
|
||||
} else {
|
||||
//make bottom of ambient indication view the top of the lock icon
|
||||
val lockLocationTop = lockIconViewController.top
|
||||
// make bottom of ambient indication view the top of the lock icon
|
||||
val lockLocationTop = lockIconViewController?.top ?: 0
|
||||
it.layout(
|
||||
ambientLeft,
|
||||
lockLocationTop.toInt() - it.measuredHeight,
|
||||
right - ambientLeft,
|
||||
lockLocationTop.toInt()
|
||||
ambientLeft,
|
||||
lockLocationTop.toInt() - it.measuredHeight,
|
||||
right - ambientLeft,
|
||||
lockLocationTop.toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,13 @@ package com.android.systemui.keyguard
|
||||
import android.content.ContentValues
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.UserHandle
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import android.view.SurfaceControlViewHost
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.widget.LockPatternUtils
|
||||
import com.android.systemui.SystemUIAppComponentFactoryBase
|
||||
@@ -36,6 +42,9 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
|
||||
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
|
||||
import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
|
||||
import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory
|
||||
import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.settings.UserFileManager
|
||||
import com.android.systemui.settings.UserTracker
|
||||
@@ -43,40 +52,53 @@ import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordance
|
||||
import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract
|
||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||
import com.android.systemui.util.FakeSharedPreferences
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var lockPatternUtils: LockPatternUtils
|
||||
@Mock private lateinit var keyguardStateController: KeyguardStateController
|
||||
@Mock private lateinit var userTracker: UserTracker
|
||||
@Mock private lateinit var activityStarter: ActivityStarter
|
||||
@Mock private lateinit var previewRendererFactory: KeyguardPreviewRendererFactory
|
||||
@Mock private lateinit var previewRenderer: KeyguardPreviewRenderer
|
||||
@Mock private lateinit var backgroundHandler: Handler
|
||||
@Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage
|
||||
|
||||
private lateinit var underTest: KeyguardQuickAffordanceProvider
|
||||
|
||||
private lateinit var testScope: TestScope
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
whenever(previewRenderer.surfacePackage).thenReturn(previewSurfacePackage)
|
||||
whenever(previewRendererFactory.create(any())).thenReturn(previewRenderer)
|
||||
whenever(backgroundHandler.looper).thenReturn(TestableLooper.get(this).looper)
|
||||
|
||||
underTest = KeyguardQuickAffordanceProvider()
|
||||
val scope = CoroutineScope(IMMEDIATE)
|
||||
val testDispatcher = StandardTestDispatcher()
|
||||
testScope = TestScope(testDispatcher)
|
||||
val localUserSelectionManager =
|
||||
KeyguardQuickAffordanceLocalUserSelectionManager(
|
||||
context = context,
|
||||
@@ -96,7 +118,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
)
|
||||
val remoteUserSelectionManager =
|
||||
KeyguardQuickAffordanceRemoteUserSelectionManager(
|
||||
scope = scope,
|
||||
scope = testScope.backgroundScope,
|
||||
userTracker = userTracker,
|
||||
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
|
||||
userHandle = UserHandle.SYSTEM,
|
||||
@@ -104,7 +126,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
appContext = context,
|
||||
scope = scope,
|
||||
scope = testScope.backgroundScope,
|
||||
localUserSelectionManager = localUserSelectionManager,
|
||||
remoteUserSelectionManager = remoteUserSelectionManager,
|
||||
userTracker = userTracker,
|
||||
@@ -123,8 +145,8 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
),
|
||||
legacySettingSyncer =
|
||||
KeyguardQuickAffordanceLegacySettingSyncer(
|
||||
scope = scope,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
scope = testScope.backgroundScope,
|
||||
backgroundDispatcher = testDispatcher,
|
||||
secureSettings = FakeSettings(),
|
||||
selectionsManager = localUserSelectionManager,
|
||||
),
|
||||
@@ -148,6 +170,12 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
},
|
||||
repository = { quickAffordanceRepository },
|
||||
)
|
||||
underTest.previewManager =
|
||||
KeyguardRemotePreviewManager(
|
||||
previewRendererFactory = previewRendererFactory,
|
||||
mainDispatcher = testDispatcher,
|
||||
backgroundHandler = backgroundHandler,
|
||||
)
|
||||
|
||||
underTest.attachInfoForTesting(
|
||||
context,
|
||||
@@ -190,7 +218,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun `insert and query selection`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
|
||||
val affordanceId = AFFORDANCE_2
|
||||
val affordanceName = AFFORDANCE_2_NAME
|
||||
@@ -214,7 +242,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun `query slots`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
assertThat(querySlots())
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
@@ -232,7 +260,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun `query affordances`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
assertThat(queryAffordances())
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
@@ -252,7 +280,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun `delete and query selection`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
insertSelection(
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
affordanceId = AFFORDANCE_1,
|
||||
@@ -286,7 +314,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun `delete all selections in a slot`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
insertSelection(
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
affordanceId = AFFORDANCE_1,
|
||||
@@ -316,6 +344,23 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preview() =
|
||||
testScope.runTest {
|
||||
val hostToken: IBinder = mock()
|
||||
whenever(previewRenderer.hostToken).thenReturn(hostToken)
|
||||
val extras = Bundle()
|
||||
|
||||
val result = underTest.call("whatever", "anything", extras)
|
||||
|
||||
verify(previewRenderer).render()
|
||||
verify(hostToken).linkToDeath(any(), anyInt())
|
||||
assertThat(result!!).isNotNull()
|
||||
assertThat(result.get(KeyguardRemotePreviewManager.KEY_PREVIEW_SURFACE_PACKAGE))
|
||||
.isEqualTo(previewSurfacePackage)
|
||||
assertThat(result.containsKey(KeyguardRemotePreviewManager.KEY_PREVIEW_CALLBACK))
|
||||
}
|
||||
|
||||
private fun insertSelection(
|
||||
slotId: String,
|
||||
affordanceId: String,
|
||||
@@ -451,7 +496,6 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
private const val AFFORDANCE_1 = "affordance_1"
|
||||
private const val AFFORDANCE_2 = "affordance_2"
|
||||
private const val AFFORDANCE_1_NAME = "affordance_1_name"
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.android.internal.widget.LockPatternUtils
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.common.shared.model.ContentDescription
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
|
||||
@@ -49,14 +50,10 @@ import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.yield
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -78,6 +75,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var underTest: KeyguardQuickAffordanceInteractor
|
||||
|
||||
private lateinit var testScope: TestScope
|
||||
private lateinit var repository: FakeKeyguardRepository
|
||||
private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
|
||||
private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig
|
||||
@@ -99,7 +97,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
qrCodeScanner =
|
||||
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
|
||||
val scope = CoroutineScope(IMMEDIATE)
|
||||
val testDispatcher = StandardTestDispatcher()
|
||||
testScope = TestScope(testDispatcher)
|
||||
|
||||
val localUserSelectionManager =
|
||||
KeyguardQuickAffordanceLocalUserSelectionManager(
|
||||
@@ -120,7 +119,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
val remoteUserSelectionManager =
|
||||
KeyguardQuickAffordanceRemoteUserSelectionManager(
|
||||
scope = scope,
|
||||
scope = testScope.backgroundScope,
|
||||
userTracker = userTracker,
|
||||
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
|
||||
userHandle = UserHandle.SYSTEM,
|
||||
@@ -128,14 +127,14 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
appContext = context,
|
||||
scope = scope,
|
||||
scope = testScope.backgroundScope,
|
||||
localUserSelectionManager = localUserSelectionManager,
|
||||
remoteUserSelectionManager = remoteUserSelectionManager,
|
||||
userTracker = userTracker,
|
||||
legacySettingSyncer =
|
||||
KeyguardQuickAffordanceLegacySettingSyncer(
|
||||
scope = scope,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
scope = testScope.backgroundScope,
|
||||
backgroundDispatcher = testDispatcher,
|
||||
secureSettings = FakeSettings(),
|
||||
selectionsManager = localUserSelectionManager,
|
||||
),
|
||||
@@ -175,88 +174,76 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quickAffordance - bottom start affordance is visible`() = runBlockingTest {
|
||||
val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
icon = ICON,
|
||||
activationState = ActivationState.Active,
|
||||
fun `quickAffordance - bottom start affordance is visible`() =
|
||||
testScope.runTest {
|
||||
val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
icon = ICON,
|
||||
activationState = ActivationState.Active,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
var latest: KeyguardQuickAffordanceModel? = null
|
||||
val job =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
.onEach { latest = it }
|
||||
.launchIn(this)
|
||||
// The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't
|
||||
// produce an initial value. We yield to give the coroutine time to emit the first real
|
||||
// value from our config.
|
||||
yield()
|
||||
val collectedValue =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
)
|
||||
|
||||
assertThat(latest).isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
|
||||
val visibleModel = latest as KeyguardQuickAffordanceModel.Visible
|
||||
assertThat(visibleModel.configKey).isEqualTo(configKey)
|
||||
assertThat(visibleModel.icon).isEqualTo(ICON)
|
||||
assertThat(visibleModel.icon.contentDescription)
|
||||
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
|
||||
assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active)
|
||||
job.cancel()
|
||||
}
|
||||
assertThat(collectedValue())
|
||||
.isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
|
||||
val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible
|
||||
assertThat(visibleModel.configKey).isEqualTo(configKey)
|
||||
assertThat(visibleModel.icon).isEqualTo(ICON)
|
||||
assertThat(visibleModel.icon.contentDescription)
|
||||
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
|
||||
assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quickAffordance - bottom end affordance is visible`() = runBlockingTest {
|
||||
val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
|
||||
quickAccessWallet.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
icon = ICON,
|
||||
fun `quickAffordance - bottom end affordance is visible`() =
|
||||
testScope.runTest {
|
||||
val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
|
||||
quickAccessWallet.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
icon = ICON,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
var latest: KeyguardQuickAffordanceModel? = null
|
||||
val job =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
|
||||
.onEach { latest = it }
|
||||
.launchIn(this)
|
||||
// The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't
|
||||
// produce an initial value. We yield to give the coroutine time to emit the first real
|
||||
// value from our config.
|
||||
yield()
|
||||
val collectedValue =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
|
||||
)
|
||||
|
||||
assertThat(latest).isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
|
||||
val visibleModel = latest as KeyguardQuickAffordanceModel.Visible
|
||||
assertThat(visibleModel.configKey).isEqualTo(configKey)
|
||||
assertThat(visibleModel.icon).isEqualTo(ICON)
|
||||
assertThat(visibleModel.icon.contentDescription)
|
||||
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
|
||||
assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported)
|
||||
job.cancel()
|
||||
}
|
||||
assertThat(collectedValue())
|
||||
.isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
|
||||
val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible
|
||||
assertThat(visibleModel.configKey).isEqualTo(configKey)
|
||||
assertThat(visibleModel.icon).isEqualTo(ICON)
|
||||
assertThat(visibleModel.icon.contentDescription)
|
||||
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
|
||||
assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quickAffordance - bottom start affordance hidden while dozing`() = runBlockingTest {
|
||||
repository.setDozing(true)
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
icon = ICON,
|
||||
fun `quickAffordance - bottom start affordance hidden while dozing`() =
|
||||
testScope.runTest {
|
||||
repository.setDozing(true)
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
icon = ICON,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
var latest: KeyguardQuickAffordanceModel? = null
|
||||
val job =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
.onEach { latest = it }
|
||||
.launchIn(this)
|
||||
assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
|
||||
job.cancel()
|
||||
}
|
||||
val collectedValue =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
)
|
||||
assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quickAffordance - bottom start affordance hidden when lockscreen is not showing`() =
|
||||
runBlockingTest {
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(false)
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
@@ -264,19 +251,45 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
)
|
||||
|
||||
var latest: KeyguardQuickAffordanceModel? = null
|
||||
val job =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
.onEach { latest = it }
|
||||
.launchIn(this)
|
||||
assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
|
||||
job.cancel()
|
||||
val collectedValue =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
)
|
||||
assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quickAffordanceAlwaysVisible - even when lock screen not showing and dozing`() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(false)
|
||||
repository.setDozing(true)
|
||||
val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||
icon = ICON,
|
||||
activationState = ActivationState.Active,
|
||||
)
|
||||
)
|
||||
|
||||
val collectedValue =
|
||||
collectLastValue(
|
||||
underTest.quickAffordanceAlwaysVisible(
|
||||
KeyguardQuickAffordancePosition.BOTTOM_START
|
||||
)
|
||||
)
|
||||
assertThat(collectedValue())
|
||||
.isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
|
||||
val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible
|
||||
assertThat(visibleModel.configKey).isEqualTo(configKey)
|
||||
assertThat(visibleModel.icon).isEqualTo(ICON)
|
||||
assertThat(visibleModel.icon.contentDescription)
|
||||
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
|
||||
assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun select() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
|
||||
@@ -296,23 +309,18 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
)
|
||||
|
||||
var startConfig: KeyguardQuickAffordanceModel? = null
|
||||
val job1 =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
.onEach { startConfig = it }
|
||||
.launchIn(this)
|
||||
var endConfig: KeyguardQuickAffordanceModel? = null
|
||||
val job2 =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
|
||||
.onEach { endConfig = it }
|
||||
.launchIn(this)
|
||||
val startConfig =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
)
|
||||
val endConfig =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
|
||||
)
|
||||
|
||||
underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
|
||||
yield()
|
||||
yield()
|
||||
assertThat(startConfig)
|
||||
|
||||
assertThat(startConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Visible(
|
||||
configKey =
|
||||
@@ -322,7 +330,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
activationState = ActivationState.NotSupported,
|
||||
)
|
||||
)
|
||||
assertThat(endConfig)
|
||||
assertThat(endConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Hidden,
|
||||
)
|
||||
@@ -345,9 +353,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
quickAccessWallet.key
|
||||
)
|
||||
yield()
|
||||
yield()
|
||||
assertThat(startConfig)
|
||||
|
||||
assertThat(startConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Visible(
|
||||
configKey =
|
||||
@@ -357,7 +364,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
activationState = ActivationState.NotSupported,
|
||||
)
|
||||
)
|
||||
assertThat(endConfig)
|
||||
assertThat(endConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Hidden,
|
||||
)
|
||||
@@ -377,9 +384,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
|
||||
underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key)
|
||||
yield()
|
||||
yield()
|
||||
assertThat(startConfig)
|
||||
|
||||
assertThat(startConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Visible(
|
||||
configKey =
|
||||
@@ -389,7 +395,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
activationState = ActivationState.NotSupported,
|
||||
)
|
||||
)
|
||||
assertThat(endConfig)
|
||||
assertThat(endConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Visible(
|
||||
configKey =
|
||||
@@ -420,14 +426,11 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
job1.cancel()
|
||||
job2.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unselect - one`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
|
||||
@@ -439,34 +442,23 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
|
||||
)
|
||||
|
||||
var startConfig: KeyguardQuickAffordanceModel? = null
|
||||
val job1 =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
.onEach { startConfig = it }
|
||||
.launchIn(this)
|
||||
var endConfig: KeyguardQuickAffordanceModel? = null
|
||||
val job2 =
|
||||
underTest
|
||||
.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
|
||||
.onEach { endConfig = it }
|
||||
.launchIn(this)
|
||||
val startConfig =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
|
||||
)
|
||||
val endConfig =
|
||||
collectLastValue(
|
||||
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
|
||||
)
|
||||
underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
|
||||
yield()
|
||||
yield()
|
||||
underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
|
||||
yield()
|
||||
yield()
|
||||
|
||||
underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
|
||||
yield()
|
||||
yield()
|
||||
|
||||
assertThat(startConfig)
|
||||
assertThat(startConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Hidden,
|
||||
)
|
||||
assertThat(endConfig)
|
||||
assertThat(endConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Visible(
|
||||
configKey =
|
||||
@@ -495,14 +487,12 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
|
||||
quickAccessWallet.key
|
||||
)
|
||||
yield()
|
||||
yield()
|
||||
|
||||
assertThat(startConfig)
|
||||
assertThat(startConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Hidden,
|
||||
)
|
||||
assertThat(endConfig)
|
||||
assertThat(endConfig())
|
||||
.isEqualTo(
|
||||
KeyguardQuickAffordanceModel.Hidden,
|
||||
)
|
||||
@@ -513,14 +503,11 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
|
||||
)
|
||||
)
|
||||
|
||||
job1.cancel()
|
||||
job2.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unselect - all`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
testScope.runTest {
|
||||
featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
|
||||
homeControls.setState(
|
||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
|
||||
@@ -533,15 +520,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
|
||||
underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
|
||||
yield()
|
||||
yield()
|
||||
underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
|
||||
yield()
|
||||
yield()
|
||||
|
||||
underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, null)
|
||||
yield()
|
||||
yield()
|
||||
|
||||
assertThat(underTest.getSelections())
|
||||
.isEqualTo(
|
||||
@@ -562,8 +542,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
|
||||
null,
|
||||
)
|
||||
yield()
|
||||
yield()
|
||||
|
||||
assertThat(underTest.getSelections())
|
||||
.isEqualTo(
|
||||
@@ -584,6 +562,5 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
}
|
||||
private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.android.internal.widget.LockPatternUtils
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.animation.Expandable
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.doze.util.BurnInHelperWrapper
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
@@ -44,20 +45,21 @@ import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAfforda
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
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.policy.KeyguardStateController
|
||||
import com.android.systemui.util.FakeSharedPreferences
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.yield
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -67,9 +69,9 @@ import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.verifyZeroInteractions
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
@@ -83,6 +85,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var underTest: KeyguardBottomAreaViewModel
|
||||
|
||||
private lateinit var testScope: TestScope
|
||||
private lateinit var repository: FakeKeyguardRepository
|
||||
private lateinit var registry: FakeKeyguardQuickAffordanceRegistry
|
||||
private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig
|
||||
@@ -123,7 +126,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
whenever(userTracker.userHandle).thenReturn(mock())
|
||||
whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
|
||||
.thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
|
||||
val scope = CoroutineScope(IMMEDIATE)
|
||||
val testDispatcher = StandardTestDispatcher()
|
||||
testScope = TestScope(testDispatcher)
|
||||
val localUserSelectionManager =
|
||||
KeyguardQuickAffordanceLocalUserSelectionManager(
|
||||
context = context,
|
||||
@@ -143,7 +147,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
)
|
||||
val remoteUserSelectionManager =
|
||||
KeyguardQuickAffordanceRemoteUserSelectionManager(
|
||||
scope = scope,
|
||||
scope = testScope.backgroundScope,
|
||||
userTracker = userTracker,
|
||||
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
|
||||
userHandle = UserHandle.SYSTEM,
|
||||
@@ -151,14 +155,14 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
appContext = context,
|
||||
scope = scope,
|
||||
scope = testScope.backgroundScope,
|
||||
localUserSelectionManager = localUserSelectionManager,
|
||||
remoteUserSelectionManager = remoteUserSelectionManager,
|
||||
userTracker = userTracker,
|
||||
legacySettingSyncer =
|
||||
KeyguardQuickAffordanceLegacySettingSyncer(
|
||||
scope = scope,
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
scope = testScope.backgroundScope,
|
||||
backgroundDispatcher = testDispatcher,
|
||||
secureSettings = FakeSettings(),
|
||||
selectionsManager = localUserSelectionManager,
|
||||
),
|
||||
@@ -194,366 +198,394 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startButton - present - visible model - starts activity on click`() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
fun `startButton - present - visible model - starts activity on click`() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val latest = collectLastValue(underTest.startButton)
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
isActivated = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `endButton - present - visible model - do nothing on click`() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.endButton.onEach { latest = it }.launchIn(this)
|
||||
|
||||
val config =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = null, // This will cause it to tell the system that the click was handled.
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig = config,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = config,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startButton - not present - model is hidden`() = runBlockingTest {
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
|
||||
val config =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = config,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = config,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateButtonReveal() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
val values = mutableListOf<Boolean>()
|
||||
val job = underTest.startButton.onEach { values.add(it.animateReveal) }.launchIn(this)
|
||||
|
||||
repository.setAnimateDozingTransitions(true)
|
||||
yield()
|
||||
repository.setAnimateDozingTransitions(false)
|
||||
yield()
|
||||
|
||||
// Note the extra false value in the beginning. This is to cover for the initial value
|
||||
// inserted by the quick affordance interactor which it does to cover for config
|
||||
// implementations that don't emit an initial value.
|
||||
assertThat(values).isEqualTo(listOf(false, false, true, false))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isOverlayContainerVisible() = runBlockingTest {
|
||||
val values = mutableListOf<Boolean>()
|
||||
val job = underTest.isOverlayContainerVisible.onEach(values::add).launchIn(this)
|
||||
|
||||
repository.setDozing(true)
|
||||
repository.setDozing(false)
|
||||
|
||||
assertThat(values).isEqualTo(listOf(true, false, true))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alpha() = runBlockingTest {
|
||||
val values = mutableListOf<Float>()
|
||||
val job = underTest.alpha.onEach(values::add).launchIn(this)
|
||||
|
||||
repository.setBottomAreaAlpha(0.1f)
|
||||
repository.setBottomAreaAlpha(0.5f)
|
||||
repository.setBottomAreaAlpha(0.2f)
|
||||
repository.setBottomAreaAlpha(0f)
|
||||
|
||||
assertThat(values).isEqualTo(listOf(1f, 0.1f, 0.5f, 0.2f, 0f))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isIndicationAreaPadded() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val values = mutableListOf<Boolean>()
|
||||
val job = underTest.isIndicationAreaPadded.onEach(values::add).launchIn(this)
|
||||
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig =
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
isActivated = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = true,
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
)
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig =
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startButton - in preview mode - visible even when keyguard not showing`() =
|
||||
testScope.runTest {
|
||||
underTest.enablePreviewMode(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
|
||||
repository.setKeyguardShowing(false)
|
||||
val latest = collectLastValue(underTest.startButton)
|
||||
|
||||
val icon: Icon = mock()
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
isActivated = true,
|
||||
icon = icon,
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
),
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = false,
|
||||
isActivated = true,
|
||||
icon = icon,
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
),
|
||||
configKey = configKey,
|
||||
)
|
||||
assertThat(latest()?.isSelected).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `endButton - present - visible model - do nothing on click`() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val latest = collectLastValue(underTest.endButton)
|
||||
|
||||
val config =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent =
|
||||
null, // This will cause it to tell the system that the click was handled.
|
||||
)
|
||||
)
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig =
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig = config,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig = config,
|
||||
configKey = configKey,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startButton - not present - model is hidden`() =
|
||||
testScope.runTest {
|
||||
val latest = collectLastValue(underTest.startButton)
|
||||
|
||||
val config =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
)
|
||||
)
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig =
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = config,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig = config,
|
||||
configKey = configKey,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateButtonReveal() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
)
|
||||
|
||||
assertThat(values)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
// Initially, no button is visible so the indication area is not padded.
|
||||
false,
|
||||
// Once we add the first visible button, the indication area becomes padded.
|
||||
// This
|
||||
// continues to be true after we add the second visible button and even after we
|
||||
// make the first button not visible anymore.
|
||||
true,
|
||||
// Once both buttons are not visible, the indication area is, again, not padded.
|
||||
false,
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
val value = collectLastValue(underTest.startButton.map { it.animateReveal })
|
||||
|
||||
assertThat(value()).isFalse()
|
||||
repository.setAnimateDozingTransitions(true)
|
||||
assertThat(value()).isTrue()
|
||||
repository.setAnimateDozingTransitions(false)
|
||||
assertThat(value()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isOverlayContainerVisible() =
|
||||
testScope.runTest {
|
||||
val value = collectLastValue(underTest.isOverlayContainerVisible)
|
||||
|
||||
assertThat(value()).isTrue()
|
||||
repository.setDozing(true)
|
||||
assertThat(value()).isFalse()
|
||||
repository.setDozing(false)
|
||||
assertThat(value()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alpha() =
|
||||
testScope.runTest {
|
||||
val value = collectLastValue(underTest.alpha)
|
||||
|
||||
assertThat(value()).isEqualTo(1f)
|
||||
repository.setBottomAreaAlpha(0.1f)
|
||||
assertThat(value()).isEqualTo(0.1f)
|
||||
repository.setBottomAreaAlpha(0.5f)
|
||||
assertThat(value()).isEqualTo(0.5f)
|
||||
repository.setBottomAreaAlpha(0.2f)
|
||||
assertThat(value()).isEqualTo(0.2f)
|
||||
repository.setBottomAreaAlpha(0f)
|
||||
assertThat(value()).isEqualTo(0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `alpha - in preview mode - does not change`() =
|
||||
testScope.runTest {
|
||||
underTest.enablePreviewMode(null)
|
||||
val value = collectLastValue(underTest.alpha)
|
||||
|
||||
assertThat(value()).isEqualTo(1f)
|
||||
repository.setBottomAreaAlpha(0.1f)
|
||||
assertThat(value()).isEqualTo(1f)
|
||||
repository.setBottomAreaAlpha(0.5f)
|
||||
assertThat(value()).isEqualTo(1f)
|
||||
repository.setBottomAreaAlpha(0.2f)
|
||||
assertThat(value()).isEqualTo(1f)
|
||||
repository.setBottomAreaAlpha(0f)
|
||||
assertThat(value()).isEqualTo(1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isIndicationAreaPadded() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val value = collectLastValue(underTest.isIndicationAreaPadded)
|
||||
|
||||
assertThat(value()).isFalse()
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = true,
|
||||
)
|
||||
)
|
||||
assertThat(value()).isTrue()
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
)
|
||||
)
|
||||
assertThat(value()).isTrue()
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
)
|
||||
)
|
||||
assertThat(value()).isTrue()
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
)
|
||||
)
|
||||
assertThat(value()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indicationAreaTranslationX() =
|
||||
testScope.runTest {
|
||||
val value = collectLastValue(underTest.indicationAreaTranslationX)
|
||||
|
||||
assertThat(value()).isEqualTo(0f)
|
||||
repository.setClockPosition(100, 100)
|
||||
assertThat(value()).isEqualTo(100f)
|
||||
repository.setClockPosition(200, 100)
|
||||
assertThat(value()).isEqualTo(200f)
|
||||
repository.setClockPosition(200, 200)
|
||||
assertThat(value()).isEqualTo(200f)
|
||||
repository.setClockPosition(300, 100)
|
||||
assertThat(value()).isEqualTo(300f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indicationAreaTranslationY() =
|
||||
testScope.runTest {
|
||||
val value =
|
||||
collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
|
||||
|
||||
// Negative 0 - apparently there's a difference in floating point arithmetic - FML
|
||||
assertThat(value()).isEqualTo(-0f)
|
||||
val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
|
||||
assertThat(value()).isEqualTo(expected1)
|
||||
val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
|
||||
assertThat(value()).isEqualTo(expected2)
|
||||
val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
|
||||
assertThat(value()).isEqualTo(expected3)
|
||||
val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
|
||||
assertThat(value()).isEqualTo(expected4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isClickable - true when alpha at threshold`() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
repository.setBottomAreaAlpha(
|
||||
KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
|
||||
)
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indicationAreaTranslationX() = runBlockingTest {
|
||||
val values = mutableListOf<Float>()
|
||||
val job = underTest.indicationAreaTranslationX.onEach(values::add).launchIn(this)
|
||||
val latest = collectLastValue(underTest.startButton)
|
||||
|
||||
repository.setClockPosition(100, 100)
|
||||
repository.setClockPosition(200, 100)
|
||||
repository.setClockPosition(200, 200)
|
||||
repository.setClockPosition(300, 100)
|
||||
|
||||
assertThat(values).isEqualTo(listOf(0f, 100f, 200f, 300f))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indicationAreaTranslationY() = runBlockingTest {
|
||||
val values = mutableListOf<Float>()
|
||||
val job =
|
||||
underTest
|
||||
.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET)
|
||||
.onEach(values::add)
|
||||
.launchIn(this)
|
||||
|
||||
val expectedTranslationValues =
|
||||
listOf(
|
||||
-0f, // Negative 0 - apparently there's a difference in floating point arithmetic -
|
||||
// FML
|
||||
setDozeAmountAndCalculateExpectedTranslationY(0.1f),
|
||||
setDozeAmountAndCalculateExpectedTranslationY(0.2f),
|
||||
setDozeAmountAndCalculateExpectedTranslationY(0.5f),
|
||||
setDozeAmountAndCalculateExpectedTranslationY(1f),
|
||||
)
|
||||
|
||||
assertThat(values).isEqualTo(expectedTranslationValues)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isClickable - true when alpha at threshold`() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
repository.setBottomAreaAlpha(
|
||||
KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
|
||||
)
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
// The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't
|
||||
// produce an initial value. We yield to give the coroutine time to emit the first real
|
||||
// value from our config.
|
||||
yield()
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isClickable - true when alpha above threshold`() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
repository.setBottomAreaAlpha(
|
||||
min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
|
||||
)
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
fun `isClickable - true when alpha above threshold`() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val latest = collectLastValue(underTest.startButton)
|
||||
repository.setBottomAreaAlpha(
|
||||
min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isClickable - false when alpha below threshold`() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
repository.setBottomAreaAlpha(
|
||||
max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
|
||||
)
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = false,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
fun `isClickable - false when alpha below threshold`() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val latest = collectLastValue(underTest.startButton)
|
||||
repository.setBottomAreaAlpha(
|
||||
max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = false,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isClickable - false when alpha at zero`() = runBlockingTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
repository.setBottomAreaAlpha(0f)
|
||||
fun `isClickable - false when alpha at zero`() =
|
||||
testScope.runTest {
|
||||
repository.setKeyguardShowing(true)
|
||||
val latest = collectLastValue(underTest.startButton)
|
||||
repository.setBottomAreaAlpha(0f)
|
||||
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = false,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
isClickable = false,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest(),
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
}
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
|
||||
private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
|
||||
repository.setDozeAmount(dozeAmount)
|
||||
return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
|
||||
}
|
||||
@@ -583,7 +615,6 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
when (testConfig.isActivated) {
|
||||
true -> ActivationState.Active
|
||||
false -> ActivationState.Inactive
|
||||
null -> ActivationState.NotSupported
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -636,6 +667,5 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
companion object {
|
||||
private const val DEFAULT_BURN_IN_OFFSET = 5
|
||||
private const val RETURNED_BURN_IN_OFFSET = 3
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user