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; 17 18 import android.app.Activity; 19 import android.content.Intent; 20 import android.graphics.drawable.Drawable; 21 import android.graphics.drawable.LayerDrawable; 22 import android.os.Bundle; 23 import android.provider.Settings; 24 import android.util.Log; 25 import android.view.Gravity; 26 import android.view.Menu; 27 import android.view.MenuItem; 28 import android.view.View; 29 30 import androidx.annotation.IdRes; 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.fragment.app.Fragment; 35 import androidx.fragment.app.FragmentActivity; 36 import androidx.fragment.app.FragmentManager; 37 import androidx.fragment.app.FragmentTransaction; 38 39 import com.android.customization.model.CustomizationManager; 40 import com.android.customization.model.CustomizationOption; 41 import com.android.customization.model.clock.ClockManager; 42 import com.android.customization.model.clock.Clockface; 43 import com.android.customization.model.clock.ContentProviderClockProvider; 44 import com.android.customization.model.grid.GridOption; 45 import com.android.customization.model.grid.GridOptionsManager; 46 import com.android.customization.model.grid.LauncherGridOptionsProvider; 47 import com.android.customization.model.theme.DefaultThemeProvider; 48 import com.android.customization.model.theme.OverlayManagerCompat; 49 import com.android.customization.model.theme.ThemeBundle; 50 import com.android.customization.model.theme.ThemeManager; 51 import com.android.customization.module.CustomizationInjector; 52 import com.android.customization.module.DefaultCustomizationPreferences; 53 import com.android.customization.module.ThemesUserEventLogger; 54 import com.android.customization.picker.clock.ClockFragment; 55 import com.android.customization.picker.clock.ClockFragment.ClockFragmentHost; 56 import com.android.customization.picker.grid.GridFragment; 57 import com.android.customization.picker.grid.GridFragment.GridFragmentHost; 58 import com.android.customization.picker.theme.ThemeFragment; 59 import com.android.customization.picker.theme.ThemeFragment.ThemeFragmentHost; 60 import com.android.customization.widget.NoTintDrawableWrapper; 61 import com.android.wallpaper.R; 62 import com.android.wallpaper.compat.BuildCompat; 63 import com.android.wallpaper.model.WallpaperInfo; 64 import com.android.wallpaper.module.DailyLoggingAlarmScheduler; 65 import com.android.wallpaper.module.FormFactorChecker; 66 import com.android.wallpaper.module.Injector; 67 import com.android.wallpaper.module.InjectorProvider; 68 import com.android.wallpaper.module.UserEventLogger; 69 import com.android.wallpaper.module.WallpaperPreferences; 70 import com.android.wallpaper.picker.BottomActionBarFragment; 71 import com.android.wallpaper.picker.CategoryFragment; 72 import com.android.wallpaper.picker.CategoryFragment.CategoryFragmentHost; 73 import com.android.wallpaper.picker.FragmentTransactionChecker; 74 import com.android.wallpaper.picker.MyPhotosStarter; 75 import com.android.wallpaper.picker.MyPhotosStarter.PermissionChangedListener; 76 import com.android.wallpaper.picker.TopLevelPickerActivity; 77 import com.android.wallpaper.picker.WallpaperPickerDelegate; 78 import com.android.wallpaper.picker.WallpapersUiContainer; 79 import com.android.wallpaper.util.DeepLinkUtils; 80 import com.android.wallpaper.widget.BottomActionBar; 81 import com.android.wallpaper.widget.BottomActionBar.BottomActionBarHost; 82 83 import com.google.android.material.bottomnavigation.BottomNavigationView; 84 85 import java.util.HashMap; 86 import java.util.Map; 87 88 /** 89 * Main Activity allowing containing a bottom nav bar for the user to switch between the different 90 * Fragments providing customization options. 91 */ 92 public class CustomizationPickerActivity extends FragmentActivity implements WallpapersUiContainer, 93 CategoryFragmentHost, ThemeFragmentHost, GridFragmentHost, ClockFragmentHost, 94 BottomActionBarHost, FragmentTransactionChecker { 95 96 public static final String WALLPAPER_FLAVOR_EXTRA = 97 "com.android.launcher3.WALLPAPER_FLAVOR"; 98 public static final String WALLPAPER_FOCUS = "focus_wallpaper"; 99 private static final String TAG = "CustomizationPickerActivity"; 100 @VisibleForTesting static final String WALLPAPER_ONLY = "wallpaper_only"; 101 102 private WallpaperPickerDelegate mDelegate; 103 private UserEventLogger mUserEventLogger; 104 private BottomNavigationView mBottomNav; 105 106 private static final Map<Integer, CustomizationSection> mSections = new HashMap<>(); 107 private CategoryFragment mWallpaperCategoryFragment; 108 private BottomActionBar mBottomActionBar; 109 private boolean mIsSafeToCommitFragmentTransaction; 110 111 @Override onCreate(@ullable Bundle savedInstanceState)112 protected void onCreate(@Nullable Bundle savedInstanceState) { 113 Injector injector = InjectorProvider.getInjector(); 114 mDelegate = new WallpaperPickerDelegate(this, this, injector); 115 mUserEventLogger = injector.getUserEventLogger(this); 116 initSections(); 117 118 // Restore this Activity's state before restoring contained Fragments state. 119 super.onCreate(savedInstanceState); 120 121 if (!supportsCustomization()) { 122 Log.w(TAG, "Themes not supported, reverting to Wallpaper Picker"); 123 skipToWallpaperPicker(); 124 return; 125 } 126 127 mDelegate.getCategoryProvider().resetIfNeeded(); 128 129 setContentView(R.layout.activity_customization_picker_main); 130 setUpBottomNavView(); 131 mBottomActionBar = findViewById(R.id.bottom_actionbar); 132 mBottomActionBar.addVisibilityChangeListener( 133 isBottomActionBarVisible -> { 134 boolean isBottomNavVisible = mBottomNav.getVisibility() == View.VISIBLE; 135 // Switch the visibility of BottomNav if visibility of BottomActionBar and 136 // BottomNav are same. 137 if (isBottomActionBarVisible == isBottomNavVisible) { 138 mBottomNav.setVisibility(isBottomActionBarVisible 139 ? View.GONE : View.VISIBLE); 140 } 141 }); 142 143 Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container); 144 if (fragment == null) { 145 // App launch specific logic: log the "app launched" event and set up daily logging. 146 mUserEventLogger.logAppLaunched(); 147 WallpaperPreferences preferences = injector.getPreferences(this); 148 preferences.incrementAppLaunched(); 149 DailyLoggingAlarmScheduler.setAlarm(getApplicationContext()); 150 151 // Navigate to the Wallpaper tab if we started directly from launcher, otherwise 152 // start at the Styles tab 153 navigateToSection( 154 WALLPAPER_FOCUS.equals(getIntent().getStringExtra(WALLPAPER_FLAVOR_EXTRA)) 155 ? R.id.nav_wallpaper : R.id.nav_theme); 156 } 157 } 158 159 @Override onResume()160 protected void onResume() { 161 super.onResume(); 162 mIsSafeToCommitFragmentTransaction = true; 163 boolean wallpaperOnly = 164 WALLPAPER_ONLY.equals(getIntent().getStringExtra(WALLPAPER_FLAVOR_EXTRA)); 165 boolean provisioned = Settings.Global.getInt(getContentResolver(), 166 Settings.Global.DEVICE_PROVISIONED, 0) != 0; 167 168 mUserEventLogger.logResumed(provisioned, wallpaperOnly); 169 // refresh the sections as the preview may have changed 170 initSections(); 171 if (mBottomNav == null) { 172 return; 173 } 174 CustomizationSection section = mSections.get(mBottomNav.getSelectedItemId()); 175 if (section == null) { 176 return; 177 } 178 // Keep CategoryFragment's design to load category within its fragment 179 if (section instanceof WallpaperSection) { 180 switchFragment(section); 181 section.onVisible(); 182 } 183 } 184 185 @Override onPause()186 protected void onPause() { 187 super.onPause(); 188 mIsSafeToCommitFragmentTransaction = false; 189 } 190 191 @Override onNewIntent(Intent intent)192 protected void onNewIntent(Intent intent) { 193 super.onNewIntent(intent); 194 if (WALLPAPER_ONLY.equals(intent.getStringExtra(WALLPAPER_FLAVOR_EXTRA))) { 195 Log.d(TAG, "WALLPAPER_ONLY intent, reverting to Wallpaper Picker"); 196 skipToWallpaperPicker(); 197 } 198 } 199 skipToWallpaperPicker()200 private void skipToWallpaperPicker() { 201 Intent intent = new Intent(this, TopLevelPickerActivity.class); 202 if (DeepLinkUtils.isDeepLink(getIntent())) { 203 intent.setData(getIntent().getData()); 204 } 205 startActivity(intent); 206 finish(); 207 } 208 supportsCustomization()209 private boolean supportsCustomization() { 210 return mDelegate.getFormFactor() == FormFactorChecker.FORM_FACTOR_MOBILE 211 && mSections.size() > 1; 212 } 213 initSections()214 private void initSections() { 215 mSections.clear(); 216 if (!BuildCompat.isAtLeastQ()) { 217 Log.d(TAG, "Build version < Q detected"); 218 return; 219 } 220 if (WALLPAPER_ONLY.equals(getIntent().getStringExtra(WALLPAPER_FLAVOR_EXTRA))) { 221 Log.d(TAG, "WALLPAPER_ONLY intent"); 222 return; 223 } 224 //Theme 225 CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector(); 226 ThemesUserEventLogger eventLogger = (ThemesUserEventLogger) injector.getUserEventLogger( 227 this); 228 ThemeManager themeManager = injector.getThemeManager( 229 new DefaultThemeProvider(this, injector.getCustomizationPreferences(this)), 230 this, new OverlayManagerCompat(this), eventLogger); 231 if (themeManager.isAvailable()) { 232 mSections.put(R.id.nav_theme, new ThemeSection(R.id.nav_theme, themeManager)); 233 } else { 234 Log.d(TAG, "ThemeManager not available, removing Style section"); 235 } 236 //Clock 237 ClockManager clockManager = new ClockManager(getContentResolver(), 238 new ContentProviderClockProvider(this), eventLogger); 239 if (clockManager.isAvailable()) { 240 mSections.put(R.id.nav_clock, new ClockSection(R.id.nav_clock, clockManager)); 241 } else { 242 Log.d(TAG, "ClockManager not available, removing Clock section"); 243 } 244 //Grid 245 GridOptionsManager gridManager = new GridOptionsManager( 246 new LauncherGridOptionsProvider(this, 247 getString(R.string.grid_control_metadata_name)), 248 eventLogger); 249 if (gridManager.isAvailable()) { 250 mSections.put(R.id.nav_grid, new GridSection(R.id.nav_grid, gridManager)); 251 } else { 252 Log.d(TAG, "GridOptionsManager not available, removing Grid section"); 253 } 254 mSections.put(R.id.nav_wallpaper, new WallpaperSection(R.id.nav_wallpaper)); 255 } 256 setUpBottomNavView()257 private void setUpBottomNavView() { 258 mBottomNav = findViewById(R.id.main_bottom_nav); 259 Menu menu = mBottomNav.getMenu(); 260 DefaultCustomizationPreferences prefs = 261 new DefaultCustomizationPreferences(getApplicationContext()); 262 for (int i = menu.size() - 1; i >= 0; i--) { 263 MenuItem item = menu.getItem(i); 264 int id = item.getItemId(); 265 if (!mSections.containsKey(id)) { 266 menu.removeItem(id); 267 } else if (!prefs.getTabVisited(getResources().getResourceName(id))) { 268 showTipDot(item); 269 } 270 } 271 272 mBottomNav.setOnNavigationItemSelectedListener(item -> { 273 int id = item.getItemId(); 274 CustomizationSection section = mSections.get(id); 275 switchFragment(section); 276 section.onVisible(); 277 String name = getResources().getResourceName(id); 278 if (!prefs.getTabVisited(name)) { 279 prefs.setTabVisited(name); 280 hideTipDot(item); 281 282 if (id == R.id.nav_theme) { 283 getThemeManager().storeEmptyTheme(); 284 } 285 } 286 return true; 287 }); 288 } 289 showTipDot(MenuItem item)290 private void showTipDot(MenuItem item) { 291 Drawable icon = item.getIcon(); 292 Drawable dot = new NoTintDrawableWrapper(getResources().getDrawable(R.drawable.tip_dot)); 293 Drawable[] layers = {icon, dot}; 294 LayerDrawable iconWithDot = new LayerDrawable(layers); 295 296 // Position dot in the upper-right corner 297 int dotSize = (int) getResources().getDimension(R.dimen.tip_dot_size) 298 + (int) getResources().getDimension(R.dimen.tip_dot_line_width) * 2; 299 int linewidth = (int) getResources().getDimension(R.dimen.tip_dot_line_width); 300 iconWithDot.setLayerGravity(1, Gravity.TOP | Gravity.RIGHT); 301 iconWithDot.setLayerWidth(1, dotSize); 302 iconWithDot.setLayerHeight(1, dotSize); 303 iconWithDot.setLayerInsetTop(1, -linewidth); 304 iconWithDot.setLayerInsetRight(1, -linewidth); 305 306 item.setIcon(iconWithDot); 307 } 308 309 hideTipDot(MenuItem item)310 private void hideTipDot(MenuItem item) { 311 Drawable iconWithDot = item.getIcon(); 312 if (iconWithDot instanceof LayerDrawable) { 313 LayerDrawable layers = (LayerDrawable) iconWithDot; 314 Drawable icon = layers.getDrawable(0); 315 item.setIcon(icon); 316 } 317 } 318 319 @Override onBackPressed()320 public void onBackPressed() { 321 Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container); 322 if (fragment instanceof BottomActionBarFragment 323 && ((BottomActionBarFragment) fragment).onBackPressed()) { 324 return; 325 } 326 327 // For wallpaper tab, since it had child fragment. 328 if (mWallpaperCategoryFragment != null && mWallpaperCategoryFragment.popChildFragment()) { 329 return; 330 } 331 332 if (getSupportFragmentManager().popBackStackImmediate()) { 333 return; 334 } 335 if (moveTaskToBack(false)) { 336 return; 337 } 338 super.onBackPressed(); 339 } 340 navigateToSection(@dRes int id)341 private void navigateToSection(@IdRes int id) { 342 // Navigate to the first section if the targeted section doesn't exist. 343 if (!mSections.containsKey(id)) { 344 id = mBottomNav.getMenu().getItem(0).getItemId(); 345 } 346 347 mBottomNav.setSelectedItemId(id); 348 } 349 switchFragment(CustomizationSection section)350 private void switchFragment(CustomizationSection section) { 351 final FragmentManager fragmentManager = getSupportFragmentManager(); 352 353 Fragment fragment = section.getFragment(); 354 355 final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 356 fragmentTransaction.replace(R.id.fragment_container, fragment); 357 fragmentTransaction.commitNow(); 358 } 359 360 361 @Override requestExternalStoragePermission(PermissionChangedListener listener)362 public void requestExternalStoragePermission(PermissionChangedListener listener) { 363 mDelegate.requestExternalStoragePermission(listener); 364 } 365 366 @Override isReadExternalStoragePermissionGranted()367 public boolean isReadExternalStoragePermissionGranted() { 368 return mDelegate.isReadExternalStoragePermissionGranted(); 369 } 370 371 @Override showViewOnlyPreview(WallpaperInfo wallpaperInfo, boolean isViewAsHome)372 public void showViewOnlyPreview(WallpaperInfo wallpaperInfo, boolean isViewAsHome) { 373 mDelegate.showViewOnlyPreview(wallpaperInfo, isViewAsHome); 374 } 375 376 @Override show(String collectionId)377 public void show(String collectionId) { 378 mDelegate.show(collectionId); 379 } 380 381 @Override isNavigationTabsContained()382 public boolean isNavigationTabsContained() { 383 return true; 384 } 385 386 @Override fetchCategories()387 public void fetchCategories() { 388 mDelegate.initialize(!mDelegate.getCategoryProvider().isCategoriesFetched()); 389 } 390 391 @Override onWallpapersReady()392 public void onWallpapersReady() { 393 394 } 395 396 @Nullable 397 @Override getCategoryFragment()398 public CategoryFragment getCategoryFragment() { 399 return mWallpaperCategoryFragment; 400 } 401 402 @Override doneFetchingCategories()403 public void doneFetchingCategories() { 404 405 } 406 407 @Override onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)408 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 409 @NonNull int[] grantResults) { 410 mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); 411 } 412 413 @Override getMyPhotosStarter()414 public MyPhotosStarter getMyPhotosStarter() { 415 return mDelegate; 416 } 417 418 @Override getClockManager()419 public ClockManager getClockManager() { 420 CustomizationSection section = mSections.get(R.id.nav_clock); 421 return section == null ? null : (ClockManager) section.customizationManager; 422 } 423 424 @Override getGridOptionsManager()425 public GridOptionsManager getGridOptionsManager() { 426 CustomizationSection section = mSections.get(R.id.nav_grid); 427 return section == null ? null : (GridOptionsManager) section.customizationManager; 428 } 429 430 @Override getThemeManager()431 public ThemeManager getThemeManager() { 432 CustomizationSection section = mSections.get(R.id.nav_theme); 433 return section == null ? null : (ThemeManager) section.customizationManager; 434 } 435 436 @Override onStop()437 protected void onStop() { 438 mUserEventLogger.logStopped(); 439 super.onStop(); 440 } 441 442 @Override onActivityResult(int requestCode, int resultCode, @Nullable Intent data)443 protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 444 super.onActivityResult(requestCode, resultCode, data); 445 if (mDelegate.handleActivityResult(requestCode, resultCode, data)) { 446 finishActivityWithResultOk(); 447 } 448 } 449 finishActivityWithResultOk()450 private void finishActivityWithResultOk() { 451 overridePendingTransition(R.anim.fade_in, R.anim.fade_out); 452 setResult(Activity.RESULT_OK); 453 finish(); 454 } 455 456 @Override getBottomActionBar()457 public BottomActionBar getBottomActionBar() { 458 return mBottomActionBar; 459 } 460 461 @Override isSafeToCommitFragmentTransaction()462 public boolean isSafeToCommitFragmentTransaction() { 463 return mIsSafeToCommitFragmentTransaction; 464 } 465 466 /** 467 * Represents a section of the Picker (eg "ThemeBundle", "Clock", etc). 468 * There should be a concrete subclass per available section, providing the corresponding 469 * Fragment to be displayed when switching to each section. 470 */ 471 static abstract class CustomizationSection<T extends CustomizationOption> { 472 473 /** 474 * IdRes used to identify this section in the BottomNavigationView menu. 475 */ 476 @IdRes final int id; 477 protected final CustomizationManager<T> customizationManager; 478 CustomizationSection(@dRes int id, CustomizationManager<T> manager)479 private CustomizationSection(@IdRes int id, CustomizationManager<T> manager) { 480 this.id = id; 481 this.customizationManager = manager; 482 } 483 484 /** 485 * @return the Fragment corresponding to this section. 486 */ getFragment()487 abstract Fragment getFragment(); 488 onVisible()489 void onVisible() {} 490 } 491 492 /** 493 * {@link CustomizationSection} corresponding to the "Wallpaper" section of the Picker. 494 */ 495 private class WallpaperSection extends CustomizationSection { 496 WallpaperSection(int id)497 private WallpaperSection(int id) { 498 super(id, null); 499 } 500 501 @Override getFragment()502 Fragment getFragment() { 503 if (mWallpaperCategoryFragment == null) { 504 mWallpaperCategoryFragment = CategoryFragment.newInstance( 505 getString(R.string.wallpaper_title)); 506 } 507 return mWallpaperCategoryFragment; 508 } 509 } 510 511 private class ThemeSection extends CustomizationSection<ThemeBundle> { 512 513 private ThemeFragment mFragment; 514 ThemeSection(int id, ThemeManager manager)515 private ThemeSection(int id, ThemeManager manager) { 516 super(id, manager); 517 } 518 519 @Override getFragment()520 Fragment getFragment() { 521 if (mFragment == null) { 522 mFragment = ThemeFragment.newInstance(getString(R.string.theme_title)); 523 } 524 return mFragment; 525 } 526 } 527 528 private class GridSection extends CustomizationSection<GridOption> { 529 530 private GridFragment mFragment; 531 GridSection(int id, GridOptionsManager manager)532 private GridSection(int id, GridOptionsManager manager) { 533 super(id, manager); 534 } 535 536 @Override getFragment()537 Fragment getFragment() { 538 if (mFragment == null) { 539 mFragment = GridFragment.newInstance(getString(R.string.grid_title)); 540 } 541 return mFragment; 542 } 543 } 544 545 private class ClockSection extends CustomizationSection<Clockface> { 546 547 private ClockFragment mFragment; 548 ClockSection(int id, ClockManager manager)549 private ClockSection(int id, ClockManager manager) { 550 super(id, manager); 551 } 552 553 @Override getFragment()554 Fragment getFragment() { 555 if (mFragment == null) { 556 mFragment = ClockFragment.newInstance(getString(R.string.clock_title)); 557 } 558 return mFragment; 559 } 560 } 561 } 562