1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.customization.picker.theme;
17 
18 import static android.app.Activity.RESULT_OK;
19 
20 import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY;
21 import static com.android.wallpaper.widget.BottomActionBar.BottomAction.CUSTOMIZE;
22 import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION;
23 
24 import android.content.Context;
25 import android.content.Intent;
26 import android.os.Bundle;
27 import android.util.Log;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.ImageView;
32 import android.widget.Toast;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.core.widget.ContentLoadingProgressBar;
37 import androidx.recyclerview.widget.RecyclerView;
38 
39 import com.android.customization.model.CustomizationManager.Callback;
40 import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
41 import com.android.customization.model.CustomizationOption;
42 import com.android.customization.model.theme.ThemeBundle;
43 import com.android.customization.model.theme.ThemeManager;
44 import com.android.customization.model.theme.custom.CustomTheme;
45 import com.android.customization.module.ThemesUserEventLogger;
46 import com.android.customization.picker.WallpaperPreviewer;
47 import com.android.customization.widget.OptionSelectorController;
48 import com.android.wallpaper.R;
49 import com.android.wallpaper.model.WallpaperInfo;
50 import com.android.wallpaper.module.CurrentWallpaperInfoFactory;
51 import com.android.wallpaper.module.InjectorProvider;
52 import com.android.wallpaper.picker.AppbarFragment;
53 import com.android.wallpaper.widget.BottomActionBar;
54 import com.android.wallpaper.widget.BottomActionBar.AccessibilityCallback;
55 import com.android.wallpaper.widget.BottomActionBar.BottomSheetContent;
56 
57 import java.util.List;
58 
59 /**
60  * Fragment that contains the main UI for selecting and applying a ThemeBundle.
61  */
62 public class ThemeFragment extends AppbarFragment {
63 
64     private static final String TAG = "ThemeFragment";
65     private static final String KEY_SELECTED_THEME = "ThemeFragment.SelectedThemeBundle";
66     private static final String KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE =
67             "ThemeFragment.bottomActionBarVisible";
68     private static final int FULL_PREVIEW_REQUEST_CODE = 1000;
69 
70     /**
71      * Interface to be implemented by an Activity hosting a {@link ThemeFragment}
72      */
73     public interface ThemeFragmentHost {
getThemeManager()74         ThemeManager getThemeManager();
75     }
newInstance(CharSequence title)76     public static ThemeFragment newInstance(CharSequence title) {
77         ThemeFragment fragment = new ThemeFragment();
78         fragment.setArguments(AppbarFragment.createArguments(title));
79         return fragment;
80     }
81 
82     private RecyclerView mOptionsContainer;
83     private OptionSelectorController<ThemeBundle> mOptionsController;
84     private ThemeManager mThemeManager;
85     private ThemesUserEventLogger mEventLogger;
86     private ThemeBundle mSelectedTheme;
87     private ContentLoadingProgressBar mLoading;
88     private View mContent;
89     private View mError;
90     private WallpaperInfo mCurrentHomeWallpaper;
91     private CurrentWallpaperInfoFactory mCurrentWallpaperFactory;
92     private BottomActionBar mBottomActionBar;
93     private WallpaperPreviewer mWallpaperPreviewer;
94     private ImageView mWallpaperImage;
95     private ThemeOptionPreviewer mThemeOptionPreviewer;
96     private ThemeInfoView mThemeInfoView;
97 
98     @Override
onAttach(Context context)99     public void onAttach(Context context) {
100         super.onAttach(context);
101         mThemeManager = ((ThemeFragmentHost) context).getThemeManager();
102         mEventLogger = (ThemesUserEventLogger)
103                 InjectorProvider.getInjector().getUserEventLogger(context);
104     }
105 
106     @Nullable
107     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)108     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
109             @Nullable Bundle savedInstanceState) {
110         View view = inflater.inflate(
111                 R.layout.fragment_theme_picker, container, /* attachToRoot */ false);
112         setUpToolbar(view);
113 
114         mContent = view.findViewById(R.id.content_section);
115         mLoading = view.findViewById(R.id.loading_indicator);
116         mError = view.findViewById(R.id.error_section);
117         mCurrentWallpaperFactory = InjectorProvider.getInjector()
118                 .getCurrentWallpaperFactory(getActivity().getApplicationContext());
119         mOptionsContainer = view.findViewById(R.id.options_container);
120 
121         mThemeOptionPreviewer = new ThemeOptionPreviewer(
122                 getLifecycle(),
123                 getContext(),
124                 view.findViewById(R.id.theme_preview_container));
125 
126         // Set Wallpaper background.
127         mWallpaperImage = view.findViewById(R.id.wallpaper_preview_image);
128         mWallpaperPreviewer = new WallpaperPreviewer(
129                 getLifecycle(),
130                 getActivity(),
131                 mWallpaperImage,
132                 view.findViewById(R.id.wallpaper_preview_surface));
133         mCurrentWallpaperFactory.createCurrentWallpaperInfos(
134                 (homeWallpaper, lockWallpaper, presentationMode) -> {
135                     mCurrentHomeWallpaper = homeWallpaper;
136                     mWallpaperPreviewer.setWallpaper(mCurrentHomeWallpaper,
137                             mThemeOptionPreviewer::updateColorForLauncherWidgets);
138                 }, false);
139         return view;
140     }
141 
142     @Override
onBottomActionBarReady(BottomActionBar bottomActionBar)143     protected void onBottomActionBarReady(BottomActionBar bottomActionBar) {
144         super.onBottomActionBarReady(bottomActionBar);
145         mBottomActionBar = bottomActionBar;
146         mBottomActionBar.showActionsOnly(INFORMATION, APPLY);
147         mBottomActionBar.setActionClickListener(APPLY, v -> {
148             mBottomActionBar.disableActions();
149             applyTheme();
150         });
151 
152         mBottomActionBar.bindBottomSheetContentWithAction(
153                 new ThemeInfoContent(getContext()), INFORMATION);
154         mBottomActionBar.setActionClickListener(CUSTOMIZE, this::onCustomizeClicked);
155 
156         // Update target view's accessibility param since it will be blocked by the bottom sheet
157         // when expanded.
158         mBottomActionBar.setAccessibilityCallback(new AccessibilityCallback() {
159             @Override
160             public void onBottomSheetCollapsed() {
161                 mOptionsContainer.setImportantForAccessibility(
162                         View.IMPORTANT_FOR_ACCESSIBILITY_YES);
163             }
164 
165             @Override
166             public void onBottomSheetExpanded() {
167                 mOptionsContainer.setImportantForAccessibility(
168                         View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
169             }
170         });
171     }
172 
173     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)174     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
175         super.onViewCreated(view, savedInstanceState);
176         // Setup options here when all views are ready(including BottomActionBar), since we need to
177         // update views after options are loaded.
178         setUpOptions(savedInstanceState);
179     }
180 
applyTheme()181     private void applyTheme() {
182         mThemeManager.apply(mSelectedTheme, new Callback() {
183             @Override
184             public void onSuccess() {
185                 Toast.makeText(getContext(), R.string.applied_theme_msg, Toast.LENGTH_LONG).show();
186                 getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
187                 getActivity().finish();
188             }
189 
190             @Override
191             public void onError(@Nullable Throwable throwable) {
192                 Log.w(TAG, "Error applying theme", throwable);
193                 // Since we disabled it when clicked apply button.
194                 mBottomActionBar.enableActions();
195                 mBottomActionBar.hide();
196                 Toast.makeText(getContext(), R.string.apply_theme_error_msg,
197                         Toast.LENGTH_LONG).show();
198             }
199         });
200     }
201 
202     @Override
onSaveInstanceState(@onNull Bundle outState)203     public void onSaveInstanceState(@NonNull Bundle outState) {
204         super.onSaveInstanceState(outState);
205         if (mSelectedTheme != null && !mSelectedTheme.isActive(mThemeManager)) {
206             outState.putString(KEY_SELECTED_THEME, mSelectedTheme.getSerializedPackages());
207         }
208         if (mBottomActionBar != null) {
209             outState.putBoolean(KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE, mBottomActionBar.isVisible());
210         }
211     }
212 
213     @Override
onActivityResult(int requestCode, int resultCode, Intent data)214     public void onActivityResult(int requestCode, int resultCode, Intent data) {
215         if (requestCode == CustomThemeActivity.REQUEST_CODE_CUSTOM_THEME) {
216             if (resultCode == CustomThemeActivity.RESULT_THEME_DELETED) {
217                 mSelectedTheme = null;
218                 reloadOptions();
219             } else if (resultCode == CustomThemeActivity.RESULT_THEME_APPLIED) {
220                 getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
221                 getActivity().finish();
222             } else {
223                 if (mSelectedTheme != null) {
224                     mOptionsController.setSelectedOption(mSelectedTheme);
225                     // Set selected option above will show BottomActionBar,
226                     // hide BottomActionBar for the mis-trigger.
227                     mBottomActionBar.hide();
228                 } else {
229                     reloadOptions();
230                 }
231             }
232         } else if (requestCode == FULL_PREVIEW_REQUEST_CODE && resultCode == RESULT_OK) {
233             applyTheme();
234         }
235         super.onActivityResult(requestCode, resultCode, data);
236     }
237 
onCustomizeClicked(View view)238     private void onCustomizeClicked(View view) {
239         if (mSelectedTheme instanceof CustomTheme) {
240             navigateToCustomTheme((CustomTheme) mSelectedTheme);
241         }
242     }
243 
hideError()244     private void hideError() {
245         mContent.setVisibility(View.VISIBLE);
246         mError.setVisibility(View.GONE);
247     }
248 
showError()249     private void showError() {
250         mLoading.hide();
251         mContent.setVisibility(View.GONE);
252         mError.setVisibility(View.VISIBLE);
253     }
254 
setUpOptions(@ullable Bundle savedInstanceState)255     private void setUpOptions(@Nullable Bundle savedInstanceState) {
256         hideError();
257         mLoading.show();
258         mThemeManager.fetchOptions(new OptionsFetchedListener<ThemeBundle>() {
259             @Override
260             public void onOptionsLoaded(List<ThemeBundle> options) {
261                 mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
262                 mOptionsController.initOptions(mThemeManager);
263 
264                 // Find out the selected theme option.
265                 // 1. Find previously selected theme.
266                 String previouslySelected = savedInstanceState != null
267                         ? savedInstanceState.getString(KEY_SELECTED_THEME) : null;
268                 ThemeBundle previouslySelectedTheme = null;
269                 ThemeBundle activeTheme = null;
270                 for (ThemeBundle theme : options) {
271                     if (previouslySelected != null
272                             && previouslySelected.equals(theme.getSerializedPackages())) {
273                         previouslySelectedTheme = theme;
274                     }
275                     if (theme.isActive(mThemeManager)) {
276                         activeTheme = theme;
277                     }
278                 }
279                 // 2. Use active theme if no previously selected theme.
280                 mSelectedTheme = previouslySelectedTheme != null
281                         ? previouslySelectedTheme
282                         : activeTheme;
283                 // 3. Select the first system theme(default theme currently)
284                 //    if there is no matching custom enabled theme.
285                 if (mSelectedTheme == null) {
286                     mSelectedTheme = findFirstSystemThemeBundle(options);
287                 }
288 
289                 mOptionsController.setSelectedOption(mSelectedTheme);
290                 onOptionSelected(mSelectedTheme);
291                 restoreBottomActionBarVisibility(savedInstanceState);
292 
293                 mOptionsController.addListener(selectedOption -> {
294                     onOptionSelected(selectedOption);
295                     if (!isAddCustomThemeOption(selectedOption)) {
296                         mBottomActionBar.show();
297                     }
298                 });
299                 mLoading.hide();
300             }
301             @Override
302             public void onError(@Nullable Throwable throwable) {
303                 if (throwable != null) {
304                     Log.e(TAG, "Error loading theme bundles", throwable);
305                 }
306                 showError();
307             }
308         }, false);
309     }
310 
reloadOptions()311     private void reloadOptions() {
312         mThemeManager.fetchOptions(options -> {
313             mOptionsController.resetOptions(options);
314             for (ThemeBundle theme : options) {
315                 if (theme.isActive(mThemeManager)) {
316                     mSelectedTheme = theme;
317                     break;
318                 }
319             }
320             if (mSelectedTheme == null) {
321                 mSelectedTheme = findFirstSystemThemeBundle(options);
322             }
323             mOptionsController.setSelectedOption(mSelectedTheme);
324             // Set selected option above will show BottomActionBar,
325             // hide BottomActionBar for the mis-trigger.
326             mBottomActionBar.hide();
327         }, true);
328     }
329 
findFirstSystemThemeBundle(List<ThemeBundle> options)330     private ThemeBundle findFirstSystemThemeBundle(List<ThemeBundle> options) {
331         for (ThemeBundle bundle : options) {
332             if (!(bundle instanceof CustomTheme)) {
333                 return bundle;
334             }
335         }
336         return null;
337     }
338 
onOptionSelected(CustomizationOption selectedOption)339     private void onOptionSelected(CustomizationOption selectedOption) {
340         if (isAddCustomThemeOption(selectedOption)) {
341             navigateToCustomTheme((CustomTheme) selectedOption);
342         } else {
343             mSelectedTheme = (ThemeBundle) selectedOption;
344             mSelectedTheme.setOverrideThemeWallpaper(mCurrentHomeWallpaper);
345             mEventLogger.logThemeSelected(mSelectedTheme,
346                     selectedOption instanceof CustomTheme);
347             mThemeOptionPreviewer.setPreviewInfo(mSelectedTheme.getPreviewInfo());
348             if (mThemeInfoView != null && mSelectedTheme != null) {
349                 mThemeInfoView.populateThemeInfo(mSelectedTheme);
350             }
351 
352             if (selectedOption instanceof CustomTheme) {
353                 mBottomActionBar.showActionsOnly(INFORMATION, CUSTOMIZE, APPLY);
354             } else {
355                 mBottomActionBar.showActionsOnly(INFORMATION, APPLY);
356             }
357         }
358     }
359 
restoreBottomActionBarVisibility(@ullable Bundle savedInstanceState)360     private void restoreBottomActionBarVisibility(@Nullable Bundle savedInstanceState) {
361         boolean isBottomActionBarVisible = savedInstanceState != null
362                 && savedInstanceState.getBoolean(KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE);
363         if (isBottomActionBarVisible) {
364             mBottomActionBar.show();
365         } else {
366             mBottomActionBar.hide();
367         }
368     }
369 
isAddCustomThemeOption(CustomizationOption option)370     private boolean isAddCustomThemeOption(CustomizationOption option) {
371         return option instanceof CustomTheme && !((CustomTheme) option).isDefined();
372     }
373 
navigateToCustomTheme(CustomTheme themeToEdit)374     private void navigateToCustomTheme(CustomTheme themeToEdit) {
375         Intent intent = new Intent(getActivity(), CustomThemeActivity.class);
376         intent.putExtra(CustomThemeActivity.EXTRA_THEME_TITLE, themeToEdit.getTitle());
377         intent.putExtra(CustomThemeActivity.EXTRA_THEME_ID, themeToEdit.getId());
378         intent.putExtra(CustomThemeActivity.EXTRA_THEME_PACKAGES,
379                 themeToEdit.getSerializedPackages());
380         startActivityForResult(intent, CustomThemeActivity.REQUEST_CODE_CUSTOM_THEME);
381     }
382 
383     private final class ThemeInfoContent extends BottomSheetContent<ThemeInfoView> {
384 
ThemeInfoContent(Context context)385         private ThemeInfoContent(Context context) {
386             super(context);
387         }
388 
389         @Override
getViewId()390         public int getViewId() {
391             return R.layout.theme_info_view;
392         }
393 
394         @Override
onViewCreated(ThemeInfoView view)395         public void onViewCreated(ThemeInfoView view) {
396             mThemeInfoView = view;
397             if (mSelectedTheme != null) {
398                 mThemeInfoView.populateThemeInfo(mSelectedTheme);
399             }
400         }
401     }
402 }
403