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/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c10e75289cf2c..35ada47d9586b 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -758,6 +758,8 @@ 48dp 48dp 24dp + + 8dp 32dp 32dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 643f831c7f94e..9d5a8c4d3cc71 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2740,6 +2740,12 @@ --> • At least one device is available + + Press and hold to activate + Cancel 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..57668c795d1c9 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 @@ -62,12 +61,20 @@ constructor( private val isUsingRepository: Boolean get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** + * Whether the UI should use the long press gesture to activate quick affordances. + * + * If `false`, the UI goes back to using single taps. + */ + val useLongPress: Boolean + get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** Returns an observable for the quick affordance at the given position. */ fun quickAffordance( position: KeyguardQuickAffordancePosition ): Flow { return combine( - quickAffordanceInternal(position), + quickAffordanceAlwaysVisible(position), keyguardInteractor.isDozing, keyguardInteractor.isKeyguardShowing, ) { affordance, isDozing, isKeyguardShowing -> @@ -79,6 +86,19 @@ constructor( } } + /** + * Returns an observable for the quick affordance at the given position but always visible, + * regardless of lock screen state. + * + * This is useful for experiences like the lock screen preview mode, where the affordances must + * always be visible. + */ + fun quickAffordanceAlwaysVisible( + position: KeyguardQuickAffordancePosition, + ): Flow { + return quickAffordanceInternal(position) + } + /** * Notifies that a quick affordance has been "triggered" (clicked) by the user. * @@ -290,15 +310,6 @@ constructor( } } - private fun KeyguardQuickAffordancePosition.toSlotId(): String { - return when (this) { - KeyguardQuickAffordancePosition.BOTTOM_START -> - KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START - KeyguardQuickAffordancePosition.BOTTOM_END -> - KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END - } - } - private fun String.encode(slotId: String): String { return "$slotId$DELIMITER$this" } 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..ae8edfece4cb8 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 @@ -16,14 +16,19 @@ package com.android.systemui.keyguard.ui.binder +import android.annotation.SuppressLint import android.graphics.drawable.Animatable2 import android.util.Size import android.util.TypedValue +import android.view.MotionEvent import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.widget.ImageView import android.widget.TextView +import androidx.core.animation.CycleInterpolator +import androidx.core.animation.ObjectAnimator import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle @@ -38,8 +43,10 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager +import kotlin.math.pow +import kotlin.math.sqrt +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -52,6 +59,7 @@ import kotlinx.coroutines.launch * view-binding, binding each view only once. It is okay and expected for the same instance of the * view-model to be reused for multiple view/view-binder bindings. */ +@OptIn(ExperimentalCoroutinesApi::class) object KeyguardBottomAreaViewBinder { private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L @@ -84,7 +92,8 @@ object KeyguardBottomAreaViewBinder { fun bind( view: ViewGroup, viewModel: KeyguardBottomAreaViewModel, - falsingManager: FalsingManager, + falsingManager: FalsingManager?, + messageDisplayer: (Int) -> Unit, ): Binding { val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area) val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container) @@ -108,6 +117,7 @@ object KeyguardBottomAreaViewBinder { view = startButton, viewModel = buttonModel, falsingManager = falsingManager, + messageDisplayer = messageDisplayer, ) } } @@ -118,6 +128,7 @@ object KeyguardBottomAreaViewBinder { view = endButton, viewModel = buttonModel, falsingManager = falsingManager, + messageDisplayer = messageDisplayer, ) } } @@ -222,10 +233,12 @@ object KeyguardBottomAreaViewBinder { } } + @SuppressLint("ClickableViewAccessibility") private fun updateButton( view: ImageView, viewModel: KeyguardQuickAffordanceViewModel, - falsingManager: FalsingManager, + falsingManager: FalsingManager?, + messageDisplayer: (Int) -> Unit, ) { if (!viewModel.isVisible) { view.isVisible = false @@ -281,21 +294,126 @@ object KeyguardBottomAreaViewBinder { }, ) ) + view.backgroundTintList = - Utils.getColorAttr( - view.context, - if (viewModel.isActivated) { - com.android.internal.R.attr.colorAccentPrimary - } else { - com.android.internal.R.attr.colorSurface - } - ) + if (!viewModel.isSelected) { + Utils.getColorAttr( + view.context, + if (viewModel.isActivated) { + com.android.internal.R.attr.colorAccentPrimary + } else { + com.android.internal.R.attr.colorSurface + } + ) + } else { + null + } view.isClickable = viewModel.isClickable if (viewModel.isClickable) { - view.setOnClickListener(OnClickListener(viewModel, falsingManager)) + if (viewModel.useLongPress) { + view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer)) + } else { + view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) + } } else { view.setOnClickListener(null) + view.setOnTouchListener(null) + } + + view.isSelected = viewModel.isSelected + } + + private class OnTouchListener( + private val view: View, + private val viewModel: KeyguardQuickAffordanceViewModel, + private val messageDisplayer: (Int) -> Unit, + ) : View.OnTouchListener { + + private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() + private var longPressAnimator: ViewPropertyAnimator? = null + private var downTimestamp = 0L + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + return when (event?.actionMasked) { + MotionEvent.ACTION_DOWN -> + if (viewModel.configKey != null) { + downTimestamp = System.currentTimeMillis() + longPressAnimator = + view + .animate() + .scaleX(PRESSED_SCALE) + .scaleY(PRESSED_SCALE) + .setDuration(longPressDurationMs) + .withEndAction { + view.setOnClickListener { + viewModel.onClicked( + KeyguardQuickAffordanceViewModel.OnClickedParameters( + configKey = viewModel.configKey, + expandable = Expandable.fromView(view), + ) + ) + } + view.performClick() + view.setOnClickListener(null) + } + true + } else { + false + } + MotionEvent.ACTION_MOVE -> { + if (event.historySize > 0) { + val distance = + sqrt( + (event.y - event.getHistoricalY(0)).pow(2) + + (event.x - event.getHistoricalX(0)).pow(2) + ) + if (distance > ViewConfiguration.getTouchSlop()) { + cancel() + } + } + true + } + MotionEvent.ACTION_UP -> { + if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { + messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short) + val shakeAnimator = + ObjectAnimator.ofFloat( + view, + "translationX", + 0f, + view.context.resources + .getDimensionPixelSize( + R.dimen.keyguard_affordance_shake_amplitude + ) + .toFloat(), + 0f, + ) + shakeAnimator.duration = 300 + shakeAnimator.interpolator = CycleInterpolator(5f) + shakeAnimator.start() + } + cancel() + true + } + MotionEvent.ACTION_CANCEL -> { + cancel() + true + } + else -> false + } + } + + private fun cancel() { + downTimestamp = 0L + longPressAnimator?.cancel() + longPressAnimator = null + view.animate().scaleX(1f).scaleY(1f) + } + + companion object { + private const val PRESSED_SCALE = 1.5f } } 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..5d85680efcf46 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,8 @@ constructor( }, isClickable = isClickable, isActivated = activationState is ActivationState.Active, + isSelected = isSelected, + useLongPress = quickAffordanceInteractor.useLongPress, ) 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..cf3a6daa40bb6 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,8 @@ data class KeyguardQuickAffordanceViewModel( val onClicked: (OnClickedParameters) -> Unit = {}, val isClickable: Boolean = false, val isActivated: Boolean = false, + val isSelected: Boolean = false, + val useLongPress: Boolean = false, ) { data class OnClickedParameters( val configKey: String, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 6ad0b9acb990a..6d6427acec4c4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1328,7 +1328,9 @@ public final class NotificationPanelViewController implements Dumpable { mKeyguardBottomArea.init( mKeyguardBottomAreaViewModel, mFalsingManager, - mLockIconViewController + mLockIconViewController, + stringResourceId -> + mKeyguardIndicationController.showTransientIndication(stringResourceId) ); } 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..2ce116394236b 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,7 @@ import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.view.WindowInsets import android.widget.FrameLayout -import com.android.keyguard.KeyguardUpdateMonitor +import androidx.annotation.StringRes import com.android.keyguard.LockIconViewController import com.android.systemui.R import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder @@ -51,21 +51,29 @@ constructor( defStyleRes, ) { + interface MessageDisplayer { + fun display(@StringRes stringResourceId: Int) + } + private var ambientIndicationArea: View? = null private lateinit var binding: KeyguardBottomAreaViewBinder.Binding - private lateinit var lockIconViewController: LockIconViewController + private var lockIconViewController: LockIconViewController? = null /** Initializes the view. */ fun init( viewModel: KeyguardBottomAreaViewModel, - falsingManager: FalsingManager, - lockIconViewController: LockIconViewController, + falsingManager: FalsingManager? = null, + lockIconViewController: LockIconViewController? = null, + messageDisplayer: MessageDisplayer? = null, ) { - binding = bind( + binding = + bind( this, viewModel, falsingManager, - ) + ) { + messageDisplayer?.display(it) + } this.lockIconViewController = lockIconViewController } @@ -129,21 +137,21 @@ constructor( findViewById(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 } }