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.picker.AppbarFragment.AppbarFragmentHost;
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         AppbarFragmentHost, 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         ThemeBundleProvider themeProvider =
88                 new DefaultThemeProvider(this, injector.getCustomizationPreferences(this));
89         Intent intent = getIntent();
90         CustomTheme customTheme = null;
91         if (intent != null && intent.hasExtra(EXTRA_THEME_PACKAGES)
92                 && intent.hasExtra(EXTRA_THEME_TITLE) && intent.hasExtra(EXTRA_THEME_ID)) {
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 OverlayManagerCompat(this),
110                 mUserEventLogger);
111         mThemeManager.fetchOptions(null, false);
112         mCustomThemeManager = CustomThemeManager.create(customTheme, mThemeManager);
113         if (savedInstanceState != null) {
114             mCustomThemeManager.readCustomTheme(themeProvider, savedInstanceState);
115         }
116 
117         int currentStep = 0;
118         if (savedInstanceState != null) {
119             currentStep = savedInstanceState.getInt(KEY_STATE_CURRENT_STEP);
120         }
121         initSteps(currentStep);
122 
123         super.onCreate(savedInstanceState);
124         setContentView(R.layout.activity_custom_theme);
125         mNextButton = findViewById(R.id.next_button);
126         mNextButton.setOnClickListener(view -> onNextOrApply());
127         mPreviousButton = findViewById(R.id.previous_button);
128         mPreviousButton.setOnClickListener(view -> onBackPressed());
129 
130         FragmentManager fm = getSupportFragmentManager();
131         Fragment fragment = fm.findFragmentById(R.id.fragment_container);
132         if (fragment == null) {
133             // Navigate to the first step
134             navigateToStep(0);
135         }
136     }
137 
138     @Override
onSaveInstanceState(Bundle outState)139     protected void onSaveInstanceState(Bundle outState) {
140         super.onSaveInstanceState(outState);
141         outState.putInt(KEY_STATE_CURRENT_STEP, mCurrentStep);
142         if (mCustomThemeManager != null) {
143             mCustomThemeManager.saveCustomTheme(this, outState);
144         }
145     }
146 
navigateToStep(int i)147     private void navigateToStep(int i) {
148         FragmentManager fragmentManager = getSupportFragmentManager();
149         ComponentStep step = mSteps.get(i);
150         Fragment fragment = step.getFragment(mCustomThemeManager.getOriginalTheme().getTitle());
151 
152         FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
153         fragmentTransaction.replace(R.id.fragment_container, fragment);
154         // Don't add step 0 to the back stack so that going back from it just finishes the Activity
155         if (i > 0) {
156             fragmentTransaction.addToBackStack("Step " + i);
157         }
158         fragmentTransaction.commit();
159         fragmentManager.executePendingTransactions();
160         updateNavigationButtonLabels();
161     }
162 
initSteps(int currentStep)163     private void initSteps(int currentStep) {
164         mSteps = new ArrayList<>();
165         OverlayManagerCompat manager = new OverlayManagerCompat(this);
166         mSteps.add(new FontStep(new FontOptionsProvider(this, manager), 0));
167         mSteps.add(new IconStep(new IconOptionsProvider(this, manager), 1));
168         mSteps.add(new ColorStep(new ColorOptionsProvider(this, manager, mCustomThemeManager), 2));
169         mSteps.add(new ShapeStep(new ShapeOptionsProvider(this, manager), 3));
170         mSteps.add(new NameStep(4));
171         mCurrentStep = currentStep;
172     }
173 
onNextOrApply()174     private void onNextOrApply() {
175         CustomThemeStepFragment stepFragment = getCurrentStepFragment();
176         if (stepFragment instanceof CustomThemeComponentFragment) {
177             CustomThemeComponentFragment fragment = (CustomThemeComponentFragment) stepFragment;
178             mCustomThemeManager.apply(fragment.getSelectedOption(), new Callback() {
179                 @Override
180                 public void onSuccess() {
181                     navigateToStep(mCurrentStep + 1);
182                 }
183 
184                 @Override
185                 public void onError(@Nullable Throwable throwable) {
186                     Log.w(TAG, "Error applying custom theme component", throwable);
187                     Toast.makeText(CustomThemeActivity.this, R.string.apply_theme_error_msg,
188                             Toast.LENGTH_LONG).show();
189                 }
190             });
191         } else if (stepFragment instanceof CustomThemeNameFragment) {
192             CustomThemeNameFragment fragment = (CustomThemeNameFragment) stepFragment;
193             CustomTheme originalTheme = mCustomThemeManager.getOriginalTheme();
194 
195             // We're on the last step, apply theme and leave
196             CustomTheme themeToApply = mCustomThemeManager.buildPartialCustomTheme(this,
197                     originalTheme.getId(), fragment.getThemeName());
198 
199             // If the current theme is equal to the original theme being edited, then
200             // don't search for an equivalent, let the user apply the same one by keeping
201             // it null.
202             ThemeBundle equivalent = (originalTheme.isEquivalent(themeToApply))
203                     ? null : mThemeManager.findThemeByPackages(themeToApply);
204 
205             if (equivalent != null) {
206                 Builder builder =
207                         new Builder(CustomThemeActivity.this);
208                 builder.setTitle(getString(R.string.use_style_instead_title,
209                         equivalent.getTitle()))
210                         .setMessage(getString(R.string.use_style_instead_body,
211                                 equivalent.getTitle()))
212                         .setPositiveButton(getString(R.string.use_style_button,
213                                 equivalent.getTitle()),
214                                 (dialogInterface, i) -> applyTheme(equivalent))
215                         .setNegativeButton(R.string.no_thanks, null)
216                         .create()
217                         .show();
218             } else {
219                 applyTheme(themeToApply);
220             }
221         } else {
222             throw new IllegalStateException("Unknown CustomThemeStepFragment");
223         }
224     }
225 
applyTheme(ThemeBundle themeToApply)226     private void applyTheme(ThemeBundle themeToApply) {
227         mThemeManager.apply(themeToApply, new Callback() {
228             @Override
229             public void onSuccess() {
230                 overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
231                 Toast.makeText(getApplicationContext(), R.string.applied_theme_msg,
232                         Toast.LENGTH_LONG).show();
233                 setResult(RESULT_THEME_APPLIED);
234                 finish();
235             }
236 
237             @Override
238             public void onError(@Nullable Throwable throwable) {
239                 Log.w(TAG, "Error applying custom theme", throwable);
240                 Toast.makeText(CustomThemeActivity.this,
241                         R.string.apply_theme_error_msg,
242                         Toast.LENGTH_LONG).show();
243             }
244         });
245     }
246 
getCurrentStepFragment()247     private CustomThemeStepFragment getCurrentStepFragment() {
248         return (CustomThemeStepFragment)
249                 getSupportFragmentManager().findFragmentById(R.id.fragment_container);
250     }
251 
252     @Override
setCurrentStep(int i)253     public void setCurrentStep(int i) {
254         mCurrentStep = i;
255         updateNavigationButtonLabels();
256     }
257 
updateNavigationButtonLabels()258     private void updateNavigationButtonLabels() {
259         mPreviousButton.setVisibility(mCurrentStep == 0 ? View.INVISIBLE : View.VISIBLE);
260         mNextButton.setText((mCurrentStep < mSteps.size() -1) ? R.string.custom_theme_next
261                 : R.string.apply_btn);
262     }
263 
264     @Override
delete()265     public void delete() {
266         mThemeManager.removeCustomTheme(mCustomThemeManager.getOriginalTheme());
267         setResult(RESULT_THEME_DELETED);
268         finish();
269     }
270 
271     @Override
cancel()272     public void cancel() {
273         finish();
274     }
275 
276     @Override
getComponentOptionProvider( int position)277     public ThemeComponentOptionProvider<? extends ThemeComponentOption> getComponentOptionProvider(
278             int position) {
279         return mSteps.get(position).provider;
280     }
281 
282     @Override
getCustomThemeManager()283     public CustomThemeManager getCustomThemeManager() {
284         return mCustomThemeManager;
285     }
286 
287     @Override
onUpArrowPressed()288     public void onUpArrowPressed() {
289         // Skip it because CustomThemeStepFragment will implement cancel button
290         // (instead of up arrow) on action bar.
291     }
292 
293     @Override
isUpArrowSupported()294     public boolean isUpArrowSupported() {
295         // Skip it because CustomThemeStepFragment will implement cancel button
296         // (instead of up arrow) on action bar.
297         return false;
298     }
299 
300     /**
301      * Represents a step in selecting a custom theme, picking a particular component (eg font,
302      * color, shape, etc).
303      * Each step has a Fragment instance associated that instances of this class will provide.
304      */
305     private static abstract class ComponentStep<T extends ThemeComponentOption> {
306         @StringRes final int titleResId;
307         @StringRes final int accessibilityResId;
308         final ThemeComponentOptionProvider<T> provider;
309         final int position;
310         private CustomThemeStepFragment mFragment;
311 
ComponentStep(@tringRes int titleResId, @StringRes int accessibilityResId, ThemeComponentOptionProvider<T> provider, int position)312         protected ComponentStep(@StringRes int titleResId, @StringRes int accessibilityResId,
313                 ThemeComponentOptionProvider<T> provider, int position) {
314             this.titleResId = titleResId;
315             this.accessibilityResId = accessibilityResId;
316             this.provider = provider;
317             this.position = position;
318         }
319 
getFragment(String title)320         CustomThemeStepFragment getFragment(String title) {
321             if (mFragment == null) {
322                 mFragment = createFragment(title);
323             }
324             return mFragment;
325         }
326 
327         /**
328          * @return a newly created fragment that will handle this step's UI.
329          */
createFragment(String title)330         abstract CustomThemeStepFragment createFragment(String title);
331     }
332 
333     private class FontStep extends ComponentStep<FontOption> {
334 
FontStep(ThemeComponentOptionProvider<FontOption> provider, int position)335         protected FontStep(ThemeComponentOptionProvider<FontOption> provider,
336                 int position) {
337             super(R.string.font_component_title, R.string.accessibility_custom_font_title, provider,
338                     position);
339         }
340 
341         @Override
createFragment(String title)342         CustomThemeComponentFragment createFragment(String title) {
343             return CustomThemeComponentFragment.newInstance(
344                     title,
345                     position,
346                     titleResId,
347                     accessibilityResId);
348         }
349     }
350 
351     private class IconStep extends ComponentStep<IconOption> {
352 
IconStep(ThemeComponentOptionProvider<IconOption> provider, int position)353         protected IconStep(ThemeComponentOptionProvider<IconOption> provider,
354                 int position) {
355             super(R.string.icon_component_title, R.string.accessibility_custom_icon_title, provider,
356                     position);
357         }
358 
359         @Override
createFragment(String title)360         CustomThemeComponentFragment createFragment(String title) {
361             return CustomThemeComponentFragment.newInstance(
362                     title,
363                     position,
364                     titleResId,
365                     accessibilityResId);
366         }
367     }
368 
369     private class ColorStep extends ComponentStep<ColorOption> {
370 
ColorStep(ThemeComponentOptionProvider<ColorOption> provider, int position)371         protected ColorStep(ThemeComponentOptionProvider<ColorOption> provider,
372                 int position) {
373             super(R.string.color_component_title, R.string.accessibility_custom_color_title,
374                     provider, position);
375         }
376 
377         @Override
createFragment(String title)378         CustomThemeComponentFragment createFragment(String title) {
379             return CustomThemeComponentFragment.newInstance(
380                     title,
381                     position,
382                     titleResId,
383                     accessibilityResId);
384         }
385     }
386 
387     private class ShapeStep extends ComponentStep<ShapeOption> {
388 
ShapeStep(ThemeComponentOptionProvider<ShapeOption> provider, int position)389         protected ShapeStep(ThemeComponentOptionProvider<ShapeOption> provider,
390                 int position) {
391             super(R.string.shape_component_title, R.string.accessibility_custom_shape_title,
392                     provider, position);
393         }
394 
395         @Override
createFragment(String title)396         CustomThemeComponentFragment createFragment(String title) {
397             return CustomThemeComponentFragment.newInstance(
398                     title,
399                     position,
400                     titleResId,
401                     accessibilityResId);
402         }
403     }
404 
405     private class NameStep extends ComponentStep {
406 
NameStep(int position)407         protected NameStep(int position) {
408             super(R.string.name_component_title, R.string.accessibility_custom_name_title, null,
409                     position);
410         }
411 
412         @Override
createFragment(String title)413         CustomThemeNameFragment createFragment(String title) {
414             return CustomThemeNameFragment.newInstance(
415                     title,
416                     position,
417                     titleResId,
418                     accessibilityResId);
419         }
420     }
421 }
422