From 832d609613c487b182f3ce73227bd20832f9b834 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 8 Dec 2022 17:05:35 -0800 Subject: [PATCH] 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 --- ...KeyguardQuickAffordancePreviewConstants.kt | 24 + .../keyguard_bottom_affordance_bg.xml | 58 +- .../android/keyguard/KeyguardClockSwitch.java | 29 +- .../KeyguardQuickAffordanceProvider.kt | 20 + .../KeyguardQuickAffordanceInteractor.kt | 25 +- .../KeyguardQuickAffordancePosition.kt | 11 +- .../ui/binder/KeyguardBottomAreaViewBinder.kt | 30 +- .../ui/preview/KeyguardPreviewRenderer.kt | 207 ++++++ .../preview/KeyguardPreviewRendererFactory.kt | 26 + .../preview/KeyguardRemotePreviewManager.kt | 138 ++++ .../viewmodel/KeyguardBottomAreaViewModel.kt | 88 ++- .../KeyguardQuickAffordanceViewModel.kt | 1 + .../statusbar/phone/KeyguardBottomAreaView.kt | 34 +- .../KeyguardQuickAffordanceProviderTest.kt | 76 +- .../KeyguardQuickAffordanceInteractorTest.kt | 287 ++++--- .../KeyguardBottomAreaViewModelTest.kt | 700 +++++++++--------- 16 files changed, 1174 insertions(+), 580 deletions(-) create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt 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 } }