1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.providers.media.photopicker.ui; 17 18 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.Style.OUTLINE; 19 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; 20 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_PERSONAL_MESSAGE; 21 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_WORK_MESSAGE; 22 import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_BANNER; 23 import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_SECTION; 24 25 import android.app.admin.DevicePolicyManager; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.UserProperties; 29 import android.content.res.ColorStateList; 30 import android.content.res.TypedArray; 31 import android.graphics.Color; 32 import android.graphics.drawable.Drawable; 33 import android.os.Build; 34 import android.os.Bundle; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.util.Pair; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.accessibility.AccessibilityManager; 42 import android.view.animation.Animation; 43 import android.view.animation.AnimationUtils; 44 import android.widget.Button; 45 import android.widget.LinearLayout; 46 import android.widget.PopupWindow; 47 import android.widget.TextView; 48 49 import androidx.annotation.ColorInt; 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.annotation.RequiresApi; 53 import androidx.core.content.ContextCompat; 54 import androidx.core.graphics.drawable.DrawableCompat; 55 import androidx.fragment.app.Fragment; 56 import androidx.fragment.app.FragmentActivity; 57 import androidx.lifecycle.LiveData; 58 import androidx.lifecycle.MutableLiveData; 59 import androidx.lifecycle.ViewModelProvider; 60 import androidx.recyclerview.widget.GridLayoutManager; 61 import androidx.recyclerview.widget.RecyclerView; 62 63 import com.android.modules.utils.build.SdkLevel; 64 import com.android.providers.media.ConfigStore; 65 import com.android.providers.media.R; 66 import com.android.providers.media.photopicker.PhotoPickerActivity; 67 import com.android.providers.media.photopicker.data.Selection; 68 import com.android.providers.media.photopicker.data.UserIdManager; 69 import com.android.providers.media.photopicker.data.UserManagerState; 70 import com.android.providers.media.photopicker.data.model.UserId; 71 import com.android.providers.media.photopicker.util.AccentColorResources; 72 import com.android.providers.media.photopicker.viewmodel.PickerViewModel; 73 74 import com.google.android.material.button.MaterialButton; 75 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; 76 77 import java.text.NumberFormat; 78 import java.util.Locale; 79 import java.util.Map; 80 81 /** 82 * The base abstract Tab fragment 83 */ 84 public abstract class TabFragment extends Fragment { 85 private static final String TAG = TabFragment.class.getSimpleName(); 86 protected PickerViewModel mPickerViewModel; 87 protected Selection mSelection; 88 protected ImageLoader mImageLoader; 89 protected AutoFitRecyclerView mRecyclerView; 90 91 private ExtendedFloatingActionButton mProfileButton; 92 private ExtendedFloatingActionButton mProfileMenuButton; 93 private UserIdManager mUserIdManager; 94 private UserManagerState mUserManagerState; 95 private boolean mHideProfileButtonAndProfileMenuButton; 96 private View mEmptyView; 97 private TextView mEmptyTextView; 98 private boolean mIsAccessibilityEnabled; 99 private AccessibilityManager mAccessibilityManager; 100 private AccessibilityManager.AccessibilityStateChangeListener mAccessibilityStateChangeListener; 101 102 private Button mAddButton; 103 104 private MaterialButton mViewSelectedButton; 105 private View mBottomBar; 106 private Animation mSlideUpAnimation; 107 private Animation mSlideDownAnimation; 108 109 @ColorInt 110 private int mButtonIconAndTextColor; 111 112 @ColorInt 113 private int mProfileMenuButtonIconAndTextColor; 114 @ColorInt 115 private int mButtonBackgroundColor; 116 117 @ColorInt 118 private int mButtonDisabledIconAndTextColor; 119 120 @ColorInt 121 private int mButtonDisabledBackgroundColor; 122 123 private int mRecyclerViewBottomPadding; 124 private boolean mIsProfileButtonVisible = false; 125 private boolean mIsProfileMenuButtonVisible = false; 126 private static PopupWindow sProfileMenuWindow = null; 127 128 private RecyclerView.OnScrollListener mOnScrollListenerForMultiProfileButton; 129 130 private final MutableLiveData<Boolean> mIsBottomBarVisible = new MutableLiveData<>(false); 131 private final MutableLiveData<Boolean> mIsProfileButtonOrProfileMenuButtonVisible = 132 new MutableLiveData<>(false); 133 private ConfigStore mConfigStore; 134 private boolean mIsCustomPickerColorSet = false; 135 136 /** 137 * In case of multiuser profile, it represents the number of profiles that are off 138 * (In quiet mode) with {@link UserProperties#SHOW_IN_QUIET_MODE_HIDDEN}. Such profiles 139 * in quiet mode will not appear in photopicker. 140 */ 141 private int mHideProfileCount = 0; 142 143 /** 144 * This member variable is relevant to get the userId (other than current user) when only two 145 * number of profiles those either unlocked/on or don't have 146 * {@link UserProperties#SHOW_IN_QUIET_MODE_HIDDEN}, are available on the device. 147 * we are using this variable to get label and icon of a userId to update the content 148 * in {@link #mProfileButton}, and at the time when user will press {@link #mProfileButton} 149 * to change the current profile. 150 */ 151 private UserId mPotentialUserForProfileButton; 152 153 @Override 154 @NonNull onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)155 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 156 Bundle savedInstanceState) { 157 super.onCreateView(inflater, container, savedInstanceState); 158 return inflater.inflate(R.layout.fragment_picker_tab, container, false); 159 } 160 161 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)162 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 163 super.onViewCreated(view, savedInstanceState); 164 165 final Context context = requireContext(); 166 final FragmentActivity activity = requireActivity(); 167 168 mImageLoader = new ImageLoader(context); 169 mRecyclerView = view.findViewById(R.id.picker_tab_recyclerview); 170 mRecyclerView.setHasFixedSize(true); 171 final ViewModelProvider viewModelProvider = new ViewModelProvider(activity); 172 mPickerViewModel = viewModelProvider.get(PickerViewModel.class); 173 mIsCustomPickerColorSet = 174 mPickerViewModel.getPickerAccentColorParameters().isCustomPickerColorSet(); 175 mConfigStore = mPickerViewModel.getConfigStore(); 176 mSelection = mPickerViewModel.getSelection(); 177 mRecyclerViewBottomPadding = getResources().getDimensionPixelSize( 178 R.dimen.picker_recycler_view_bottom_padding); 179 180 mIsBottomBarVisible.observe(this, val -> updateRecyclerViewBottomPadding()); 181 mIsProfileButtonOrProfileMenuButtonVisible.observe( 182 this, val -> updateRecyclerViewBottomPadding()); 183 184 mEmptyView = view.findViewById(android.R.id.empty); 185 mEmptyTextView = mEmptyView.findViewById(R.id.empty_text_view); 186 187 final int[] attrsDisabled = 188 new int[]{R.attr.pickerDisabledProfileButtonColor, 189 R.attr.pickerDisabledProfileButtonTextColor}; 190 final TypedArray taDisabled = context.obtainStyledAttributes(attrsDisabled); 191 mButtonDisabledBackgroundColor = taDisabled.getColor(/* index */ 0, /* defValue */ -1); 192 mButtonDisabledIconAndTextColor = taDisabled.getColor(/* index */ 1, /* defValue */ -1); 193 taDisabled.recycle(); 194 195 final int[] attrs = 196 new int[]{R.attr.pickerProfileButtonColor, R.attr.pickerProfileButtonTextColor, 197 android.R.attr.textColorPrimary}; 198 final TypedArray ta = context.obtainStyledAttributes(attrs); 199 mButtonBackgroundColor = ta.getColor(/* index */ 0, /* defValue */ -1); 200 mButtonIconAndTextColor = ta.getColor(/* index */ 1, /* defValue */ -1); 201 mProfileMenuButtonIconAndTextColor = ta.getColor(/* index */ 2, /* defValue */ -1); 202 ta.recycle(); 203 204 mProfileButton = activity.findViewById(R.id.profile_button); 205 mProfileMenuButton = activity.findViewById(R.id.profile_menu_button); 206 mUserManagerState = mPickerViewModel.getUserManagerState(); 207 mUserIdManager = mPickerViewModel.getUserIdManager(); 208 209 final boolean canSelectMultiple = mSelection.canSelectMultiple(); 210 if (canSelectMultiple) { 211 mAddButton = activity.findViewById(R.id.button_add); 212 213 mViewSelectedButton = activity.findViewById(R.id.button_view_selected); 214 215 if (mIsCustomPickerColorSet) { 216 setCustomPickerButtonColors( 217 mPickerViewModel.getPickerAccentColorParameters().getPickerAccentColor()); 218 } 219 mAddButton.setOnClickListener(v -> { 220 try { 221 requirePickerActivity().setResultAndFinishSelf(); 222 } catch (RuntimeException e) { 223 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 224 } 225 }); 226 // Transition to PreviewFragment on clicking "View Selected". 227 mViewSelectedButton.setOnClickListener(v -> { 228 // Load items for preview that are pre granted but not yet loaded for UI. This is an 229 // async call. Until the items are loaded, we can still preview already available 230 // items 231 mPickerViewModel.getRemainingPreGrantedItems(); 232 mSelection.prepareSelectedItemsForPreviewAll(); 233 234 int selectedItemCount = mSelection.getSelectedItemCount().getValue(); 235 mPickerViewModel.logPreviewAllSelected(selectedItemCount); 236 237 try { 238 PreviewFragment.show(requireActivity().getSupportFragmentManager(), 239 PreviewFragment.getArgsForPreviewOnViewSelected()); 240 } catch (RuntimeException e) { 241 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 242 } 243 }); 244 245 mBottomBar = activity.findViewById(R.id.picker_bottom_bar); 246 247 if (mIsCustomPickerColorSet) { 248 mBottomBar.setBackgroundColor( 249 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 250 AccentColorResources.SURFACE_CONTAINER_COLOR_LIGHT, 251 AccentColorResources.SURFACE_CONTAINER_COLOR_DARK 252 )); 253 } 254 // consume the event so that it doesn't get passed through to the next view b/287661737 255 mBottomBar.setOnClickListener(v -> { 256 }); 257 mSlideUpAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_up); 258 mSlideDownAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_down); 259 260 mSelection.getSelectedItemCount().observe(this, selectedItemListSize -> { 261 // Fetch activity or context again instead of capturing existing variable in lambdas 262 // to avoid memory leaks. 263 try { 264 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() 265 && SdkLevel.isAtLeastS()) { 266 updateProfileButtonAndProfileMenuButtonVisibility(); 267 } else { 268 updateProfileButtonVisibility(); 269 } 270 updateVisibilityAndAnimateBottomBar(requireContext(), selectedItemListSize); 271 } catch (RuntimeException e) { 272 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 273 } 274 }); 275 } 276 277 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 278 setUpObserverForCrossProfileAndMultiUserChangeGeneric(); 279 280 // Initial setup 281 setUpProfileButtonAndProfileMenuButtonWithListeners( 282 mUserManagerState.isMultiUserProfiles()); 283 284 } else { 285 setupObserverForCrossProfileAccess(); 286 287 // Initial setup 288 setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles()); 289 } 290 291 292 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 293 mIsAccessibilityEnabled = mAccessibilityManager.isEnabled(); 294 mAccessibilityStateChangeListener = 295 enabled -> { 296 mIsAccessibilityEnabled = enabled; 297 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() 298 && SdkLevel.isAtLeastS()) { 299 setUpProfileButtonAndProfileMenuButtonWithListeners( 300 mUserManagerState.isMultiUserProfiles()); 301 } else { 302 setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles()); 303 } 304 }; 305 mAccessibilityManager.addAccessibilityStateChangeListener( 306 mAccessibilityStateChangeListener); 307 } 308 309 @Override onDestroyView()310 public void onDestroyView() { 311 super.onDestroyView(); 312 if (mAccessibilityManager != null) { 313 mAccessibilityManager.removeAccessibilityStateChangeListener( 314 mAccessibilityStateChangeListener); 315 } 316 } 317 setupObserverForCrossProfileAccess()318 private void setupObserverForCrossProfileAccess() { 319 // Observe for cross profile access changes. 320 final LiveData<Boolean> crossProfileAllowed = mUserIdManager.getCrossProfileAllowed(); 321 if (crossProfileAllowed != null) { 322 crossProfileAllowed.observe(this, isCrossProfileAllowed -> { 323 setUpProfileButton(); 324 if (Boolean.TRUE.equals( 325 mIsProfileButtonOrProfileMenuButtonVisible.getValue())) { 326 if (isCrossProfileAllowed) { 327 mPickerViewModel.logProfileSwitchButtonEnabled(); 328 } else { 329 mPickerViewModel.logProfileSwitchButtonDisabled(); 330 } 331 } 332 }); 333 } 334 335 // Observe for multi-user changes. 336 final LiveData<Boolean> isMultiUserProfiles = mUserIdManager.getIsMultiUserProfiles(); 337 if (isMultiUserProfiles != null) { 338 isMultiUserProfiles.observe(this, this::setUpProfileButtonWithListeners); 339 } 340 } 341 342 @RequiresApi(Build.VERSION_CODES.S) setUpObserverForCrossProfileAndMultiUserChangeGeneric()343 private void setUpObserverForCrossProfileAndMultiUserChangeGeneric() { 344 // Observe for cross profile access changes. 345 final LiveData<Map<UserId, Boolean>> crossProfileAllowed = 346 mUserManagerState.getCrossProfileAllowed(); 347 if (crossProfileAllowed != null) { 348 crossProfileAllowed.observe(this, crossProfileAllowedStatus -> { 349 setUpProfileButtonAndProfileMenuButton(); 350 if (mIsProfileButtonVisible) { 351 boolean isDisabled = true; 352 UserId userIdToSwitch = getUserToSwitchFromProfileButton(); 353 if (userIdToSwitch != null) { 354 isDisabled = !canSwitchToUser(userIdToSwitch); 355 } 356 if (isDisabled) { 357 mPickerViewModel.logProfileSwitchButtonDisabled(); 358 } else { 359 mPickerViewModel.logProfileSwitchButtonEnabled(); 360 } 361 } else if (mIsProfileMenuButtonVisible) { 362 mPickerViewModel.logProfileSwitchMenuButtonVisible(); 363 } 364 }); 365 } 366 367 // Observe for multi-user changes. 368 final LiveData<Boolean> isMultiUserProfiles = 369 mUserManagerState.getIsMultiUserProfiles(); 370 if (isMultiUserProfiles != null) { 371 isMultiUserProfiles.observe(this, isMultiUserProfilesAvailable -> { 372 setUpProfileButtonAndProfileMenuButtonWithListeners(isMultiUserProfilesAvailable); 373 }); 374 } 375 } 376 377 @RequiresApi(Build.VERSION_CODES.S) updateUserForProfileButtonAndHideProfileCount()378 private void updateUserForProfileButtonAndHideProfileCount() { 379 mHideProfileCount = 0; 380 mPotentialUserForProfileButton = null; 381 for (UserId userId : mUserManagerState.getAllUserProfileIds()) { 382 if (isProfileHideInQuietMode(userId)) { 383 mHideProfileCount += 1; 384 } else if (!userId.equals(UserId.CURRENT_USER)) { 385 mPotentialUserForProfileButton = userId; 386 } 387 } 388 389 // we will use {@link #mPotentialUserForProfileButton} only to show profile button and 390 // profile button will only be visible when two profiles are available on the device 391 if (mUserManagerState.getProfileCount() - mHideProfileCount != 2) { 392 mPotentialUserForProfileButton = null; 393 } 394 } 395 isProfileHideInQuietMode(UserId userId)396 private boolean isProfileHideInQuietMode(UserId userId) { 397 if (!SdkLevel.isAtLeastV()) { 398 return false; 399 } 400 /* 401 * Any profile with {@link UserProperties.SHOW_IN_QUIET_MODE_HIDDEN} will not appear in 402 * quiet mode in Photopicker. 403 */ 404 return mUserManagerState.isProfileOff(userId) 405 && mUserManagerState.getShowInQuietMode(userId) 406 == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; 407 } 408 setCustomPickerButtonColors(int accentColor)409 private void setCustomPickerButtonColors(int accentColor) { 410 String addButtonTextColor = 411 mPickerViewModel.getPickerAccentColorParameters().isAccentColorBright() 412 ? AccentColorResources.DARK_TEXT_COLOR 413 : AccentColorResources.LIGHT_TEXT_COLOR; 414 mAddButton.setBackgroundColor(accentColor); 415 mAddButton.setTextColor(Color.parseColor(addButtonTextColor)); 416 mViewSelectedButton.setTextColor(accentColor); 417 mViewSelectedButton.setIconTint(ColorStateList.valueOf(accentColor)); 418 419 } 420 updateRecyclerViewBottomPadding()421 private void updateRecyclerViewBottomPadding() { 422 final int recyclerViewBottomPadding; 423 if (mIsProfileButtonOrProfileMenuButtonVisible.getValue() 424 || mIsBottomBarVisible.getValue()) { 425 recyclerViewBottomPadding = mRecyclerViewBottomPadding; 426 } else { 427 recyclerViewBottomPadding = 0; 428 } 429 430 mRecyclerView.setPadding(0, 0, 0, recyclerViewBottomPadding); 431 } 432 updateVisibilityAndAnimateBottomBar(@onNull Context context, int selectedItemListSize)433 private void updateVisibilityAndAnimateBottomBar(@NonNull Context context, 434 int selectedItemListSize) { 435 if (!mSelection.canSelectMultiple()) { 436 return; 437 } 438 439 if (mPickerViewModel.isManagedSelectionEnabled()) { 440 animateAndShowBottomBar(context, selectedItemListSize); 441 if (selectedItemListSize == 0) { 442 mViewSelectedButton.setVisibility(View.INVISIBLE); 443 // Update the add button to show "Allow none". 444 mAddButton.setText(R.string.picker_add_button_allow_none_option); 445 } 446 } else { 447 if (selectedItemListSize == 0) { 448 animateAndHideBottomBar(); 449 } else { 450 animateAndShowBottomBar(context, selectedItemListSize); 451 } 452 } 453 mIsBottomBarVisible.setValue( 454 mPickerViewModel.isManagedSelectionEnabled() || selectedItemListSize > 0); 455 } 456 animateAndShowBottomBar(Context context, int selectedItemListSize)457 private void animateAndShowBottomBar(Context context, int selectedItemListSize) { 458 if (mBottomBar.getVisibility() == View.GONE) { 459 mBottomBar.setVisibility(View.VISIBLE); 460 mBottomBar.startAnimation(mSlideUpAnimation); 461 } 462 mViewSelectedButton.setVisibility(View.VISIBLE); 463 mAddButton.setText(generateAddButtonString(context, selectedItemListSize)); 464 } 465 animateAndHideBottomBar()466 private void animateAndHideBottomBar() { 467 if (mBottomBar.getVisibility() == View.VISIBLE) { 468 mBottomBar.setVisibility(View.GONE); 469 mBottomBar.startAnimation(mSlideDownAnimation); 470 } 471 } 472 setUpListenersForProfileButton()473 private void setUpListenersForProfileButton() { 474 mProfileButton.setOnClickListener(v -> onClickProfileButton()); 475 mOnScrollListenerForMultiProfileButton = new RecyclerView.OnScrollListener() { 476 @Override 477 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 478 super.onScrolled(recyclerView, dx, dy); 479 480 // Do not change profile button visibility on scroll if Accessibility mode is 481 // enabled. This is done to enhance button visibility in Accessibility mode. 482 if (mIsAccessibilityEnabled) { 483 return; 484 } 485 486 if (dy > 0) { 487 mProfileButton.hide(); 488 } else { 489 updateProfileButtonVisibility(); 490 } 491 } 492 }; 493 mRecyclerView.addOnScrollListener(mOnScrollListenerForMultiProfileButton); 494 } 495 496 @RequiresApi(Build.VERSION_CODES.S) setUpListenersForProfileButtonAndProfileMenuButton()497 private void setUpListenersForProfileButtonAndProfileMenuButton() { 498 mProfileButton.setOnClickListener(v -> onClickProfileButtonGeneric()); 499 mProfileMenuButton.setOnClickListener(v -> onClickProfileMenuButton(v)); 500 mOnScrollListenerForMultiProfileButton = new RecyclerView.OnScrollListener() { 501 @Override 502 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 503 super.onScrolled(recyclerView, dx, dy); 504 505 // Do not change profile button visibility on scroll if Accessibility mode is 506 // enabled. This is done to enhance button visibility in Accessibility mode. 507 if (mIsAccessibilityEnabled) { 508 return; 509 } 510 511 if (dy > 0) { 512 mProfileButton.hide(); 513 mProfileMenuButton.hide(); 514 } else { 515 updateProfileButtonAndProfileMenuButtonVisibility(); 516 } 517 } 518 }; 519 mRecyclerView.addOnScrollListener(mOnScrollListenerForMultiProfileButton); 520 } 521 522 @RequiresApi(Build.VERSION_CODES.S) onClickProfileMenuButton(View view)523 private void onClickProfileMenuButton(View view) { 524 mPickerViewModel.logProfileSwitchMenuButtonClick(); 525 initialiseProfileMenuWindow(); 526 View profileMenuView = LayoutInflater.from(requireContext()).inflate( 527 R.layout.profile_menu_layout, null); 528 sProfileMenuWindow.setContentView(profileMenuView); 529 LinearLayout profileMenuContainer = profileMenuView.findViewById( 530 R.id.profile_menu_container); 531 532 Map<UserId, Drawable> profileBadges = mUserManagerState.getProfileBadgeForAll(); 533 Map<UserId, String> profileLabels = mUserManagerState.getProfileLabelsForAll(); 534 535 // Add profile menu items to profile menu. 536 for (UserId userId : mUserManagerState.getAllUserProfileIds()) { 537 if (!isProfileHideInQuietMode(userId)) { 538 View profileMenuItemView = LayoutInflater.from(requireContext()).inflate( 539 R.layout.profile_menu_item, profileMenuContainer, false); 540 541 // Set label and icon in profile menu item 542 TextView profileMenuItem = profileMenuItemView.findViewById(R.id.profile_label); 543 String label = profileLabels.get(userId); 544 Drawable icon = profileBadges.get(userId); 545 boolean isSwitchingAllowed = canSwitchToUser(userId); 546 final int textAndIconColor = isSwitchingAllowed 547 ? mProfileMenuButtonIconAndTextColor : mButtonDisabledIconAndTextColor; 548 DrawableCompat.setTintList(icon, ColorStateList.valueOf(textAndIconColor)); 549 profileMenuItem.setTextColor(ColorStateList.valueOf(textAndIconColor)); 550 profileMenuItem.setText(label); 551 profileMenuItem.setCompoundDrawablesWithIntrinsicBounds( 552 icon, null, null, null); 553 // Set padding between icon anf label in profile menu button 554 int paddingDp = getResources().getDimensionPixelSize( 555 R.dimen.popup_window_title_icon_padding); 556 int paddingPixels = (int) (paddingDp * getResources().getDisplayMetrics().density); 557 profileMenuItem.setCompoundDrawablePadding(paddingPixels); 558 559 // Add click listener 560 profileMenuItemView.setOnClickListener(v -> onClickProfileMenuItem(userId)); 561 profileMenuContainer.addView(profileMenuItemView); 562 } 563 } 564 565 /* 566 * we need estimated dimensions of {@link #sProfileMenuWindow} to open dropdown just above 567 * the {@link #mProfileMenuButton} 568 */ 569 sProfileMenuWindow.showAsDropDown( 570 view, view.getWidth() / 2 - getProfileMenuWindowDimensions().first / 2, 571 -(getProfileMenuWindowDimensions().second + view.getHeight())); 572 } 573 initialiseProfileMenuWindow()574 private void initialiseProfileMenuWindow() { 575 if (sProfileMenuWindow != null) { 576 sProfileMenuWindow.dismiss(); 577 } 578 sProfileMenuWindow = new PopupWindow(requireContext()); 579 sProfileMenuWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); 580 sProfileMenuWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 581 sProfileMenuWindow.setFocusable(true); 582 sProfileMenuWindow.setBackgroundDrawable( 583 ContextCompat.getDrawable(requireContext(), R.drawable.profile_menu_background)); 584 } 585 586 @RequiresApi(Build.VERSION_CODES.S) canSwitchToUser(UserId userId)587 private boolean canSwitchToUser(UserId userId) { 588 return mUserManagerState.getCrossProfileAllowedStatusForAll().get(userId); 589 } 590 591 @RequiresApi(Build.VERSION_CODES.S) onClickProfileMenuItem(UserId userId)592 private void onClickProfileMenuItem(UserId userId) { 593 // Check if current user profileId is not same as given userId, where user want to switch 594 if (!userId.equals(mUserManagerState.getCurrentUserProfileId())) { 595 if (canSwitchToUser(userId)) { 596 changeProfileGeneric(userId); 597 } else { 598 try { 599 ProfileDialogFragment.show( 600 requireActivity().getSupportFragmentManager(), userId); 601 } catch (RuntimeException e) { 602 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 603 } 604 } 605 } 606 sProfileMenuWindow.dismiss(); 607 } 608 609 /** 610 * To get estimated dimensions of {@link #sProfileMenuWindow}; 611 * 612 * @return a pair of two Integers, first represents width and second represents height 613 */ getProfileMenuWindowDimensions()614 private Pair<Integer, Integer> getProfileMenuWindowDimensions() { 615 int width = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 616 int height = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 617 sProfileMenuWindow.getContentView().measure(width, height); 618 619 return new Pair<>(sProfileMenuWindow.getContentView().getMeasuredWidth(), 620 sProfileMenuWindow.getContentView().getMeasuredHeight()); 621 } 622 623 @Override onDestroy()624 public void onDestroy() { 625 super.onDestroy(); 626 if (mRecyclerView != null) { 627 mRecyclerView.clearOnScrollListeners(); 628 } 629 } 630 setUpProfileButtonWithListeners(boolean isMultiUserProfile)631 private void setUpProfileButtonWithListeners(boolean isMultiUserProfile) { 632 if (mOnScrollListenerForMultiProfileButton != null) { 633 mRecyclerView.removeOnScrollListener(mOnScrollListenerForMultiProfileButton); 634 } 635 if (isMultiUserProfile) { 636 setUpListenersForProfileButton(); 637 } 638 setUpProfileButton(); 639 } 640 641 @RequiresApi(Build.VERSION_CODES.S) setUpProfileButtonAndProfileMenuButtonWithListeners(boolean isMultiUserProfile)642 private void setUpProfileButtonAndProfileMenuButtonWithListeners(boolean isMultiUserProfile) { 643 if (mOnScrollListenerForMultiProfileButton != null) { 644 mRecyclerView.removeOnScrollListener(mOnScrollListenerForMultiProfileButton); 645 } 646 if (isMultiUserProfile) { 647 setUpListenersForProfileButtonAndProfileMenuButton(); 648 } 649 setUpProfileButtonAndProfileMenuButton(); 650 651 } 652 setUpProfileButton()653 private void setUpProfileButton() { 654 updateProfileButtonVisibility(); 655 if (!mUserIdManager.isMultiUserProfiles()) { 656 return; 657 } 658 659 updateProfileButtonContent(mUserIdManager.isManagedUserSelected()); 660 updateProfileButtonColor(/* isDisabled */ !mUserIdManager.isCrossProfileAllowed()); 661 } 662 663 @RequiresApi(Build.VERSION_CODES.S) setUpProfileButtonAndProfileMenuButton()664 private void setUpProfileButtonAndProfileMenuButton() { 665 // Dismiss profile menu if user remove/lock any profile in the background while profile 666 // menu window was opened. 667 if (sProfileMenuWindow != null) { 668 sProfileMenuWindow.dismiss(); 669 } 670 updateUserForProfileButtonAndHideProfileCount(); 671 updateProfileButtonAndProfileMenuButtonVisibility(); 672 if (!mUserManagerState.isMultiUserProfiles()) { 673 return; 674 } 675 676 updateProfileButtonAndProfileMenuButtonContent(); 677 updateProfileButtonAndProfileMenuButtonColor(); 678 } 679 shouldShowProfileButton()680 private boolean shouldShowProfileButton() { 681 return mUserIdManager.isMultiUserProfiles() 682 && !mHideProfileButtonAndProfileMenuButton 683 && !mPickerViewModel.isUserSelectForApp() 684 && (!mSelection.canSelectMultiple() 685 || mSelection.getSelectedItemCount().getValue() == 0); 686 } 687 688 @RequiresApi(Build.VERSION_CODES.S) shouldShowProfileButtonOrProfileMenuButton()689 private boolean shouldShowProfileButtonOrProfileMenuButton() { 690 return mUserManagerState.isMultiUserProfiles() 691 && (mUserManagerState.getProfileCount() - mHideProfileCount) > 1 692 && !mHideProfileButtonAndProfileMenuButton 693 && !mPickerViewModel.isUserSelectForApp() 694 && (!mSelection.canSelectMultiple() 695 || mSelection.getSelectedItemCount().getValue() == 0); 696 } 697 onClickProfileButton()698 private void onClickProfileButton() { 699 mPickerViewModel.logProfileSwitchButtonClick(); 700 701 if (!mUserIdManager.isCrossProfileAllowed()) { 702 try { 703 ProfileDialogFragment.show(requireActivity().getSupportFragmentManager(), 704 (UserId) null); 705 } catch (RuntimeException e) { 706 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 707 } 708 } else { 709 changeProfile(); 710 } 711 } 712 713 714 /** 715 * This method is relevant to get the userId (other than current user profile) when only two 716 * number of profiles those either unlocked/on or don't have 717 * {@link UserProperties#SHOW_IN_QUIET_MODE_HIDDEN}, are available on the device. 718 * we are using this method to get label and icon of a userId to update the content 719 * in {@link #mProfileButton}, and at the time when user will press {@link #mProfileButton} 720 * to change the current profile. 721 */ 722 @RequiresApi(Build.VERSION_CODES.S) getUserToSwitchFromProfileButton()723 private UserId getUserToSwitchFromProfileButton() { 724 if (mPotentialUserForProfileButton != null 725 && mPotentialUserForProfileButton.equals( 726 mUserManagerState.getCurrentUserProfileId())) { 727 return UserId.CURRENT_USER; 728 } 729 return mPotentialUserForProfileButton; 730 } 731 732 @RequiresApi(Build.VERSION_CODES.S) onClickProfileButtonGeneric()733 private void onClickProfileButtonGeneric() { 734 mPickerViewModel.logProfileSwitchButtonClick(); 735 UserId userIdToSwitch = getUserToSwitchFromProfileButton(); 736 if (userIdToSwitch != null) { 737 if (canSwitchToUser(userIdToSwitch)) { 738 changeProfileGeneric(userIdToSwitch); 739 } else { 740 try { 741 ProfileDialogFragment.show( 742 requireActivity().getSupportFragmentManager(), userIdToSwitch); 743 } catch (RuntimeException e) { 744 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 745 } 746 } 747 } 748 } 749 changeProfile()750 private void changeProfile() { 751 if (mUserIdManager.isManagedUserSelected()) { 752 // TODO(b/190024747): Add caching for performance before switching data to and fro 753 // work profile 754 mUserIdManager.setPersonalAsCurrentUserProfile(); 755 756 } else { 757 // TODO(b/190024747): Add caching for performance before switching data to and fro 758 // work profile 759 mUserIdManager.setManagedAsCurrentUserProfile(); 760 } 761 762 updateProfileButtonContent(mUserIdManager.isManagedUserSelected()); 763 764 mPickerViewModel.onSwitchedProfile(); 765 } 766 767 @RequiresApi(Build.VERSION_CODES.S) changeProfileGeneric(UserId userIdSwitchTo)768 private void changeProfileGeneric(UserId userIdSwitchTo) { 769 mUserManagerState.setUserAsCurrentUserProfile(userIdSwitchTo); 770 updateProfileButtonAndProfileMenuButtonContent(); 771 772 mPickerViewModel.onSwitchedProfile(); 773 } 774 updateProfileButtonContent(boolean isManagedUserSelected)775 private void updateProfileButtonContent(boolean isManagedUserSelected) { 776 final Drawable icon; 777 final String text; 778 final Context context; 779 try { 780 context = requireContext(); 781 } catch (RuntimeException e) { 782 Log.e(TAG, "Could not update profile button content because the fragment is not" 783 + " attached."); 784 return; 785 } 786 787 if (isManagedUserSelected) { 788 icon = context.getDrawable(R.drawable.ic_personal_mode); 789 text = getSwitchToPersonalMessage(context); 790 } else { 791 icon = getWorkProfileIcon(context); 792 text = getSwitchToWorkMessage(context); 793 } 794 mProfileButton.setIcon(icon); 795 mProfileButton.setText(text); 796 } 797 798 @RequiresApi(Build.VERSION_CODES.S) updateProfileButtonAndProfileMenuButtonContent()799 private void updateProfileButtonAndProfileMenuButtonContent() { 800 final Context context; 801 final Drawable profileButtonIcon, profileMenuButtonIcon; 802 final String profileButtonText, profileMenuButtonText; 803 final UserId currentUserProfileId = mUserManagerState.getCurrentUserProfileId(); 804 try { 805 context = requireContext(); 806 } catch (RuntimeException e) { 807 Log.e(TAG, "Could not update profile button content because the fragment is not" 808 + " attached."); 809 return; 810 } 811 812 if (mIsProfileMenuButtonVisible) { 813 profileMenuButtonIcon = 814 mUserManagerState.getProfileBadgeForAll().get(currentUserProfileId); 815 profileMenuButtonText = 816 mUserManagerState.getProfileLabelsForAll().get(currentUserProfileId); 817 mProfileMenuButton.setIcon(profileMenuButtonIcon); 818 mProfileMenuButton.setText(profileMenuButtonText); 819 } 820 821 if (mIsProfileButtonVisible) { 822 UserId userIdToSwitch = getUserToSwitchFromProfileButton(); 823 if (userIdToSwitch != null) { 824 if (SdkLevel.isAtLeastV()) { 825 profileButtonIcon = 826 mUserManagerState.getProfileBadgeForAll().get(userIdToSwitch); 827 profileButtonText = context.getString(R.string.picker_profile_switch_message, 828 mUserManagerState.getProfileLabelsForAll().get(userIdToSwitch)); 829 } else { 830 if (mUserManagerState.isManagedUserProfile(currentUserProfileId)) { 831 profileButtonIcon = context.getDrawable(R.drawable.ic_personal_mode); 832 profileButtonText = getSwitchToPersonalMessage(context); 833 } else { 834 profileButtonIcon = getWorkProfileIcon(context); 835 profileButtonText = getSwitchToWorkMessage(context); 836 } 837 } 838 mProfileButton.setIcon(profileButtonIcon); 839 mProfileButton.setText(profileButtonText); 840 } 841 } 842 } 843 844 getSwitchToPersonalMessage(@onNull Context context)845 private String getSwitchToPersonalMessage(@NonNull Context context) { 846 if (SdkLevel.isAtLeastT()) { 847 return getUpdatedEnterpriseString( 848 context, SWITCH_TO_PERSONAL_MESSAGE, R.string.picker_personal_profile); 849 } else { 850 return context.getString(R.string.picker_personal_profile); 851 } 852 } 853 getSwitchToWorkMessage(@onNull Context context)854 private String getSwitchToWorkMessage(@NonNull Context context) { 855 if (SdkLevel.isAtLeastT()) { 856 return getUpdatedEnterpriseString( 857 context, SWITCH_TO_WORK_MESSAGE, R.string.picker_work_profile); 858 } else { 859 return context.getString(R.string.picker_work_profile); 860 } 861 } 862 863 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatedEnterpriseString(@onNull Context context, @NonNull String updatableStringId, int defaultStringId)864 private String getUpdatedEnterpriseString(@NonNull Context context, 865 @NonNull String updatableStringId, 866 int defaultStringId) { 867 final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); 868 return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId)); 869 } 870 getWorkProfileIcon(@onNull Context context)871 private Drawable getWorkProfileIcon(@NonNull Context context) { 872 if (SdkLevel.isAtLeastT()) { 873 return getUpdatedWorkProfileIcon(context); 874 } else { 875 return context.getDrawable(R.drawable.ic_work_outline); 876 } 877 } 878 879 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatedWorkProfileIcon(@onNull Context context)880 private Drawable getUpdatedWorkProfileIcon(@NonNull Context context) { 881 DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); 882 return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () -> { 883 // Fetch activity or context again instead of capturing existing variable in 884 // lambdas to avoid memory leaks. 885 try { 886 return requireContext().getDrawable(R.drawable.ic_work_outline); 887 } catch (RuntimeException e) { 888 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 889 return null; 890 } 891 }); 892 } 893 894 private void updateProfileButtonColor(boolean isDisabled) { 895 int textAndIconColor = 896 isDisabled ? mButtonDisabledIconAndTextColor : mButtonIconAndTextColor; 897 int backgroundTintColor = 898 isDisabled ? mButtonDisabledBackgroundColor : mButtonBackgroundColor; 899 900 if (mIsCustomPickerColorSet) { 901 textAndIconColor = mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 902 AccentColorResources.ON_SURFACE_VARIANT_LIGHT, 903 AccentColorResources.ON_SURFACE_VARIANT_DARK 904 ); 905 backgroundTintColor = 906 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 907 AccentColorResources.SURFACE_CONTAINER_LOW_LIGHT, 908 AccentColorResources.SURFACE_CONTAINER_LOW_DARK 909 ); 910 } 911 912 mProfileButton.setTextColor(ColorStateList.valueOf(textAndIconColor)); 913 mProfileButton.setIconTint(ColorStateList.valueOf(textAndIconColor)); 914 mProfileButton.setBackgroundTintList(ColorStateList.valueOf(backgroundTintColor)); 915 } 916 917 @RequiresApi(Build.VERSION_CODES.S) 918 private void updateProfileButtonAndProfileMenuButtonColor() { 919 if (mIsProfileButtonVisible) { 920 boolean isDisabled = true; 921 UserId userIdToSwitch = getUserToSwitchFromProfileButton(); 922 if (userIdToSwitch != null) { 923 isDisabled = !canSwitchToUser(userIdToSwitch); 924 } 925 926 final int textAndIconColor = 927 isDisabled ? mButtonDisabledIconAndTextColor : mButtonIconAndTextColor; 928 final int backgroundTintColor = 929 isDisabled ? mButtonDisabledBackgroundColor : mButtonBackgroundColor; 930 931 mProfileButton.setTextColor(ColorStateList.valueOf(textAndIconColor)); 932 mProfileButton.setIconTint(ColorStateList.valueOf(textAndIconColor)); 933 mProfileButton.setBackgroundTintList(ColorStateList.valueOf(backgroundTintColor)); 934 } 935 936 if (mIsProfileMenuButtonVisible) { 937 mProfileMenuButton.setIconTint(ColorStateList.valueOf(mButtonIconAndTextColor)); 938 } 939 } 940 941 942 protected void hideProfileButton(boolean hide) { 943 mHideProfileButtonAndProfileMenuButton = hide; 944 updateProfileButtonVisibility(); 945 } 946 947 @RequiresApi(Build.VERSION_CODES.S) 948 protected void hideProfileButtonAndProfileMenuButton(boolean hide) { 949 mHideProfileButtonAndProfileMenuButton = hide; 950 updateProfileButtonAndProfileMenuButtonVisibility(); 951 } 952 953 954 private void updateProfileButtonVisibility() { 955 final boolean shouldShowProfileButton = shouldShowProfileButton(); 956 if (shouldShowProfileButton) { 957 mIsProfileButtonVisible = true; 958 mProfileButton.show(); 959 } else { 960 mIsProfileButtonVisible = false; 961 mProfileButton.hide(); 962 } 963 mIsProfileButtonOrProfileMenuButtonVisible.setValue(shouldShowProfileButton); 964 } 965 966 @RequiresApi(Build.VERSION_CODES.S) 967 private void updateProfileButtonAndProfileMenuButtonVisibility() { 968 // The button could be either profile button or profile menu button. 969 final boolean shouldShowButton = shouldShowProfileButtonOrProfileMenuButton(); 970 setProfileButtonVisibility(shouldShowButton); 971 setProfileMenuButtonVisibility(shouldShowButton); 972 mIsProfileButtonOrProfileMenuButtonVisible.setValue( 973 shouldShowButton); 974 } 975 976 @RequiresApi(Build.VERSION_CODES.S) 977 private void setProfileMenuButtonVisibility(boolean shouldShowProfileMenuButton) { 978 mIsProfileMenuButtonVisible = false; 979 /* 980 * Check if the total number of profiles that will appear separately in PhotoPicker 981 * is less than three. If more than two such profiles are available, we will show a 982 * dropdown menu with {@link #mProfileMenuButton} representing all available visible 983 * profiles instead of showing a {@link #mProfileMenuButton} 984 */ 985 if (shouldShowProfileMenuButton 986 && (mUserManagerState.getProfileCount() - mHideProfileCount) >= 3) { 987 mIsProfileMenuButtonVisible = true; 988 mProfileMenuButton.show(); 989 } 990 991 if (!mIsProfileMenuButtonVisible) { 992 mProfileMenuButton.hide(); 993 } 994 } 995 996 @RequiresApi(Build.VERSION_CODES.S) 997 private void setProfileButtonVisibility(boolean shouldShowProfileButton) { 998 mIsProfileButtonVisible = false; 999 /* 1000 * Check if the total number of profiles that will appear separately in PhotoPicker 1001 * is less than three. If more than two such profiles are available, we will show a 1002 * dropdown menu with {@link #mProfileMenuButton} representing all available visible 1003 * profiles instead of showing a {@link #mProfileMenuButton} 1004 */ 1005 if (shouldShowProfileButton 1006 && (mUserManagerState.getProfileCount() - mHideProfileCount) < 3) { 1007 mIsProfileButtonVisible = true; 1008 mProfileButton.show(); 1009 } 1010 if (!mIsProfileButtonVisible) { 1011 mProfileButton.hide(); 1012 } 1013 } 1014 1015 protected void setEmptyMessage(int resId) { 1016 mEmptyTextView.setText(resId); 1017 } 1018 1019 /** 1020 * If we show the {@link #mEmptyView}, hide the {@link #mRecyclerView}. If we don't hide the 1021 * {@link #mEmptyView}, show the {@link #mRecyclerView} 1022 * when user switches the profile ,till the time when updated profile data is loading, 1023 * on the UI we hide {@link #mEmptyView} and show Empty {@link #mRecyclerView} 1024 */ 1025 protected void updateVisibilityForEmptyView(boolean shouldShowEmptyView) { 1026 mEmptyView.setVisibility(shouldShowEmptyView ? View.VISIBLE : View.GONE); 1027 mRecyclerView.setVisibility(shouldShowEmptyView ? View.GONE : View.VISIBLE); 1028 } 1029 1030 /** 1031 * Generates the Button Label for the {@link TabFragment#mAddButton}. 1032 * 1033 * @param context The current application context. 1034 * @param size The current size of the selection. 1035 * @return Localized, formatted string. 1036 */ 1037 private String generateAddButtonString(Context context, int size) { 1038 final String sizeString = NumberFormat.getInstance(Locale.getDefault()).format(size); 1039 final String template = 1040 mPickerViewModel.isUserSelectForApp() 1041 ? context.getString(R.string.picker_add_button_multi_select_permissions) 1042 : context.getString(R.string.picker_add_button_multi_select); 1043 1044 return TextUtils.expandTemplate(template, sizeString).toString(); 1045 } 1046 1047 /** 1048 * Returns {@link PhotoPickerActivity} if the fragment is attached to one. Otherwise, throws an 1049 * {@link IllegalStateException}. 1050 */ 1051 protected final PhotoPickerActivity requirePickerActivity() throws IllegalStateException { 1052 return (PhotoPickerActivity) requireActivity(); 1053 } 1054 1055 protected final void setLayoutManager(@NonNull Context context, 1056 @NonNull TabAdapter adapter, int spanCount) { 1057 final GridLayoutManager layoutManager = 1058 new GridLayoutManager(context, spanCount); 1059 final GridLayoutManager.SpanSizeLookup lookup = new GridLayoutManager.SpanSizeLookup() { 1060 @Override 1061 public int getSpanSize(int position) { 1062 final int itemViewType = adapter.getItemViewType(position); 1063 // For the item view types ITEM_TYPE_BANNER and ITEM_TYPE_SECTION, it is full 1064 // span, return the span count of the layoutManager. 1065 if (itemViewType == ITEM_TYPE_BANNER || itemViewType == ITEM_TYPE_SECTION) { 1066 return layoutManager.getSpanCount(); 1067 } else { 1068 return 1; 1069 } 1070 } 1071 }; 1072 layoutManager.setSpanSizeLookup(lookup); 1073 mRecyclerView.setLayoutManager(layoutManager); 1074 } 1075 1076 private abstract class OnBannerEventListener implements TabAdapter.OnBannerEventListener { 1077 @Override 1078 public void onActionButtonClick() { 1079 mPickerViewModel.logBannerActionButtonClicked(); 1080 dismissBanner(); 1081 launchCloudProviderSettings(); 1082 } 1083 1084 @Override 1085 public void onDismissButtonClick() { 1086 mPickerViewModel.logBannerDismissed(); 1087 dismissBanner(); 1088 } 1089 1090 @Override 1091 public void onBannerClick() { 1092 mPickerViewModel.logBannerClicked(); 1093 dismissBanner(); 1094 launchCloudProviderSettings(); 1095 } 1096 1097 @Override 1098 public void onBannerAdded(@NonNull String name) { 1099 mPickerViewModel.logBannerAdded(name); 1100 1101 // Should scroll to the banner only if the first completely visible item is the one 1102 // just below it. The possible adapter item positions of such an item are 0 and 1. 1103 // During onViewCreated, before restoring the state, the first visible item position 1104 // is -1, and we should not scroll to position 0 in such cases, else the previously 1105 // saved recycler view position may get overridden. 1106 int firstItemPosition = -1; 1107 1108 final RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); 1109 if (layoutManager instanceof GridLayoutManager) { 1110 firstItemPosition = ((GridLayoutManager) layoutManager) 1111 .findFirstCompletelyVisibleItemPosition(); 1112 } 1113 1114 if (firstItemPosition == 0 || firstItemPosition == 1) { 1115 mRecyclerView.scrollToPosition(/* position */ 0); 1116 } 1117 } 1118 1119 abstract void dismissBanner(); 1120 1121 private void launchCloudProviderSettings() { 1122 final Intent accountChangeIntent = 1123 mPickerViewModel.getChooseCloudMediaAccountActivityIntent(); 1124 1125 try { 1126 if (accountChangeIntent != null) { 1127 requirePickerActivity().startActivity(accountChangeIntent); 1128 } else { 1129 requirePickerActivity().startSettingsActivity(); 1130 } 1131 } catch (RuntimeException e) { 1132 Log.e(TAG, "Fragment is likely not attached to an activity. ", e); 1133 } 1134 } 1135 } 1136 1137 protected final OnBannerEventListener mOnChooseAppBannerEventListener = 1138 new OnBannerEventListener() { 1139 @Override 1140 void dismissBanner() { 1141 mPickerViewModel.onUserDismissedChooseAppBanner(); 1142 } 1143 }; 1144 1145 protected final OnBannerEventListener mOnCloudMediaAvailableBannerEventListener = 1146 new OnBannerEventListener() { 1147 @Override 1148 void dismissBanner() { 1149 mPickerViewModel.onUserDismissedCloudMediaAvailableBanner(); 1150 } 1151 1152 @Override 1153 public boolean shouldShowActionButton() { 1154 return mPickerViewModel.getChooseCloudMediaAccountActivityIntent() != null; 1155 } 1156 }; 1157 1158 protected final OnBannerEventListener mOnAccountUpdatedBannerEventListener = 1159 new OnBannerEventListener() { 1160 @Override 1161 void dismissBanner() { 1162 mPickerViewModel.onUserDismissedAccountUpdatedBanner(); 1163 } 1164 }; 1165 1166 protected final OnBannerEventListener mOnChooseAccountBannerEventListener = 1167 new OnBannerEventListener() { 1168 @Override 1169 void dismissBanner() { 1170 mPickerViewModel.onUserDismissedChooseAccountBanner(); 1171 } 1172 1173 @Override 1174 public boolean shouldShowActionButton() { 1175 return mPickerViewModel.getChooseCloudMediaAccountActivityIntent() != null; 1176 } 1177 }; 1178 } 1179