Merge "[4/n] Add Apply Button to grid preview page" into udc-qpr-dev am: 8e191fd9a1 am: 210d17c227

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/ThemePicker/+/24435476

Change-Id: I43715e218a5e7c05136cecb8589be0ea050ebb86
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/res/layout/fragment_grid.xml b/res/layout/fragment_grid.xml
index 4f0aaef..8c97d45 100644
--- a/res/layout/fragment_grid.xml
+++ b/res/layout/fragment_grid.xml
@@ -88,6 +88,24 @@
 
         </FrameLayout>
 
+        <Button
+            android:id="@+id/apply_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:visibility="gone"
+            android:enabled="false"
+            style="@style/ActionPrimaryButton"
+            android:text="@string/apply_btn" />
+
+        <TextView
+            android:id="@+id/apply_button_note"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:textSize="@dimen/grid_apply_button_note_text_size"
+            android:visibility="gone"
+            android:text="@string/apply_grid_btn_note" />
     </LinearLayout>
 
 </LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 47171e5..aa6c477 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -61,6 +61,7 @@
     <!-- Dimensions for the grid options -->
     <dimen name="grid_options_container_bottom_margin">@dimen/bottom_actions_height</dimen>
     <dimen name="grid_options_container_horizontal_margin">24dp</dimen>
+    <dimen name="grid_apply_button_note_text_size">11sp</dimen>
 
     <dimen name="card_title_text_size">16sp</dimen>
     <dimen name="card_header_icon_size">32dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eb68dce..9b55965 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -178,6 +178,15 @@
     <!--Title for a grid option, describing the number of columns and rows, eg: 4x4 [CHAR LIMIT=10] -->
     <string name="grid_title_pattern"><xliff:g name="num_cols" example="1">%1$d</xliff:g>x<xliff:g name="num_rows" example="1">%2$d</xliff:g></string>
 
+    <!-- Label of what will happen when user tap on apply button to change grid. [CHAR LIMIT=50] -->
+    <string name="apply_grid_btn_note">Changing grid size will reload workspace and may take a few seconds.</string>
+
+    <!-- Toast of reloading workspace with new grid. -->
+    <string name="toast_of_changing_grid">Reloading workspace with %1$s grid</string>
+
+    <!-- Toast of failure to reload workspace with new grid. -->
+    <string name="toast_of_failure_to_change_grid">Failed to reload workspace with %1$s grid</string>
+
     <!-- Message shown when a theme has been applied successfully in the system [CHAR LIMIT=NONE] -->
     <string name="applied_theme_msg">Style set successfully</string>
 
diff --git a/src/com/android/customization/model/grid/GridOption.java b/src/com/android/customization/model/grid/GridOption.java
index 17ba115..a5307c9 100644
--- a/src/com/android/customization/model/grid/GridOption.java
+++ b/src/com/android/customization/model/grid/GridOption.java
@@ -48,9 +48,9 @@
         }
     };
 
-    private final String mTitle;
     private final String mIconShapePath;
     private final GridTileDrawable mTileDrawable;
+    public final String title;
     public final String name;
     public final int rows;
     public final int cols;
@@ -60,7 +60,7 @@
 
     public GridOption(String title, String name, boolean isCurrent, int rows, int cols,
             Uri previewImageUri, int previewPagesCount, String iconShapePath) {
-        mTitle = title;
+        this.title = title;
         mIsCurrent = isCurrent;
         mIconShapePath = iconShapePath;
         mTileDrawable = new GridTileDrawable(rows, cols, mIconShapePath);
@@ -76,7 +76,7 @@
     }
 
     protected GridOption(Parcel in) {
-        mTitle = in.readString();
+        title = in.readString();
         mIsCurrent = in.readByte() != 0;
         mIconShapePath = in.readString();
         name = in.readString();
@@ -89,7 +89,7 @@
 
     @Override
     public String getTitle() {
-        return mTitle;
+        return title;
     }
 
     @Override
@@ -143,7 +143,7 @@
 
     @Override
     public void writeToParcel(Parcel parcel, int i) {
-        parcel.writeString(mTitle);
+        parcel.writeString(title);
         parcel.writeByte((byte) (mIsCurrent ? 1 : 0));
         parcel.writeString(mIconShapePath);
         parcel.writeString(name);
@@ -158,7 +158,7 @@
         return String.format(
                 "GridOption{mTitle='%s', mIsCurrent=%s, mTileDrawable=%s, name='%s', rows=%d, "
                         + "cols=%d, previewImageUri=%s, previewPagesCount=%d}\n",
-                mTitle, mIsCurrent, mTileDrawable, name, rows, cols, previewImageUri,
+                title, mIsCurrent, mTileDrawable, name, rows, cols, previewImageUri,
                 previewPagesCount);
     }
 }
diff --git a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
index 8f1860e..e71cca9 100644
--- a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
+++ b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
@@ -33,6 +33,7 @@
 
 import com.android.customization.model.ResourceConstants;
 import com.android.wallpaper.R;
+import com.android.wallpaper.config.BaseFlags;
 import com.android.wallpaper.util.PreviewUtils;
 
 import java.util.ArrayList;
@@ -57,12 +58,14 @@
 
     private final Context mContext;
     private final PreviewUtils mPreviewUtils;
+    private final boolean mIsGridApplyButtonEnabled;
     private List<GridOption> mOptions;
     private OptionChangeLiveData mLiveData;
 
     public LauncherGridOptionsProvider(Context context, String authorityMetadataKey) {
         mPreviewUtils = new PreviewUtils(context, authorityMetadataKey);
         mContext = context;
+        mIsGridApplyButtonEnabled = BaseFlags.get().isGridApplyButtonEnabled(context);
     }
 
     boolean areGridsAvailable() {
@@ -124,6 +127,7 @@
     int applyGrid(String name) {
         ContentValues values = new ContentValues();
         values.put("name", name);
+        values.put("enable_apply_button", mIsGridApplyButtonEnabled);
         return mContext.getContentResolver().update(mPreviewUtils.getUri(DEFAULT_GRID), values,
                 null, null);
     }
@@ -157,6 +161,14 @@
             mContentObserver = new ContentObserver(handler) {
                 @Override
                 public void onChange(boolean selfChange) {
+                    // If grid apply button is enabled, user has previewed the grid before applying
+                    // the grid change. Thus there is no need to preview again (which will cause a
+                    // blank preview as launcher's is loader thread is busy reloading workspace)
+                    // after applying grid change. Thus we should ignore ContentObserver#onChange
+                    // from launcher
+                    if (BaseFlags.get().isGridApplyButtonEnabled(context.getApplicationContext())) {
+                        return;
+                    }
                     postValue(new Object());
                 }
             };
diff --git a/src/com/android/customization/model/grid/data/repository/GridRepository.kt b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
index e65c18e..4379dad 100644
--- a/src/com/android/customization/model/grid/data/repository/GridRepository.kt
+++ b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
@@ -19,6 +19,7 @@
 
 import androidx.lifecycle.asFlow
 import com.android.customization.model.CustomizationManager
+import com.android.customization.model.CustomizationManager.Callback
 import com.android.customization.model.grid.GridOption
 import com.android.customization.model.grid.GridOptionsManager
 import com.android.customization.model.grid.shared.model.GridOptionItemModel
@@ -39,6 +40,9 @@
     fun getOptionChanges(): Flow<Unit>
     suspend fun getOptions(): GridOptionItemsModel
     fun getSelectedOption(): GridOption?
+    fun applySelectedOption(callback: Callback)
+    fun clearSelectedOption()
+    fun isSelectedOptionApplied(): Boolean
 }
 
 class GridRepositoryImpl(
@@ -57,6 +61,8 @@
 
     private val selectedOption = MutableStateFlow<GridOption?>(null)
 
+    private var appliedOption: GridOption? = null
+
     override fun getSelectedOption() = selectedOption.value
 
     override suspend fun getOptions(): GridOptionItemsModel {
@@ -71,6 +77,9 @@
                             if (!isGridApplyButtonEnabled || selectedOption.value == null) {
                                 selectedOption.value = optionsOrEmpty.find { it.isActive(manager) }
                             }
+                            if (isGridApplyButtonEnabled && appliedOption == null) {
+                                appliedOption = selectedOption.value
+                            }
                             continuation.resume(
                                 GridOptionItemsModel.Loaded(
                                     optionsOrEmpty.map { option -> toModel(option) }
@@ -137,6 +146,35 @@
         }
     }
 
+    override fun applySelectedOption(callback: Callback) {
+        val option = getSelectedOption()
+        manager.apply(
+            option,
+            if (isGridApplyButtonEnabled) {
+                object : Callback {
+                    override fun onSuccess() {
+                        callback.onSuccess()
+                        appliedOption = option
+                    }
+
+                    override fun onError(throwable: Throwable?) {
+                        callback.onError(throwable)
+                    }
+                }
+            } else callback
+        )
+    }
+
+    override fun clearSelectedOption() {
+        if (!isGridApplyButtonEnabled) {
+            return
+        }
+        selectedOption.value?.setIsCurrent(false)
+        selectedOption.value = null
+    }
+
+    override fun isSelectedOptionApplied() = selectedOption.value?.name == appliedOption?.name
+
     private fun GridOption?.key(): String? {
         return if (this != null) "${cols}x${rows}" else null
     }
diff --git a/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt b/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
index 6cb00ff..7abd605 100644
--- a/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
+++ b/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
@@ -17,6 +17,8 @@
 
 package com.android.customization.model.grid.domain.interactor
 
+import com.android.customization.model.CustomizationManager
+import com.android.customization.model.grid.GridOption
 import com.android.customization.model.grid.data.repository.GridRepository
 import com.android.customization.model.grid.shared.model.GridOptionItemModel
 import com.android.customization.model.grid.shared.model.GridOptionItemsModel
@@ -73,6 +75,16 @@
         }
     }
 
+    fun getSelectOptionNonSuspend(): GridOption? = repository.getSelectedOption()
+
+    fun clearSelectedOption() = repository.clearSelectedOption()
+
+    fun isSelectedOptionApplied() = repository.isSelectedOptionApplied()
+
+    fun applySelectedOption(callback: CustomizationManager.Callback) {
+        repository.applySelectedOption(callback)
+    }
+
     private suspend fun reload(): GridOptionItemsModel {
         val model = repository.getOptions()
         return if (model is GridOptionItemsModel.Loaded) {
@@ -95,6 +107,4 @@
             model
         }
     }
-
-    fun getSelectedOptionName(): String? = repository.getSelectedOption()?.name
 }
diff --git a/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
index 1c3561d..56fe425 100644
--- a/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
+++ b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
@@ -18,6 +18,7 @@
 package com.android.customization.model.grid.ui.binder
 
 import android.view.View
+import android.widget.Button
 import android.widget.ImageView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -41,6 +42,8 @@
         lifecycleOwner: LifecycleOwner,
         backgroundDispatcher: CoroutineDispatcher,
         onOptionsChanged: () -> Unit,
+        isGridApplyButtonEnabled: Boolean,
+        onOptionApplied: () -> Unit,
     ) {
         val optionView: RecyclerView = view.requireViewById(R.id.options)
         optionView.layoutManager =
@@ -67,6 +70,13 @@
             )
         optionView.adapter = adapter
 
+        if (isGridApplyButtonEnabled) {
+            val applyButton: Button = view.requireViewById(R.id.apply_button)
+            applyButton.visibility = View.VISIBLE
+            view.requireViewById<View>(R.id.apply_button_note).visibility = View.VISIBLE
+            applyButton.setOnClickListener { onOptionApplied() }
+        }
+
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
diff --git a/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt b/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
index 14d1344..9e99efe 100644
--- a/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
+++ b/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
@@ -18,19 +18,24 @@
 package com.android.customization.model.grid.ui.fragment
 
 import android.os.Bundle
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.Button
+import android.widget.Toast
 import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.lifecycle.ViewModelProvider
 import androidx.transition.Transition
 import androidx.transition.doOnStart
+import com.android.customization.model.CustomizationManager.Callback
 import com.android.customization.model.grid.domain.interactor.GridInteractor
 import com.android.customization.model.grid.ui.binder.GridScreenBinder
 import com.android.customization.model.grid.ui.viewmodel.GridScreenViewModel
 import com.android.customization.module.ThemePickerInjector
 import com.android.wallpaper.R
+import com.android.wallpaper.config.BaseFlags
 import com.android.wallpaper.module.CurrentWallpaperInfoFactory
 import com.android.wallpaper.module.CustomizationSections
 import com.android.wallpaper.module.InjectorProvider
@@ -43,9 +48,13 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.suspendCancellableCoroutine
 
+private val TAG = GridFragment2::class.java.simpleName
+
 @OptIn(ExperimentalCoroutinesApi::class)
 class GridFragment2 : AppbarFragment() {
 
+    private lateinit var gridInteractor: GridInteractor
+
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -59,6 +68,8 @@
             )
         setUpToolbar(view)
 
+        val isGridApplyButtonEnabled = BaseFlags.get().isGridApplyButtonEnabled(requireContext())
+
         val injector = InjectorProvider.getInjector() as ThemePickerInjector
 
         val wallpaperInfoFactory = injector.getCurrentWallpaperInfoFactory(requireContext())
@@ -71,6 +82,7 @@
             )
 
         val viewModelFactory = injector.getGridScreenViewModelFactory(requireContext())
+        gridInteractor = injector.getGridInteractor(requireContext())
         GridScreenBinder.bind(
             view = view,
             viewModel =
@@ -87,8 +99,42 @@
                         view,
                         wallpaperInfoFactory,
                         injector.getWallpaperInteractor(requireContext()),
-                        injector.getGridInteractor(requireContext())
+                        gridInteractor,
                     )
+                if (isGridApplyButtonEnabled) {
+                    val applyButton: Button = view.requireViewById(R.id.apply_button)
+                    applyButton.isEnabled = !gridInteractor.isSelectedOptionApplied()
+                }
+            },
+            isGridApplyButtonEnabled = isGridApplyButtonEnabled,
+            onOptionApplied = {
+                gridInteractor.applySelectedOption(
+                    object : Callback {
+                        override fun onSuccess() {
+                            Toast.makeText(
+                                    context,
+                                    getString(
+                                        R.string.toast_of_changing_grid,
+                                        gridInteractor.getSelectOptionNonSuspend()?.title
+                                    ),
+                                    Toast.LENGTH_SHORT
+                                )
+                                .show()
+                            val applyButton: Button = view.requireViewById(R.id.apply_button)
+                            applyButton.isEnabled = false
+                        }
+
+                        override fun onError(throwable: Throwable?) {
+                            val errorMsg =
+                                getString(
+                                    R.string.toast_of_failure_to_change_grid,
+                                    gridInteractor.getSelectOptionNonSuspend()?.title
+                                )
+                            Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show()
+                            Log.e(TAG, errorMsg, throwable)
+                        }
+                    }
+                )
             }
         )
 
@@ -133,7 +179,7 @@
                         ),
                     initialExtrasProvider = {
                         val bundle = Bundle()
-                        bundle.putString("name", gridInteractor.getSelectedOptionName())
+                        bundle.putString("name", gridInteractor.getSelectOptionNonSuspend()?.name)
                         bundle
                     },
                     wallpaperInfoProvider = {
@@ -154,4 +200,11 @@
             onWallpaperPreviewDirty = { activity?.recreate() },
         )
     }
+
+    override fun onBackPressed(): Boolean {
+        if (BaseFlags.get().isGridApplyButtonEnabled(requireContext())) {
+            gridInteractor.clearSelectedOption()
+        }
+        return super.onBackPressed()
+    }
 }
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index a4b1d27..1486884 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -566,9 +566,7 @@
                 .also { gridScreenViewModelFactory = it }
     }
 
-    fun getGridInteractor(
-        context: Context,
-    ): GridInteractor {
+    fun getGridInteractor(context: Context): GridInteractor {
         val appContext = context.applicationContext
         return gridInteractor
             ?: GridInteractor(
diff --git a/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt b/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
index 0c80e28..317ad3a 100644
--- a/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
+++ b/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
@@ -17,6 +17,7 @@
 
 package com.android.customization.model.grid.data.repository
 
+import com.android.customization.model.CustomizationManager
 import com.android.customization.model.grid.GridOption
 import com.android.customization.model.grid.shared.model.GridOptionItemModel
 import com.android.customization.model.grid.shared.model.GridOptionItemsModel
@@ -54,6 +55,12 @@
 
     override fun getSelectedOption(): GridOption? = null
 
+    override fun applySelectedOption(callback: CustomizationManager.Callback) {}
+
+    override fun clearSelectedOption() {}
+
+    override fun isSelectedOptionApplied() = false
+
     fun setOptions(
         count: Int,
         selectedIndex: Int = 0,