• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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