1 /*
2  * Copyright (C) 2021 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.wallpaper.picker;
17 
18 import static com.android.wallpaper.util.ActivityUtils.isSUWMode;
19 import static com.android.wallpaper.util.ActivityUtils.isWallpaperOnlyMode;
20 import static com.android.wallpaper.util.ActivityUtils.startActivityForResultSafely;
21 
22 import android.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.content.Intent;
25 import android.content.pm.ActivityInfo;
26 import android.content.res.Configuration;
27 import android.os.Bundle;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.view.Window;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.core.view.WindowCompat;
35 import androidx.fragment.app.Fragment;
36 import androidx.fragment.app.FragmentActivity;
37 import androidx.fragment.app.FragmentManager;
38 
39 import com.android.wallpaper.R;
40 import com.android.wallpaper.config.BaseFlags;
41 import com.android.wallpaper.model.Category;
42 import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController;
43 import com.android.wallpaper.model.PermissionRequester;
44 import com.android.wallpaper.model.WallpaperCategory;
45 import com.android.wallpaper.model.WallpaperInfo;
46 import com.android.wallpaper.model.WallpaperPreviewNavigator;
47 import com.android.wallpaper.module.DailyLoggingAlarmScheduler;
48 import com.android.wallpaper.module.Injector;
49 import com.android.wallpaper.module.InjectorProvider;
50 import com.android.wallpaper.module.LargeScreenMultiPanesChecker;
51 import com.android.wallpaper.module.MultiPanesChecker;
52 import com.android.wallpaper.module.NetworkStatusNotifier;
53 import com.android.wallpaper.module.NetworkStatusNotifier.NetworkStatus;
54 import com.android.wallpaper.module.logging.UserEventLogger;
55 import com.android.wallpaper.picker.AppbarFragment.AppbarFragmentHost;
56 import com.android.wallpaper.picker.CategorySelectorFragment.CategorySelectorFragmentHost;
57 import com.android.wallpaper.picker.MyPhotosStarter.PermissionChangedListener;
58 import com.android.wallpaper.picker.individual.IndividualPickerFragment.IndividualPickerFragmentHost;
59 import com.android.wallpaper.util.ActivityUtils;
60 import com.android.wallpaper.util.DeepLinkUtils;
61 import com.android.wallpaper.util.DisplayUtils;
62 import com.android.wallpaper.util.LaunchUtils;
63 import com.android.wallpaper.widget.BottomActionBar;
64 import com.android.wallpaper.widget.BottomActionBar.BottomActionBarHost;
65 
66 import dagger.hilt.android.AndroidEntryPoint;
67 
68 /**
69  *  Main Activity allowing containing view sections for the user to switch between the different
70  *  Fragments providing customization options.
71  */
72 @AndroidEntryPoint(FragmentActivity.class)
73 public class CustomizationPickerActivity extends Hilt_CustomizationPickerActivity implements
74         AppbarFragmentHost, WallpapersUiContainer, BottomActionBarHost, FragmentTransactionChecker,
75         PermissionRequester, CategorySelectorFragmentHost, IndividualPickerFragmentHost,
76         WallpaperPreviewNavigator {
77 
78     private static final String TAG = "CustomizationPickerActivity";
79     private static final String EXTRA_DESTINATION = "destination";
80 
81     private WallpaperPickerDelegate mDelegate;
82     private UserEventLogger mUserEventLogger;
83     private NetworkStatusNotifier mNetworkStatusNotifier;
84     private NetworkStatusNotifier.Listener mNetworkStatusListener;
85     @NetworkStatus private int mNetworkStatus;
86     private DisplayUtils mDisplayUtils;
87 
88     private BottomActionBar mBottomActionBar;
89     private boolean mIsSafeToCommitFragmentTransaction;
90 
91     @Override
onCreate(@ullable Bundle savedInstanceState)92     protected void onCreate(@Nullable Bundle savedInstanceState) {
93         Injector injector = InjectorProvider.getInjector();
94         mDelegate = new WallpaperPickerDelegate(this, this, injector);
95         mUserEventLogger = injector.getUserEventLogger();
96         mNetworkStatusNotifier = injector.getNetworkStatusNotifier(this);
97         mNetworkStatus = mNetworkStatusNotifier.getNetworkStatus();
98         mDisplayUtils = injector.getDisplayUtils(this);
99         enforcePortraitForHandheldAndFoldedDisplay();
100 
101         BaseFlags flags = injector.getFlags();
102         if (flags.isMultiCropEnabled()) {
103             getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
104         }
105 
106         // Restore this Activity's state before restoring contained Fragments state.
107         super.onCreate(savedInstanceState);
108         // Trampoline for the two panes
109         final MultiPanesChecker mMultiPanesChecker = new LargeScreenMultiPanesChecker();
110         if (mMultiPanesChecker.isMultiPanesEnabled(this)) {
111             Intent intent = getIntent();
112             if (!ActivityUtils.isLaunchedFromSettingsTrampoline(intent)
113                     && !ActivityUtils.isLaunchedFromSettingsRelated(intent)) {
114                 startActivityForResultSafely(this,
115                         mMultiPanesChecker.getMultiPanesIntent(intent), /* requestCode= */ 0);
116                 finish();
117             }
118         }
119 
120         setContentView(R.layout.activity_customization_picker);
121         mBottomActionBar = findViewById(R.id.bottom_actionbar);
122 
123         // See go/pdr-edge-to-edge-guide.
124         WindowCompat.setDecorFitsSystemWindows(getWindow(), isSUWMode(this));
125 
126         final boolean startFromLockScreen = getIntent() == null
127                 || !ActivityUtils.isLaunchedFromLauncher(getIntent());
128 
129         Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
130         if (fragment == null) {
131             // App launch specific logic: log the "app launch source" event.
132             if (getIntent() != null) {
133                 mUserEventLogger.logAppLaunched(getIntent());
134             }
135             injector.getPreferences(this).incrementAppLaunched();
136             DailyLoggingAlarmScheduler.setAlarm(getApplicationContext());
137 
138             // Switch to the target fragment.
139             switchFragment(isWallpaperOnlyMode(getIntent())
140                     ? WallpaperOnlyFragment.newInstance()
141                     : CustomizationPickerFragment.newInstance(startFromLockScreen));
142 
143             // Cache the categories, but only if we're not restoring state (b/276767415).
144             mDelegate.prefetchCategories();
145         }
146 
147         if (savedInstanceState == null) {
148             // We only want to start a new undo session if this activity is brand-new. A non-new
149             // activity will have a non-null savedInstanceState.
150             injector.getUndoInteractor(this, this).startSession();
151         }
152 
153         final Intent intent = getIntent();
154         final String navigationDestination = intent.getStringExtra(EXTRA_DESTINATION);
155         // Consume the destination and commit the intent back so the OS doesn't revert to the same
156         // destination when we change color or wallpaper (which causes the activity to be
157         // recreated).
158         intent.removeExtra(EXTRA_DESTINATION);
159         setIntent(intent);
160 
161         final String deepLinkCollectionId = DeepLinkUtils.getCollectionId(intent);
162 
163         if (!TextUtils.isEmpty(navigationDestination)) {
164             // Navigation deep link case
165             fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
166             if (fragment instanceof CustomizationSectionNavigationController) {
167                 final CustomizationSectionNavigationController navController =
168                         (CustomizationSectionNavigationController) fragment;
169                 navController.standaloneNavigateTo(navigationDestination);
170             }
171         } else if (!TextUtils.isEmpty(deepLinkCollectionId)) {
172             // Wallpaper Collection deep link case
173             switchFragmentWithBackStack(new CategorySelectorFragment());
174             switchFragmentWithBackStack(InjectorProvider.getInjector().getIndividualPickerFragment(
175                     this, deepLinkCollectionId));
176             intent.setData(null);
177         }
178     }
179 
180     @Override
onStart()181     protected void onStart() {
182         super.onStart();
183         if (mNetworkStatusListener == null) {
184             mNetworkStatusListener = status -> {
185                 if (status == mNetworkStatus) {
186                     return;
187                 }
188                 Log.i(TAG, "Network status changes, refresh wallpaper categories.");
189                 mNetworkStatus = status;
190                 mDelegate.initialize(/* forceCategoryRefresh= */ true);
191             };
192             // Upon registering a listener, the onNetworkChanged method is immediately called with
193             // the initial network status.
194             mNetworkStatusNotifier.registerListener(mNetworkStatusListener);
195         }
196     }
197 
198     @Override
onResume()199     protected void onResume() {
200         super.onResume();
201         mIsSafeToCommitFragmentTransaction = true;
202     }
203 
204     @Override
onPause()205     protected void onPause() {
206         super.onPause();
207         mIsSafeToCommitFragmentTransaction = false;
208     }
209 
210     @Override
onStop()211     protected void onStop() {
212         if (mNetworkStatusListener != null) {
213             mNetworkStatusNotifier.unregisterListener(mNetworkStatusListener);
214             mNetworkStatusListener = null;
215         }
216         super.onStop();
217     }
218 
219     @Override
onBackPressed()220     public void onBackPressed() {
221         Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
222         if (fragment instanceof BottomActionBarFragment
223                 && ((BottomActionBarFragment) fragment).onBackPressed()) {
224             return;
225         }
226 
227         if (getSupportFragmentManager().popBackStackImmediate()) {
228             return;
229         }
230         if (moveTaskToBack(false)) {
231             return;
232         }
233         super.onBackPressed();
234     }
235 
switchFragment(Fragment fragment)236     private void switchFragment(Fragment fragment) {
237         getSupportFragmentManager()
238                 .beginTransaction()
239                 .replace(R.id.fragment_container, fragment)
240                 .commitNow();
241     }
242 
switchFragmentWithBackStack(Fragment fragment)243     private void switchFragmentWithBackStack(Fragment fragment) {
244         getSupportFragmentManager()
245                 .beginTransaction()
246                 .replace(R.id.fragment_container, fragment)
247                 .addToBackStack(null)
248                 .commit();
249         getSupportFragmentManager().executePendingTransactions();
250     }
251 
252 
253     @Override
requestExternalStoragePermission(PermissionChangedListener listener)254     public void requestExternalStoragePermission(PermissionChangedListener listener) {
255         mDelegate.requestExternalStoragePermission(listener);
256     }
257 
258     @Override
showViewOnlyPreview(WallpaperInfo wallpaperInfo, boolean isAssetIdPresent)259     public void showViewOnlyPreview(WallpaperInfo wallpaperInfo, boolean isAssetIdPresent) {
260         mDelegate.showViewOnlyPreview(wallpaperInfo, isAssetIdPresent);
261     }
262 
263     @Override
requestCustomPhotoPicker(PermissionChangedListener listener)264     public void requestCustomPhotoPicker(PermissionChangedListener listener) {
265         mDelegate.requestCustomPhotoPicker(listener);
266     }
267 
268     @Override
show(Category category)269     public void show(Category category) {
270         if (!(category instanceof WallpaperCategory)) {
271             mDelegate.show(category.getCollectionId());
272             return;
273         }
274         switchFragmentWithBackStack(InjectorProvider.getInjector().getIndividualPickerFragment(
275                 this, category.getCollectionId()));
276     }
277 
278     @Override
isHostToolbarShown()279     public boolean isHostToolbarShown() {
280         return false;
281     }
282 
283     @Override
setToolbarTitle(CharSequence title)284     public void setToolbarTitle(CharSequence title) {
285 
286     }
287 
288     @Override
setToolbarMenu(int menuResId)289     public void setToolbarMenu(int menuResId) {
290 
291     }
292 
293     @Override
removeToolbarMenu()294     public void removeToolbarMenu() {
295 
296     }
297 
298     @Override
moveToPreviousFragment()299     public void moveToPreviousFragment() {
300         getSupportFragmentManager().popBackStack();
301     }
302 
303     @Override
fetchCategories()304     public void fetchCategories() {
305         mDelegate.initialize(mDelegate.getCategoryProvider().shouldForceReload(this));
306     }
307 
308     @Override
cleanUp()309     public void cleanUp() {
310         mDelegate.cleanUp();
311     }
312 
313     @Override
onWallpapersReady()314     public void onWallpapersReady() {
315 
316     }
317 
318     @Nullable
319     @Override
getCategorySelectorFragment()320     public CategorySelectorFragment getCategorySelectorFragment() {
321         FragmentManager fm = getSupportFragmentManager();
322         Fragment fragment = fm.findFragmentById(R.id.fragment_container);
323         if (fragment instanceof CategorySelectorFragment) {
324             return (CategorySelectorFragment) fragment;
325         } else {
326             return null;
327         }
328     }
329 
330     @Override
onDestroy()331     protected void onDestroy() {
332         super.onDestroy();
333     }
334 
335     @Override
doneFetchingCategories()336     public void doneFetchingCategories() {
337 
338     }
339 
340     @SuppressWarnings("MissingSuperCall") // TODO: Fix me
341     @Override
onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)342     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
343             @NonNull int[] grantResults) {
344         mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
345     }
346 
347     @Override
onActivityResult(int requestCode, int resultCode, @Nullable Intent data)348     protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
349         super.onActivityResult(requestCode, resultCode, data);
350         if (mDelegate.handleActivityResult(requestCode, resultCode, data)) {
351             if (isSUWMode(this)) {
352                 finishActivityForSUW();
353             } else {
354                 // We don't finish in the revamped UI to let the user have a chance to reset the
355                 // change they made, should they want to. We do, however, remove all the fragments
356                 // from our back stack to reveal the root fragment, revealing the main screen of the
357                 // app.
358                 final FragmentManager fragmentManager = getSupportFragmentManager();
359                 while (fragmentManager.getBackStackEntryCount() > 0) {
360                     fragmentManager.popBackStackImmediate();
361                 }
362             }
363         }
364     }
365 
finishActivityWithResultOk()366     private void finishActivityWithResultOk() {
367         overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
368         setResult(Activity.RESULT_OK);
369         finish();
370 
371         // Go back to launcher home
372         LaunchUtils.launchHome(this);
373     }
374 
finishActivityForSUW()375     private void finishActivityForSUW() {
376         overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
377         // Return RESULT_CANCELED to make the "Change wallpaper" tile in SUW not be disabled.
378         setResult(Activity.RESULT_CANCELED);
379         finish();
380     }
381 
382     @Override
getBottomActionBar()383     public BottomActionBar getBottomActionBar() {
384         return mBottomActionBar;
385     }
386 
387     @Override
isSafeToCommitFragmentTransaction()388     public boolean isSafeToCommitFragmentTransaction() {
389         return mIsSafeToCommitFragmentTransaction;
390     }
391 
392     @Override
onUpArrowPressed()393     public void onUpArrowPressed() {
394         // TODO(b/189166781): Remove interface AppbarFragmentHost#onUpArrowPressed.
395         onBackPressed();
396     }
397 
398     @Override
isUpArrowSupported()399     public boolean isUpArrowSupported() {
400         return !isSUWMode(this);
401     }
402 
403     @Override
onConfigurationChanged(@onNull Configuration newConfig)404     public void onConfigurationChanged(@NonNull Configuration newConfig) {
405         super.onConfigurationChanged(newConfig);
406         enforcePortraitForHandheldAndFoldedDisplay();
407     }
408 
409     /**
410      * If the display is a handheld display or a folded display from a foldable, we enforce the
411      * activity to be portrait.
412      *
413      * This method should be called upon initialization of this activity, and whenever there is a
414      * configuration change.
415      */
416     @SuppressLint("SourceLockedOrientationActivity")
enforcePortraitForHandheldAndFoldedDisplay()417     private void enforcePortraitForHandheldAndFoldedDisplay() {
418         int wantedOrientation = mDisplayUtils.isLargeScreenOrUnfoldedDisplay(this)
419                 ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
420                 : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
421         if (getRequestedOrientation() != wantedOrientation) {
422             setRequestedOrientation(wantedOrientation);
423         }
424     }
425 }
426