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