Lock screen preview.

Adds code on the system UI side that renders a preview of the lock
screen. This is done through the content provider, which receives a
handle to a SurfaceView from wallpaper picker, where the UI is actually
shown to the user.

There are several changes here, following the path of the code, they
are:
1. KeyguardQuickAffordanceProvider can handle the "call" from wallpaper
   picker and delegates the rendering to the preview system
2. The preview system starts with KeyguardRemotePreviewManager which is
   responsible for driving the lifecycle of the rendering as the
   connection to the remote SurfaceView is established or torn down
3. The preview system continues with the KeyguardPreviewRenderer which
   builds the view hierarchy and hooks up each view to its appropriate
   view-binder or view-controller
4. For quick affordances, we are making a couple of minor changes to the
   view-binder, view-model, and interactor to allow for them to run in
   "preview" mode where different rules apply: no clicks allowed, the
   affordances are ever-visible (as opposed to only when the lock screen
   is shown), etc.

Bug: 261362750
Test: unit tests created/updated, manually verified in the shortcuts
settings screen

Change-Id: I5d1c51dacbedf0a7a1fb3ec742244141a2fe8a32
This commit is contained in:
Alejandro Nijamkin
2022-12-08 17:05:35 -08:00
parent a5ad61f2bf
commit 832d609613
16 changed files with 1174 additions and 580 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

@@ -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
@@ -67,7 +66,7 @@ constructor(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceModel> {
return combine(
quickAffordanceInternal(position),
quickAffordanceAlwaysVisible(position),
keyguardInteractor.isDozing,
keyguardInteractor.isKeyguardShowing,
) { affordance, isDozing, isKeyguardShowing ->
@@ -79,6 +78,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 +302,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

@@ -39,7 +39,6 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewMod
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
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
@@ -84,7 +83,7 @@ object KeyguardBottomAreaViewBinder {
fun bind(
view: ViewGroup,
viewModel: KeyguardBottomAreaViewModel,
falsingManager: FalsingManager,
falsingManager: FalsingManager?,
): Binding {
val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
@@ -225,7 +224,7 @@ object KeyguardBottomAreaViewBinder {
private fun updateButton(
view: ImageView,
viewModel: KeyguardQuickAffordanceViewModel,
falsingManager: FalsingManager,
falsingManager: FalsingManager?,
) {
if (!viewModel.isVisible) {
view.isVisible = false
@@ -281,22 +280,29 @@ 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))
view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
} else {
view.setOnClickListener(null)
}
view.isSelected = viewModel.isSelected
}
private class OnClickListener(

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,7 @@ constructor(
},
isClickable = isClickable,
isActivated = activationState is ActivationState.Active,
isSelected = isSelected,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
}

View File

@@ -29,6 +29,7 @@ data class KeyguardQuickAffordanceViewModel(
val onClicked: (OnClickedParameters) -> Unit = {},
val isClickable: Boolean = false,
val isActivated: Boolean = false,
val isSelected: Boolean = false,
) {
data class OnClickedParameters(
val configKey: String,

View File

@@ -23,7 +23,6 @@ import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.view.WindowInsets
import android.widget.FrameLayout
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.LockIconViewController
import com.android.systemui.R
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
@@ -53,19 +52,20 @@ constructor(
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,
) {
binding = bind(
binding =
bind(
this,
viewModel,
falsingManager,
)
)
this.lockIconViewController = lockIconViewController
}
@@ -129,21 +129,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
}
}