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