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