summaryrefslogtreecommitdiff
path: root/src/com/android/customization/picker/clock
diff options
context:
space:
mode:
authorGeorge Zacharia <george.zcharia@gmail.com>2023-07-02 14:33:47 +0530
committerGeorge Zacharia <george.zcharia@gmail.com>2023-07-02 14:33:47 +0530
commit913b11dfd2b52e445c773838c766f0d4f8ba0d05 (patch)
treeadb07f584833593bad6fca5495927c276ceef531 /src/com/android/customization/picker/clock
parentb2d9a4961b3804f79c151630421d480846fd0176 (diff)
parentcc6f666d7c0bc3b6927f6e9e3c7e46123be6263d (diff)
Merge tag 'android-13.0.0_r52' of https://android.googlesource.com/platform/packages/apps/ThemePicker into HEADHEADt13.0
Android 13.0.0 Release 52 (TQ3A.230605.012) Change-Id: I2cea11fa2f1f02fbd3c9d21cfc1697a79d42a5b7
Diffstat (limited to 'src/com/android/customization/picker/clock')
-rw-r--r--src/com/android/customization/picker/clock/ClockCustomDemoFragment.kt191
-rw-r--r--src/com/android/customization/picker/clock/ClockCustomFragment.java76
-rw-r--r--src/com/android/customization/picker/clock/ClockFacePickerActivity.java82
-rw-r--r--src/com/android/customization/picker/clock/ClockFragment.java209
-rw-r--r--src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt52
-rw-r--r--src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt193
-rw-r--r--src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt121
-rw-r--r--src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt65
-rw-r--r--src/com/android/customization/picker/clock/domain/interactor/ClocksSnapshotRestorer.kt5
-rw-r--r--src/com/android/customization/picker/clock/shared/ClockSize.kt22
-rw-r--r--src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt34
-rw-r--r--src/com/android/customization/picker/clock/ui/adapter/ClockSettingsTabAdapter.kt69
-rw-r--r--src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt110
-rw-r--r--src/com/android/customization/picker/clock/ui/binder/ClockSectionViewBinder.kt53
-rw-r--r--src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt181
-rw-r--r--src/com/android/customization/picker/clock/ui/fragment/ClockCustomDemoFragment.kt93
-rw-r--r--src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt135
-rw-r--r--src/com/android/customization/picker/clock/ui/section/ClockSectionController.kt62
-rw-r--r--src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt86
-rw-r--r--src/com/android/customization/picker/clock/ui/view/ClockSectionView.kt (renamed from src/com/android/customization/picker/clock/ClockSectionView.kt)2
-rw-r--r--src/com/android/customization/picker/clock/ui/view/ClockSizeRadioButtonGroup.kt50
-rw-r--r--src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt75
-rw-r--r--src/com/android/customization/picker/clock/ui/view/SaturationProgressDrawable.kt107
-rw-r--r--src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt98
-rw-r--r--src/com/android/customization/picker/clock/ui/viewmodel/ClockColorViewModel.kt69
-rw-r--r--src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt51
-rw-r--r--src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModel.kt28
-rw-r--r--src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt309
28 files changed, 2067 insertions, 561 deletions
diff --git a/src/com/android/customization/picker/clock/ClockCustomDemoFragment.kt b/src/com/android/customization/picker/clock/ClockCustomDemoFragment.kt
deleted file mode 100644
index 8648dca8..00000000
--- a/src/com/android/customization/picker/clock/ClockCustomDemoFragment.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-package com.android.customization.picker.clock
-
-import android.app.NotificationManager
-import android.content.ComponentName
-import android.content.Context
-import android.os.Bundle
-import android.os.Handler
-import android.os.UserHandle
-import android.view.ContextThemeWrapper
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.widget.FrameLayout
-import android.widget.TextView
-import androidx.core.view.setPadding
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.plugins.ClockMetadata
-import com.android.systemui.plugins.ClockProviderPlugin
-import com.android.systemui.plugins.Plugin
-import com.android.systemui.plugins.PluginListener
-import com.android.systemui.plugins.PluginManager
-import com.android.systemui.shared.clocks.ClockRegistry
-import com.android.systemui.shared.clocks.DefaultClockProvider
-import com.android.systemui.shared.plugins.PluginActionManager
-import com.android.systemui.shared.plugins.PluginEnabler
-import com.android.systemui.shared.plugins.PluginEnabler.ENABLED
-import com.android.systemui.shared.plugins.PluginInstance
-import com.android.systemui.shared.plugins.PluginManagerImpl
-import com.android.systemui.shared.plugins.PluginPrefs
-import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager_Factory
-import com.android.wallpaper.R
-import com.android.wallpaper.picker.AppbarFragment
-import java.util.concurrent.Executors
-
-private val TAG = ClockCustomDemoFragment::class.simpleName
-
-class ClockCustomDemoFragment : AppbarFragment() {
- @VisibleForTesting lateinit var clockRegistry: ClockRegistry
- val isDebugDevice = true
- val privilegedPlugins = listOf<String>()
- val action = ClockProviderPlugin.ACTION
- lateinit var view: ViewGroup
- @VisibleForTesting lateinit var recyclerView: RecyclerView
- lateinit var pluginManager: PluginManager
- @VisibleForTesting
- val pluginListener =
- object : PluginListener<ClockProviderPlugin> {
- override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) {
- val listInUse = clockRegistry.getClocks().filter { "NOT_IN_USE" !in it.clockId }
- recyclerView.adapter = ClockRecyclerAdapter(listInUse, context, clockRegistry)
- }
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- val defaultClockProvider =
- DefaultClockProvider(context, LayoutInflater.from(context), context.resources)
- pluginManager = createPluginManager(context)
- clockRegistry =
- ClockRegistry(
- context,
- pluginManager,
- Handler.getMain(),
- isEnabled = true,
- userHandle = UserHandle.USER_OWNER,
- defaultClockProvider
- )
- pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java, true)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- val view = inflater.inflate(R.layout.fragment_clock_custom_picker_demo, container, false)
- setUpToolbar(view)
- return view
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- recyclerView = view.requireViewById(R.id.clock_preview_card_list_demo)
- recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
- super.onViewCreated(view, savedInstanceState)
- }
-
- override fun getDefaultTitle(): CharSequence {
- return getString(R.string.clock_title)
- }
-
- private fun createPluginManager(context: Context): PluginManager {
- val instanceFactory =
- PluginInstance.Factory(
- this::class.java.classLoader,
- PluginInstance.InstanceFactory<Plugin>(),
- PluginInstance.VersionChecker(),
- privilegedPlugins,
- isDebugDevice
- )
-
- /*
- * let SystemUI handle plugin, in this class assume plugins are enabled
- */
- val pluginEnabler =
- object : PluginEnabler {
- override fun setEnabled(component: ComponentName) {}
-
- override fun setDisabled(
- component: ComponentName,
- @PluginEnabler.DisableReason reason: Int
- ) {}
-
- override fun isEnabled(component: ComponentName): Boolean {
- return true
- }
-
- @PluginEnabler.DisableReason
- override fun getDisableReason(componentName: ComponentName): Int {
- return ENABLED
- }
- }
-
- val pluginActionManager =
- PluginActionManager.Factory(
- context,
- context.packageManager,
- context.mainExecutor,
- Executors.newSingleThreadExecutor(),
- context.getSystemService(NotificationManager::class.java),
- pluginEnabler,
- privilegedPlugins,
- instanceFactory
- )
- return PluginManagerImpl(
- context,
- pluginActionManager,
- isDebugDevice,
- uncaughtExceptionPreHandlerManager,
- pluginEnabler,
- PluginPrefs(context),
- listOf()
- )
- }
-
- companion object {
- private val uncaughtExceptionPreHandlerManager =
- UncaughtExceptionPreHandlerManager_Factory.create().get()
- }
-
- internal class ClockRecyclerAdapter(
- val list: List<ClockMetadata>,
- val context: Context,
- val clockRegistry: ClockRegistry
- ) : RecyclerView.Adapter<ClockRecyclerAdapter.ViewHolder>() {
- class ViewHolder(val view: View, val textView: TextView, val onItemClicked: (Int) -> Unit) :
- RecyclerView.ViewHolder(view) {
- init {
- itemView.setOnClickListener { onItemClicked(absoluteAdapterPosition) }
- }
- }
-
- override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
- val rootView = FrameLayout(viewGroup.context)
- val textView =
- TextView(ContextThemeWrapper(viewGroup.context, R.style.SectionTitleTextStyle))
- textView.setPadding(ITEM_PADDING)
- rootView.addView(textView)
- val lp = RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
- rootView.setLayoutParams(lp)
- return ViewHolder(
- rootView,
- textView,
- { clockRegistry.currentClockId = list[it].clockId }
- )
- }
-
- override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
- viewHolder.textView.text = list[position].name
- }
-
- override fun getItemCount() = list.size
-
- companion object {
- val ITEM_PADDING = 40
- }
- }
-}
diff --git a/src/com/android/customization/picker/clock/ClockCustomFragment.java b/src/com/android/customization/picker/clock/ClockCustomFragment.java
deleted file mode 100644
index 56860fea..00000000
--- a/src/com/android/customization/picker/clock/ClockCustomFragment.java
+++ /dev/null
@@ -1,76 +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.clock;
-
-import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY;
-import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.customization.model.clock.custom.ClockCustomManager;
-import com.android.customization.model.clock.custom.ClockOption;
-import com.android.customization.widget.OptionSelectorController;
-import com.android.wallpaper.R;
-import com.android.wallpaper.picker.AppbarFragment;
-import com.android.wallpaper.widget.BottomActionBar;
-
-import com.google.common.collect.Lists;
-
-/**
- * Fragment that contains the main UI for selecting and applying a custom clock.
- */
-public class ClockCustomFragment extends AppbarFragment {
-
- OptionSelectorController<ClockOption> mClockSelectorController;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_clock_custom_picker, container, false);
-
- setUpToolbar(view);
-
- RecyclerView clockPreviewCardList = view.requireViewById(R.id.clock_preview_card_list);
-
- mClockSelectorController = new OptionSelectorController<>(clockPreviewCardList,
- Lists.newArrayList(new ClockOption(), new ClockOption(), new ClockOption(),
- new ClockOption(), new ClockOption()), false,
- OptionSelectorController.CheckmarkStyle.CENTER_CHANGE_COLOR_WHEN_NOT_SELECTED);
- mClockSelectorController.initOptions(new ClockCustomManager());
-
- return view;
- }
-
- @Override
- public CharSequence getDefaultTitle() {
- return getString(R.string.clock_title);
- }
-
- @Override
- protected void onBottomActionBarReady(BottomActionBar bottomActionBar) {
- super.onBottomActionBarReady(bottomActionBar);
- bottomActionBar.showActionsOnly(INFORMATION, APPLY);
- bottomActionBar.show();
- }
-}
diff --git a/src/com/android/customization/picker/clock/ClockFacePickerActivity.java b/src/com/android/customization/picker/clock/ClockFacePickerActivity.java
deleted file mode 100644
index 5e512341..00000000
--- a/src/com/android/customization/picker/clock/ClockFacePickerActivity.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2019 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.clock;
-
-import android.content.Intent;
-import android.os.Bundle;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentTransaction;
-import com.android.customization.model.clock.BaseClockManager;
-import com.android.customization.model.clock.Clockface;
-import com.android.customization.model.clock.ContentProviderClockProvider;
-import com.android.customization.picker.clock.ClockFragment.ClockFragmentHost;
-import com.android.wallpaper.R;
-
-/**
- * Activity allowing for the clock face picker to be linked to from other setup flows.
- *
- * This should be used with startActivityForResult. The resulting intent contains an extra
- * "clock_face_name" with the id of the picked clock face.
- */
-public class ClockFacePickerActivity extends FragmentActivity implements ClockFragmentHost {
-
- private static final String EXTRA_CLOCK_FACE_NAME = "clock_face_name";
-
- private BaseClockManager mClockManager;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_clock_face_picker);
-
- // Creating a class that overrides {@link ClockManager#apply} to return the clock id to the
- // calling activity instead of putting the value into settings.
- //
- mClockManager = new BaseClockManager(
- new ContentProviderClockProvider(ClockFacePickerActivity.this)) {
-
- @Override
- protected void handleApply(Clockface option, Callback callback) {
- Intent result = new Intent();
- result.putExtra(EXTRA_CLOCK_FACE_NAME, option.getId());
- setResult(RESULT_OK, result);
- callback.onSuccess();
- finish();
- }
-
- @Override
- protected String lookUpCurrentClock() {
- return getIntent().getStringExtra(EXTRA_CLOCK_FACE_NAME);
- }
- };
- if (!mClockManager.isAvailable()) {
- finish();
- } else {
- final FragmentManager fm = getSupportFragmentManager();
- final FragmentTransaction fragmentTransaction = fm.beginTransaction();
- final ClockFragment clockFragment = ClockFragment.newInstance(
- getString(R.string.clock_title));
- fragmentTransaction.replace(R.id.fragment_container, clockFragment);
- fragmentTransaction.commitNow();
- }
- }
-
- @Override
- public BaseClockManager getClockManager() {
- return mClockManager;
- }
-}
diff --git a/src/com/android/customization/picker/clock/ClockFragment.java b/src/com/android/customization/picker/clock/ClockFragment.java
deleted file mode 100644
index bc02ae34..00000000
--- a/src/com/android/customization/picker/clock/ClockFragment.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2019 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.clock;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.widget.ContentLoadingProgressBar;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.customization.model.CustomizationManager.Callback;
-import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
-import com.android.customization.model.clock.BaseClockManager;
-import com.android.customization.model.clock.Clockface;
-import com.android.customization.module.ThemesUserEventLogger;
-import com.android.customization.picker.BasePreviewAdapter;
-import com.android.customization.picker.BasePreviewAdapter.PreviewPage;
-import com.android.customization.widget.OptionSelectorController;
-import com.android.wallpaper.R;
-import com.android.wallpaper.asset.Asset;
-import com.android.wallpaper.module.InjectorProvider;
-import com.android.wallpaper.picker.AppbarFragment;
-import com.android.wallpaper.widget.PreviewPager;
-
-import java.util.List;
-
-/**
- * Fragment that contains the main UI for selecting and applying a Clockface.
- */
-public class ClockFragment extends AppbarFragment {
-
- private static final String TAG = "ClockFragment";
-
- /**
- * Interface to be implemented by an Activity hosting a {@link ClockFragment}
- */
- public interface ClockFragmentHost {
- BaseClockManager getClockManager();
- }
-
- public static ClockFragment newInstance(CharSequence title) {
- ClockFragment fragment = new ClockFragment();
- fragment.setArguments(AppbarFragment.createArguments(title));
- return fragment;
- }
-
- private RecyclerView mOptionsContainer;
- private OptionSelectorController<Clockface> mOptionsController;
- private Clockface mSelectedOption;
- private BaseClockManager mClockManager;
- private PreviewPager mPreviewPager;
- private ContentLoadingProgressBar mLoading;
- private View mContent;
- private View mError;
- private ThemesUserEventLogger mEventLogger;
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mClockManager = ((ClockFragmentHost) context).getClockManager();
- mEventLogger = (ThemesUserEventLogger)
- InjectorProvider.getInjector().getUserEventLogger(context);
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View view = inflater.inflate(
- R.layout.fragment_clock_picker, container, /* attachToRoot */ false);
- setUpToolbar(view);
- mContent = view.findViewById(R.id.content_section);
- mPreviewPager = view.findViewById(R.id.clock_preview_pager);
- mOptionsContainer = view.findViewById(R.id.options_container);
- mLoading = view.findViewById(R.id.loading_indicator);
- mError = view.findViewById(R.id.error_section);
- setUpOptions();
- view.findViewById(R.id.apply_button).setOnClickListener(v -> {
- mClockManager.apply(mSelectedOption, new Callback() {
- @Override
- public void onSuccess() {
- mOptionsController.setAppliedOption(mSelectedOption);
- Toast.makeText(getContext(), R.string.applied_clock_msg,
- Toast.LENGTH_SHORT).show();
- }
-
- @Override
- public void onError(@Nullable Throwable throwable) {
- if (throwable != null) {
- Log.e(TAG, "Error loading clockfaces", throwable);
- }
- //TODO(santie): handle
- }
- });
-
- });
- return view;
- }
-
- private void createAdapter() {
- mPreviewPager.setAdapter(new ClockPreviewAdapter(getActivity(), mSelectedOption));
- }
-
- private void setUpOptions() {
- hideError();
- mLoading.show();
- mClockManager.fetchOptions(new OptionsFetchedListener<Clockface>() {
- @Override
- public void onOptionsLoaded(List<Clockface> options) {
- mLoading.hide();
- mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
-
- mOptionsController.addListener(selected -> {
- mSelectedOption = (Clockface) selected;
- mEventLogger.logClockSelected(mSelectedOption);
- createAdapter();
- });
- mOptionsController.initOptions(mClockManager);
- for (Clockface option : options) {
- if (option.isActive(mClockManager)) {
- mSelectedOption = option;
- }
- }
- // For development only, as there should always be a grid set.
- if (mSelectedOption == null) {
- mSelectedOption = options.get(0);
- }
- createAdapter();
- }
- @Override
- public void onError(@Nullable Throwable throwable) {
- if (throwable != null) {
- Log.e(TAG, "Error loading clockfaces", throwable);
- }
- showError();
- }
- }, false);
- }
-
- private void hideError() {
- mContent.setVisibility(View.VISIBLE);
- mError.setVisibility(View.GONE);
- }
-
- private void showError() {
- mLoading.hide();
- mContent.setVisibility(View.GONE);
- mError.setVisibility(View.VISIBLE);
- }
-
- private static class ClockfacePreviewPage extends PreviewPage {
-
- private final Asset mPreviewAsset;
-
- public ClockfacePreviewPage(String title, Activity activity, Asset previewAsset) {
- super(title, activity);
- mPreviewAsset = previewAsset;
- }
-
- @Override
- public void bindPreviewContent() {
- ImageView previewImage = card.findViewById(R.id.clock_preview_image);
- Context context = previewImage.getContext();
- Resources res = previewImage.getResources();
- mPreviewAsset.loadDrawableWithTransition(context, previewImage,
- 100 /* transitionDurationMillis */,
- null /* drawableLoadedListener */,
- res.getColor(android.R.color.transparent, null) /* placeholderColor */);
- card.setContentDescription(card.getResources().getString(
- R.string.clock_preview_content_description, title));
- }
- }
-
- /**
- * Adapter class for mPreviewPager.
- * This is a ViewPager as it allows for a nice pagination effect (ie, pages snap on swipe,
- * we don't want to just scroll)
- */
- private static class ClockPreviewAdapter extends BasePreviewAdapter<ClockfacePreviewPage> {
- ClockPreviewAdapter(Activity activity, Clockface clockface) {
- super(activity, R.layout.clock_preview_card);
- addPage(new ClockfacePreviewPage(
- clockface.getTitle(), activity , clockface.getPreviewAsset()));
- }
- }
-}
diff --git a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
new file mode 100644
index 00000000..ae66ce3d
--- /dev/null
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.picker.clock.data.repository
+
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+import com.android.customization.picker.clock.shared.ClockSize
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Repository for accessing application clock settings, as well as selecting and configuring custom
+ * clocks.
+ */
+interface ClockPickerRepository {
+ val allClocks: Flow<List<ClockMetadataModel>>
+
+ val selectedClock: Flow<ClockMetadataModel>
+
+ val selectedClockSize: Flow<ClockSize>
+
+ fun setSelectedClock(clockId: String)
+
+ /**
+ * Set clock color to the settings.
+ *
+ * @param selectedColor selected color in the color option list.
+ * @param colorToneProgress color tone from 0 to 100 to apply to the selected color
+ * @param seedColor the actual clock color after blending the selected color and color tone
+ */
+ fun setClockColor(
+ selectedColorId: String?,
+ @IntRange(from = 0, to = 100) colorToneProgress: Int,
+ @ColorInt seedColor: Int?,
+ )
+
+ suspend fun setClockSize(size: ClockSize)
+}
diff --git a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
new file mode 100644
index 00000000..880a00be
--- /dev/null
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.picker.clock.data.repository
+
+import android.provider.Settings
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+import com.android.customization.picker.clock.shared.ClockSize
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.wallpaper.settings.data.repository.SecureSettingsRepository
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.shareIn
+import org.json.JSONObject
+
+/** Implementation of [ClockPickerRepository], using [ClockRegistry]. */
+class ClockPickerRepositoryImpl(
+ private val secureSettingsRepository: SecureSettingsRepository,
+ private val registry: ClockRegistry,
+ scope: CoroutineScope,
+) : ClockPickerRepository {
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override val allClocks: Flow<List<ClockMetadataModel>> =
+ callbackFlow {
+ fun send() {
+ val allClocks =
+ registry
+ .getClocks()
+ .filter { "NOT_IN_USE" !in it.clockId }
+ .map { it.toModel() }
+ trySend(allClocks)
+ }
+
+ val listener =
+ object : ClockRegistry.ClockChangeListener {
+ override fun onAvailableClocksChanged() {
+ send()
+ }
+ }
+ registry.registerClockChangeListener(listener)
+ send()
+ awaitClose { registry.unregisterClockChangeListener(listener) }
+ }
+ .mapLatest { allClocks ->
+ // Loading list of clock plugins can cause many consecutive calls of
+ // onAvailableClocksChanged(). We only care about the final fully-initiated clock
+ // list. Delay to avoid unnecessary too many emits.
+ delay(100)
+ allClocks
+ }
+
+ /** The currently-selected clock. This also emits the clock color information. */
+ override val selectedClock: Flow<ClockMetadataModel> =
+ callbackFlow {
+ fun send() {
+ val currentClockId = registry.currentClockId
+ val metadata = registry.settings?.metadata
+ val model =
+ registry
+ .getClocks()
+ .find { clockMetadata -> clockMetadata.clockId == currentClockId }
+ ?.toModel(
+ selectedColorId = metadata?.getSelectedColorId(),
+ colorTone = metadata?.getColorTone()
+ ?: ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ seedColor = registry.seedColor
+ )
+ trySend(model)
+ }
+
+ val listener =
+ object : ClockRegistry.ClockChangeListener {
+ override fun onCurrentClockChanged() {
+ send()
+ }
+
+ override fun onAvailableClocksChanged() {
+ send()
+ }
+ }
+ registry.registerClockChangeListener(listener)
+ send()
+ awaitClose { registry.unregisterClockChangeListener(listener) }
+ }
+ .mapNotNull { it }
+
+ override fun setSelectedClock(clockId: String) {
+ registry.mutateSetting { oldSettings ->
+ val newSettings = oldSettings.copy(clockId = clockId)
+ newSettings.metadata = oldSettings.metadata
+ newSettings
+ }
+ }
+
+ override fun setClockColor(
+ selectedColorId: String?,
+ @IntRange(from = 0, to = 100) colorToneProgress: Int,
+ @ColorInt seedColor: Int?,
+ ) {
+ registry.mutateSetting { oldSettings ->
+ val newSettings = oldSettings.copy(seedColor = seedColor)
+ newSettings.metadata =
+ oldSettings.metadata
+ .put(KEY_METADATA_SELECTED_COLOR_ID, selectedColorId)
+ .put(KEY_METADATA_COLOR_TONE_PROGRESS, colorToneProgress)
+ newSettings
+ }
+ }
+
+ override val selectedClockSize: SharedFlow<ClockSize> =
+ secureSettingsRepository
+ .intSetting(
+ name = Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
+ )
+ .map { setting -> setting == 1 }
+ .map { isDynamic -> if (isDynamic) ClockSize.DYNAMIC else ClockSize.SMALL }
+ .shareIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ replay = 1,
+ )
+
+ override suspend fun setClockSize(size: ClockSize) {
+ secureSettingsRepository.set(
+ name = Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
+ value = if (size == ClockSize.DYNAMIC) 1 else 0,
+ )
+ }
+
+ private fun JSONObject.getSelectedColorId(): String? {
+ return if (this.isNull(KEY_METADATA_SELECTED_COLOR_ID)) {
+ null
+ } else {
+ this.getString(KEY_METADATA_SELECTED_COLOR_ID)
+ }
+ }
+
+ private fun JSONObject.getColorTone(): Int {
+ return this.optInt(
+ KEY_METADATA_COLOR_TONE_PROGRESS,
+ ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
+ )
+ }
+
+ /** By default, [ClockMetadataModel] has no color information unless specified. */
+ private fun ClockMetadata.toModel(
+ selectedColorId: String? = null,
+ @IntRange(from = 0, to = 100) colorTone: Int = 0,
+ @ColorInt seedColor: Int? = null,
+ ): ClockMetadataModel {
+ return ClockMetadataModel(
+ clockId = clockId,
+ name = name,
+ selectedColorId = selectedColorId,
+ colorToneProgress = colorTone,
+ seedColor = seedColor,
+ )
+ }
+
+ companion object {
+ // The selected color in the color option list
+ private const val KEY_METADATA_SELECTED_COLOR_ID = "metadataSelectedColorId"
+
+ // The color tone to apply to the selected color
+ private const val KEY_METADATA_COLOR_TONE_PROGRESS = "metadataColorToneProgress"
+ }
+}
diff --git a/src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt b/src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt
new file mode 100644
index 00000000..bfe87c9c
--- /dev/null
+++ b/src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.picker.clock.data.repository
+
+import android.app.NotificationManager
+import android.content.ComponentName
+import android.content.Context
+import android.view.LayoutInflater
+import com.android.systemui.plugins.Plugin
+import com.android.systemui.plugins.PluginManager
+import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.systemui.shared.clocks.DefaultClockProvider
+import com.android.systemui.shared.plugins.PluginActionManager
+import com.android.systemui.shared.plugins.PluginEnabler
+import com.android.systemui.shared.plugins.PluginInstance
+import com.android.systemui.shared.plugins.PluginManagerImpl
+import com.android.systemui.shared.plugins.PluginPrefs
+import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager_Factory
+import java.util.concurrent.Executors
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Provide the [ClockRegistry] singleton. Note that we need to make sure that the [PluginManager]
+ * needs to be connected before [ClockRegistry] is ready to use.
+ */
+class ClockRegistryProvider(
+ private val context: Context,
+ private val coroutineScope: CoroutineScope,
+ private val mainDispatcher: CoroutineDispatcher,
+ private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ private val pluginManager: PluginManager by lazy { createPluginManager(context) }
+ private val clockRegistry: ClockRegistry by lazy {
+ ClockRegistry(
+ context,
+ pluginManager,
+ coroutineScope,
+ mainDispatcher,
+ backgroundDispatcher,
+ isEnabled = true,
+ handleAllUsers = false,
+ DefaultClockProvider(context, LayoutInflater.from(context), context.resources)
+ )
+ .apply { registerListeners() }
+ }
+
+ fun get(): ClockRegistry {
+ return clockRegistry
+ }
+
+ private fun createPluginManager(context: Context): PluginManager {
+ val privilegedPlugins = listOf<String>()
+ val isDebugDevice = true
+
+ val instanceFactory =
+ PluginInstance.Factory(
+ this::class.java.classLoader,
+ PluginInstance.InstanceFactory<Plugin>(),
+ PluginInstance.VersionChecker(),
+ privilegedPlugins,
+ isDebugDevice,
+ )
+
+ /*
+ * let SystemUI handle plugin, in this class assume plugins are enabled
+ */
+ val pluginEnabler =
+ object : PluginEnabler {
+ override fun setEnabled(component: ComponentName) = Unit
+
+ override fun setDisabled(
+ component: ComponentName,
+ @PluginEnabler.DisableReason reason: Int
+ ) = Unit
+
+ override fun isEnabled(component: ComponentName): Boolean {
+ return true
+ }
+
+ @PluginEnabler.DisableReason
+ override fun getDisableReason(componentName: ComponentName): Int {
+ return PluginEnabler.ENABLED
+ }
+ }
+
+ val pluginActionManager =
+ PluginActionManager.Factory(
+ context,
+ context.packageManager,
+ context.mainExecutor,
+ Executors.newSingleThreadExecutor(),
+ context.getSystemService(NotificationManager::class.java),
+ pluginEnabler,
+ privilegedPlugins,
+ instanceFactory,
+ )
+ return PluginManagerImpl(
+ context,
+ pluginActionManager,
+ isDebugDevice,
+ UncaughtExceptionPreHandlerManager_Factory.create().get(),
+ pluginEnabler,
+ PluginPrefs(context),
+ listOf(),
+ )
+ }
+}
diff --git a/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
new file mode 100644
index 00000000..6f3657ab
--- /dev/null
+++ b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.picker.clock.domain.interactor
+
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+import com.android.customization.picker.clock.data.repository.ClockPickerRepository
+import com.android.customization.picker.clock.shared.ClockSize
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/**
+ * Interactor for accessing application clock settings, as well as selecting and configuring custom
+ * clocks.
+ */
+class ClockPickerInteractor(private val repository: ClockPickerRepository) {
+
+ val allClocks: Flow<List<ClockMetadataModel>> = repository.allClocks
+
+ val selectedClockId: Flow<String> =
+ repository.selectedClock.map { clock -> clock.clockId }.distinctUntilChanged()
+
+ val selectedColorId: Flow<String?> =
+ repository.selectedClock.map { clock -> clock.selectedColorId }.distinctUntilChanged()
+
+ val colorToneProgress: Flow<Int> =
+ repository.selectedClock.map { clock -> clock.colorToneProgress }
+
+ val seedColor: Flow<Int?> = repository.selectedClock.map { clock -> clock.seedColor }
+
+ val selectedClockSize: Flow<ClockSize> = repository.selectedClockSize
+
+ fun setSelectedClock(clockId: String) {
+ repository.setSelectedClock(clockId)
+ }
+
+ fun setClockColor(
+ selectedColorId: String?,
+ @IntRange(from = 0, to = 100) colorToneProgress: Int,
+ @ColorInt seedColor: Int?,
+ ) {
+ repository.setClockColor(selectedColorId, colorToneProgress, seedColor)
+ }
+
+ suspend fun setClockSize(size: ClockSize) {
+ repository.setClockSize(size)
+ }
+}
diff --git a/src/com/android/customization/picker/clock/domain/interactor/ClocksSnapshotRestorer.kt b/src/com/android/customization/picker/clock/domain/interactor/ClocksSnapshotRestorer.kt
index 63b4a9ba..7bb3232b 100644
--- a/src/com/android/customization/picker/clock/domain/interactor/ClocksSnapshotRestorer.kt
+++ b/src/com/android/customization/picker/clock/domain/interactor/ClocksSnapshotRestorer.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -18,12 +18,13 @@
package com.android.customization.picker.clock.domain.interactor
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
/** Handles state restoration for clocks. */
class ClocksSnapshotRestorer : SnapshotRestorer {
override suspend fun setUpSnapshotRestorer(
- updater: (RestorableSnapshot) -> Unit,
+ store: SnapshotStore,
): RestorableSnapshot {
// TODO(b/262924055): implement as part of the clock settings screen.
return RestorableSnapshot(mapOf())
diff --git a/src/com/android/customization/picker/clock/shared/ClockSize.kt b/src/com/android/customization/picker/clock/shared/ClockSize.kt
new file mode 100644
index 00000000..279ee54b
--- /dev/null
+++ b/src/com/android/customization/picker/clock/shared/ClockSize.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.picker.clock.shared
+
+enum class ClockSize {
+ DYNAMIC,
+ SMALL,
+}
diff --git a/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt b/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt
new file mode 100644
index 00000000..bd87ba6e
--- /dev/null
+++ b/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.picker.clock.shared.model
+
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+
+/** Model for clock metadata. */
+data class ClockMetadataModel(
+ val clockId: String,
+ val name: String,
+ val selectedColorId: String?,
+ @IntRange(from = 0, to = 100) val colorToneProgress: Int,
+ @ColorInt val seedColor: Int?,
+) {
+ companion object {
+ const val DEFAULT_COLOR_TONE_PROGRESS = 75
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/adapter/ClockSettingsTabAdapter.kt b/src/com/android/customization/picker/clock/ui/adapter/ClockSettingsTabAdapter.kt
new file mode 100644
index 00000000..746fdb30
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/adapter/ClockSettingsTabAdapter.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.picker.clock.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsTabViewModel
+import com.android.wallpaper.R
+
+/** Adapter for the tab recycler view on the clock settings screen. */
+class ClockSettingsTabAdapter : RecyclerView.Adapter<ClockSettingsTabAdapter.ViewHolder>() {
+
+ private val items = mutableListOf<ClockSettingsTabViewModel>()
+
+ fun setItems(items: List<ClockSettingsTabViewModel>) {
+ this.items.clear()
+ this.items.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context)
+ .inflate(
+ R.layout.picker_fragment_tab,
+ parent,
+ false,
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ holder.itemView.isSelected = item.isSelected
+ holder.textView.text = item.name
+ holder.textView.setOnClickListener(
+ if (item.onClicked != null) {
+ View.OnClickListener { item.onClicked.invoke() }
+ } else {
+ null
+ }
+ )
+ }
+
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val textView: TextView = itemView.requireViewById(R.id.text)
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
new file mode 100644
index 00000000..9ad735d3
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.picker.clock.ui.binder
+
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.customization.picker.clock.ui.view.ClockCarouselView
+import com.android.customization.picker.clock.ui.view.ClockViewFactory
+import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.launch
+
+object ClockCarouselViewBinder {
+ /**
+ * The binding is used by the view where there is an action executed from another view, e.g.
+ * toggling show/hide of the view that the binder is holding.
+ */
+ interface Binding {
+ fun show()
+ fun hide()
+ }
+
+ @JvmStatic
+ fun bind(
+ carouselView: ClockCarouselView,
+ singleClockView: ViewGroup,
+ viewModel: ClockCarouselViewModel,
+ clockViewFactory: ClockViewFactory,
+ lifecycleOwner: LifecycleOwner,
+ ): Binding {
+ val singleClockHostView =
+ singleClockView.requireViewById<FrameLayout>(R.id.single_clock_host_view)
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch { viewModel.isCarouselVisible.collect { carouselView.isVisible = it } }
+
+ launch {
+ viewModel.allClockIds.collect { allClockIds ->
+ carouselView.setUpClockCarouselView(
+ clockIds = allClockIds,
+ onGetClockPreview = { clockId -> clockViewFactory.getView(clockId) },
+ onClockSelected = { clockId -> viewModel.setSelectedClock(clockId) },
+ )
+ }
+ }
+
+ launch {
+ viewModel.selectedIndex.collect { selectedIndex ->
+ carouselView.setSelectedClockIndex(selectedIndex)
+ }
+ }
+
+ launch {
+ viewModel.seedColor.collect { clockViewFactory.updateColorForAllClocks(it) }
+ }
+
+ launch {
+ viewModel.isSingleClockViewVisible.collect { singleClockView.isVisible = it }
+ }
+
+ launch {
+ viewModel.clockId.collect { clockId ->
+ singleClockHostView.removeAllViews()
+ val clockView = clockViewFactory.getView(clockId)
+ // The clock view might still be attached to an existing parent. Detach
+ // before adding to another parent.
+ (clockView.parent as? ViewGroup)?.removeView(clockView)
+ singleClockHostView.addView(clockView)
+ }
+ }
+ }
+ }
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ clockViewFactory.registerTimeTicker()
+ }
+ // When paused
+ clockViewFactory.unregisterTimeTicker()
+ }
+
+ return object : Binding {
+ override fun show() {
+ viewModel.showClockCarousel(true)
+ }
+
+ override fun hide() {
+ viewModel.showClockCarousel(false)
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSectionViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSectionViewBinder.kt
new file mode 100644
index 00000000..7dc0d0c4
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockSectionViewBinder.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.picker.clock.ui.binder
+
+import android.view.View
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+object ClockSectionViewBinder {
+ fun bind(
+ view: View,
+ viewModel: ClockSectionViewModel,
+ lifecycleOwner: LifecycleOwner,
+ onClicked: () -> Unit,
+ ) {
+ view.setOnClickListener { onClicked() }
+
+ val selectedClockColorAndSize: TextView =
+ view.requireViewById(R.id.selected_clock_color_and_size)
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.selectedClockColorAndSizeText.collectLatest {
+ selectedClockColorAndSize.text = it
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
new file mode 100644
index 00000000..66d92513
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.picker.clock.ui.binder
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.SeekBar
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+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.picker.clock.shared.ClockSize
+import com.android.customization.picker.clock.ui.adapter.ClockSettingsTabAdapter
+import com.android.customization.picker.clock.ui.view.ClockSizeRadioButtonGroup
+import com.android.customization.picker.clock.ui.view.ClockViewFactory
+import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
+import com.android.customization.picker.color.ui.adapter.ColorOptionAdapter
+import com.android.customization.picker.common.ui.view.ItemSpacing
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+
+/** Bind between the clock settings screen and its view model. */
+object ClockSettingsBinder {
+ fun bind(
+ view: View,
+ viewModel: ClockSettingsViewModel,
+ clockViewFactory: ClockViewFactory,
+ lifecycleOwner: LifecycleOwner,
+ ) {
+ val clockHostView: FrameLayout = view.requireViewById(R.id.clock_host_view)
+
+ val tabView: RecyclerView = view.requireViewById(R.id.tabs)
+ val tabAdapter = ClockSettingsTabAdapter()
+ tabView.adapter = tabAdapter
+ tabView.layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+ tabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP))
+
+ val colorOptionContainerView: RecyclerView = view.requireViewById(R.id.color_options)
+ val colorOptionAdapter = ColorOptionAdapter()
+ colorOptionContainerView.adapter = colorOptionAdapter
+ colorOptionContainerView.layoutManager =
+ LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+ colorOptionContainerView.addItemDecoration(ItemSpacing(ItemSpacing.ITEM_SPACING_DP))
+
+ val slider: SeekBar = view.requireViewById(R.id.slider)
+ slider.setOnSeekBarChangeListener(
+ object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ viewModel.onSliderProgressChanged(progress)
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {
+ seekBar?.progress?.let { viewModel.onSliderProgressStop(it) }
+ }
+ }
+ )
+
+ val sizeOptions =
+ view.requireViewById<ClockSizeRadioButtonGroup>(R.id.clock_size_radio_button_group)
+ sizeOptions.onRadioButtonClickListener =
+ object : ClockSizeRadioButtonGroup.OnRadioButtonClickListener {
+ override fun onClick(size: ClockSize) {
+ viewModel.setClockSize(size)
+ }
+ }
+
+ val colorOptionContainer = view.requireViewById<View>(R.id.color_picker_container)
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.selectedClockId
+ .mapNotNull { it }
+ .collect { clockId ->
+ val clockView = clockViewFactory.getView(clockId)
+ (clockView.parent as? ViewGroup)?.removeView(clockView)
+ clockHostView.removeAllViews()
+ clockHostView.addView(clockView)
+ }
+ }
+
+ launch {
+ viewModel.seedColor.collect { seedColor ->
+ viewModel.selectedClockId.value?.let { selectedClockId ->
+ clockViewFactory.updateColor(selectedClockId, seedColor)
+ }
+ }
+ }
+
+ launch { viewModel.tabs.collect { tabAdapter.setItems(it) } }
+
+ launch {
+ viewModel.selectedTab.collect { tab ->
+ when (tab) {
+ ClockSettingsViewModel.Tab.COLOR -> {
+ colorOptionContainer.isVisible = true
+ sizeOptions.isInvisible = true
+ }
+ ClockSettingsViewModel.Tab.SIZE -> {
+ colorOptionContainer.isInvisible = true
+ sizeOptions.isVisible = true
+ }
+ }
+ }
+ }
+
+ launch {
+ viewModel.colorOptions.collect { colorOptions ->
+ colorOptionAdapter.setItems(colorOptions)
+ }
+ }
+
+ launch {
+ viewModel.selectedColorOptionPosition.collect { selectedPosition ->
+ if (selectedPosition != -1) {
+ // We use "post" because we need to give the adapter item a pass to
+ // update the view.
+ colorOptionContainerView.post {
+ colorOptionContainerView.smoothScrollToPosition(selectedPosition)
+ }
+ }
+ }
+ }
+
+ launch {
+ viewModel.selectedClockSize.collect { size ->
+ when (size) {
+ ClockSize.DYNAMIC -> {
+ sizeOptions.radioButtonDynamic.isChecked = true
+ sizeOptions.radioButtonSmall.isChecked = false
+ }
+ ClockSize.SMALL -> {
+ sizeOptions.radioButtonDynamic.isChecked = false
+ sizeOptions.radioButtonSmall.isChecked = true
+ }
+ }
+ }
+ }
+
+ launch {
+ viewModel.sliderProgress.collect { progress ->
+ slider.setProgress(progress, true)
+ }
+ }
+
+ launch {
+ viewModel.isSliderEnabled.collect { isEnabled -> slider.isEnabled = isEnabled }
+ }
+ }
+ }
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ clockViewFactory.registerTimeTicker()
+ }
+ // When paused
+ clockViewFactory.unregisterTimeTicker()
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockCustomDemoFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockCustomDemoFragment.kt
new file mode 100644
index 00000000..7e53ac44
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/fragment/ClockCustomDemoFragment.kt
@@ -0,0 +1,93 @@
+package com.android.customization.picker.clock.ui.fragment
+
+import android.content.Context
+import android.os.Bundle
+import android.util.TypedValue
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.FrameLayout
+import android.widget.TextView
+import android.widget.Toast
+import androidx.core.view.setPadding
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.module.ThemePickerInjector
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.wallpaper.R
+import com.android.wallpaper.module.InjectorProvider
+import com.android.wallpaper.picker.AppbarFragment
+
+class ClockCustomDemoFragment : AppbarFragment() {
+ @VisibleForTesting lateinit var recyclerView: RecyclerView
+ @VisibleForTesting lateinit var clockRegistry: ClockRegistry
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view = inflater.inflate(R.layout.fragment_clock_custom_picker_demo, container, false)
+ setUpToolbar(view)
+ clockRegistry =
+ (InjectorProvider.getInjector() as ThemePickerInjector).getClockRegistry(
+ requireContext()
+ )
+ val listInUse = clockRegistry.getClocks().filter { "NOT_IN_USE" !in it.clockId }
+
+ recyclerView = view.requireViewById(R.id.clock_preview_card_list_demo)
+ recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ recyclerView.adapter =
+ ClockRecyclerAdapter(listInUse, requireContext()) {
+ clockRegistry.currentClockId = it.clockId
+ Toast.makeText(context, "${it.name} selected", Toast.LENGTH_SHORT).show()
+ }
+ return view
+ }
+
+ override fun getDefaultTitle(): CharSequence {
+ return getString(R.string.clock_title)
+ }
+
+ internal class ClockRecyclerAdapter(
+ val list: List<ClockMetadata>,
+ val context: Context,
+ val onClockSelected: (ClockMetadata) -> Unit
+ ) : RecyclerView.Adapter<ClockRecyclerAdapter.ViewHolder>() {
+ class ViewHolder(val view: View, val textView: TextView, val onItemClicked: (Int) -> Unit) :
+ RecyclerView.ViewHolder(view) {
+ init {
+ itemView.setOnClickListener { onItemClicked(absoluteAdapterPosition) }
+ }
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
+ val rootView = FrameLayout(viewGroup.context)
+ val textView =
+ TextView(ContextThemeWrapper(viewGroup.context, R.style.SectionTitleTextStyle))
+ textView.setPadding(ITEM_PADDING)
+ rootView.addView(textView)
+ val outValue = TypedValue()
+ context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true)
+ rootView.setBackgroundResource(outValue.resourceId)
+ val lp = RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ rootView.layoutParams = lp
+ return ViewHolder(rootView, textView) { onClockSelected(list[it]) }
+ }
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+ viewHolder.textView.text = list[position].name
+ }
+
+ override fun getItemCount() = list.size
+
+ companion object {
+ val ITEM_PADDING = 40
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
new file mode 100644
index 00000000..c5cde530
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.picker.clock.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.cardview.widget.CardView
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.get
+import com.android.customization.module.ThemePickerInjector
+import com.android.customization.picker.clock.ui.binder.ClockSettingsBinder
+import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
+import com.android.wallpaper.R
+import com.android.wallpaper.module.InjectorProvider
+import com.android.wallpaper.picker.AppbarFragment
+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.ExperimentalCoroutinesApi
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ClockSettingsFragment : AppbarFragment() {
+ companion object {
+ const val DESTINATION_ID = "clock_settings"
+
+ @JvmStatic
+ fun newInstance(): ClockSettingsFragment {
+ return ClockSettingsFragment()
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view =
+ inflater.inflate(
+ R.layout.fragment_clock_settings,
+ container,
+ false,
+ )
+ setUpToolbar(view)
+
+ val context = requireContext()
+ val activity = requireActivity()
+ val injector = InjectorProvider.getInjector() as ThemePickerInjector
+
+ val lockScreenView: CardView = view.requireViewById(R.id.lock_preview)
+ val colorViewModel = injector.getWallpaperColorsViewModel()
+ val displayUtils = injector.getDisplayUtils(context)
+ ScreenPreviewBinder.bind(
+ activity = activity,
+ previewView = lockScreenView,
+ viewModel =
+ ScreenPreviewViewModel(
+ previewUtils =
+ PreviewUtils(
+ context = context,
+ authority =
+ resources.getString(
+ R.string.lock_screen_preview_provider_authority,
+ ),
+ ),
+ wallpaperInfoProvider = {
+ suspendCancellableCoroutine { continuation ->
+ injector
+ .getCurrentWallpaperInfoFactory(context)
+ .createCurrentWallpaperInfos(
+ { homeWallpaper, lockWallpaper, _ ->
+ continuation.resume(
+ homeWallpaper ?: lockWallpaper,
+ null,
+ )
+ },
+ /* forceRefresh= */ true,
+ )
+ }
+ },
+ onWallpaperColorChanged = { colors ->
+ colorViewModel.setLockWallpaperColors(colors)
+ },
+ initialExtrasProvider = {
+ Bundle().apply {
+ // Hide the clock from the system UI rendered preview so we can
+ // place the carousel on top of it.
+ putBoolean(
+ ClockPreviewConstants.KEY_HIDE_CLOCK,
+ true,
+ )
+ }
+ },
+ ),
+ lifecycleOwner = this,
+ offsetToStart = displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(activity),
+ )
+ .show()
+
+ ClockSettingsBinder.bind(
+ view,
+ ViewModelProvider(
+ requireActivity(),
+ injector.getClockSettingsViewModelFactory(
+ context,
+ injector.getWallpaperColorsViewModel(),
+ ),
+ )
+ .get(),
+ injector.getClockViewFactory(activity),
+ this@ClockSettingsFragment,
+ )
+
+ return view
+ }
+
+ override fun getDefaultTitle(): CharSequence {
+ return requireContext().getString(R.string.clock_settings_title)
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/section/ClockSectionController.kt b/src/com/android/customization/picker/clock/ui/section/ClockSectionController.kt
new file mode 100644
index 00000000..b47c2433
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/section/ClockSectionController.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.clock.ui.section
+
+import android.content.Context
+import android.view.LayoutInflater
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.android.customization.picker.clock.ui.binder.ClockSectionViewBinder
+import com.android.customization.picker.clock.ui.fragment.ClockSettingsFragment
+import com.android.customization.picker.clock.ui.view.ClockSectionView
+import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.wallpaper.R
+import com.android.wallpaper.config.BaseFlags
+import com.android.wallpaper.model.CustomizationSectionController
+import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController
+import kotlinx.coroutines.launch
+
+/** A [CustomizationSectionController] for clock customization. */
+class ClockSectionController(
+ private val navigationController: CustomizationSectionNavigationController,
+ private val lifecycleOwner: LifecycleOwner,
+ private val flag: BaseFlags,
+ private val viewModel: ClockSectionViewModel,
+) : CustomizationSectionController<ClockSectionView> {
+
+ override fun isAvailable(context: Context): Boolean {
+ return flag.isCustomClocksEnabled(context!!)
+ }
+
+ override fun createView(context: Context): ClockSectionView {
+ val view =
+ LayoutInflater.from(context)
+ .inflate(
+ R.layout.clock_section_view,
+ null,
+ ) as ClockSectionView
+ lifecycleOwner.lifecycleScope.launch {
+ ClockSectionViewBinder.bind(
+ view = view,
+ viewModel = viewModel,
+ lifecycleOwner = lifecycleOwner
+ ) {
+ navigationController.navigateTo(ClockSettingsFragment())
+ }
+ }
+ return view
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
new file mode 100644
index 00000000..90d7c42d
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.picker.clock.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.constraintlayout.helper.widget.Carousel
+import androidx.core.view.get
+import com.android.wallpaper.R
+
+class ClockCarouselView(
+ context: Context,
+ attrs: AttributeSet,
+) :
+ FrameLayout(
+ context,
+ attrs,
+ ) {
+
+ private val carousel: Carousel
+ private lateinit var adapter: ClockCarouselAdapter
+
+ init {
+ val view = LayoutInflater.from(context).inflate(R.layout.clock_carousel, this)
+ carousel = view.requireViewById(R.id.carousel)
+ }
+
+ fun setUpClockCarouselView(
+ clockIds: List<String>,
+ onGetClockPreview: (clockId: String) -> View,
+ onClockSelected: (clockId: String) -> Unit,
+ ) {
+ adapter = ClockCarouselAdapter(clockIds, onGetClockPreview, onClockSelected)
+ carousel.setAdapter(adapter)
+ carousel.refresh()
+ }
+
+ fun setSelectedClockIndex(
+ index: Int,
+ ) {
+ carousel.jumpToIndex(index)
+ }
+
+ class ClockCarouselAdapter(
+ val clockIds: List<String>,
+ val onGetClockPreview: (clockId: String) -> View,
+ val onClockSelected: (clockId: String) -> Unit,
+ ) : Carousel.Adapter {
+
+ override fun count(): Int {
+ return clockIds.size
+ }
+
+ override fun populate(view: View?, index: Int) {
+ val viewRoot = view as ViewGroup
+ val clockHostView = viewRoot[0] as ViewGroup
+ clockHostView.removeAllViews()
+ val clockView = onGetClockPreview(clockIds[index])
+ // The clock view might still be attached to an existing parent. Detach before adding to
+ // another parent.
+ (clockView.parent as? ViewGroup)?.removeView(clockView)
+ clockHostView.addView(clockView)
+ }
+
+ override fun onNewItem(index: Int) {
+ onClockSelected.invoke(clockIds[index])
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ClockSectionView.kt b/src/com/android/customization/picker/clock/ui/view/ClockSectionView.kt
index fac975ab..cca107c4 100644
--- a/src/com/android/customization/picker/clock/ClockSectionView.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockSectionView.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.customization.picker.clock
+package com.android.customization.picker.clock.ui.view
import android.content.Context
import android.util.AttributeSet
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockSizeRadioButtonGroup.kt b/src/com/android/customization/picker/clock/ui/view/ClockSizeRadioButtonGroup.kt
new file mode 100644
index 00000000..909491a3
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/view/ClockSizeRadioButtonGroup.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.picker.clock.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.RadioButton
+import com.android.customization.picker.clock.shared.ClockSize
+import com.android.wallpaper.R
+
+/** The radio button group to pick the clock size. */
+class ClockSizeRadioButtonGroup(
+ context: Context,
+ attrs: AttributeSet?,
+) : FrameLayout(context, attrs) {
+
+ interface OnRadioButtonClickListener {
+ fun onClick(size: ClockSize)
+ }
+
+ val radioButtonDynamic: RadioButton
+ val radioButtonSmall: RadioButton
+ var onRadioButtonClickListener: OnRadioButtonClickListener? = null
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.clock_size_radio_button_group, this, true)
+ radioButtonDynamic = requireViewById(R.id.radio_button_dynamic)
+ val buttonDynamic = requireViewById<View>(R.id.button_container_dynamic)
+ buttonDynamic.setOnClickListener { onRadioButtonClickListener?.onClick(ClockSize.DYNAMIC) }
+ radioButtonSmall = requireViewById(R.id.radio_button_large)
+ val buttonLarge = requireViewById<View>(R.id.button_container_small)
+ buttonLarge.setOnClickListener { onRadioButtonClickListener?.onClick(ClockSize.SMALL) }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
new file mode 100644
index 00000000..7f480dee
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.picker.clock.ui.view
+
+import android.app.Activity
+import android.view.View
+import androidx.annotation.ColorInt
+import com.android.systemui.plugins.ClockController
+import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.wallpaper.R
+import com.android.wallpaper.util.ScreenSizeCalculator
+import com.android.wallpaper.util.TimeUtils.TimeTicker
+
+class ClockViewFactory(
+ private val activity: Activity,
+ private val registry: ClockRegistry,
+) {
+ private val clockControllers: HashMap<String, ClockController> = HashMap()
+ private var ticker: TimeTicker? = null
+
+ fun getView(clockId: String): View {
+ return (clockControllers[clockId] ?: initClockController(clockId)).largeClock.view
+ }
+
+ fun updateColorForAllClocks(@ColorInt seedColor: Int?) {
+ clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) }
+ }
+
+ fun updateColor(clockId: String, @ColorInt seedColor: Int?) {
+ return (clockControllers[clockId] ?: initClockController(clockId))
+ .events
+ .onSeedColorChanged(seedColor)
+ }
+
+ fun registerTimeTicker() {
+ ticker =
+ TimeTicker.registerNewReceiver(activity.applicationContext) {
+ clockControllers.values.forEach { it.largeClock.events.onTimeTick() }
+ }
+ }
+
+ fun unregisterTimeTicker() {
+ activity.applicationContext.unregisterReceiver(ticker)
+ }
+
+ private fun initClockController(clockId: String): ClockController {
+ val controller =
+ registry.createExampleClock(clockId).also { it?.initialize(activity.resources, 0f, 0f) }
+ checkNotNull(controller)
+ val screenSizeCalculator = ScreenSizeCalculator.getInstance()
+ val screenSize = screenSizeCalculator.getScreenSize(activity.windowManager.defaultDisplay)
+ val ratio =
+ activity.resources.getDimensionPixelSize(R.dimen.screen_preview_height).toFloat() /
+ screenSize.y.toFloat()
+ controller.largeClock.events.onFontSettingChanged(
+ activity.resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() *
+ ratio
+ )
+ clockControllers[clockId] = controller
+ return controller
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/view/SaturationProgressDrawable.kt b/src/com/android/customization/picker/clock/ui/view/SaturationProgressDrawable.kt
new file mode 100644
index 00000000..9e1d7890
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/view/SaturationProgressDrawable.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.picker.clock.ui.view
+
+import android.content.pm.ActivityInfo
+import android.content.res.Resources
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.DrawableWrapper
+import android.graphics.drawable.InsetDrawable
+
+/**
+ * [DrawableWrapper] to use in the progress of brightness slider.
+ *
+ * This drawable is used to change the bounds of the enclosed drawable depending on the level to
+ * simulate a sliding progress, instead of using clipping or scaling. That way, the shape of the
+ * edges is maintained.
+ *
+ * Meant to be used with a rounded ends background, it will also prevent deformation when the slider
+ * is meant to be smaller than the rounded corner. The background should have rounded corners that
+ * are half of the height.
+ *
+ * This class also assumes that a "thumb" icon exists within the end's edge of the progress
+ * drawable, and the slider's width, when interacted on, if offset by half the size of the thumb
+ * icon which puts the icon directly underneath the user's finger.
+ */
+class SaturationProgressDrawable @JvmOverloads constructor(drawable: Drawable? = null) :
+ InsetDrawable(drawable, 0) {
+
+ companion object {
+ private const val MAX_LEVEL = 10000
+ }
+
+ override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
+ onLevelChange(level)
+ return super.onLayoutDirectionChanged(layoutDirection)
+ }
+
+ override fun onBoundsChange(bounds: Rect) {
+ super.onBoundsChange(bounds)
+ onLevelChange(level)
+ }
+
+ override fun onLevelChange(level: Int): Boolean {
+ val drawableBounds = drawable?.bounds!!
+
+ // The thumb offset shifts the sun icon directly under the user's thumb
+ val thumbOffset = bounds.height() / 2
+ val width = bounds.width() * level / MAX_LEVEL + thumbOffset
+
+ // On 0, the width is bounds.height (a circle), and on MAX_LEVEL, the width is bounds.width
+ // TODO (b/268541542) Test if RTL devices also works for the slider
+ drawable?.setBounds(
+ bounds.left,
+ drawableBounds.top,
+ width.coerceAtMost(bounds.width()).coerceAtLeast(bounds.height()),
+ drawableBounds.bottom
+ )
+ return super.onLevelChange(level)
+ }
+
+ override fun getConstantState(): ConstantState {
+ // This should not be null as it was created with a state in the constructor.
+ return RoundedCornerState(super.getConstantState()!!)
+ }
+
+ override fun getChangingConfigurations(): Int {
+ return super.getChangingConfigurations() or ActivityInfo.CONFIG_DENSITY
+ }
+
+ override fun canApplyTheme(): Boolean {
+ return (drawable?.canApplyTheme() ?: false) || super.canApplyTheme()
+ }
+
+ private class RoundedCornerState(private val wrappedState: ConstantState) : ConstantState() {
+ override fun newDrawable(): Drawable {
+ return newDrawable(null, null)
+ }
+
+ override fun newDrawable(res: Resources?, theme: Resources.Theme?): Drawable {
+ val wrapper = wrappedState.newDrawable(res, theme) as DrawableWrapper
+ return SaturationProgressDrawable(wrapper.drawable)
+ }
+
+ override fun getChangingConfigurations(): Int {
+ return wrappedState.changingConfigurations
+ }
+
+ override fun canApplyTheme(): Boolean {
+ return true
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
new file mode 100644
index 00000000..60a9e851
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.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.picker.clock.ui.viewmodel
+
+import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+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.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
+
+/**
+ * Clock carousel view model that provides data for the carousel of clock previews. When there is
+ * only one item, we should show a single clock preview instead of a carousel.
+ */
+class ClockCarouselViewModel(
+ private val interactor: ClockPickerInteractor,
+) {
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val allClockIds: Flow<List<String>> =
+ interactor.allClocks.mapLatest { allClocks ->
+ // Delay to avoid the case that the full list of clocks is not initiated.
+ delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
+ allClocks.map { it.clockId }
+ }
+
+ val seedColor: Flow<Int?> = interactor.seedColor
+
+ private val shouldShowCarousel = MutableStateFlow(false)
+ val isCarouselVisible: Flow<Boolean> =
+ combine(allClockIds.map { it.size > 1 }.distinctUntilChanged(), shouldShowCarousel) {
+ hasMoreThanOneClock,
+ shouldShowCarousel ->
+ hasMoreThanOneClock && shouldShowCarousel
+ }
+ .distinctUntilChanged()
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val selectedIndex: Flow<Int> =
+ allClockIds
+ .flatMapLatest { allClockIds ->
+ interactor.selectedClockId.map { selectedClockId ->
+ val index = allClockIds.indexOf(selectedClockId)
+ if (index >= 0) {
+ index
+ } else {
+ null
+ }
+ }
+ }
+ .mapNotNull { it }
+
+ // Handle the case when there is only one clock in the carousel
+ private val shouldShowSingleClock = MutableStateFlow(false)
+ val isSingleClockViewVisible: Flow<Boolean> =
+ combine(allClockIds.map { it.size == 1 }.distinctUntilChanged(), shouldShowSingleClock) {
+ hasOneClock,
+ shouldShowSingleClock ->
+ hasOneClock && shouldShowSingleClock
+ }
+ .distinctUntilChanged()
+
+ val clockId: Flow<String> =
+ allClockIds
+ .map { allClockIds -> if (allClockIds.size == 1) allClockIds[0] else null }
+ .mapNotNull { it }
+
+ fun setSelectedClock(clockId: String) {
+ interactor.setSelectedClock(clockId)
+ }
+
+ fun showClockCarousel(shouldShow: Boolean) {
+ shouldShowCarousel.value = shouldShow
+ shouldShowSingleClock.value = shouldShow
+ }
+
+ companion object {
+ const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockColorViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockColorViewModel.kt
new file mode 100644
index 00000000..ea60ae39
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockColorViewModel.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.picker.clock.ui.viewmodel
+
+import android.annotation.ColorInt
+import android.content.res.Resources
+import android.graphics.Color
+import com.android.wallpaper.R
+
+/** The view model that defines custom clock colors. */
+data class ClockColorViewModel(
+ val colorId: String,
+ val colorName: String?,
+ @ColorInt val color: Int,
+ private val colorToneMin: Double,
+ private val colorToneMax: Double,
+) {
+
+ fun getColorTone(progress: Int): Double {
+ return colorToneMin + (progress.toDouble() * (colorToneMax - colorToneMin)) / 100
+ }
+
+ companion object {
+ const val DEFAULT_COLOR_TONE_MIN = 0
+ const val DEFAULT_COLOR_TONE_MAX = 100
+
+ fun getPresetColorMap(resources: Resources): Map<String, ClockColorViewModel> {
+ val ids = resources.getStringArray(R.array.clock_color_ids)
+ val names = resources.obtainTypedArray(R.array.clock_color_names)
+ val colors = resources.obtainTypedArray(R.array.clock_colors)
+ val colorToneMinList = resources.obtainTypedArray(R.array.clock_color_tone_min)
+ val colorToneMaxList = resources.obtainTypedArray(R.array.clock_color_tone_max)
+ return buildList {
+ ids.indices.forEach { index ->
+ add(
+ ClockColorViewModel(
+ ids[index],
+ names.getString(index),
+ colors.getColor(index, Color.TRANSPARENT),
+ colorToneMinList.getInt(index, DEFAULT_COLOR_TONE_MIN).toDouble(),
+ colorToneMaxList.getInt(index, DEFAULT_COLOR_TONE_MAX).toDouble(),
+ )
+ )
+ }
+ }
+ .associateBy { it.colorId }
+ .also {
+ names.recycle()
+ colors.recycle()
+ colorToneMinList.recycle()
+ colorToneMaxList.recycle()
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt
new file mode 100644
index 00000000..008a1252
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.picker.clock.ui.viewmodel
+
+import android.content.Context
+import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import com.android.customization.picker.clock.shared.ClockSize
+import com.android.wallpaper.R
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+/** View model for the clock section view on the lockscreen customization surface. */
+class ClockSectionViewModel(context: Context, interactor: ClockPickerInteractor) {
+ val appContext: Context = context.applicationContext
+ val clockColorMap: Map<String, ClockColorViewModel> =
+ ClockColorViewModel.getPresetColorMap(appContext.resources)
+ val selectedClockColorAndSizeText: Flow<String> =
+ combine(interactor.selectedColorId, interactor.selectedClockSize, ::Pair).map {
+ (selectedColorId, selectedClockSize) ->
+ val colorText =
+ clockColorMap[selectedColorId]?.colorName
+ ?: context.getString(R.string.default_theme_title)
+ val sizeText =
+ when (selectedClockSize) {
+ ClockSize.SMALL -> appContext.getString(R.string.clock_size_small)
+ ClockSize.DYNAMIC -> appContext.getString(R.string.clock_size_dynamic)
+ }
+ appContext
+ .getString(R.string.clock_color_and_size_description, colorText, sizeText)
+ .lowercase()
+ .replaceFirstChar {
+ if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModel.kt
new file mode 100644
index 00000000..7c30ca2b
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModel.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.picker.clock.ui.viewmodel
+
+/** View model for the tabs on the clock settings screen. */
+class ClockSettingsTabViewModel(
+ /** User-visible name for the tab. */
+ val name: String,
+
+ /** Whether this is the currently-selected tab in the picker. */
+ val isSelected: Boolean,
+
+ /** Notifies that the tab has been clicked by the user. */
+ val onClicked: (() -> Unit)?,
+)
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
new file mode 100644
index 00000000..c3cd2170
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
@@ -0,0 +1,309 @@
+/*
+ * 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.picker.clock.ui.viewmodel
+
+import android.content.Context
+import androidx.core.graphics.ColorUtils
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.customization.model.color.ColorBundle
+import com.android.customization.model.color.ColorSeedOption
+import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import com.android.customization.picker.clock.shared.ClockSize
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** View model for the clock settings screen. */
+class ClockSettingsViewModel
+private constructor(
+ context: Context,
+ private val clockPickerInteractor: ClockPickerInteractor,
+ private val colorPickerInteractor: ColorPickerInteractor,
+) : ViewModel() {
+
+ enum class Tab {
+ COLOR,
+ SIZE,
+ }
+
+ val colorMap = ClockColorViewModel.getPresetColorMap(context.resources)
+
+ val selectedClockId: StateFlow<String?> =
+ clockPickerInteractor.selectedClockId
+ .distinctUntilChanged()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, null)
+
+ val selectedColorId: StateFlow<String?> =
+ clockPickerInteractor.selectedColorId.stateIn(viewModelScope, SharingStarted.Eagerly, null)
+
+ private val sliderColorToneProgress =
+ MutableStateFlow(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS)
+ val isSliderEnabled: Flow<Boolean> =
+ clockPickerInteractor.selectedColorId.map { it != null }.distinctUntilChanged()
+ val sliderProgress: Flow<Int> =
+ merge(clockPickerInteractor.colorToneProgress, sliderColorToneProgress)
+
+ private val _seedColor: MutableStateFlow<Int?> = MutableStateFlow(null)
+ val seedColor: Flow<Int?> = merge(clockPickerInteractor.seedColor, _seedColor)
+
+ /**
+ * The slider color tone updates are quick. Do not set color tone and the blended color to the
+ * settings until [onSliderProgressStop] is called. Update to a locally cached temporary
+ * [sliderColorToneProgress] and [_seedColor] instead.
+ */
+ fun onSliderProgressChanged(progress: Int) {
+ sliderColorToneProgress.value = progress
+ val selectedColorId = selectedColorId.value ?: return
+ val clockColorViewModel = colorMap[selectedColorId] ?: return
+ _seedColor.value =
+ blendColorWithTone(
+ color = clockColorViewModel.color,
+ colorTone = clockColorViewModel.getColorTone(progress),
+ )
+ }
+
+ fun onSliderProgressStop(progress: Int) {
+ val selectedColorId = selectedColorId.value ?: return
+ val clockColorViewModel = colorMap[selectedColorId] ?: return
+ clockPickerInteractor.setClockColor(
+ selectedColorId = selectedColorId,
+ colorToneProgress = progress,
+ seedColor =
+ blendColorWithTone(
+ color = clockColorViewModel.color,
+ colorTone = clockColorViewModel.getColorTone(progress),
+ )
+ )
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val colorOptions: StateFlow<List<ColorOptionViewModel>> =
+ combine(colorPickerInteractor.colorOptions, clockPickerInteractor.selectedColorId, ::Pair)
+ .mapLatest { (colorOptions, selectedColorId) ->
+ // Use mapLatest and delay(100) here to prevent too many selectedClockColor update
+ // events from ClockRegistry upstream, caused by sliding the saturation level bar.
+ delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
+ buildList {
+ val defaultThemeColorOptionViewModel =
+ (colorOptions[ColorType.WALLPAPER_COLOR]
+ ?.find { it.isSelected }
+ ?.colorOption as? ColorSeedOption)
+ ?.toColorOptionViewModel(
+ context,
+ selectedColorId,
+ )
+ ?: (colorOptions[ColorType.PRESET_COLOR]
+ ?.find { it.isSelected }
+ ?.colorOption as? ColorBundle)
+ ?.toColorOptionViewModel(
+ context,
+ selectedColorId,
+ )
+ if (defaultThemeColorOptionViewModel != null) {
+ add(defaultThemeColorOptionViewModel)
+ }
+
+ val selectedColorPosition = colorMap.keys.indexOf(selectedColorId)
+
+ colorMap.values.forEachIndexed { index, colorModel ->
+ val isSelected = selectedColorPosition == index
+ val colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
+ add(
+ ColorOptionViewModel(
+ color0 = colorModel.color,
+ color1 = colorModel.color,
+ color2 = colorModel.color,
+ color3 = colorModel.color,
+ contentDescription =
+ context.getString(
+ R.string.content_description_color_option,
+ index,
+ ),
+ isSelected = isSelected,
+ onClick =
+ if (isSelected) {
+ null
+ } else {
+ {
+ clockPickerInteractor.setClockColor(
+ selectedColorId = colorModel.colorId,
+ colorToneProgress = colorToneProgress,
+ seedColor =
+ blendColorWithTone(
+ color = colorModel.color,
+ colorTone =
+ colorModel.getColorTone(
+ colorToneProgress,
+ ),
+ ),
+ )
+ }
+ },
+ )
+ )
+ }
+ }
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = emptyList(),
+ )
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val selectedColorOptionPosition: Flow<Int> =
+ colorOptions.mapLatest { it.indexOfFirst { colorOption -> colorOption.isSelected } }
+
+ private fun ColorSeedOption.toColorOptionViewModel(
+ context: Context,
+ selectedColorId: String?,
+ ): ColorOptionViewModel {
+ val colors = previewInfo.resolveColors(context.resources)
+ return ColorOptionViewModel(
+ color0 = colors[0],
+ color1 = colors[1],
+ color2 = colors[2],
+ color3 = colors[3],
+ contentDescription = getContentDescription(context).toString(),
+ title = context.getString(R.string.default_theme_title),
+ isSelected = selectedColorId == null,
+ onClick =
+ if (selectedColorId == null) {
+ null
+ } else {
+ {
+ clockPickerInteractor.setClockColor(
+ selectedColorId = null,
+ colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ seedColor = null,
+ )
+ }
+ },
+ )
+ }
+
+ private fun ColorBundle.toColorOptionViewModel(
+ context: Context,
+ selectedColorId: String?
+ ): ColorOptionViewModel {
+ val primaryColor = previewInfo.resolvePrimaryColor(context.resources)
+ val secondaryColor = previewInfo.resolveSecondaryColor(context.resources)
+ return ColorOptionViewModel(
+ color0 = primaryColor,
+ color1 = secondaryColor,
+ color2 = primaryColor,
+ color3 = secondaryColor,
+ contentDescription = getContentDescription(context).toString(),
+ title = context.getString(R.string.default_theme_title),
+ isSelected = selectedColorId == null,
+ onClick =
+ if (selectedColorId == null) {
+ null
+ } else {
+ {
+ clockPickerInteractor.setClockColor(
+ selectedColorId = null,
+ colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ seedColor = null,
+ )
+ }
+ },
+ )
+ }
+
+ val selectedClockSize: Flow<ClockSize> = clockPickerInteractor.selectedClockSize
+
+ fun setClockSize(size: ClockSize) {
+ viewModelScope.launch { clockPickerInteractor.setClockSize(size) }
+ }
+
+ private val _selectedTabPosition = MutableStateFlow(Tab.COLOR)
+ val selectedTab: StateFlow<Tab> = _selectedTabPosition.asStateFlow()
+ val tabs: Flow<List<ClockSettingsTabViewModel>> =
+ selectedTab.map {
+ listOf(
+ ClockSettingsTabViewModel(
+ name = context.resources.getString(R.string.clock_color),
+ isSelected = it == Tab.COLOR,
+ onClicked =
+ if (it == Tab.COLOR) {
+ null
+ } else {
+ { _selectedTabPosition.tryEmit(Tab.COLOR) }
+ }
+ ),
+ ClockSettingsTabViewModel(
+ name = context.resources.getString(R.string.clock_size),
+ isSelected = it == Tab.SIZE,
+ onClicked =
+ if (it == Tab.SIZE) {
+ null
+ } else {
+ { _selectedTabPosition.tryEmit(Tab.SIZE) }
+ }
+ ),
+ )
+ }
+
+ companion object {
+ private val helperColorLab: DoubleArray by lazy { DoubleArray(3) }
+
+ fun blendColorWithTone(color: Int, colorTone: Double): Int {
+ ColorUtils.colorToLAB(color, helperColorLab)
+ return ColorUtils.LABToColor(
+ colorTone,
+ helperColorLab[1],
+ helperColorLab[2],
+ )
+ }
+
+ const val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+ }
+
+ class Factory(
+ private val context: Context,
+ private val clockPickerInteractor: ClockPickerInteractor,
+ private val colorPickerInteractor: ColorPickerInteractor,
+ ) : ViewModelProvider.Factory {
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ @Suppress("UNCHECKED_CAST")
+ return ClockSettingsViewModel(
+ context = context,
+ clockPickerInteractor = clockPickerInteractor,
+ colorPickerInteractor = colorPickerInteractor,
+ )
+ as T
+ }
+ }
+}