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