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