Lock screen preview in quick affordance picker.
ThemePicker parts.
- Hooks up the fragment to use the new lock screen preview binder
- Updates the UI such that the fragment layout is stable (the
RecyclerViews for slots and affordance items don't change in height,
allowing the preview view to have a stable size)
Note that this is adding the new lock screen preview to the quick
affordance screen only. This is where we'll do the majority of the
development for now and then move the reusable parts to the right screen
once the rest of the team works on that.
Bug: 261362750
Test: manual verification that the lock screen preview is showing up on
the quick affordance picker screen.
Change-Id: I6cd744053180fe706a344eb10e80f1231106f081
diff --git a/res/layout/fragment_lock_screen_quick_affordances.xml b/res/layout/fragment_lock_screen_quick_affordances.xml
index 9927e6a..331d501 100644
--- a/res/layout/fragment_lock_screen_quick_affordances.xml
+++ b/res/layout/fragment_lock_screen_quick_affordances.xml
@@ -15,8 +15,7 @@
~
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -30,11 +29,21 @@
</FrameLayout>
- <!-- TODO(b/254858701): plug in the preview here. -->
- <View
+ <com.android.wallpaper.picker.DisplayAspectRatioFrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_weight="1" />
+ android:layout_weight="1"
+ android:paddingTop="36dp"
+ android:paddingBottom="40dp">
+
+ <include
+ android:id="@+id/preview"
+ layout="@layout/wallpaper_preview_card"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"/>
+
+ </com.android.wallpaper.picker.DisplayAspectRatioFrameLayout>
<LinearLayout
android:layout_width="match_parent"
@@ -46,23 +55,66 @@
android:paddingTop="22dp"
android:paddingBottom="62dp">
- <androidx.recyclerview.widget.RecyclerView
- android:id="@id/slot_tabs"
+ <FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:clipToPadding="false"
- android:paddingHorizontal="16dp" />
+ >
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@id/slot_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:paddingHorizontal="16dp" />
+
+ <!--
+ This is just an invisible placeholder put in place so that the parent keeps its height
+ stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
+ layout logic to keep the size of the preview container stable as well, which bodes well
+ for setting up the SurfaceView for remote rendering without changing its size after the
+ content is loaded into the RecyclerView.
+
+ It's critical for any TextViews inside the included layout to have text.
+ -->
+ <include
+ layout="@layout/keyguard_quick_affordance_slot_tab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible" />
+
+ </FrameLayout>
<View
android:layout_width="0dp"
android:layout_height="22dp" />
- <androidx.recyclerview.widget.RecyclerView
- android:id="@id/affordances"
+ <FrameLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:clipToPadding="false"
- android:paddingHorizontal="16dp" />
+ android:layout_height="wrap_content">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@id/affordances"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:paddingHorizontal="16dp" />
+
+ <!--
+ This is just an invisible placeholder put in place so that the parent keeps its height
+ stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
+ layout logic to keep the size of the preview container stable as well, which bodes well
+ for setting up the SurfaceView for remote rendering without changing its size after the
+ content is loaded into the RecyclerView.
+
+ It's critical for any TextViews inside the included layout to have text.
+ -->
+ <include
+ layout="@layout/keyguard_quick_affordance"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible" />
+
+ </FrameLayout>
</LinearLayout>
diff --git a/res/layout/keyguard_quick_affordance.xml b/res/layout/keyguard_quick_affordance.xml
index 90ba68e..b3b6893 100644
--- a/res/layout/keyguard_quick_affordance.xml
+++ b/res/layout/keyguard_quick_affordance.xml
@@ -18,7 +18,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="@dimen/keyguard_quick_affordance_picker_item_width"
android:layout_height="wrap_content"
android:orientation="vertical"
@@ -48,6 +48,8 @@
android:layout_height="wrap_content"
android:textColor="@color/text_color_primary"
android:singleLine="true"
- android:ellipsize="end"/>
+ android:ellipsize="end"
+ android:text="Placeholder for stable size calculation, please do not remove."
+ tools:ignore="HardcodedText" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/keyguard_quick_affordance_slot_tab.xml b/res/layout/keyguard_quick_affordance_slot_tab.xml
index ba233cd..c4934d8 100644
--- a/res/layout/keyguard_quick_affordance_slot_tab.xml
+++ b/res/layout/keyguard_quick_affordance_slot_tab.xml
@@ -17,6 +17,7 @@
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -26,4 +27,6 @@
android:minWidth="48dp"
android:minHeight="48dp"
android:gravity="center"
- android:background="@drawable/keyguard_quick_affordance_slot_tab_background" />
+ android:background="@drawable/keyguard_quick_affordance_slot_tab_background"
+ android:text="Placeholder for stable size calculation, please do not remove."
+ tools:ignore="HardcodedText" />
diff --git a/src/com/android/customization/picker/grid/GridOptionPreviewer.java b/src/com/android/customization/picker/grid/GridOptionPreviewer.java
index 5cf327e..7786d35 100644
--- a/src/com/android/customization/picker/grid/GridOptionPreviewer.java
+++ b/src/com/android/customization/picker/grid/GridOptionPreviewer.java
@@ -22,6 +22,7 @@
import com.android.customization.model.grid.GridOption;
import com.android.customization.model.grid.GridOptionsManager;
+import com.android.wallpaper.R;
import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback;
import com.android.wallpaper.util.PreviewUtils;
import com.android.wallpaper.util.SurfaceViewUtils;
@@ -80,7 +81,10 @@
private class GridOptionSurfaceHolderCallback extends WorkspaceSurfaceHolderCallback {
private GridOptionSurfaceHolderCallback(SurfaceView workspaceSurface, Context context) {
- super(workspaceSurface, context);
+ super(
+ workspaceSurface,
+ new PreviewUtils(
+ context, context.getString(R.string.grid_control_metadata_name)));
}
@Override
diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
deleted file mode 100644
index 13ee553..0000000
--- a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.customization.picker.quickaffordance.ui.binder
-
-import android.view.View
-import android.widget.ImageView
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
-import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
-import com.android.wallpaper.R
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.launch
-
-object KeyguardQuickAffordancePickerPreviewBinder {
-
- /** Binds view with view-model for a lock screen quick affordance preview experience. */
- @JvmStatic
- fun bind(
- view: View,
- viewModel: KeyguardQuickAffordancePickerViewModel,
- lifecycleOwner: LifecycleOwner,
- ) {
- val startView: ImageView = view.requireViewById(R.id.start_affordance)
- val endView: ImageView = view.requireViewById(R.id.end_affordance)
- lifecycleOwner.lifecycleScope.launch {
- lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
- updateView(
- view = startView,
- viewModel = viewModel,
- slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
- )
- }
-
- launch {
- updateView(
- view = endView,
- viewModel = viewModel,
- slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
- )
- }
- }
- }
- }
-
- private suspend fun updateView(
- view: ImageView,
- viewModel: KeyguardQuickAffordancePickerViewModel,
- slotId: String,
- ) {
- viewModel.slots
- .mapNotNull { slotById -> slotById[slotId] }
- .map { slot -> slot.selectedQuickAffordances.firstOrNull() }
- .collect { affordance ->
- view.setImageDrawable(affordance?.icon)
- view.contentDescription = affordance?.contentDescription
- }
- }
-}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePreviewBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePreviewBinder.kt
new file mode 100644
index 0000000..0af6bf4
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePreviewBinder.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.customization.picker.quickaffordance.ui.binder
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.service.wallpaper.WallpaperService
+import android.view.SurfaceView
+import androidx.cardview.widget.CardView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
+import com.android.wallpaper.R
+import com.android.wallpaper.asset.Asset
+import com.android.wallpaper.asset.BitmapCachingAsset
+import com.android.wallpaper.model.LiveWallpaperInfo
+import com.android.wallpaper.model.WallpaperInfo
+import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback
+import com.android.wallpaper.util.PreviewUtils
+import com.android.wallpaper.util.ResourceUtils
+import com.android.wallpaper.util.WallpaperConnection
+import com.android.wallpaper.util.WallpaperSurfaceCallback
+import java.util.concurrent.CompletableFuture
+import kotlinx.coroutines.launch
+
+object KeyguardQuickAffordancePreviewBinder {
+
+ /** Binds view for the preview of the lock screen. */
+ @JvmStatic
+ fun bind(
+ activity: Activity,
+ previewView: CardView,
+ viewModel: KeyguardQuickAffordancePickerViewModel,
+ lifecycleOwner: LifecycleOwner,
+ wallpaperInfoProvider: suspend () -> WallpaperInfo?,
+ ) {
+ val workspaceSurface: SurfaceView = previewView.requireViewById(R.id.workspace_surface)
+ val wallpaperSurface: SurfaceView = previewView.requireViewById(R.id.wallpaper_surface)
+
+ previewView.radius =
+ previewView.resources.getDimension(R.dimen.wallpaper_picker_entry_card_corner_radius)
+ previewView.contentDescription =
+ previewView.context.getString(
+ R.string.lockscreen_wallpaper_preview_card_content_description
+ )
+
+ var previewSurfaceCallback: WorkspaceSurfaceHolderCallback? = null
+ var wallpaperSurfaceCallback: WallpaperSurfaceCallback? = null
+ var wallpaperConnection: WallpaperConnection? = null
+ var wallpaperInfo: WallpaperInfo? = null
+
+ lifecycleOwner.lifecycle.addObserver(
+ LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> {
+ previewSurfaceCallback =
+ WorkspaceSurfaceHolderCallback(
+ workspaceSurface,
+ PreviewUtils(
+ context = previewView.context,
+ authority =
+ previewView.context.getString(
+ R.string.lock_screen_preview_provider_authority
+ ),
+ ),
+ Bundle().apply {
+ putString(
+ KeyguardQuickAffordancePreviewConstants
+ .KEY_INITIALLY_SELECTED_SLOT_ID,
+ viewModel.selectedSlotId.value,
+ )
+ },
+ )
+ workspaceSurface.holder.addCallback(previewSurfaceCallback)
+ workspaceSurface.setZOrderMediaOverlay(true)
+
+ wallpaperSurfaceCallback =
+ WallpaperSurfaceCallback(
+ previewView.context,
+ previewView,
+ wallpaperSurface,
+ CompletableFuture.completedFuture(
+ WallpaperInfo.ColorInfo(
+ /* wallpaperColors= */ null,
+ ResourceUtils.getColorAttr(
+ previewView.context,
+ android.R.attr.colorSecondary,
+ )
+ )
+ ),
+ ) {
+ maybeLoadThumbnail(
+ activity = activity,
+ wallpaperInfo = wallpaperInfo,
+ surfaceCallback = wallpaperSurfaceCallback,
+ )
+ }
+ wallpaperSurface.holder.addCallback(wallpaperSurfaceCallback)
+ wallpaperSurface.setZOrderMediaOverlay(true)
+ }
+ Lifecycle.Event.ON_DESTROY -> {
+ workspaceSurface.holder.removeCallback(previewSurfaceCallback)
+ previewSurfaceCallback?.cleanUp()
+ wallpaperSurface.holder.removeCallback(wallpaperSurfaceCallback)
+ wallpaperSurfaceCallback?.cleanUp()
+ }
+ Lifecycle.Event.ON_RESUME -> {
+ lifecycleOwner.lifecycleScope.launch {
+ wallpaperInfo = wallpaperInfoProvider()
+ (wallpaperInfo as? LiveWallpaperInfo)?.let { liveWallpaperInfo ->
+ if (WallpaperConnection.isPreviewAvailable()) {
+ wallpaperConnection =
+ WallpaperConnection(
+ Intent(WallpaperService.SERVICE_INTERFACE).apply {
+ setClassName(
+ liveWallpaperInfo.wallpaperComponent
+ .packageName,
+ liveWallpaperInfo.wallpaperComponent.serviceName
+ )
+ },
+ previewView.context,
+ null,
+ wallpaperSurface,
+ null,
+ )
+
+ wallpaperConnection?.connect()
+ wallpaperConnection?.setVisibility(true)
+ }
+ }
+ maybeLoadThumbnail(
+ activity = activity,
+ wallpaperInfo = wallpaperInfo,
+ surfaceCallback = wallpaperSurfaceCallback,
+ )
+ }
+ }
+ Lifecycle.Event.ON_PAUSE -> {
+ wallpaperConnection?.setVisibility(false)
+ }
+ Lifecycle.Event.ON_STOP -> {
+ wallpaperConnection?.disconnect()
+ }
+ else -> Unit
+ }
+ }
+ )
+
+ lifecycleOwner.lifecycleScope.launch {
+ viewModel.selectedSlotId
+ .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
+ .collect { slotId ->
+ previewSurfaceCallback?.send(
+ KeyguardQuickAffordancePreviewConstants.MESSAGE_ID_SLOT_SELECTED,
+ Bundle().apply {
+ putString(KeyguardQuickAffordancePreviewConstants.KEY_SLOT_ID, slotId)
+ },
+ )
+ }
+ }
+ }
+
+ private fun maybeLoadThumbnail(
+ activity: Activity,
+ wallpaperInfo: WallpaperInfo?,
+ surfaceCallback: WallpaperSurfaceCallback?,
+ ) {
+ if (wallpaperInfo == null || surfaceCallback == null) {
+ return
+ }
+
+ val imageView = surfaceCallback.homeImageWallpaper
+ val thumbAsset: Asset = BitmapCachingAsset(activity, wallpaperInfo.getThumbAsset(activity))
+ if (imageView != null && imageView.drawable == null) {
+ thumbAsset.loadPreviewImage(
+ activity,
+ imageView,
+ ResourceUtils.getColorAttr(activity, android.R.attr.colorSecondary)
+ )
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
index c99c6e8..251cdca 100644
--- a/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
@@ -25,10 +25,15 @@
import androidx.lifecycle.get
import com.android.customization.module.ThemePickerInjector
import com.android.customization.picker.quickaffordance.ui.binder.KeyguardQuickAffordancePickerBinder
+import com.android.customization.picker.quickaffordance.ui.binder.KeyguardQuickAffordancePreviewBinder
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
import com.android.wallpaper.R
import com.android.wallpaper.module.InjectorProvider
import com.android.wallpaper.picker.AppbarFragment
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.suspendCancellableCoroutine
+@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardQuickAffordancePickerFragment : AppbarFragment() {
companion object {
const val DESTINATION_ID = "quick_affordances"
@@ -51,14 +56,32 @@
)
setUpToolbar(view)
val injector = InjectorProvider.getInjector() as ThemePickerInjector
+ val wallpaperInfoFactory = injector.getCurrentWallpaperInfoFactory(requireContext())
+ val viewModel: KeyguardQuickAffordancePickerViewModel =
+ ViewModelProvider(
+ requireActivity(),
+ injector.getKeyguardQuickAffordancePickerViewModelFactory(requireContext()),
+ )
+ .get()
+ KeyguardQuickAffordancePreviewBinder.bind(
+ activity = requireActivity(),
+ previewView = view.requireViewById(R.id.preview),
+ viewModel = viewModel,
+ lifecycleOwner = this,
+ wallpaperInfoProvider = {
+ suspendCancellableCoroutine { continuation ->
+ wallpaperInfoFactory.createCurrentWallpaperInfos(
+ { homeWallpaper, lockWallpaper, _ ->
+ continuation.resume(lockWallpaper ?: homeWallpaper, null)
+ },
+ /* forceRefresh= */ true,
+ )
+ }
+ },
+ )
KeyguardQuickAffordancePickerBinder.bind(
view = view,
- viewModel =
- ViewModelProvider(
- requireActivity(),
- injector.getKeyguardQuickAffordancePickerViewModelFactory(requireContext()),
- )
- .get(),
+ viewModel = viewModel,
lifecycleOwner = this,
)
return view
diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
index 7ea776b..774ff22 100644
--- a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
@@ -32,6 +32,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
@@ -47,7 +48,8 @@
@SuppressLint("StaticFieldLeak") private val applicationContext = context.applicationContext
- private val selectedSlotId = MutableStateFlow<String?>(null)
+ private val _selectedSlotId = MutableStateFlow<String?>(null)
+ val selectedSlotId: StateFlow<String?> = _selectedSlotId.asStateFlow()
/** View-models for each slot, keyed by slot ID. */
val slots: Flow<Map<String, KeyguardQuickAffordanceSlotViewModel>> =
@@ -90,7 +92,7 @@
if (isSelected) {
null
} else {
- { this.selectedSlotId.value = slot.id }
+ { _selectedSlotId.tryEmit(slot.id) }
},
)
}