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 17 package com.android.providers.media.photopicker; 18 19 import static android.content.Intent.ACTION_GET_CONTENT; 20 import static android.provider.MediaStore.ACTION_PICK_IMAGES; 21 import static android.provider.MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP; 22 import static android.provider.MediaStore.grantMediaReadForPackage; 23 24 import static com.android.providers.media.photopicker.PhotoPickerSettingsActivity.EXTRA_CURRENT_USER_ID; 25 import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY; 26 import static com.android.providers.media.photopicker.data.PickerResult.getPickerResponseIntent; 27 import static com.android.providers.media.photopicker.data.PickerResult.getPickerUrisForItems; 28 import static com.android.providers.media.photopicker.util.LayoutModeUtils.MODE_PHOTOS_TAB; 29 30 import android.annotation.SuppressLint; 31 import android.annotation.UserIdInt; 32 import android.app.Activity; 33 import android.content.BroadcastReceiver; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.content.IntentFilter; 38 import android.content.pm.PackageManager; 39 import android.content.res.Configuration; 40 import android.content.res.TypedArray; 41 import android.graphics.Color; 42 import android.graphics.Outline; 43 import android.graphics.Rect; 44 import android.graphics.drawable.ColorDrawable; 45 import android.graphics.drawable.Drawable; 46 import android.graphics.drawable.GradientDrawable; 47 import android.net.Uri; 48 import android.os.Binder; 49 import android.os.Build; 50 import android.os.Bundle; 51 import android.os.Handler; 52 import android.os.UserHandle; 53 import android.provider.MediaStore; 54 import android.util.Log; 55 import android.util.TypedValue; 56 import android.view.Menu; 57 import android.view.MenuItem; 58 import android.view.MotionEvent; 59 import android.view.View; 60 import android.view.ViewOutlineProvider; 61 import android.view.WindowInsetsController; 62 import android.view.WindowManager; 63 import android.view.accessibility.AccessibilityManager; 64 import android.widget.ImageView; 65 import android.widget.TextView; 66 67 import androidx.annotation.ColorInt; 68 import androidx.annotation.NonNull; 69 import androidx.annotation.Nullable; 70 import androidx.annotation.VisibleForTesting; 71 import androidx.appcompat.app.AppCompatActivity; 72 import androidx.appcompat.widget.Toolbar; 73 import androidx.fragment.app.FragmentManager; 74 import androidx.lifecycle.LiveData; 75 import androidx.lifecycle.MutableLiveData; 76 import androidx.lifecycle.ViewModel; 77 import androidx.lifecycle.ViewModelProvider; 78 79 import com.android.modules.utils.build.SdkLevel; 80 import com.android.providers.media.ConfigStore; 81 import com.android.providers.media.R; 82 import com.android.providers.media.photopicker.data.PickerResult; 83 import com.android.providers.media.photopicker.data.Selection; 84 import com.android.providers.media.photopicker.data.UserIdManager; 85 import com.android.providers.media.photopicker.data.UserManagerState; 86 import com.android.providers.media.photopicker.data.model.Item; 87 import com.android.providers.media.photopicker.data.model.UserId; 88 import com.android.providers.media.photopicker.ui.TabContainerFragment; 89 import com.android.providers.media.photopicker.util.AccentColorResources; 90 import com.android.providers.media.photopicker.util.LayoutModeUtils; 91 import com.android.providers.media.photopicker.util.MimeFilterUtils; 92 import com.android.providers.media.photopicker.util.RecentsPreviewUtil; 93 import com.android.providers.media.photopicker.viewmodel.PickerViewModel; 94 import com.android.providers.media.util.ForegroundThread; 95 96 import com.google.android.material.bottomsheet.BottomSheetBehavior; 97 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback; 98 import com.google.android.material.tabs.TabLayout; 99 import com.google.common.collect.Lists; 100 101 import java.util.List; 102 import java.util.stream.Collectors; 103 104 /** 105 * Photo Picker allows users to choose one or more photos and/or videos to share with an app. The 106 * app does not get access to all photos/videos. 107 */ 108 public class PhotoPickerActivity extends AppCompatActivity { 109 private static final String TAG = "PhotoPickerActivity"; 110 private static final float BOTTOM_SHEET_PEEK_HEIGHT_PERCENTAGE = 0.60f; 111 private static final float HIDE_PROFILE_BUTTON_THRESHOLD = -0.5f; 112 private static final String LOGGER_INSTANCE_ID_ARG = "loggerInstanceIdArg"; 113 private static final String EXTRA_PRELOAD_SELECTED = 114 "com.android.providers.media.photopicker.extra.PRELOAD_SELECTED"; 115 private ViewModelProvider mViewModelProvider; 116 private PickerViewModel mPickerViewModel; 117 private PreloaderInstanceHolder mPreloaderInstanceHolder; 118 119 private Selection mSelection; 120 private BottomSheetBehavior mBottomSheetBehavior; 121 private View mBottomBar; 122 private View mBottomSheetView; 123 private View mFragmentContainerView; 124 private View mDragBar; 125 private View mProfileButton; 126 private View mProfileMenuButton; 127 private TextView mPrivacyText; 128 private TabLayout mTabLayout; 129 private Toolbar mToolbar; 130 private CrossProfileListeners mCrossProfileListeners; 131 private ConfigStore mConfigStore; 132 private boolean mIsPostResumeCallFinished = true; 133 private UserId mTurnedOffProfileUserId = null; 134 private UserId mRemovedProfileUserId = null; 135 136 137 @NonNull 138 private final MutableLiveData<Boolean> mIsItemPhotoGridViewChanged = 139 new MutableLiveData<>(false); 140 141 @ColorInt 142 private int mDefaultBackgroundColor; 143 144 @ColorInt 145 private int mToolBarIconColor; 146 147 private int mToolbarHeight = 0; 148 private boolean mShouldLogCancelledResult = true; 149 150 private AccessibilityManager mAccessibilityManager; 151 private boolean mIsAccessibilityEnabled; 152 private boolean mIsCustomPickerColorSet = false; 153 154 @Override onCreate(Bundle savedInstanceState)155 public void onCreate(Bundle savedInstanceState) { 156 // This is required as GET_CONTENT with type "*/*" is also received by PhotoPicker due 157 // to higher priority than DocumentsUi. "*/*" mime type filter is caught as it is a superset 158 // of "image/*" and "video/*". 159 if (rerouteGetContentRequestIfRequired()) { 160 // This activity is finishing now: we should not run the setup below, 161 // BUT before we return we have to call super.onCreate() (otherwise we are we will get 162 // SuperNotCalledException: Activity did not call through to super.onCreate()) 163 super.onCreate(savedInstanceState); 164 return; 165 } 166 167 // We use the device default theme as the base theme. Apply the material them for the 168 // material components. We use force "false" here, only values that are not already defined 169 // in the base theme will be copied. 170 getTheme().applyStyle(R.style.PickerMaterialTheme, /* force */ false); 171 172 // TODO(b/309578419): Make this activity handle insets properly and then remove this. 173 getTheme().applyStyle(R.style.OptOutEdgeToEdgeEnforcement, /* force */ false); 174 175 super.onCreate(savedInstanceState); 176 177 setContentView(R.layout.activity_photo_picker); 178 mToolbar = findViewById(R.id.toolbar); 179 setSupportActionBar(mToolbar); 180 getSupportActionBar().setDisplayHomeAsUpEnabled(true); 181 182 final int[] attrs = new int[]{R.attr.actionBarSize, R.attr.pickerTextColor}; 183 final TypedArray ta = obtainStyledAttributes(attrs); 184 // Save toolbar height so that we can use it as padding for FragmentContainerView 185 mToolbarHeight = ta.getDimensionPixelSize(/* index */ 0, /* defValue */ -1); 186 mToolBarIconColor = ta.getColor(/* index */ 1,/* defValue */ -1); 187 ta.recycle(); 188 189 mDefaultBackgroundColor = getColor(R.color.picker_background_color); 190 191 mViewModelProvider = new ViewModelProvider(this); 192 mPickerViewModel = getOrCreateViewModel(); 193 mConfigStore = mPickerViewModel.getConfigStore(); 194 195 final Intent intent = getIntent(); 196 try { 197 mPickerViewModel.parseValuesFromIntent(intent); 198 mIsCustomPickerColorSet = 199 mPickerViewModel.getPickerAccentColorParameters().isCustomPickerColorSet(); 200 201 // This needs to happen after we have parsed values from Intent. 202 mPickerViewModel.maybeInitPhotoPickerData(); 203 204 if (mIsCustomPickerColorSet) { 205 mDefaultBackgroundColor = 206 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 207 AccentColorResources.SURFACE_CONTAINER_COLOR_LIGHT, 208 AccentColorResources.SURFACE_CONTAINER_COLOR_DARK 209 ); 210 mToolBarIconColor = 211 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 212 AccentColorResources.ON_SURFACE_COLOR_LIGHT, 213 AccentColorResources.ON_SURFACE_COLOR_DARK 214 ); 215 } 216 } catch (IllegalArgumentException e) { 217 Log.e(TAG, "Finish activity due to an exception while parsing extras", e); 218 finishWithoutLoggingCancelledResult(); 219 return; 220 } 221 mSelection = mPickerViewModel.getSelection(); 222 mDragBar = findViewById(R.id.drag_bar); 223 mPrivacyText = findViewById(R.id.privacy_text); 224 mBottomBar = findViewById(R.id.picker_bottom_bar); 225 mProfileButton = findViewById(R.id.profile_button); 226 mProfileMenuButton = findViewById(R.id.profile_menu_button); 227 mTabLayout = findViewById(R.id.tab_layout); 228 229 mAccessibilityManager = getSystemService(AccessibilityManager.class); 230 mIsAccessibilityEnabled = mAccessibilityManager.isEnabled(); 231 232 initBottomSheetBehavior(); 233 234 // Save the fragment container layout so that we can adjust the padding based on preview or 235 // non-preview mode. 236 mFragmentContainerView = findViewById(R.id.fragment_container); 237 238 mCrossProfileListeners = new CrossProfileListeners(); 239 240 mPreloaderInstanceHolder = mViewModelProvider.get(PreloaderInstanceHolder.class); 241 if (mPreloaderInstanceHolder.preloader != null) { 242 subscribeToSelectedMediaPreloader(mPreloaderInstanceHolder.preloader); 243 } 244 245 observeRefreshUiNotificationLiveData(); 246 247 if (SdkLevel.isAtLeastV()) { 248 updateRecentsScreenshotSetting(); 249 } 250 251 // Restore state operation should always be kept at the end of this method. 252 restoreState(savedInstanceState); 253 254 // Call this after state is restored, to use the correct LOGGER_INSTANCE_ID_ARG 255 if (savedInstanceState == null) { 256 final String intentAction = intent != null ? intent.getAction() : null; 257 mPickerViewModel.logPickerOpened(getCallingUid(), getCallingPackage(), intentAction); 258 } 259 } 260 getCallingUid()261 private int getCallingUid() { 262 final String callingPackage = getCallingPackage(); 263 try { 264 return getPackageManager().getPackageUid(callingPackage, /* flags= */ 0); 265 } catch (PackageManager.NameNotFoundException e) { 266 Log.d(TAG, "Returning calling uid as -1; callingPackage: " + callingPackage + ".", e); 267 return -1; 268 } 269 } 270 271 @Override onDestroy()272 public void onDestroy() { 273 super.onDestroy(); 274 if (mCrossProfileListeners != null) { 275 // This is required to unregister any broadcast receivers. 276 mCrossProfileListeners.onDestroy(); 277 } 278 } 279 280 /** 281 * Gets PickerViewModel instance populated with the current calling package's uid. 282 * 283 * This method is also needed for tests, allowing ourselves to control ViewModel creation 284 * helps us mock the ViewModel for test. 285 */ 286 @VisibleForTesting 287 @NonNull getOrCreateViewModel()288 protected PickerViewModel getOrCreateViewModel() { 289 PickerViewModel viewModel = mViewModelProvider.get(PickerViewModel.class); 290 // populate calling package UID in PickerViewModel instance. 291 try { 292 if (getCallingPackage() != null) { 293 viewModel.setCallingPackageUid( 294 getPackageManager().getPackageUid(getCallingPackage(), 0)); 295 } 296 } catch (PackageManager.NameNotFoundException ignored) { 297 // no-op since the default value is -1. 298 } 299 return viewModel; 300 } 301 302 @Override dispatchTouchEvent(MotionEvent event)303 public boolean dispatchTouchEvent(MotionEvent event){ 304 if (event.getAction() == MotionEvent.ACTION_DOWN) { 305 if (mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { 306 307 Rect outRect = new Rect(); 308 mBottomSheetView.getGlobalVisibleRect(outRect); 309 310 if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) { 311 mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); 312 } 313 } 314 } 315 return super.dispatchTouchEvent(event); 316 } 317 318 /** 319 * This method is called on action bar home button clicks if 320 * {@link androidx.appcompat.app.ActionBar#setDisplayHomeAsUpEnabled(boolean)} is set 321 * {@code true}. 322 */ 323 @Override onSupportNavigateUp()324 public boolean onSupportNavigateUp() { 325 int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount(); 326 mPickerViewModel.logActionBarHomeButtonClick(backStackEntryCount); 327 super.onBackPressed(); 328 return true; 329 } 330 331 @Override onBackPressed()332 public void onBackPressed() { 333 int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount(); 334 mPickerViewModel.logBackGestureWithStackCount(backStackEntryCount); 335 super.onBackPressed(); 336 } 337 338 @Override onMenuOpened(int featureId, Menu menu)339 public boolean onMenuOpened(int featureId, Menu menu) { 340 mPickerViewModel.logMenuOpened(); 341 return super.onMenuOpened(featureId, menu); 342 } 343 344 @Override setTitle(CharSequence title)345 public void setTitle(CharSequence title) { 346 super.setTitle(title); 347 getSupportActionBar().setTitle(title); 348 } 349 350 /** 351 * Called when owning activity is saving state to be used to restore state during creation. 352 * 353 * @param state Bundle to save state 354 */ 355 @Override onSaveInstanceState(Bundle state)356 public void onSaveInstanceState(Bundle state) { 357 // The below assignment is necessary to track activity resume status, when PhotoPicker is 358 // opened in the background and any profile that is currently selected in PhotoPicker is 359 // turned-off or removed by the user. 360 // So if mIsPostResumeCallFinished is false means currently we can not refresh UI until 361 // the activity is fully resumed (Until mIsPostResumeCallFinished is assigned as true 362 // in onPostResume()) to avoid fragment state loss in onSaveInstanceState. 363 mIsPostResumeCallFinished = false; 364 super.onSaveInstanceState(state); 365 saveBottomSheetState(); 366 state.putParcelable(LOGGER_INSTANCE_ID_ARG, mPickerViewModel.getInstanceId()); 367 } 368 369 @Override onPostResume()370 public void onPostResume() { 371 super.onPostResume(); 372 // The below task will handle turned-off/removed request, when PhotoPicker is 373 // opened in the background and any profile is turned-off or removed by the user. 374 // when activity is not fully resumed and a profile is requested to be turned-off or removed 375 // in between, we will handle those requests in onPostResume to avoid fragment state 376 // loss in onSaveInstanceState. 377 new Handler().post(() -> { 378 mCrossProfileListeners.handleTurnedOffOrRemovedProfile(); 379 mIsPostResumeCallFinished = true; 380 }); 381 } 382 383 @Override onCreateOptionsMenu(@onNull Menu menu)384 public boolean onCreateOptionsMenu(@NonNull Menu menu) { 385 getMenuInflater().inflate(R.menu.picker_overflow_menu, menu); 386 return true; 387 } 388 389 @Override onPrepareOptionsMenu(@onNull Menu menu)390 public boolean onPrepareOptionsMenu(@NonNull Menu menu) { 391 super.onPrepareOptionsMenu(menu); 392 // All logic to hide/show an item in the menu must be in this method 393 final MenuItem settingsMenuItem = menu.findItem(R.id.settings); 394 395 // TODO(b/195009187): Settings menu item is hidden by default till Settings page is 396 // completely developed. 397 settingsMenuItem.setVisible(shouldShowSettingsScreen()); 398 399 // Browse menu item allows users to launch DocumentsUI. This item should only be shown if 400 // PhotoPicker was opened via {@link #ACTION_GET_CONTENT}. 401 menu.findItem(R.id.browse).setVisible(isGetContentAction()); 402 403 return menu.hasVisibleItems(); 404 } 405 406 @Override onOptionsItemSelected(MenuItem item)407 public boolean onOptionsItemSelected(MenuItem item) { 408 switch (item.getItemId()) { 409 case R.id.browse: 410 mPickerViewModel.logBrowseToDocumentsUi(Binder.getCallingUid(), 411 getCallingPackage()); 412 launchDocumentsUiAndFinishPicker(); 413 return true; 414 case R.id.settings: 415 startSettingsActivity(); 416 return true; 417 default: 418 // Continue to return the result of base class' onOptionsItemSelected(item) 419 } 420 return super.onOptionsItemSelected(item); 421 } 422 423 /** 424 * Launch the Photo Picker settings page where user can view/edit current cloud media provider. 425 */ startSettingsActivity()426 public void startSettingsActivity() { 427 final Intent intent = new Intent(this, PhotoPickerSettingsActivity.class); 428 intent.putExtra(EXTRA_CURRENT_USER_ID, getCurrentUserId()); 429 startActivity(intent); 430 } 431 432 /** 433 * @return {@code true} if the intent was re-routed to the DocumentsUI (and this 434 * {@code PhotoPickerActivity} is {@link #isFinishing()} now). {@code false} - otherwise. 435 */ rerouteGetContentRequestIfRequired()436 private boolean rerouteGetContentRequestIfRequired() { 437 final Intent intent = getIntent(); 438 if (!ACTION_GET_CONTENT.equals(intent.getAction())) { 439 return false; 440 } 441 442 // TODO(b/232775643): Workaround to support PhotoPicker invoked from DocumentsUi. 443 // GET_CONTENT for all (media and non-media) files opens DocumentsUi, but it still shows 444 // "Photo Picker app option. When the user clicks on "Photo Picker", the same intent which 445 // includes filters to show non-media files as well is forwarded to PhotoPicker. 446 // Make sure Photo Picker is opened when the intent is explicitly forwarded by documentsUi 447 if (isIntentReferredByDocumentsUi(getReferrer())) { 448 Log.i(TAG, "Open PhotoPicker when a forwarded ACTION_GET_CONTENT intent is received"); 449 return false; 450 } 451 452 // Check if we can handle the specified MIME types. 453 // If we can - do not reroute and thus return false. 454 if (!MimeFilterUtils.requiresUnsupportedFilters(intent)) return false; 455 456 launchDocumentsUiAndFinishPicker(); 457 return true; 458 } 459 isIntentReferredByDocumentsUi(Uri referrerAppUri)460 private boolean isIntentReferredByDocumentsUi(Uri referrerAppUri) { 461 ComponentName documentsUiComponentName = getDocumentsUiComponentName(this); 462 String documentsUiPackageName = documentsUiComponentName != null 463 ? documentsUiComponentName.getPackageName() : null; 464 return referrerAppUri != null && referrerAppUri.getHost().equals(documentsUiPackageName); 465 } 466 launchDocumentsUiAndFinishPicker()467 private void launchDocumentsUiAndFinishPicker() { 468 Log.i(TAG, "Launch DocumentsUI and finish picker"); 469 470 startActivityAsUser(getDocumentsUiForwardingIntent(this, getIntent()), 471 UserId.CURRENT_USER.getUserHandle()); 472 // RESULT_CANCELLED is not returned to the calling app as the DocumentsUi result will be 473 // returned. We don't have to log as this flow can be called in 2 cases: 474 // 1. GET_CONTENT had non-media filters, so the user or the app should be unaffected as they 475 // see that DocumentsUi was opened directly. 476 // 2. User clicked on "Browse.." button, in that case we already log that event separately. 477 finishWithoutLoggingCancelledResult(); 478 } 479 480 @VisibleForTesting getDocumentsUiForwardingIntent(Context context, Intent intent)481 static Intent getDocumentsUiForwardingIntent(Context context, Intent intent) { 482 intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 483 intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 484 intent.setComponent(getDocumentsUiComponentName(context)); 485 return intent; 486 } 487 getDocumentsUiComponentName(Context context)488 private static ComponentName getDocumentsUiComponentName(Context context) { 489 final PackageManager pm = context.getPackageManager(); 490 // DocumentsUI is the default handler for ACTION_OPEN_DOCUMENT 491 final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); 492 intent.addCategory(Intent.CATEGORY_OPENABLE); 493 intent.setType("*/*"); 494 return intent.resolveActivity(pm); 495 } 496 restoreState(Bundle savedInstanceState)497 private void restoreState(Bundle savedInstanceState) { 498 if (savedInstanceState != null) { 499 restoreBottomSheetState(); 500 mPickerViewModel.setInstanceId( 501 savedInstanceState.getParcelable(LOGGER_INSTANCE_ID_ARG)); 502 } else { 503 setupInitialLaunchState(); 504 } 505 } 506 507 /** 508 * Sets up states for the initial launch. This includes updating common layouts, selecting 509 * Photos tab item and saving the current bottom sheet state for later. 510 */ setupInitialLaunchState()511 private void setupInitialLaunchState() { 512 updateCommonLayouts(MODE_PHOTOS_TAB, /* title */ ""); 513 TabContainerFragment.show(getSupportFragmentManager()); 514 saveBottomSheetState(); 515 } 516 initBottomSheetBehavior()517 private void initBottomSheetBehavior() { 518 mBottomSheetView = findViewById(R.id.bottom_sheet); 519 mBottomSheetBehavior = BottomSheetBehavior.from(mBottomSheetView); 520 initStateForBottomSheet(); 521 522 mBottomSheetBehavior.addBottomSheetCallback(createBottomSheetCallBack()); 523 setRoundedCornersForBottomSheet(); 524 if (mIsCustomPickerColorSet) { 525 setCustomPickerColorsInBottomSheet( 526 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 527 AccentColorResources.SURFACE_CONTAINER_COLOR_LIGHT, 528 AccentColorResources.SURFACE_CONTAINER_COLOR_DARK), 529 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 530 AccentColorResources.SURFACE_CONTAINER_HIGHEST_LIGHT, 531 AccentColorResources.SURFACE_CONTAINER_HIGHEST_DARK) 532 ); 533 } 534 } 535 setCustomPickerColorsInBottomSheet(int backgroundColor, int dragBarColor)536 private void setCustomPickerColorsInBottomSheet(int backgroundColor, int dragBarColor) { 537 mBottomSheetView.setBackgroundColor(backgroundColor); 538 ImageView dragBarImageView = findViewById(R.id.drag_bar); 539 GradientDrawable dragBarDrawable = (GradientDrawable) dragBarImageView.getDrawable(); 540 dragBarDrawable.setColor(dragBarColor); 541 } 542 createBottomSheetCallBack()543 private BottomSheetCallback createBottomSheetCallBack() { 544 return new BottomSheetCallback() { 545 private boolean mIsProfileButtonHiddenDueToBottomSheetClosing = false; 546 private boolean mIsProfileMenuButtonHiddenDueToBottomSheetClosing = false; 547 @Override 548 public void onStateChanged(@NonNull View bottomSheet, int newState) { 549 if (newState == BottomSheetBehavior.STATE_HIDDEN) { 550 mPickerViewModel.logSwipeDownExit(); 551 finish(); 552 } else if (newState == BottomSheetBehavior.STATE_EXPANDED) { 553 mPickerViewModel.logExpandToFullScreen(); 554 } 555 saveBottomSheetState(); 556 } 557 558 @Override 559 public void onSlide(@NonNull View bottomSheet, float slideOffset) { 560 // slideOffset = -1 is when bottomsheet is completely hidden 561 // slideOffset = 0 is when bottomsheet is in collapsed mode 562 // slideOffset = 1 is when bottomsheet is in expanded mode 563 // We hide the Profile button if the bottomsheet is 50% in between collapsed state 564 // and hidden state. 565 onSlideProfileButton(slideOffset); 566 onSlideProfileMenuButton(slideOffset); 567 568 } 569 570 void onSlideProfileButton(float slideOffset) { 571 // We hide the Profile button if the bottomsheet is 50% in between collapsed state 572 // and hidden state. 573 if (slideOffset < HIDE_PROFILE_BUTTON_THRESHOLD 574 && mProfileButton.getVisibility() == View.VISIBLE) { 575 mProfileButton.setVisibility(View.GONE); 576 mIsProfileButtonHiddenDueToBottomSheetClosing = true; 577 return; 578 } 579 580 // We need to handle this state if the user is swiping till the bottom of the 581 // screen but then swipes up bottom sheet suddenly 582 if (slideOffset > HIDE_PROFILE_BUTTON_THRESHOLD 583 && mIsProfileButtonHiddenDueToBottomSheetClosing) { 584 mProfileButton.setVisibility(View.VISIBLE); 585 mIsProfileButtonHiddenDueToBottomSheetClosing = false; 586 } 587 } 588 589 void onSlideProfileMenuButton(float slideOffset) { 590 if (!(mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS())) { 591 return; 592 } 593 if (slideOffset < HIDE_PROFILE_BUTTON_THRESHOLD 594 && mProfileMenuButton.getVisibility() == View.VISIBLE) { 595 mProfileMenuButton.setVisibility(View.GONE); 596 mIsProfileMenuButtonHiddenDueToBottomSheetClosing = true; 597 return; 598 } 599 600 // We need to handle this state if the user is swiping till the bottom of the 601 // screen but then swipes up bottom sheet suddenly 602 if (slideOffset > HIDE_PROFILE_BUTTON_THRESHOLD 603 && mIsProfileMenuButtonHiddenDueToBottomSheetClosing) { 604 mProfileMenuButton.setVisibility(View.VISIBLE); 605 mIsProfileMenuButtonHiddenDueToBottomSheetClosing = false; 606 } 607 } 608 }; 609 } 610 611 setRoundedCornersForBottomSheet()612 private void setRoundedCornersForBottomSheet() { 613 final float cornerRadius = 614 getResources().getDimensionPixelSize(R.dimen.picker_top_corner_radius); 615 final ViewOutlineProvider viewOutlineProvider = new ViewOutlineProvider() { 616 @Override 617 public void getOutline(final View view, final Outline outline) { 618 outline.setRoundRect(0, 0, view.getWidth(), 619 (int)(view.getHeight() + cornerRadius), cornerRadius); 620 } 621 }; 622 mBottomSheetView.setOutlineProvider(viewOutlineProvider); 623 } 624 initStateForBottomSheet()625 private void initStateForBottomSheet() { 626 if (!isAccessibilityEnabled() && !mSelection.canSelectMultiple() 627 && !isOrientationLandscape()) { 628 final int peekHeight = getBottomSheetPeekHeight(this); 629 mBottomSheetBehavior.setPeekHeight(peekHeight); 630 mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); 631 } else { 632 mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); 633 mBottomSheetBehavior.setSkipCollapsed(true); 634 } 635 } 636 637 /** 638 * Warning: This method is visible for espresso tests, we are not customizing anything here. 639 * Allowing ourselves to control the accessibility state helps us mock it for these tests. 640 */ 641 @VisibleForTesting isAccessibilityEnabled()642 protected boolean isAccessibilityEnabled() { 643 return mIsAccessibilityEnabled; 644 } 645 getBottomSheetPeekHeight(Context context)646 private static int getBottomSheetPeekHeight(Context context) { 647 final WindowManager windowManager = context.getSystemService(WindowManager.class); 648 final Rect displayBounds = windowManager.getCurrentWindowMetrics().getBounds(); 649 return (int) (displayBounds.height() * BOTTOM_SHEET_PEEK_HEIGHT_PERCENTAGE); 650 } 651 restoreBottomSheetState()652 private void restoreBottomSheetState() { 653 // BottomSheet is always EXPANDED for landscape 654 if (isOrientationLandscape()) { 655 return; 656 } 657 final int savedState = mPickerViewModel.getBottomSheetState(); 658 if (isValidBottomSheetState(savedState)) { 659 mBottomSheetBehavior.setState(savedState); 660 } 661 } 662 saveBottomSheetState()663 private void saveBottomSheetState() { 664 // Do not save state for landscape or preview mode. This is because they are always in 665 // STATE_EXPANDED state. 666 if (isOrientationLandscape() || !mBottomSheetView.getClipToOutline()) { 667 return; 668 } 669 mPickerViewModel.setBottomSheetState(mBottomSheetBehavior.getState()); 670 } 671 isValidBottomSheetState(int state)672 private boolean isValidBottomSheetState(int state) { 673 return state == BottomSheetBehavior.STATE_COLLAPSED || 674 state == BottomSheetBehavior.STATE_EXPANDED; 675 } 676 isOrientationLandscape()677 private boolean isOrientationLandscape() { 678 return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 679 } 680 isItemPhotoGridViewChanged()681 public LiveData<Boolean> isItemPhotoGridViewChanged() { 682 return mIsItemPhotoGridViewChanged; 683 } 684 setResultAndFinishSelf()685 public void setResultAndFinishSelf() { 686 logPickerSelectionConfirmed(mSelection.getSelectedItems().size()); 687 if (shouldPreloadSelectedItems()) { 688 final var uris = PickerResult.getPickerUrisForItems( 689 getIntent().getAction(), mSelection.getSelectedItems()); 690 mPickerViewModel.logPreloadingStarted(uris.size()); 691 mPreloaderInstanceHolder.preloader = 692 SelectedMediaPreloader.preload(/* activity */ this, uris); 693 deSelectUnavailableMedia(mPreloaderInstanceHolder.preloader); 694 subscribeToSelectedMediaPreloader(mPreloaderInstanceHolder.preloader); 695 } else { 696 setResultAndFinishSelfInternal(); 697 } 698 } 699 setResultAndFinishSelfInternal()700 private void setResultAndFinishSelfInternal() { 701 // In addition to the activity result, add the selected files to the MediaProvider 702 // media_grants database. 703 if (isUserSelectImagesForAppAction()) { 704 setResultForUserSelectImagesForAppAction(); 705 } else { 706 setResultForPickImagesOrGetContentAction(); 707 } 708 709 finishWithoutLoggingCancelledResult(); 710 } 711 setResultForUserSelectImagesForAppAction()712 private void setResultForUserSelectImagesForAppAction() { 713 // Since Photopicker is in permission mode, don't send back URI grants. 714 setResult(RESULT_OK); 715 // The permission controller will pass the requesting package's UID here 716 final Bundle extras = getIntent().getExtras(); 717 final int uid = extras.getInt(Intent.EXTRA_UID); 718 final List<Uri> uris = getPickerUrisForItems(getIntent().getAction(), 719 mSelection.getNewlySelectedItems()); 720 if (!uris.isEmpty()) { 721 ForegroundThread.getExecutor().execute(() -> { 722 // Handle grants in another thread to not block the UI. 723 grantMediaReadForPackage(getApplicationContext(), uid, uris); 724 mPickerViewModel.logPickerChoiceAddedGrantsCount(uris.size(), extras); 725 }); 726 } 727 728 // Revoke READ_GRANT for items that were pre-granted but now in the current session user has 729 // deselected them. 730 if (mPickerViewModel.isManagedSelectionEnabled()) { 731 final List<Uri> urisForItemsWhoseGrantsNeedsToBeRevoked = getPickerUrisForItems( 732 getIntent().getAction(), mSelection.getDeselectedItemsToBeRevoked() 733 .stream().collect(Collectors.toList())); 734 if (!urisForItemsWhoseGrantsNeedsToBeRevoked.isEmpty()) { 735 ForegroundThread.getExecutor().execute(() -> { 736 // Handle grants in another thread to not block the UI. 737 MediaStore.revokeMediaReadForPackages(getApplicationContext(), uid, 738 urisForItemsWhoseGrantsNeedsToBeRevoked); 739 mPickerViewModel.logPickerChoiceRevokedGrantsCount( 740 urisForItemsWhoseGrantsNeedsToBeRevoked.size(), extras); 741 }); 742 } 743 } 744 } 745 setResultForPickImagesOrGetContentAction()746 private void setResultForPickImagesOrGetContentAction() { 747 final Intent resultData = getPickerResponseIntent(getIntent().getAction(), 748 mSelection.canSelectMultiple(), mSelection.getSelectedItems()); 749 setResult(RESULT_OK, resultData); 750 } 751 752 /** 753 * Inspects the current selection list to see if any items in the selection have an authority 754 * that does not match the {@link MediaStore.AUTHORITY} 755 * 756 * <p>If all items have the MediaStore authority, it is presumed that the selection only 757 * contains local items. 758 * 759 * @return Whether the selection includes only local items 760 */ isSelectionOnlyLocalItems()761 private boolean isSelectionOnlyLocalItems() { 762 763 for (Item item : mSelection.getSelectedItems()) { 764 if (!item.getContentUri().getAuthority().equals(LOCAL_PICKER_PROVIDER_AUTHORITY)) { 765 return false; 766 } 767 } 768 return true; 769 } 770 shouldPreloadSelectedItems()771 private boolean shouldPreloadSelectedItems() { 772 // Only preload if the cloud media may be shown in the PhotoPicker. 773 if (isSelectionOnlyLocalItems()) { 774 return false; 775 } 776 777 final boolean isGetContent = isGetContentAction(); 778 final boolean isPickImages = isPickImagesAction(); 779 final ConfigStore cs = mPickerViewModel.getConfigStore(); 780 781 if (getIntent().hasExtra(EXTRA_PRELOAD_SELECTED)) { 782 if (Build.isDebuggable() 783 || (isPickImages && cs.shouldPickerRespectPreloadArgumentForPickImages())) { 784 return getIntent().getBooleanExtra(EXTRA_PRELOAD_SELECTED, 785 /* default, not used */ false); 786 } 787 } 788 789 if (isGetContent) { 790 return cs.shouldPickerPreloadForGetContent(); 791 } else if (isPickImages) { 792 return cs.shouldPickerPreloadForPickImages(); 793 } else { 794 Log.w(TAG, "Not preloading selection for \"" + getIntent().getAction() + "\" action"); 795 return false; 796 } 797 } 798 subscribeToSelectedMediaPreloader(@onNull SelectedMediaPreloader preloader)799 private void subscribeToSelectedMediaPreloader(@NonNull SelectedMediaPreloader preloader) { 800 preloader.getIsFinishedLiveData().observe( 801 /* lifecycleOwner */ PhotoPickerActivity.this, 802 isFinished -> { 803 if (isFinished) { 804 mPickerViewModel.logPreloadingFinished(); 805 setResultAndFinishSelfInternal(); 806 } 807 }); 808 } 809 810 // This method is responsible for deselecting all unavailable items from selection list 811 // when user tries selecting unavailable could only media (not cached) while offline deSelectUnavailableMedia(@onNull SelectedMediaPreloader preloader)812 private void deSelectUnavailableMedia(@NonNull SelectedMediaPreloader preloader) { 813 preloader.getUnavailableMediaIndexes().observe( 814 /* lifecycleOwner */ PhotoPickerActivity.this, 815 unavailableMediaIndexes -> { 816 if (unavailableMediaIndexes.size() > 0) { 817 // To notify the fragment to uncheck the unavailable items at UI those are 818 // no longer available in the selection list. 819 mIsItemPhotoGridViewChanged.postValue(true); 820 821 // Checking if preloading was intentionally be cancelled by the user 822 if (unavailableMediaIndexes.get(unavailableMediaIndexes.size() - 1) != -1) { 823 // Displaying error dialog with an error message when the user tries 824 // to add unavailable cloud only media (not cached) while offline. 825 DialogUtils.showDialog(this, 826 getResources().getString(R.string.dialog_error_title), 827 getResources().getString(R.string.dialog_error_message)); 828 mPickerViewModel.logPreloadingFailed(unavailableMediaIndexes.size()); 829 } else { 830 unavailableMediaIndexes.remove( 831 unavailableMediaIndexes.size() - 1); 832 mPickerViewModel.logPreloadingCancelled(unavailableMediaIndexes.size()); 833 } 834 List<Item> selectedItems = mSelection.getSelectedItems(); 835 for (var mediaIndex : unavailableMediaIndexes) { 836 mSelection.removeSelectedItem(selectedItems.get(mediaIndex)); 837 } 838 } 839 }); 840 } 841 842 /** 843 * NOTE: this may wrongly return {@code false} if called before {@link PickerViewModel} had a 844 * chance to fetch the authority and the account of the current 845 * {@link android.provider.CloudMediaProvider}. 846 * However, this may only happen very early on in the lifecycle. 847 */ isCloudMediaAvailable()848 private boolean isCloudMediaAvailable() { 849 return mPickerViewModel.getCloudMediaProviderAuthorityLiveData().getValue() != null 850 && mPickerViewModel.getCloudMediaAccountNameLiveData().getValue() != null; 851 } 852 853 /** 854 * This should be called if: 855 * * We are finishing Picker explicitly before the user has seen PhotoPicker UI due to known 856 * checks/workflow. 857 * * We are not returning {@link Activity#RESULT_CANCELED} 858 */ finishWithoutLoggingCancelledResult()859 private void finishWithoutLoggingCancelledResult() { 860 mShouldLogCancelledResult = false; 861 finish(); 862 } 863 864 @Override finish()865 public void finish() { 866 if (mShouldLogCancelledResult) { 867 logPickerCancelled(); 868 } 869 super.finish(); 870 } 871 logPickerSelectionConfirmed(int countOfItemsConfirmed)872 private void logPickerSelectionConfirmed(int countOfItemsConfirmed) { 873 mPickerViewModel.logPickerConfirm(Binder.getCallingUid(), getCallingPackage(), 874 countOfItemsConfirmed); 875 } 876 logPickerCancelled()877 private void logPickerCancelled() { 878 mPickerViewModel.logPickerCancel(Binder.getCallingUid(), getCallingPackage()); 879 } 880 881 @UserIdInt getCurrentUserId()882 private int getCurrentUserId() { 883 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 884 return mPickerViewModel.getUserManagerState().getCurrentUserProfileId().getIdentifier(); 885 } 886 return mPickerViewModel.getUserIdManager().getCurrentUserProfileId().getIdentifier(); 887 } 888 889 /** 890 * Updates the common views such as Title, Toolbar, Navigation bar, status bar and bottom sheet 891 * behavior 892 * 893 * @param mode {@link LayoutModeUtils.Mode} which describes the layout mode to update. 894 * @param title the title to set for the Activity 895 */ updateCommonLayouts(LayoutModeUtils.Mode mode, String title)896 public void updateCommonLayouts(LayoutModeUtils.Mode mode, String title) { 897 updateTitle(title); 898 updateToolbar(mode); 899 updateStatusBarAndNavigationBar(mode); 900 updateBottomSheetBehavior(mode); 901 updateFragmentContainerViewPadding(mode); 902 updateDragBarVisibility(mode); 903 updateHeaderTextVisibility(mode); 904 // The bottom bar and profile button are not shown on preview, hide them in preview. We 905 // handle the visibility of them in TabFragment. We don't need to make them shown in 906 // non-preview page here. 907 if (mode.isPreview) { 908 mBottomBar.setVisibility(View.GONE); 909 mProfileButton.setVisibility(View.GONE); 910 mProfileMenuButton.setVisibility(View.GONE); 911 } 912 } 913 updateTitle(String title)914 private void updateTitle(String title) { 915 setTitle(title); 916 } 917 918 /** 919 * Updates the icons and show/hide the tab layout with {@code mode}. 920 * 921 * @param mode {@link LayoutModeUtils.Mode} which describes the layout mode to update. 922 */ updateToolbar(@onNull LayoutModeUtils.Mode mode)923 private void updateToolbar(@NonNull LayoutModeUtils.Mode mode) { 924 final boolean isPreview = mode.isPreview; 925 final boolean shouldShowTabLayout = mode.isPhotosTabOrAlbumsTab; 926 // 1. Set the tabLayout visibility 927 mTabLayout.setVisibility(shouldShowTabLayout ? View.VISIBLE : View.GONE); 928 929 // 2. Set the toolbar color 930 final ColorDrawable toolbarColor; 931 if (isPreview && !shouldShowTabLayout) { 932 if (isOrientationLandscape()) { 933 // Toolbar in Preview will have transparent color in Landscape mode. 934 toolbarColor = new ColorDrawable(getColor(android.R.color.transparent)); 935 } else { 936 // Toolbar in Preview will have a solid color with 90% opacity in Portrait mode. 937 toolbarColor = new ColorDrawable(getColor(R.color.preview_scrim_solid_color)); 938 } 939 } else { 940 toolbarColor = new ColorDrawable(mDefaultBackgroundColor); 941 } 942 getSupportActionBar().setBackgroundDrawable(toolbarColor); 943 944 // 3. Set the toolbar icon. 945 final Drawable icon; 946 if (shouldShowTabLayout) { 947 icon = getDrawable(R.drawable.ic_close); 948 if (mIsCustomPickerColorSet) { 949 icon.setTint(mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 950 AccentColorResources.ON_SURFACE_COLOR_LIGHT, 951 AccentColorResources.ON_SURFACE_COLOR_DARK 952 )); 953 } 954 } else { 955 icon = getDrawable(R.drawable.ic_arrow_back); 956 // Preview mode has dark background, hence icons will be WHITE in color 957 icon.setTint(isPreview ? Color.WHITE : mToolBarIconColor); 958 } 959 getSupportActionBar().setHomeAsUpIndicator(icon); 960 getSupportActionBar().setHomeActionContentDescription( 961 shouldShowTabLayout ? android.R.string.cancel 962 : R.string.abc_action_bar_up_description); 963 if (mToolbar.getOverflowIcon() != null) { 964 mToolbar.getOverflowIcon().setTint(isPreview ? Color.WHITE : mToolBarIconColor); 965 } 966 } 967 968 /** 969 * Updates status bar and navigation bar 970 * 971 * @param mode {@link LayoutModeUtils.Mode} which describes the layout mode to update. 972 */ updateStatusBarAndNavigationBar(@onNull LayoutModeUtils.Mode mode)973 private void updateStatusBarAndNavigationBar(@NonNull LayoutModeUtils.Mode mode) { 974 final boolean isPreview = mode.isPreview; 975 final int navigationBarColor = isPreview ? getColor(R.color.preview_background_color) : 976 mDefaultBackgroundColor; 977 getWindow().setNavigationBarColor(navigationBarColor); 978 979 final int statusBarColor = isPreview ? getColor(R.color.preview_background_color) : 980 getColor(android.R.color.transparent); 981 getWindow().setStatusBarColor(statusBarColor); 982 983 // Update the system bar appearance 984 final int mask = WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; 985 int appearance = 0; 986 if (!isPreview) { 987 final int uiModeNight = 988 getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; 989 990 if (uiModeNight == Configuration.UI_MODE_NIGHT_NO) { 991 // If the system is not in Dark theme, set the system bars to light mode. 992 appearance = mask; 993 } 994 } 995 getWindow().getInsetsController().setSystemBarsAppearance(appearance, mask); 996 } 997 998 /** 999 * Updates the bottom sheet behavior 1000 * 1001 * @param mode {@link LayoutModeUtils.Mode} which describes the layout mode to update. 1002 */ updateBottomSheetBehavior(@onNull LayoutModeUtils.Mode mode)1003 private void updateBottomSheetBehavior(@NonNull LayoutModeUtils.Mode mode) { 1004 final boolean isPreview = mode.isPreview; 1005 if (mBottomSheetView != null) { 1006 mBottomSheetView.setClipToOutline(!isPreview); 1007 // TODO(b/197241815): Add animation downward swipe for preview should go back to 1008 // the photo in photos grid 1009 mBottomSheetBehavior.setDraggable(!isPreview); 1010 } 1011 if (isPreview) { 1012 if (mBottomSheetBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED) { 1013 // Sets bottom sheet behavior state to STATE_EXPANDED if it's not already expanded. 1014 // This is useful when user goes to Preview mode which is always Full screen. 1015 // TODO(b/197241815): Add animation preview to full screen and back transition to 1016 // partial screen. This is similar to long press animation. 1017 mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); 1018 } 1019 } else { 1020 restoreBottomSheetState(); 1021 } 1022 } 1023 1024 /** 1025 * Updates the FragmentContainerView padding. 1026 * <p> 1027 * For Preview mode, toolbar overlaps the Fragment content, hence the padding will be set to 0. 1028 * For Non-Preview mode, toolbar doesn't overlap the contents of the fragment, hence we set the 1029 * padding as the height of the toolbar. 1030 */ updateFragmentContainerViewPadding(@onNull LayoutModeUtils.Mode mode)1031 private void updateFragmentContainerViewPadding(@NonNull LayoutModeUtils.Mode mode) { 1032 if (mFragmentContainerView == null) return; 1033 1034 final int topPadding; 1035 if (mode.isPreview) { 1036 topPadding = 0; 1037 } else { 1038 topPadding = mToolbarHeight; 1039 } 1040 1041 mFragmentContainerView.setPadding(mFragmentContainerView.getPaddingLeft(), 1042 topPadding, mFragmentContainerView.getPaddingRight(), 1043 mFragmentContainerView.getPaddingBottom()); 1044 } 1045 updateDragBarVisibility(@onNull LayoutModeUtils.Mode mode)1046 private void updateDragBarVisibility(@NonNull LayoutModeUtils.Mode mode) { 1047 final boolean shouldShowDragBar = !mode.isPreview; 1048 mDragBar.setVisibility(shouldShowDragBar ? View.VISIBLE : View.GONE); 1049 } 1050 updateHeaderTextVisibility(@onNull LayoutModeUtils.Mode mode)1051 private void updateHeaderTextVisibility(@NonNull LayoutModeUtils.Mode mode) { 1052 // The privacy text is only shown on the Photos tab and Albums tab when not in 1053 // permission select mode. 1054 final boolean shouldShowPrivacyMessage = mode.isPhotosTabOrAlbumsTab; 1055 1056 if (!shouldShowPrivacyMessage) { 1057 mPrivacyText.setVisibility(View.GONE); 1058 return; 1059 } 1060 1061 if (mPickerViewModel.isUserSelectForApp()) { 1062 mPrivacyText.setText(R.string.picker_header_permissions); 1063 mPrivacyText.setTextSize( 1064 TypedValue.COMPLEX_UNIT_PX, 1065 getResources().getDimension(R.dimen.picker_user_select_header_text_size)); 1066 } else { 1067 mPrivacyText.setText(R.string.picker_privacy_message); 1068 mPrivacyText.setTextSize( 1069 TypedValue.COMPLEX_UNIT_PX, 1070 getResources().getDimension(R.dimen.picker_privacy_text_size)); 1071 if (mIsCustomPickerColorSet) { 1072 mPrivacyText.setTextColor( 1073 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 1074 AccentColorResources.ON_SURFACE_VARIANT_LIGHT, 1075 AccentColorResources.ON_SURFACE_VARIANT_DARK 1076 )); 1077 } 1078 } 1079 1080 mPrivacyText.setVisibility(View.VISIBLE); 1081 } 1082 1083 /** 1084 * Clear all the fragments in the FragmentManager 1085 */ clearFragments()1086 void clearFragments() { 1087 final FragmentManager fragmentManager = getSupportFragmentManager(); 1088 fragmentManager.popBackStackImmediate(/* name */ null, 1089 FragmentManager.POP_BACK_STACK_INCLUSIVE); 1090 } 1091 1092 /** 1093 * Reset to Photo Picker initial launch state (Photos grid tab) in personal profile mode. 1094 */ resetToPersonalProfile()1095 private void resetToPersonalProfile() { 1096 // Clear all the fragments in the FragmentManager 1097 clearFragments(); 1098 1099 // Reset all content to the personal profile 1100 mPickerViewModel.resetToPersonalProfile(); 1101 1102 // Set up the fragments same as the initial launch state 1103 setupInitialLaunchState(); 1104 } 1105 1106 /** 1107 * Reset to Photo Picker initial launch state (Photos grid tab) in user profile mode that 1108 * started the photopicker. 1109 */ resetToCurrentUserProfile()1110 private void resetToCurrentUserProfile() { 1111 // Clear all the fragments in the FragmentManager 1112 clearFragments(); 1113 1114 // Reset all content to the start user profile 1115 mPickerViewModel.resetToCurrentUserProfile(); 1116 1117 // Set up the fragments same as the initial launch state 1118 setupInitialLaunchState(); 1119 } 1120 1121 /** 1122 * Reset to Photo Picker initial launch state (Photos grid tab) in the current profile mode. 1123 */ resetInCurrentProfile(boolean shouldSendInitRequest)1124 private void resetInCurrentProfile(boolean shouldSendInitRequest) { 1125 // Clear all the fragments in the FragmentManager 1126 clearFragments(); 1127 1128 // Reset all content in the current profile 1129 mPickerViewModel.resetAllContentInCurrentProfile(shouldSendInitRequest); 1130 1131 // Set up the fragments same as the initial launch state 1132 setupInitialLaunchState(); 1133 } 1134 1135 /** 1136 * Returns {@code true} if settings page is enabled. 1137 */ shouldShowSettingsScreen()1138 private boolean shouldShowSettingsScreen() { 1139 if (mPickerViewModel.shouldShowOnlyLocalFeatures()) { 1140 return false; 1141 } 1142 1143 final ComponentName componentName = new ComponentName(this, 1144 PhotoPickerSettingsActivity.class); 1145 return getPackageManager().getComponentEnabledSetting(componentName) 1146 == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; 1147 } 1148 1149 /** 1150 * Returns {@code true} if intent action is {@link ACTION_GET_CONTENT}. 1151 */ isGetContentAction()1152 private boolean isGetContentAction() { 1153 return ACTION_GET_CONTENT.equals(getIntent().getAction()); 1154 } 1155 1156 /** 1157 * Returns {@code true} if intent action is {@link ACTION_PICK_IMAGES}. 1158 */ isPickImagesAction()1159 private boolean isPickImagesAction() { 1160 return ACTION_PICK_IMAGES.equals(getIntent().getAction()); 1161 } 1162 1163 /** 1164 * Returns {@code true} if intent action is {@link ACTION_USER_SELECT_IMAGES_FOR_APP} 1165 * (the 3-way storage permission grant flow) 1166 */ isUserSelectImagesForAppAction()1167 private boolean isUserSelectImagesForAppAction() { 1168 return ACTION_USER_SELECT_IMAGES_FOR_APP.equals(getIntent().getAction()); 1169 } 1170 1171 private class CrossProfileListeners { 1172 private final List<String> mProfileFilterActions = Lists.newArrayList( 1173 Intent.ACTION_MANAGED_PROFILE_ADDED, // add profile button switch 1174 Intent.ACTION_MANAGED_PROFILE_REMOVED, // remove profile button switch 1175 Intent.ACTION_MANAGED_PROFILE_UNLOCKED, // activate profile button switch 1176 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE // disable profile button switch 1177 ); 1178 1179 private final UserIdManager mUserIdManager; 1180 private final UserManagerState mUserManagerState; 1181 CrossProfileListeners()1182 public CrossProfileListeners() { 1183 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastV()) { 1184 mProfileFilterActions.add(Intent.ACTION_PROFILE_ADDED); 1185 mProfileFilterActions.add(Intent.ACTION_PROFILE_REMOVED); 1186 mProfileFilterActions.add(Intent.ACTION_PROFILE_UNAVAILABLE); 1187 mProfileFilterActions.add(Intent.ACTION_PROFILE_AVAILABLE); 1188 } 1189 1190 mUserManagerState = mPickerViewModel.getUserManagerState(); 1191 mUserIdManager = mPickerViewModel.getUserIdManager(); 1192 1193 registerBroadcastReceivers(); 1194 } 1195 onDestroy()1196 public void onDestroy() { 1197 unregisterReceiver(mReceiver); 1198 } 1199 1200 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 1201 @Override 1202 public void onReceive(Context context, Intent intent) { 1203 final String action = intent.getAction(); 1204 1205 final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER); 1206 final UserId userId = UserId.of(userHandle); 1207 1208 // We only need to refresh the layout when the received profile user is the 1209 // managed user corresponding to the current profile or a new work profile is added 1210 // for the current user. 1211 if (!(mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) 1212 && !userId.equals(mUserIdManager.getManagedUserId()) 1213 && !action.equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) { 1214 return; 1215 } 1216 1217 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastV()) { 1218 switch (action) { 1219 case Intent.ACTION_PROFILE_ADDED: 1220 handleProfileAdded(); 1221 break; 1222 case Intent.ACTION_PROFILE_REMOVED: 1223 notifyProfileRemoved(userId); 1224 break; 1225 case Intent.ACTION_PROFILE_UNAVAILABLE: 1226 notifyProfileOff(userId); 1227 break; 1228 case Intent.ACTION_PROFILE_AVAILABLE: 1229 handleProfileOn(userId); 1230 break; 1231 default: 1232 // do nothing 1233 } 1234 } 1235 1236 switch (action) { 1237 case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE: 1238 notifyProfileOff(userId); 1239 break; 1240 case Intent.ACTION_MANAGED_PROFILE_REMOVED: 1241 if (!(mConfigStore.isPrivateSpaceInPhotoPickerEnabled() 1242 && SdkLevel.isAtLeastV())) { 1243 notifyProfileRemoved(userId); 1244 } 1245 break; 1246 case Intent.ACTION_MANAGED_PROFILE_UNLOCKED: 1247 handleProfileOn(userId); 1248 break; 1249 case Intent.ACTION_MANAGED_PROFILE_ADDED: 1250 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() 1251 && !SdkLevel.isAtLeastV() && SdkLevel.isAtLeastS()) { 1252 handleProfileAdded(); 1253 } else if (!(mConfigStore.isPrivateSpaceInPhotoPickerEnabled() 1254 && SdkLevel.isAtLeastS())) { 1255 handleWorkProfileAdded(); 1256 } 1257 break; 1258 default: 1259 // do nothing 1260 } 1261 } 1262 }; 1263 registerBroadcastReceivers()1264 private void registerBroadcastReceivers() { 1265 final IntentFilter profileFilter = new IntentFilter(); 1266 for (String profileAction : mProfileFilterActions) { 1267 profileFilter.addAction(profileAction); 1268 } 1269 registerReceiver(mReceiver, profileFilter); 1270 } 1271 handleTurnedOffOrRemovedProfile()1272 private void handleTurnedOffOrRemovedProfile() { 1273 if (mTurnedOffProfileUserId != null) { 1274 handleProfileOff(mTurnedOffProfileUserId); 1275 mTurnedOffProfileUserId = null; 1276 } 1277 1278 if (mRemovedProfileUserId != null) { 1279 if (!(mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS())) { 1280 handleWorkProfileRemoved(); 1281 } else { 1282 handleProfileRemoved(mRemovedProfileUserId); 1283 } 1284 mRemovedProfileUserId = null; 1285 } 1286 } handleWorkProfileOff()1287 private void handleWorkProfileOff() { 1288 if (mUserIdManager.isManagedUserSelected()) { 1289 switchToPersonalProfileInitialLaunchState(); 1290 } 1291 mUserIdManager.updateWorkProfileOffValue(); 1292 } notifyProfileOff(UserId userId)1293 private void notifyProfileOff(UserId userId) { 1294 mTurnedOffProfileUserId = userId; 1295 if (mIsPostResumeCallFinished) { 1296 handleTurnedOffOrRemovedProfile(); 1297 } 1298 } 1299 notifyProfileRemoved(UserId userId)1300 private void notifyProfileRemoved(UserId userId) { 1301 mRemovedProfileUserId = userId; 1302 if (mIsPostResumeCallFinished) { 1303 handleTurnedOffOrRemovedProfile(); 1304 } 1305 } handleProfileOff(UserId userId)1306 private void handleProfileOff(UserId userId) { 1307 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1308 if (mUserManagerState.isUserSelectedAsCurrentUserProfile(userId)) { 1309 switchToCurrentUserProfileInitialLaunchState(); 1310 } 1311 mUserManagerState.updateProfileOffValuesAndPostCrossProfileStatus(); 1312 if (SdkLevel.isAtLeastV()) { 1313 updateRecentsScreenshotSetting(); 1314 } 1315 return; 1316 } 1317 handleWorkProfileOff(); 1318 } 1319 1320 handleWorkProfileRemoved()1321 private void handleWorkProfileRemoved() { 1322 if (mUserIdManager.isManagedUserSelected()) { 1323 switchToPersonalProfileInitialLaunchState(); 1324 } 1325 mUserIdManager.resetUserIds(); 1326 } 1327 handleProfileRemoved(UserId userId)1328 private void handleProfileRemoved(UserId userId) { 1329 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1330 if (mUserManagerState.isUserSelectedAsCurrentUserProfile(userId)) { 1331 switchToCurrentUserProfileInitialLaunchState(); 1332 } 1333 mUserManagerState.resetUserIdsAndSetCrossProfileValues(getIntent()); 1334 if (SdkLevel.isAtLeastV()) { 1335 updateRecentsScreenshotSetting(); 1336 } 1337 } 1338 } 1339 handleWorkProfileAdded()1340 private void handleWorkProfileAdded() { 1341 mUserIdManager.resetUserIds(); 1342 } 1343 handleProfileAdded()1344 private void handleProfileAdded() { 1345 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1346 mUserManagerState.resetUserIdsAndSetCrossProfileValues(getIntent()); 1347 if (SdkLevel.isAtLeastV()) { 1348 updateRecentsScreenshotSetting(); 1349 } 1350 } 1351 } 1352 handleWorkProfileOn()1353 private void handleWorkProfileOn() { 1354 // Update UI for switch to profile button 1355 // When the managed profile becomes available, the provider may not be available 1356 // immediately, we need to check if it is ready before we reload the content. 1357 mUserIdManager.waitForMediaProviderToBeAvailable(); 1358 } 1359 handleProfileOn(UserId userId)1360 private void handleProfileOn(UserId userId) { 1361 // Update UI for switch to profile button 1362 // When the managed profile becomes available, the provider may not be available 1363 // immediately, we need to check if it is ready before we reload the content. 1364 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1365 mUserManagerState.waitForMediaProviderToBeAvailable(userId); 1366 if (SdkLevel.isAtLeastV()) { 1367 updateRecentsScreenshotSetting(); 1368 } 1369 return; 1370 } 1371 handleWorkProfileOn(); 1372 } 1373 switchToPersonalProfileInitialLaunchState()1374 private void switchToPersonalProfileInitialLaunchState() { 1375 // We reset the state of the PhotoPicker as we do not want to make any 1376 // assumptions on the state of the PhotoPicker when it was in Work Profile mode. 1377 resetToPersonalProfile(); 1378 } 1379 switchToCurrentUserProfileInitialLaunchState()1380 private void switchToCurrentUserProfileInitialLaunchState() { 1381 // We reset the state of the PhotoPicker as we do not want to make any 1382 // assumptions on the state of the PhotoPicker when it was in other Profile mode. 1383 resetToCurrentUserProfile(); 1384 } 1385 } 1386 1387 /** 1388 * A {@link ViewModel} class only responsible for keeping track of "active" 1389 * {@link SelectedMediaPreloader} instance (if any). 1390 * This class has to be public, since somewhere in {@link ViewModelProvider} it will try to use 1391 * reflection to create an instance of this class. 1392 */ 1393 public static class PreloaderInstanceHolder extends ViewModel { 1394 @Nullable 1395 SelectedMediaPreloader preloader; 1396 } 1397 1398 /** 1399 * Reset the picker view model content when launched with cloud features and notified to 1400 * refresh the UI. 1401 */ observeRefreshUiNotificationLiveData()1402 private void observeRefreshUiNotificationLiveData() { 1403 mPickerViewModel.refreshUiLiveData() 1404 .observe(this, refreshRequest -> { 1405 if (refreshRequest.shouldRefreshPicker() 1406 && !mPickerViewModel.shouldShowOnlyLocalFeatures()) { 1407 resetInCurrentProfile(refreshRequest.shouldInitPicker()); 1408 } 1409 }); 1410 } 1411 1412 @SuppressLint("NewApi") updateRecentsScreenshotSetting()1413 private void updateRecentsScreenshotSetting() { 1414 if (!(mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastV())) return; 1415 RecentsPreviewUtil.updateRecentsVisibilitySetting(mConfigStore, 1416 mPickerViewModel.getUserManagerState(), this); 1417 } 1418 } 1419