summaryrefslogtreecommitdiff
path: root/src/com/android/customization/model/grid
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/customization/model/grid')
-rw-r--r--src/com/android/customization/model/grid/GridOptionsManager.java29
-rw-r--r--src/com/android/customization/model/grid/GridSectionController.java91
-rw-r--r--src/com/android/customization/model/grid/LauncherGridOptionsProvider.java54
-rw-r--r--src/com/android/customization/model/grid/data/repository/GridRepository.kt127
-rw-r--r--src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt98
-rw-r--r--src/com/android/customization/model/grid/domain/interactor/GridSnapshotRestorer.kt74
-rw-r--r--src/com/android/customization/model/grid/shared/model/GridOptionItemModel.kt28
-rw-r--r--src/com/android/customization/model/grid/shared/model/GridOptionItemsModel.kt27
-rw-r--r--src/com/android/customization/model/grid/ui/binder/GridIconViewBinder.kt17
-rw-r--r--src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt81
-rw-r--r--src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt132
-rw-r--r--src/com/android/customization/model/grid/ui/viewmodel/GridIconViewModel.kt24
-rw-r--r--src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt106
13 files changed, 858 insertions, 30 deletions
diff --git a/src/com/android/customization/model/grid/GridOptionsManager.java b/src/com/android/customization/model/grid/GridOptionsManager.java
index 7f15d836..b7ee37fd 100644
--- a/src/com/android/customization/model/grid/GridOptionsManager.java
+++ b/src/com/android/customization/model/grid/GridOptionsManager.java
@@ -21,7 +21,9 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LiveData;
import com.android.customization.model.CustomizationManager;
import com.android.customization.module.CustomizationInjector;
@@ -47,6 +49,7 @@ public class GridOptionsManager implements CustomizationManager<GridOption> {
private final LauncherGridOptionsProvider mProvider;
private final ThemesUserEventLogger mEventLogger;
+ private int mGridOptionSize = -1;
/** Returns the {@link GridOptionsManager} instance. */
public static GridOptionsManager getInstance(Context context) {
@@ -71,16 +74,17 @@ public class GridOptionsManager implements CustomizationManager<GridOption> {
@Override
public boolean isAvailable() {
- int gridOptionSize = 0;
- try {
- gridOptionSize = sExecutorService.submit(() -> {
- List<GridOption> gridOptions = mProvider.fetch(/* reload= */true);
- return gridOptions == null ? 0 : gridOptions.size();
- }).get();
- } catch (InterruptedException | ExecutionException e) {
- Log.w(TAG, "could not get gridOptionSize", e);
+ if (mGridOptionSize < 0) {
+ try {
+ mGridOptionSize = sExecutorService.submit(() -> {
+ List<GridOption> gridOptions = mProvider.fetch(/* reload= */true);
+ return gridOptions == null ? 0 : gridOptions.size();
+ }).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.w(TAG, "could not get gridOptionSize", e);
+ }
}
- return gridOptionSize > 1 && mProvider.areGridsAvailable();
+ return mGridOptionSize > 1 && mProvider.areGridsAvailable();
}
@Override
@@ -110,6 +114,13 @@ public class GridOptionsManager implements CustomizationManager<GridOption> {
});
}
+ /**
+ * Returns an observable that receives a new value each time that the grid options are changed.
+ */
+ public LiveData<Object> getOptionChangeObservable(@Nullable Handler handler) {
+ return mProvider.getOptionChangeObservable(handler);
+ }
+
/** Call through content provider API to render preview */
public void renderPreview(Bundle bundle, String gridName,
PreviewUtils.WorkspacePreviewCallback callback) {
diff --git a/src/com/android/customization/model/grid/GridSectionController.java b/src/com/android/customization/model/grid/GridSectionController.java
index 2f54a1bf..c50bfcc2 100644
--- a/src/com/android/customization/model/grid/GridSectionController.java
+++ b/src/com/android/customization/model/grid/GridSectionController.java
@@ -22,8 +22,12 @@ import android.view.View;
import android.widget.TextView;
import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.Observer;
import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.grid.ui.fragment.GridFragment2;
import com.android.customization.picker.grid.GridFragment;
import com.android.customization.picker.grid.GridSectionView;
import com.android.wallpaper.R;
@@ -38,11 +42,22 @@ public class GridSectionController implements CustomizationSectionController<Gri
private final GridOptionsManager mGridOptionsManager;
private final CustomizationSectionNavigationController mSectionNavigationController;
+ private final boolean mIsRevampedUiEnabled;
+ private final Observer<Object> mOptionChangeObserver;
+ private final LifecycleOwner mLifecycleOwner;
+ private TextView mSectionDescription;
+ private View mSectionTile;
- public GridSectionController(GridOptionsManager gridOptionsManager,
- CustomizationSectionNavigationController sectionNavigationController) {
+ public GridSectionController(
+ GridOptionsManager gridOptionsManager,
+ CustomizationSectionNavigationController sectionNavigationController,
+ LifecycleOwner lifecycleOwner,
+ boolean isRevampedUiEnabled) {
mGridOptionsManager = gridOptionsManager;
mSectionNavigationController = sectionNavigationController;
+ mIsRevampedUiEnabled = isRevampedUiEnabled;
+ mLifecycleOwner = lifecycleOwner;
+ mOptionChangeObserver = o -> updateUi(/* reload= */ true);
}
@Override
@@ -52,34 +67,68 @@ public class GridSectionController implements CustomizationSectionController<Gri
@Override
public GridSectionView createView(Context context) {
- GridSectionView gridSectionView = (GridSectionView) LayoutInflater.from(context)
+ final GridSectionView gridSectionView = (GridSectionView) LayoutInflater.from(context)
.inflate(R.layout.grid_section_view, /* root= */ null);
- TextView sectionDescription = gridSectionView.findViewById(R.id.grid_section_description);
- View sectionTile = gridSectionView.findViewById(R.id.grid_section_tile);
+ mSectionDescription = gridSectionView.findViewById(R.id.grid_section_description);
+ mSectionTile = gridSectionView.findViewById(R.id.grid_section_tile);
// Fetch grid options to show currently set grid.
- mGridOptionsManager.fetchOptions(new OptionsFetchedListener<GridOption>() {
- @Override
- public void onOptionsLoaded(List<GridOption> options) {
- sectionDescription.setText(getActiveOption(options).getTitle());
- }
-
- @Override
- public void onError(@Nullable Throwable throwable) {
- if (throwable != null) {
- Log.e(TAG, "Error loading grid options", throwable);
- }
- sectionDescription.setText(R.string.something_went_wrong);
- sectionTile.setVisibility(View.GONE);
- }
- }, /* The result is getting when calling isAvailable(), so reload= */ false);
+ updateUi(/* The result is getting when calling isAvailable(), so reload= */ false);
+ if (mIsRevampedUiEnabled) {
+ mGridOptionsManager.getOptionChangeObservable(/* handler= */ null).observe(
+ mLifecycleOwner,
+ mOptionChangeObserver);
+ }
gridSectionView.setOnClickListener(
- v -> mSectionNavigationController.navigateTo(new GridFragment()));
+ v -> {
+ final Fragment gridFragment;
+ if (mIsRevampedUiEnabled) {
+ gridFragment = new GridFragment2();
+ } else {
+ gridFragment = new GridFragment();
+ }
+ mSectionNavigationController.navigateTo(gridFragment);
+ });
return gridSectionView;
}
+ @Override
+ public void release() {
+ if (mIsRevampedUiEnabled && mGridOptionsManager.isAvailable()) {
+ mGridOptionsManager.getOptionChangeObservable(/* handler= */ null).removeObserver(
+ mOptionChangeObserver
+ );
+ }
+ }
+
+ @Override
+ public void onTransitionOut() {
+ CustomizationSectionController.super.onTransitionOut();
+ }
+
+ private void updateUi(final boolean reload) {
+ mGridOptionsManager.fetchOptions(
+ new OptionsFetchedListener<GridOption>() {
+ @Override
+ public void onOptionsLoaded(List<GridOption> options) {
+ final String title = getActiveOption(options).getTitle();
+ mSectionDescription.setText(title);
+ }
+
+ @Override
+ public void onError(@Nullable Throwable throwable) {
+ if (throwable != null) {
+ Log.e(TAG, "Error loading grid options", throwable);
+ }
+ mSectionDescription.setText(R.string.something_went_wrong);
+ mSectionTile.setVisibility(View.GONE);
+ }
+ },
+ reload);
+ }
+
private GridOption getActiveOption(List<GridOption> options) {
return options.stream()
.filter(option -> option.isActive(mGridOptionsManager))
diff --git a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
index fd403631..4e775c62 100644
--- a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
+++ b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
@@ -19,12 +19,17 @@ import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
+import android.database.ContentObserver;
import android.database.Cursor;
+import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
import android.view.SurfaceView;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import com.android.customization.model.ResourceConstants;
import com.android.wallpaper.R;
@@ -53,6 +58,7 @@ public class LauncherGridOptionsProvider {
private final Context mContext;
private final PreviewUtils mPreviewUtils;
private List<GridOption> mOptions;
+ private OptionChangeLiveData mLiveData;
public LauncherGridOptionsProvider(Context context, String authorityMetadataKey) {
mPreviewUtils = new PreviewUtils(context, authorityMetadataKey);
@@ -117,4 +123,52 @@ public class LauncherGridOptionsProvider {
return mContext.getContentResolver().update(mPreviewUtils.getUri(DEFAULT_GRID), values,
null, null);
}
+
+ /**
+ * Returns an observable that receives a new value each time that the grid options are changed.
+ * Do not call if {@link #areGridsAvailable()} returns false
+ */
+ public LiveData<Object> getOptionChangeObservable(
+ @Nullable Handler handler) {
+ if (mLiveData == null) {
+ mLiveData = new OptionChangeLiveData(
+ mContext, mPreviewUtils.getUri(DEFAULT_GRID), handler);
+ }
+
+ return mLiveData;
+ }
+
+ private static class OptionChangeLiveData extends MutableLiveData<Object> {
+
+ private final ContentResolver mContentResolver;
+ private final Uri mUri;
+ private final ContentObserver mContentObserver;
+
+ OptionChangeLiveData(
+ Context context,
+ Uri uri,
+ @Nullable Handler handler) {
+ mContentResolver = context.getContentResolver();
+ mUri = uri;
+ mContentObserver = new ContentObserver(handler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ postValue(new Object());
+ }
+ };
+ }
+
+ @Override
+ protected void onActive() {
+ mContentResolver.registerContentObserver(
+ mUri,
+ /* notifyForDescendants= */ true,
+ mContentObserver);
+ }
+
+ @Override
+ protected void onInactive() {
+ mContentResolver.unregisterContentObserver(mContentObserver);
+ }
+ }
}
diff --git a/src/com/android/customization/model/grid/data/repository/GridRepository.kt b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
new file mode 100644
index 00000000..9a3be0cc
--- /dev/null
+++ b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 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.model.grid.data.repository
+
+import androidx.lifecycle.asFlow
+import com.android.customization.model.CustomizationManager
+import com.android.customization.model.grid.GridOption
+import com.android.customization.model.grid.GridOptionsManager
+import com.android.customization.model.grid.shared.model.GridOptionItemModel
+import com.android.customization.model.grid.shared.model.GridOptionItemsModel
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+interface GridRepository {
+ suspend fun isAvailable(): Boolean
+ fun getOptionChanges(): Flow<Unit>
+ suspend fun getOptions(): GridOptionItemsModel
+}
+
+class GridRepositoryImpl(
+ private val applicationScope: CoroutineScope,
+ private val manager: GridOptionsManager,
+ private val backgroundDispatcher: CoroutineDispatcher,
+) : GridRepository {
+
+ override suspend fun isAvailable(): Boolean {
+ return withContext(backgroundDispatcher) { manager.isAvailable }
+ }
+
+ override fun getOptionChanges(): Flow<Unit> =
+ manager.getOptionChangeObservable(/* handler= */ null).asFlow().map {}
+
+ private val selectedOption = MutableStateFlow<GridOption?>(null)
+
+ override suspend fun getOptions(): GridOptionItemsModel {
+ return withContext(backgroundDispatcher) {
+ suspendCancellableCoroutine { continuation ->
+ manager.fetchOptions(
+ object : CustomizationManager.OptionsFetchedListener<GridOption> {
+ override fun onOptionsLoaded(options: MutableList<GridOption>?) {
+ val optionsOrEmpty = options ?: emptyList()
+ selectedOption.value = optionsOrEmpty.find { it.isActive(manager) }
+ continuation.resume(
+ GridOptionItemsModel.Loaded(
+ optionsOrEmpty.map { option -> toModel(option) }
+ )
+ )
+ }
+
+ override fun onError(throwable: Throwable?) {
+ continuation.resume(
+ GridOptionItemsModel.Error(
+ throwable ?: Exception("Failed to load grid options!")
+ ),
+ )
+ }
+ },
+ /* reload= */ true,
+ )
+ }
+ }
+ }
+
+ private fun toModel(option: GridOption): GridOptionItemModel {
+ return GridOptionItemModel(
+ name = option.title,
+ rows = option.rows,
+ cols = option.cols,
+ isSelected =
+ selectedOption
+ .map { it.key() }
+ .map { selectedOptionKey -> option.key() == selectedOptionKey }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = false,
+ ),
+ onSelected = { onSelected(option) },
+ )
+ }
+
+ private suspend fun onSelected(option: GridOption) {
+ withContext(backgroundDispatcher) {
+ suspendCancellableCoroutine { continuation ->
+ manager.apply(
+ option,
+ object : CustomizationManager.Callback {
+ override fun onSuccess() {
+ continuation.resume(true)
+ }
+
+ override fun onError(throwable: Throwable?) {
+ continuation.resume(false)
+ }
+ },
+ )
+ }
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..cdb679dd
--- /dev/null
+++ b/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 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.model.grid.domain.interactor
+
+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
+import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+
+class GridInteractor(
+ private val applicationScope: CoroutineScope,
+ private val repository: GridRepository,
+ private val snapshotRestorer: Provider<GridSnapshotRestorer>,
+) {
+ val options: Flow<GridOptionItemsModel> =
+ flow { emit(repository.isAvailable()) }
+ .flatMapLatest { isAvailable ->
+ if (isAvailable) {
+ // this upstream flow tells us each time the options are changed.
+ repository
+ .getOptionChanges()
+ // when we start, we pretend the options _just_ changed. This way, we load
+ // something as soon as possible into the flow so it's ready by the time the
+ // first observer starts to observe.
+ .onStart { emit(Unit) }
+ // each time the options changed, we load them.
+ .map { reload() }
+ // we place the loaded options in a SharedFlow so downstream observers all
+ // share the same flow and don't trigger a new one each time they want to
+ // start observing.
+ .shareIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ replay = 1,
+ )
+ } else {
+ emptyFlow()
+ }
+ }
+
+ suspend fun setSelectedOption(model: GridOptionItemModel) {
+ model.onSelected.invoke()
+ }
+
+ suspend fun getSelectedOption(): GridOptionItemModel? {
+ return (repository.getOptions() as? GridOptionItemsModel.Loaded)?.options?.firstOrNull {
+ optionItem ->
+ optionItem.isSelected.value
+ }
+ }
+
+ private suspend fun reload(): GridOptionItemsModel {
+ val model = repository.getOptions()
+ return if (model is GridOptionItemsModel.Loaded) {
+ GridOptionItemsModel.Loaded(
+ options =
+ model.options.map { option ->
+ GridOptionItemModel(
+ name = option.name,
+ cols = option.cols,
+ rows = option.rows,
+ isSelected = option.isSelected,
+ onSelected = {
+ option.onSelected()
+ snapshotRestorer.get().store(option)
+ },
+ )
+ }
+ )
+ } else {
+ model
+ }
+ }
+}
diff --git a/src/com/android/customization/model/grid/domain/interactor/GridSnapshotRestorer.kt b/src/com/android/customization/model/grid/domain/interactor/GridSnapshotRestorer.kt
new file mode 100644
index 00000000..19d4c77e
--- /dev/null
+++ b/src/com/android/customization/model/grid/domain/interactor/GridSnapshotRestorer.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 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.model.grid.domain.interactor
+
+import android.util.Log
+import com.android.customization.model.grid.shared.model.GridOptionItemModel
+import com.android.wallpaper.picker.undo.domain.interactor.SnapshotRestorer
+import com.android.wallpaper.picker.undo.domain.interactor.SnapshotStore
+import com.android.wallpaper.picker.undo.shared.model.RestorableSnapshot
+
+class GridSnapshotRestorer(
+ private val interactor: GridInteractor,
+) : SnapshotRestorer {
+
+ private var store: SnapshotStore = SnapshotStore.NOOP
+ private var originalOption: GridOptionItemModel? = null
+
+ override suspend fun setUpSnapshotRestorer(store: SnapshotStore): RestorableSnapshot {
+ this.store = store
+ val option = interactor.getSelectedOption()
+ originalOption = option
+ return snapshot(option)
+ }
+
+ override suspend fun restoreToSnapshot(snapshot: RestorableSnapshot) {
+ val optionNameFromSnapshot = snapshot.args[KEY_GRID_OPTION_NAME]
+ originalOption?.let { optionToRestore ->
+ if (optionToRestore.name != optionNameFromSnapshot) {
+ Log.wtf(
+ TAG,
+ """Original snapshot name was ${optionToRestore.name} but we're being told to
+ | restore to $optionNameFromSnapshot. The current implementation doesn't
+ | support undo, only a reset back to the original grid option."""
+ .trimMargin(),
+ )
+ }
+
+ interactor.setSelectedOption(optionToRestore)
+ }
+ }
+
+ fun store(option: GridOptionItemModel) {
+ store.store(snapshot(option))
+ }
+
+ private fun snapshot(option: GridOptionItemModel?): RestorableSnapshot {
+ return RestorableSnapshot(
+ args =
+ buildMap {
+ option?.name?.let { optionName -> put(KEY_GRID_OPTION_NAME, optionName) }
+ }
+ )
+ }
+
+ companion object {
+ private const val TAG = "GridSnapshotRestorer"
+ private const val KEY_GRID_OPTION_NAME = "grid_option"
+ }
+}
diff --git a/src/com/android/customization/model/grid/shared/model/GridOptionItemModel.kt b/src/com/android/customization/model/grid/shared/model/GridOptionItemModel.kt
new file mode 100644
index 00000000..2eabeab5
--- /dev/null
+++ b/src/com/android/customization/model/grid/shared/model/GridOptionItemModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.model.grid.shared.model
+
+import kotlinx.coroutines.flow.StateFlow
+
+data class GridOptionItemModel(
+ val name: String,
+ val cols: Int,
+ val rows: Int,
+ val isSelected: StateFlow<Boolean>,
+ val onSelected: suspend () -> Unit,
+)
diff --git a/src/com/android/customization/model/grid/shared/model/GridOptionItemsModel.kt b/src/com/android/customization/model/grid/shared/model/GridOptionItemsModel.kt
new file mode 100644
index 00000000..e969be88
--- /dev/null
+++ b/src/com/android/customization/model/grid/shared/model/GridOptionItemsModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 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.model.grid.shared.model
+
+sealed class GridOptionItemsModel {
+ data class Loaded(
+ val options: List<GridOptionItemModel>,
+ ) : GridOptionItemsModel()
+ data class Error(
+ val throwable: Throwable?,
+ ) : GridOptionItemsModel()
+}
diff --git a/src/com/android/customization/model/grid/ui/binder/GridIconViewBinder.kt b/src/com/android/customization/model/grid/ui/binder/GridIconViewBinder.kt
new file mode 100644
index 00000000..fba89a74
--- /dev/null
+++ b/src/com/android/customization/model/grid/ui/binder/GridIconViewBinder.kt
@@ -0,0 +1,17 @@
+package com.android.customization.model.grid.ui.binder
+
+import android.widget.ImageView
+import com.android.customization.model.grid.ui.viewmodel.GridIconViewModel
+import com.android.customization.widget.GridTileDrawable
+
+object GridIconViewBinder {
+ fun bind(view: ImageView, viewModel: GridIconViewModel) {
+ view.setImageDrawable(
+ GridTileDrawable(
+ viewModel.columns,
+ viewModel.rows,
+ viewModel.path,
+ )
+ )
+ }
+}
diff --git a/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
new file mode 100644
index 00000000..78536ca9
--- /dev/null
+++ b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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.model.grid.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 androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.model.grid.ui.viewmodel.GridIconViewModel
+import com.android.customization.model.grid.ui.viewmodel.GridScreenViewModel
+import com.android.customization.picker.common.ui.view.ItemSpacing
+import com.android.wallpaper.R
+import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
+import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.launch
+
+object GridScreenBinder {
+ fun bind(
+ view: View,
+ viewModel: GridScreenViewModel,
+ lifecycleOwner: LifecycleOwner,
+ backgroundDispatcher: CoroutineDispatcher,
+ onOptionsChanged: () -> Unit,
+ ) {
+ val optionView: RecyclerView = view.requireViewById(R.id.options)
+ optionView.layoutManager =
+ LinearLayoutManager(
+ view.context,
+ RecyclerView.HORIZONTAL,
+ /* reverseLayout= */ false,
+ )
+ optionView.addItemDecoration(ItemSpacing(ItemSpacing.ITEM_SPACING_DP))
+ val adapter =
+ OptionItemAdapter(
+ layoutResourceId = R.layout.grid_option_2,
+ lifecycleOwner = lifecycleOwner,
+ backgroundDispatcher = backgroundDispatcher,
+ foregroundTintSpec =
+ OptionItemBinder.TintSpec(
+ selectedColor = view.context.getColor(R.color.text_color_primary),
+ unselectedColor = view.context.getColor(R.color.text_color_secondary),
+ ),
+ bindIcon = { foregroundView: View, gridIcon: GridIconViewModel ->
+ val imageView = foregroundView as? ImageView
+ imageView?.let { GridIconViewBinder.bind(imageView, gridIcon) }
+ }
+ )
+ optionView.adapter = adapter
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.optionItems.collect { options ->
+ adapter.setItems(options)
+ onOptionsChanged()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt b/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
new file mode 100644
index 00000000..d8cad828
--- /dev/null
+++ b/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 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.model.grid.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.ViewModelProvider
+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.module.CurrentWallpaperInfoFactory
+import com.android.wallpaper.module.CustomizationSections
+import com.android.wallpaper.module.InjectorProvider
+import com.android.wallpaper.picker.AppbarFragment
+import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor
+import com.android.wallpaper.picker.customization.ui.binder.ScreenPreviewBinder
+import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel
+import com.android.wallpaper.util.PreviewUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class GridFragment2 : AppbarFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view =
+ inflater.inflate(
+ R.layout.fragment_grid,
+ container,
+ false,
+ )
+ setUpToolbar(view)
+
+ val injector = InjectorProvider.getInjector() as ThemePickerInjector
+
+ val wallpaperInfoFactory = injector.getCurrentWallpaperInfoFactory(requireContext())
+ var screenPreviewBinding =
+ bindScreenPreview(
+ view,
+ wallpaperInfoFactory,
+ injector.getWallpaperInteractor(requireContext())
+ )
+
+ val viewModelFactory = injector.getGridScreenViewModelFactory(requireContext())
+ GridScreenBinder.bind(
+ view = view,
+ viewModel =
+ ViewModelProvider(
+ this,
+ viewModelFactory,
+ )[GridScreenViewModel::class.java],
+ lifecycleOwner = this,
+ backgroundDispatcher = Dispatchers.IO,
+ onOptionsChanged = {
+ screenPreviewBinding.destroy()
+ screenPreviewBinding =
+ bindScreenPreview(
+ view,
+ wallpaperInfoFactory,
+ injector.getWallpaperInteractor(requireContext())
+ )
+ }
+ )
+
+ return view
+ }
+
+ override fun getDefaultTitle(): CharSequence {
+ return getString(R.string.grid_title)
+ }
+
+ private fun bindScreenPreview(
+ view: View,
+ wallpaperInfoFactory: CurrentWallpaperInfoFactory,
+ wallpaperInteractor: WallpaperInteractor,
+ ): ScreenPreviewBinder.Binding {
+ return ScreenPreviewBinder.bind(
+ activity = requireActivity(),
+ previewView = view.requireViewById(R.id.preview),
+ viewModel =
+ ScreenPreviewViewModel(
+ previewUtils =
+ PreviewUtils(
+ context = requireContext(),
+ authorityMetadataKey =
+ requireContext()
+ .getString(
+ R.string.grid_control_metadata_name,
+ ),
+ ),
+ wallpaperInfoProvider = {
+ suspendCancellableCoroutine { continuation ->
+ wallpaperInfoFactory.createCurrentWallpaperInfos(
+ { homeWallpaper, lockWallpaper, _ ->
+ continuation.resume(homeWallpaper ?: lockWallpaper, null)
+ },
+ /* forceRefresh= */ true,
+ )
+ }
+ },
+ wallpaperInteractor = wallpaperInteractor,
+ ),
+ lifecycleOwner = this,
+ offsetToStart = false,
+ screen = CustomizationSections.Screen.HOME_SCREEN,
+ onPreviewDirty = { activity?.recreate() },
+ )
+ }
+}
diff --git a/src/com/android/customization/model/grid/ui/viewmodel/GridIconViewModel.kt b/src/com/android/customization/model/grid/ui/viewmodel/GridIconViewModel.kt
new file mode 100644
index 00000000..3942d7cc
--- /dev/null
+++ b/src/com/android/customization/model/grid/ui/viewmodel/GridIconViewModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.model.grid.ui.viewmodel
+
+data class GridIconViewModel(
+ val columns: Int,
+ val rows: Int,
+ val path: String,
+)
diff --git a/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt b/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt
new file mode 100644
index 00000000..c11a5947
--- /dev/null
+++ b/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 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.model.grid.ui.viewmodel
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.Resources
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.customization.model.ResourceConstants
+import com.android.customization.model.grid.domain.interactor.GridInteractor
+import com.android.customization.model.grid.shared.model.GridOptionItemsModel
+import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+class GridScreenViewModel(
+ context: Context,
+ private val interactor: GridInteractor,
+) : ViewModel() {
+
+ @SuppressLint("StaticFieldLeak") // We're not leaking this context as it is the app context.
+ private val applicationContext = context.applicationContext
+
+ val optionItems: Flow<List<OptionItemViewModel<GridIconViewModel>>> =
+ interactor.options.map { model -> toViewModel(model) }
+
+ private fun toViewModel(
+ model: GridOptionItemsModel,
+ ): List<OptionItemViewModel<GridIconViewModel>> {
+ val iconShapePath =
+ applicationContext.resources.getString(
+ Resources.getSystem()
+ .getIdentifier(
+ ResourceConstants.CONFIG_ICON_MASK,
+ "string",
+ ResourceConstants.ANDROID_PACKAGE,
+ )
+ )
+
+ return when (model) {
+ is GridOptionItemsModel.Loaded ->
+ model.options.map { option ->
+ val text = Text.Loaded(option.name)
+ OptionItemViewModel<GridIconViewModel>(
+ key =
+ MutableStateFlow("${option.cols}x${option.rows}") as StateFlow<String>,
+ payload =
+ GridIconViewModel(
+ columns = option.cols,
+ rows = option.rows,
+ path = iconShapePath,
+ ),
+ text = text,
+ isSelected = option.isSelected,
+ onClicked =
+ option.isSelected.map { isSelected ->
+ if (!isSelected) {
+ { viewModelScope.launch { option.onSelected() } }
+ } else {
+ null
+ }
+ },
+ )
+ }
+ is GridOptionItemsModel.Error -> emptyList()
+ }
+ }
+
+ class Factory(
+ context: Context,
+ private val interactor: GridInteractor,
+ ) : ViewModelProvider.Factory {
+
+ private val applicationContext = context.applicationContext
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ return GridScreenViewModel(
+ context = applicationContext,
+ interactor = interactor,
+ )
+ as T
+ }
+ }
+}