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:
Ale Nijamkin
2022-12-14 21:15:39 +00:00
committed by Android (Google) Code Review
19 changed files with 1315 additions and 581 deletions

View File

@@ -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"
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -2740,6 +2740,12 @@
-->
<string name="keyguard_affordance_enablement_dialog_home_instruction_2">&#8226; 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] -->

View File

@@ -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));
}
}
}

View File

@@ -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!")

View File

@@ -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"
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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()
}

View File

@@ -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,

View File

@@ -1328,7 +1328,9 @@ public final class NotificationPanelViewController implements Dumpable {
mKeyguardBottomArea.init(
mKeyguardBottomAreaViewModel,
mFalsingManager,
mLockIconViewController
mLockIconViewController,
stringResourceId ->
mKeyguardIndicationController.showTransientIndication(stringResourceId)
);
}

View File

@@ -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()
)
}
}

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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
}
}