1 /*
2  * Copyright (C) 2019 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 android.app.AlertDialog.Builder;
19 import android.content.Intent;
20 import android.os.Bundle;
21 import android.util.Log;
22 import android.view.View;
23 import android.widget.TextView;
24 import android.widget.Toast;
25 
26 import androidx.annotation.Nullable;
27 import androidx.annotation.StringRes;
28 import androidx.fragment.app.Fragment;
29 import androidx.fragment.app.FragmentActivity;
30 import androidx.fragment.app.FragmentManager;
31 import androidx.fragment.app.FragmentTransaction;
32 
33 import com.android.customization.model.CustomizationManager.Callback;
34 import com.android.customization.model.theme.DefaultThemeProvider;
35 import com.android.customization.model.theme.OverlayManagerCompat;
36 import com.android.customization.model.theme.ThemeBundle;
37 import com.android.customization.model.theme.ThemeBundleProvider;
38 import com.android.customization.model.theme.ThemeManager;
39 import com.android.customization.model.theme.custom.ColorOptionsProvider;
40 import com.android.customization.model.theme.custom.CustomTheme;
41 import com.android.customization.model.theme.custom.CustomThemeManager;
42 import com.android.customization.model.theme.custom.FontOptionsProvider;
43 import com.android.customization.model.theme.custom.IconOptionsProvider;
44 import com.android.customization.model.theme.custom.ShapeOptionsProvider;
45 import com.android.customization.model.theme.custom.ThemeComponentOption;
46 import com.android.customization.model.theme.custom.ThemeComponentOption.ColorOption;
47 import com.android.customization.model.theme.custom.ThemeComponentOption.FontOption;
48 import com.android.customization.model.theme.custom.ThemeComponentOption.IconOption;
49 import com.android.customization.model.theme.custom.ThemeComponentOption.ShapeOption;
50 import com.android.customization.model.theme.custom.ThemeComponentOptionProvider;
51 import com.android.customization.module.CustomizationInjector;
52 import com.android.customization.module.ThemesUserEventLogger;
53 import com.android.customization.picker.theme.CustomThemeStepFragment.CustomThemeComponentStepHost;
54 import com.android.wallpaper.R;
55 import com.android.wallpaper.module.InjectorProvider;
56 import com.android.wallpaper.module.WallpaperSetter;
57 
58 import org.json.JSONException;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 public class CustomThemeActivity extends FragmentActivity implements
64         CustomThemeComponentStepHost {
65     public static final String EXTRA_THEME_ID = "CustomThemeActivity.ThemeId";
66     public static final String EXTRA_THEME_TITLE = "CustomThemeActivity.ThemeTitle";
67     public static final String EXTRA_THEME_PACKAGES = "CustomThemeActivity.ThemePackages";
68     public static final int REQUEST_CODE_CUSTOM_THEME = 1;
69     public static final int RESULT_THEME_DELETED = 10;
70     public static final int RESULT_THEME_APPLIED = 20;
71 
72     private static final String TAG = "CustomThemeActivity";
73     private static final String KEY_STATE_CURRENT_STEP = "CustomThemeActivity.currentStep";
74 
75     private ThemesUserEventLogger mUserEventLogger;
76     private List<ComponentStep<?>> mSteps;
77     private int mCurrentStep;
78     private CustomThemeManager mCustomThemeManager;
79     private ThemeManager mThemeManager;
80     private TextView mNextButton;
81     private TextView mPreviousButton;
82 
83     @Override
onCreate(Bundle savedInstanceState)84     protected void onCreate(Bundle savedInstanceState) {
85         CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector();
86         mUserEventLogger = (ThemesUserEventLogger) injector.getUserEventLogger(this);
87         Intent intent = getIntent();
88         CustomTheme customTheme = null;
89         if (intent != null && intent.hasExtra(EXTRA_THEME_PACKAGES)
90                 && intent.hasExtra(EXTRA_THEME_TITLE) && intent.hasExtra(EXTRA_THEME_ID)) {
91             ThemeBundleProvider themeProvider =
92                     new DefaultThemeProvider(this, injector.getCustomizationPreferences(this));
93             try {
94                 CustomTheme.Builder themeBuilder = themeProvider.parseCustomTheme(
95                         intent.getStringExtra(EXTRA_THEME_PACKAGES));
96                 if (themeBuilder != null) {
97                     themeBuilder.setId(intent.getStringExtra(EXTRA_THEME_ID));
98                     themeBuilder.setTitle(intent.getStringExtra(EXTRA_THEME_TITLE));
99                     customTheme = themeBuilder.build(this);
100                 }
101             } catch (JSONException e) {
102                 Log.w(TAG, "Couldn't parse provided custom theme, will override it");
103             }
104         }
105 
106         mThemeManager = injector.getThemeManager(
107                 new DefaultThemeProvider(this, injector.getCustomizationPreferences(this)),
108                 this,
109                 new WallpaperSetter(injector.getWallpaperPersister(this),
110                         injector.getPreferences(this), mUserEventLogger, false),
111                 new OverlayManagerCompat(this),
112                 mUserEventLogger);
113         mThemeManager.fetchOptions(null, false);
114         mCustomThemeManager = CustomThemeManager.create(customTheme, mThemeManager);
115 
116         int currentStep = 0;
117         if (savedInstanceState != null) {
118             currentStep = savedInstanceState.getInt(KEY_STATE_CURRENT_STEP);
119         }
120         initSteps(currentStep);
121 
122         super.onCreate(savedInstanceState);
123         setContentView(R.layout.activity_custom_theme);
124         mNextButton = findViewById(R.id.next_button);
125         mNextButton.setOnClickListener(view -> onNextOrApply());
126         mPreviousButton = findViewById(R.id.previous_button);
127         mPreviousButton.setOnClickListener(view -> onBackPressed());
128 
129         FragmentManager fm = getSupportFragmentManager();
130         Fragment fragment = fm.findFragmentById(R.id.fragment_container);
131         if (fragment == null) {
132             // Navigate to the first step
133             navigateToStep(0);
134         }
135     }
136 
137     @Override
onSaveInstanceState(Bundle outState)138     protected void onSaveInstanceState(Bundle outState) {
139         super.onSaveInstanceState(outState);
140         outState.putInt(KEY_STATE_CURRENT_STEP, mCurrentStep);
141     }
142 
navigateToStep(int i)143     private void navigateToStep(int i) {
144         FragmentManager fragmentManager = getSupportFragmentManager();
145         ComponentStep step = mSteps.get(i);
146         Fragment fragment = step.getFragment(mCustomThemeManager.getOriginalTheme().getTitle());
147 
148         FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
149         fragmentTransaction.replace(R.id.fragment_container, fragment);
150         // Don't add step 0 to the back stack so that going back from it just finishes the Activity
151         if (i > 0) {
152             fragmentTransaction.addToBackStack("Step " + i);
153         }
154         fragmentTransaction.commit();
155         fragmentManager.executePendingTransactions();
156         updateNavigationButtonLabels();
157     }
158 
initSteps(int currentStep)159     private void initSteps(int currentStep) {
160         mSteps = new ArrayList<>();
161         OverlayManagerCompat manager = new OverlayManagerCompat(this);
162         mSteps.add(new FontStep(new FontOptionsProvider(this, manager), 0));
163         mSteps.add(new IconStep(new IconOptionsProvider(this, manager), 1));
164         mSteps.add(new ColorStep(new ColorOptionsProvider(this, manager, mCustomThemeManager), 2));
165         mSteps.add(new ShapeStep(new ShapeOptionsProvider(this, manager), 3));
166         mSteps.add(new NameStep(4));
167         mCurrentStep = currentStep;
168     }
169 
onNextOrApply()170     private void onNextOrApply() {
171         CustomThemeStepFragment stepFragment = getCurrentStepFragment();
172         if (stepFragment instanceof CustomThemeComponentFragment) {
173             CustomThemeComponentFragment fragment = (CustomThemeComponentFragment) stepFragment;
174             mCustomThemeManager.apply(fragment.getSelectedOption(), new Callback() {
175                 @Override
176                 public void onSuccess() {
177                     navigateToStep(mCurrentStep + 1);
178                 }
179 
180                 @Override
181                 public void onError(@Nullable Throwable throwable) {
182                     Log.w(TAG, "Error applying custom theme component", throwable);
183                     Toast.makeText(CustomThemeActivity.this, R.string.apply_theme_error_msg,
184                             Toast.LENGTH_LONG).show();
185                 }
186             });
187         } else if (stepFragment instanceof CustomThemeNameFragment) {
188             CustomThemeNameFragment fragment = (CustomThemeNameFragment) stepFragment;
189             CustomTheme originalTheme = mCustomThemeManager.getOriginalTheme();
190 
191             // We're on the last step, apply theme and leave
192             CustomTheme themeToApply = mCustomThemeManager.buildPartialCustomTheme(this,
193                     originalTheme.getId(), fragment.getThemeName());
194 
195             // If the current theme is equal to the original theme being edited, then
196             // don't search for an equivalent, let the user apply the same one by keeping
197             // it null.
198             ThemeBundle equivalent = (originalTheme.isEquivalent(themeToApply))
199                     ? null : mThemeManager.findThemeByPackages(themeToApply);
200 
201             if (equivalent != null) {
202                 Builder builder =
203                         new Builder(CustomThemeActivity.this);
204                 builder.setTitle(getString(R.string.use_style_instead_title,
205                         equivalent.getTitle()))
206                         .setMessage(getString(R.string.use_style_instead_body,
207                                 equivalent.getTitle()))
208                         .setPositiveButton(getString(R.string.use_style_button,
209                                 equivalent.getTitle()),
210                                 (dialogInterface, i) -> applyTheme(equivalent))
211                         .setNegativeButton(R.string.no_thanks, null)
212                         .create()
213                         .show();
214             } else {
215                 applyTheme(themeToApply);
216             }
217         } else {
218             throw new IllegalStateException("Unknown CustomThemeStepFragment");
219         }
220     }
221 
applyTheme(ThemeBundle themeToApply)222     private void applyTheme(ThemeBundle themeToApply) {
223         mThemeManager.apply(themeToApply, new Callback() {
224             @Override
225             public void onSuccess() {
226                 Toast.makeText(CustomThemeActivity.this, R.string.applied_theme_msg,
227                         Toast.LENGTH_LONG).show();
228                 setResult(RESULT_THEME_APPLIED);
229                 finish();
230             }
231 
232             @Override
233             public void onError(@Nullable Throwable throwable) {
234                 Log.w(TAG, "Error applying custom theme", throwable);
235                 Toast.makeText(CustomThemeActivity.this,
236                         R.string.apply_theme_error_msg,
237                         Toast.LENGTH_LONG).show();
238             }
239         });
240     }
241 
getCurrentStepFragment()242     private CustomThemeStepFragment getCurrentStepFragment() {
243         return (CustomThemeStepFragment)
244                 getSupportFragmentManager().findFragmentById(R.id.fragment_container);
245     }
246 
247     @Override
setCurrentStep(int i)248     public void setCurrentStep(int i) {
249         mCurrentStep = i;
250         updateNavigationButtonLabels();
251     }
252 
updateNavigationButtonLabels()253     private void updateNavigationButtonLabels() {
254         mPreviousButton.setVisibility(mCurrentStep == 0 ? View.INVISIBLE : View.VISIBLE);
255         mNextButton.setText((mCurrentStep < mSteps.size() -1) ? R.string.custom_theme_next
256                 : R.string.apply_btn);
257     }
258 
259     @Override
260     public void delete() {
261         mThemeManager.removeCustomTheme(mCustomThemeManager.getOriginalTheme());
262         setResult(RESULT_THEME_DELETED);
263         finish();
264     }
265 
266     @Override
267     public void cancel() {
268         finish();
269     }
270 
271     @Override
272     public ThemeComponentOptionProvider<? extends ThemeComponentOption> getComponentOptionProvider(
273             int position) {
274         return mSteps.get(position).provider;
275     }
276 
277     @Override
278     public CustomThemeManager getCustomThemeManager() {
279         return mCustomThemeManager;
280     }
281 
282     /**
283      * Represents a step in selecting a custom theme, picking a particular component (eg font,
284      * color, shape, etc).
285      * Each step has a Fragment instance associated that instances of this class will provide.
286      */
287     private static abstract class ComponentStep<T extends ThemeComponentOption> {
288         @StringRes final int titleResId;
289         final ThemeComponentOptionProvider<T> provider;
290         final int position;
291         private CustomThemeStepFragment mFragment;
292 
293         protected ComponentStep(@StringRes int titleResId, ThemeComponentOptionProvider<T> provider,
294                                 int position) {
295             this.titleResId = titleResId;
296             this.provider = provider;
297             this.position = position;
298         }
299 
300         CustomThemeStepFragment getFragment(String title) {
301             if (mFragment == null) {
302                 mFragment = createFragment(title);
303             }
304             return mFragment;
305         }
306 
307         /**
308          * @return a newly created fragment that will handle this step's UI.
309          */
310         abstract CustomThemeStepFragment createFragment(String title);
311     }
312 
313     private class FontStep extends ComponentStep<FontOption> {
314 
315         protected FontStep(ThemeComponentOptionProvider<FontOption> provider,
316                 int position) {
317             super(R.string.font_component_title, provider, position);
318         }
319 
320         @Override
321         CustomThemeComponentFragment createFragment(String title) {
322             return CustomThemeComponentFragment.newInstance(
323                     title,
324                     position,
325                     titleResId);
326         }
327     }
328 
329     private class IconStep extends ComponentStep<IconOption> {
330 
331         protected IconStep(ThemeComponentOptionProvider<IconOption> provider,
332                 int position) {
333             super(R.string.icon_component_title, provider, position);
334         }
335 
336         @Override
337         CustomThemeComponentFragment createFragment(String title) {
338             return CustomThemeComponentFragment.newInstance(
339                     title,
340                     position,
341                     titleResId);
342         }
343     }
344 
345     private class ColorStep extends ComponentStep<ColorOption> {
346 
347         protected ColorStep(ThemeComponentOptionProvider<ColorOption> provider,
348                 int position) {
349             super(R.string.color_component_title, provider, position);
350         }
351 
352         @Override
353         CustomThemeComponentFragment createFragment(String title) {
354             return CustomThemeComponentFragment.newInstance(
355                     title,
356                     position,
357                     titleResId,
358                     true);
359         }
360     }
361 
362     private class ShapeStep extends ComponentStep<ShapeOption> {
363 
364         protected ShapeStep(ThemeComponentOptionProvider<ShapeOption> provider,
365                 int position) {
366             super(R.string.shape_component_title, provider, position);
367         }
368 
369         @Override
370         CustomThemeComponentFragment createFragment(String title) {
371             return CustomThemeComponentFragment.newInstance(
372                     title,
373                     position,
374                     titleResId);
375         }
376     }
377 
378     private class NameStep extends ComponentStep {
379 
380         protected NameStep(int position) {
381             super(R.string.name_component_title, null, position);
382         }
383 
384         @Override
385         CustomThemeNameFragment createFragment(String title) {
386             return CustomThemeNameFragment.newInstance(
387                     title,
388                     position,
389                     titleResId);
390         }
391     }
392 }
393