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