diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
new file mode 100644
index 0000000000000..18e8a962dc70e
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
@@ -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"
+}
diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
index 41123c84ded13..18fcebbb65a00 100644
--- a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
+++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
@@ -16,13 +16,53 @@
* limitations under the License.
*/
-->
-
-
-
-
-
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+
+ -
+
+
-
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 40423cd9ac2c8..62babadc45d83 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -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));
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
index 4ae37c51f278f..cbcede0237089 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
@@ -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!")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 748c6e8b75b9d..0ec2a287732ca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -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 {
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 {
+ 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"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
index a18b036c51897..2581b595d8121 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
@@ -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
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index cbe512ff83ba6..348564601c935 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -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(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
new file mode 100644
index 0000000000000..a5ae8ba58d459
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -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()
+ 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"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt
new file mode 100644
index 0000000000000..be1d3a18520a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt
@@ -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
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
new file mode 100644
index 0000000000000..50722d5c68f8c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
@@ -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 =
+ ArrayMap()
+
+ 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"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 227796f43e350..780728448e18c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -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 =
keyguardInteractor.isDozing.map { !it }.distinctUntilChanged()
/** An observable for the alpha level for the entire bottom area. */
- val alpha: Flow = bottomAreaInteractor.alpha.distinctUntilChanged()
+ val alpha: Flow =
+ isInPreviewMode.flatMapLatest { isInPreviewMode ->
+ if (isInPreviewMode) {
+ flowOf(1f)
+ } else {
+ bottomAreaInteractor.alpha.distinctUntilChanged()
+ }
+ }
/** An observable for whether the indication area should be padded. */
val isIndicationAreaPadded: Flow =
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 {
- 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()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index 44f48f97b62e2..e47559c671cd7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -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,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
index 78b28d203629a..90d2ef38448ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -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(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()
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
index cef452b8ec220..09c8e6ac12686 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
@@ -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"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 11fe905b1d1f1..d97571bcd8ef8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -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
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 83a5d0e90c842..0abff88b5faf3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -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()
- 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()
- 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()
- 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()
- 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()
- 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()
- 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
}
}