1 /* 2 * Copyright (C) 2016 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.documentsui; 18 19 import static com.android.documentsui.base.SharedMinimal.VERBOSE; 20 import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; 21 import static com.android.documentsui.util.FlagUtils.isVisualSignalsFlagEnabled; 22 23 import android.content.res.Resources; 24 import android.content.res.TypedArray; 25 import android.graphics.Outline; 26 import android.graphics.drawable.ColorDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.util.Log; 29 import android.view.MenuItem; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewOutlineProvider; 33 import android.view.Window; 34 import android.view.WindowManager; 35 36 import androidx.annotation.ColorRes; 37 import androidx.annotation.Nullable; 38 import androidx.appcompat.widget.Toolbar; 39 import androidx.core.content.ContextCompat; 40 import androidx.recyclerview.selection.SelectionTracker; 41 42 import com.android.documentsui.Injector.Injected; 43 import com.android.documentsui.base.EventHandler; 44 import com.android.documentsui.base.RootInfo; 45 import com.android.documentsui.base.State; 46 import com.android.documentsui.base.UserId; 47 import com.android.documentsui.dirlist.AnimationView; 48 import com.android.documentsui.util.VersionUtils; 49 import com.android.modules.utils.build.SdkLevel; 50 51 import com.google.android.material.appbar.AppBarLayout; 52 import com.google.android.material.appbar.CollapsingToolbarLayout; 53 54 import java.util.function.IntConsumer; 55 56 /** A facade over the portions of the app and drawer toolbars. */ 57 public class NavigationViewManager extends SelectionTracker.SelectionObserver<String> 58 implements AppBarLayout.OnOffsetChangedListener { 59 60 private static final String TAG = "NavigationViewManager"; 61 62 private final DrawerController mDrawer; 63 private final Toolbar mToolbar; 64 private final BaseActivity mActivity; 65 private final View mHeader; 66 private final State mState; 67 private final NavigationViewManager.Environment mEnv; 68 private final Breadcrumb mBreadcrumb; 69 private final ProfileTabs mProfileTabs; 70 private final View mSearchBarView; 71 private final CollapsingToolbarLayout mCollapsingBarLayout; 72 private final Drawable mDefaultActionBarBackground; 73 private final ViewOutlineProvider mDefaultOutlineProvider; 74 private final ViewOutlineProvider mSearchBarOutlineProvider; 75 private final boolean mShowSearchBar; 76 private final ConfigStore mConfigStore; 77 @Injected private final Injector<?> mInjector; 78 private boolean mIsActionModeActivated = false; 79 @ColorRes private int mDefaultStatusBarColorResId; 80 private MenuManager.SelectionDetails mSelectionDetails; 81 private EventHandler<MenuItem> mActionMenuItemClicker; 82 NavigationViewManager( BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, Breadcrumb breadcrumb, View tabLayoutContainer, UserIdManager userIdManager, ConfigStore configStore, Injector injector)83 public NavigationViewManager( 84 BaseActivity activity, 85 DrawerController drawer, 86 State state, 87 NavigationViewManager.Environment env, 88 Breadcrumb breadcrumb, 89 View tabLayoutContainer, 90 UserIdManager userIdManager, 91 ConfigStore configStore, 92 Injector injector) { 93 this( 94 activity, 95 drawer, 96 state, 97 env, 98 breadcrumb, 99 tabLayoutContainer, 100 userIdManager, 101 null, 102 configStore, 103 injector); 104 } 105 NavigationViewManager( BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, Breadcrumb breadcrumb, View tabLayoutContainer, UserManagerState userManagerState, ConfigStore configStore, Injector injector)106 public NavigationViewManager( 107 BaseActivity activity, 108 DrawerController drawer, 109 State state, 110 NavigationViewManager.Environment env, 111 Breadcrumb breadcrumb, 112 View tabLayoutContainer, 113 UserManagerState userManagerState, 114 ConfigStore configStore, 115 Injector injector) { 116 this( 117 activity, 118 drawer, 119 state, 120 env, 121 breadcrumb, 122 tabLayoutContainer, 123 null, 124 userManagerState, 125 configStore, 126 injector); 127 } 128 NavigationViewManager( BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, Breadcrumb breadcrumb, View tabLayoutContainer, UserIdManager userIdManager, UserManagerState userManagerState, ConfigStore configStore, Injector injector)129 public NavigationViewManager( 130 BaseActivity activity, 131 DrawerController drawer, 132 State state, 133 NavigationViewManager.Environment env, 134 Breadcrumb breadcrumb, 135 View tabLayoutContainer, 136 UserIdManager userIdManager, 137 UserManagerState userManagerState, 138 ConfigStore configStore, 139 Injector injector) { 140 141 mActivity = activity; 142 mToolbar = activity.findViewById(R.id.toolbar); 143 mHeader = activity.findViewById(R.id.directory_header); 144 mDrawer = drawer; 145 mState = state; 146 mEnv = env; 147 mBreadcrumb = breadcrumb; 148 mBreadcrumb.setup( 149 env, 150 state, 151 this::onNavigationItemSelected, 152 isUseMaterial3FlagEnabled() 153 ? activity.findViewById(R.id.breadcrumb_top_divider) 154 : null); 155 mConfigStore = configStore; 156 mInjector = injector; 157 mProfileTabs = 158 getProfileTabs(tabLayoutContainer, userIdManager, userManagerState, activity); 159 160 mToolbar.setNavigationOnClickListener( 161 new View.OnClickListener() { 162 @Override 163 public void onClick(View v) { 164 onNavigationIconClicked(); 165 } 166 }); 167 if (isUseMaterial3FlagEnabled()) { 168 mToolbar.setOnMenuItemClickListener( 169 new Toolbar.OnMenuItemClickListener() { 170 @Override 171 public boolean onMenuItemClick(MenuItem menuItem) { 172 return onToolbarMenuItemClicked(menuItem); 173 } 174 }); 175 } 176 mSearchBarView = activity.findViewById(R.id.searchbar_title); 177 mCollapsingBarLayout = activity.findViewById(R.id.collapsing_toolbar); 178 mDefaultActionBarBackground = mToolbar.getBackground(); 179 mDefaultOutlineProvider = mToolbar.getOutlineProvider(); 180 mShowSearchBar = activity.getResources().getBoolean(R.bool.show_search_bar); 181 182 final int[] styledAttrs = {android.R.attr.statusBarColor}; 183 TypedArray a = mActivity.obtainStyledAttributes(styledAttrs); 184 mDefaultStatusBarColorResId = a.getResourceId(0, -1); 185 if (mDefaultStatusBarColorResId == -1) { 186 Log.w(TAG, "Retrieve statusBarColorResId from theme failed, assigned default"); 187 mDefaultStatusBarColorResId = R.color.app_background_color; 188 } 189 a.recycle(); 190 191 final Resources resources = mToolbar.getResources(); 192 final int radius = resources.getDimensionPixelSize(R.dimen.search_bar_radius); 193 final int marginStart = 194 resources.getDimensionPixelSize(R.dimen.search_bar_background_margin_start); 195 final int marginEnd = 196 resources.getDimensionPixelSize(R.dimen.search_bar_background_margin_end); 197 mSearchBarOutlineProvider = new ViewOutlineProvider() { 198 @Override 199 public void getOutline(View view, Outline outline) { 200 outline.setRoundRect(marginStart, 0, 201 view.getWidth() - marginEnd, view.getHeight(), radius); 202 } 203 }; 204 } 205 getProfileTabs(View tabLayoutContainer, UserIdManager userIdManager, UserManagerState userManagerState, BaseActivity activity)206 private ProfileTabs getProfileTabs(View tabLayoutContainer, UserIdManager userIdManager, 207 UserManagerState userManagerState, BaseActivity activity) { 208 return mConfigStore.isPrivateSpaceInDocsUIEnabled() 209 ? new ProfileTabs(tabLayoutContainer, mState, userManagerState, mEnv, activity, 210 mConfigStore) 211 : new ProfileTabs(tabLayoutContainer, mState, userIdManager, mEnv, activity, 212 mConfigStore); 213 } 214 215 @Override onOffsetChanged(AppBarLayout appBarLayout, int offset)216 public void onOffsetChanged(AppBarLayout appBarLayout, int offset) { 217 if (!VersionUtils.isAtLeastS()) { 218 return; 219 } 220 221 // For S+ Only. Change toolbar color dynamically based on scroll offset. 222 // Usually this can be done in xml using app:contentScrim and app:statusBarScrim, however 223 // in our case since we also put directory_header.xml inside the CollapsingToolbarLayout, 224 // the scrim will also cover the directory header. Long term need to think about how to 225 // move directory_header out of the AppBarLayout. 226 227 Window window = mActivity.getWindow(); 228 View actionBar = 229 window.getDecorView().findViewById(androidx.appcompat.R.id.action_mode_bar); 230 int dynamicHeaderColor = ContextCompat.getColor(mActivity, 231 offset == 0 ? mDefaultStatusBarColorResId : R.color.color_surface_header); 232 if (actionBar != null) { 233 // Action bar needs to be updated separately for selection mode. 234 actionBar.setBackgroundColor(dynamicHeaderColor); 235 } 236 237 window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 238 window.setStatusBarColor(dynamicHeaderColor); 239 if (shouldShowSearchBar()) { 240 // Do not change search bar background. 241 } else { 242 mToolbar.setBackground(new ColorDrawable(dynamicHeaderColor)); 243 } 244 } 245 setSearchBarClickListener(View.OnClickListener listener)246 public void setSearchBarClickListener(View.OnClickListener listener) { 247 mSearchBarView.setOnClickListener(listener); 248 if (SdkLevel.isAtLeastU()) { 249 try { 250 mSearchBarView.setHandwritingDelegatorCallback( 251 () -> listener.onClick(mSearchBarView)); 252 } catch (LinkageError e) { 253 // Running on a device with an older build of Android U 254 // TODO(b/274154553): Remove try/catch block after Android U Beta 1 is released 255 } 256 } 257 } 258 getProfileTabsAddons()259 public ProfileTabsAddons getProfileTabsAddons() { 260 return mProfileTabs; 261 } 262 263 /** 264 * Sets a listener to the profile tabs. 265 */ setProfileTabsListener(ProfileTabs.Listener listener)266 public void setProfileTabsListener(ProfileTabs.Listener listener) { 267 mProfileTabs.setListener(listener); 268 } 269 onNavigationIconClicked()270 private void onNavigationIconClicked() { 271 if (isUseMaterial3FlagEnabled() && inSelectionMode()) { 272 closeSelectionBar(); 273 } else if (mDrawer.isPresent()) { 274 mDrawer.setOpen(true); 275 } 276 } 277 onToolbarMenuItemClicked(MenuItem menuItem)278 private boolean onToolbarMenuItemClicked(MenuItem menuItem) { 279 if (inSelectionMode()) { 280 mActionMenuItemClicker.accept(menuItem); 281 return true; 282 } 283 return mActivity.onOptionsItemSelected(menuItem); 284 } 285 onNavigationItemSelected(int position)286 void onNavigationItemSelected(int position) { 287 boolean changed = false; 288 while (mState.stack.size() > position + 1) { 289 changed = true; 290 mState.stack.pop(); 291 } 292 if (changed) { 293 mEnv.refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE); 294 } 295 } 296 getSelectedUser()297 public UserId getSelectedUser() { 298 return mProfileTabs.getSelectedUser(); 299 } 300 setActionModeActivated(boolean actionModeActivated)301 public void setActionModeActivated(boolean actionModeActivated) { 302 mIsActionModeActivated = actionModeActivated; 303 update(); 304 } 305 update()306 public void update() { 307 // If use_material3 flag is ON, we don't want any scroll behavior, thus skipping this logic. 308 if (!isUseMaterial3FlagEnabled()) { 309 updateScrollFlag(); 310 } 311 updateToolbar(); 312 mProfileTabs.updateView(); 313 314 // TODO: Looks to me like this block is never getting hit. 315 if (mEnv.isSearchExpanded()) { 316 mToolbar.setTitle(null); 317 mBreadcrumb.show(false); 318 return; 319 } 320 321 mDrawer.setTitle(mEnv.getDrawerTitle()); 322 323 boolean showBurgerMenuOnToolbar = true; 324 if (isUseMaterial3FlagEnabled()) { 325 View navRailRoots = mActivity.findViewById(R.id.nav_rail_container_roots); 326 if (navRailRoots != null) { 327 // If nav rail exists, burger menu will show on the nav rail instead. 328 showBurgerMenuOnToolbar = false; 329 } 330 } 331 332 if (showBurgerMenuOnToolbar) { 333 mToolbar.setNavigationIcon(getActionBarIcon()); 334 mToolbar.setNavigationContentDescription(R.string.drawer_open); 335 } else { 336 mToolbar.setNavigationIcon(null); 337 mToolbar.setNavigationContentDescription(null); 338 } 339 340 if (shouldShowSearchBar()) { 341 mBreadcrumb.show(false); 342 mToolbar.setTitle(null); 343 mSearchBarView.setVisibility(View.VISIBLE); 344 return; 345 } 346 347 mSearchBarView.setVisibility(View.GONE); 348 349 if (isUseMaterial3FlagEnabled()) { 350 updateActionMenu(); 351 if (inSelectionMode()) { 352 final int quantity = mInjector.selectionMgr.getSelection().size(); 353 final String title = 354 mToolbar.getContext() 355 .getResources() 356 .getQuantityString(R.plurals.elements_selected, quantity, quantity); 357 mToolbar.setTitle(title); 358 mActivity.getWindow().setTitle(title); 359 mToolbar.setNavigationIcon(R.drawable.ic_cancel); 360 mToolbar.setNavigationContentDescription(android.R.string.cancel); 361 return; 362 } 363 } 364 365 String title = 366 mState.stack.size() <= 1 ? mEnv.getCurrentRoot().title : mState.stack.getTitle(); 367 if (VERBOSE) Log.v(TAG, "New toolbar title is: " + title); 368 mToolbar.setTitle(title); 369 mBreadcrumb.show(true); 370 mBreadcrumb.postUpdate(); 371 } 372 373 @Override onSelectionChanged()374 public void onSelectionChanged() { 375 update(); 376 } 377 378 /** Identifies if the `NavigationViewManager` is in selection mode or not. */ inSelectionMode()379 public boolean inSelectionMode() { 380 return mInjector != null 381 && mInjector.selectionMgr != null 382 && mInjector.selectionMgr.hasSelection(); 383 } 384 hasActionMenu()385 private boolean hasActionMenu() { 386 return mToolbar.getMenu().findItem(R.id.action_menu_open_with) != null; 387 } 388 389 /** Updates the action menu based on whether a selection is currently being made or not. */ updateActionMenu()390 public void updateActionMenu() { 391 // For the first start up of the application, the menu might not exist at all but we also 392 // don't want to inflate the menu multiple times. So along with checking if the expected 393 // menu is already inflated, validate that a menu exists at all as well. 394 boolean isMenuInflated = mToolbar.getMenu() != null && mToolbar.getMenu().size() > 0; 395 if (inSelectionMode()) { 396 if (!isMenuInflated || !hasActionMenu()) { 397 mToolbar.getMenu().clear(); 398 mToolbar.inflateMenu(R.menu.action_mode_menu); 399 mToolbar.invalidateMenu(); 400 } 401 mInjector.menuManager.updateActionMenu(mToolbar.getMenu(), mSelectionDetails); 402 return; 403 } 404 405 if (!isMenuInflated || hasActionMenu()) { 406 mToolbar.getMenu().clear(); 407 mToolbar.inflateMenu(R.menu.activity); 408 mToolbar.invalidateMenu(); 409 boolean fullBarSearch = 410 mActivity.getResources().getBoolean(R.bool.full_bar_search_view); 411 boolean showSearchBar = mActivity.getResources().getBoolean(R.bool.show_search_bar); 412 mInjector.searchManager.install(mToolbar.getMenu(), fullBarSearch, showSearchBar); 413 if (isVisualSignalsFlagEnabled()) { 414 mInjector.menuManager.instantiateJobProgress(mToolbar.getMenu()); 415 } 416 } 417 mInjector.menuManager.updateOptionMenu(mToolbar.getMenu()); 418 mInjector.searchManager.showMenu(mState.stack); 419 } 420 421 /** Everytime a selection is made, update the selection. */ updateSelection( MenuManager.SelectionDetails selectionDetails, EventHandler<MenuItem> actionMenuItemClicker)422 public void updateSelection( 423 MenuManager.SelectionDetails selectionDetails, 424 EventHandler<MenuItem> actionMenuItemClicker) { 425 mSelectionDetails = selectionDetails; 426 mActionMenuItemClicker = actionMenuItemClicker; 427 } 428 updateScrollFlag()429 private void updateScrollFlag() { 430 if (mCollapsingBarLayout == null) { 431 return; 432 } 433 434 AppBarLayout.LayoutParams lp = 435 (AppBarLayout.LayoutParams) mCollapsingBarLayout.getLayoutParams(); 436 lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 437 | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); 438 mCollapsingBarLayout.setLayoutParams(lp); 439 } 440 updateToolbar()441 private void updateToolbar() { 442 if (mCollapsingBarLayout == null) { 443 // Tablet mode does not use CollapsingBarLayout 444 // (res/layout-sw720dp/directory_app_bar.xml or res/layout/fixed_layout.xml) 445 if (shouldShowSearchBar()) { 446 mToolbar.setBackgroundResource(R.drawable.search_bar_background); 447 mToolbar.setOutlineProvider(mSearchBarOutlineProvider); 448 } else { 449 mToolbar.setBackground(mDefaultActionBarBackground); 450 mToolbar.setOutlineProvider(null); 451 } 452 return; 453 } 454 455 CollapsingToolbarLayout.LayoutParams toolbarLayoutParams = 456 (CollapsingToolbarLayout.LayoutParams) mToolbar.getLayoutParams(); 457 458 int headerTopOffset = 0; 459 if (shouldShowSearchBar() && !mIsActionModeActivated) { 460 mToolbar.setBackgroundResource(R.drawable.search_bar_background); 461 mToolbar.setOutlineProvider(mSearchBarOutlineProvider); 462 int searchBarMargin = mToolbar.getResources().getDimensionPixelSize( 463 R.dimen.search_bar_margin); 464 toolbarLayoutParams.setMargins(searchBarMargin, searchBarMargin, searchBarMargin, 465 searchBarMargin); 466 mToolbar.setLayoutParams(toolbarLayoutParams); 467 mToolbar.setElevation( 468 mToolbar.getResources().getDimensionPixelSize(R.dimen.search_bar_elevation)); 469 headerTopOffset = toolbarLayoutParams.height + searchBarMargin * 2; 470 } else { 471 mToolbar.setBackground(mDefaultActionBarBackground); 472 mToolbar.setOutlineProvider(mDefaultOutlineProvider); 473 int actionBarMargin = mToolbar.getResources().getDimensionPixelSize( 474 R.dimen.action_bar_margin); 475 toolbarLayoutParams.setMargins(0, 0, 0, /* bottom= */ actionBarMargin); 476 mToolbar.setLayoutParams(toolbarLayoutParams); 477 mToolbar.setElevation( 478 mToolbar.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation)); 479 headerTopOffset = toolbarLayoutParams.height + actionBarMargin; 480 } 481 482 if (!mIsActionModeActivated) { 483 // This could be either FrameLayout.LayoutParams (when use_material3 flag is OFF) or 484 // LinearLayout.LayoutParams (when use_material3 flag is ON), so use the common parent 485 // class instead to make it work for both scenarios. 486 ViewGroup.MarginLayoutParams headerLayoutParams = 487 (ViewGroup.MarginLayoutParams) mHeader.getLayoutParams(); 488 headerLayoutParams.setMargins(0, /* top= */ headerTopOffset, 0, 0); 489 mHeader.setLayoutParams(headerLayoutParams); 490 } 491 } 492 shouldShowSearchBar()493 private boolean shouldShowSearchBar() { 494 return mState.stack.isRecents() && !mEnv.isSearchExpanded() && mShowSearchBar; 495 } 496 497 // Hamburger if drawer is present, else sad nullness. 498 private @Nullable getActionBarIcon()499 Drawable getActionBarIcon() { 500 if (mDrawer.isPresent()) { 501 return mToolbar.getContext().getDrawable(R.drawable.ic_hamburger); 502 } else { 503 return null; 504 } 505 } 506 revealRootsDrawer(boolean open)507 void revealRootsDrawer(boolean open) { 508 mDrawer.setOpen(open); 509 } 510 511 /** Helper method to close the selection bar. */ closeSelectionBar()512 public void closeSelectionBar() { 513 mInjector.selectionMgr.clearSelection(); 514 } 515 516 interface Breadcrumb { setup(Environment env, State state, IntConsumer listener, @Nullable View topDivider)517 void setup(Environment env, State state, IntConsumer listener, @Nullable View topDivider); 518 show(boolean visibility)519 void show(boolean visibility); 520 postUpdate()521 void postUpdate(); 522 } 523 524 interface Environment { 525 @Deprecated 526 // Use CommonAddones#getCurrentRoot getCurrentRoot()527 RootInfo getCurrentRoot(); 528 getDrawerTitle()529 String getDrawerTitle(); 530 531 @Deprecated 532 // Use CommonAddones#refreshCurrentRootAndDirectory refreshCurrentRootAndDirectory(int animation)533 void refreshCurrentRootAndDirectory(int animation); 534 isSearchExpanded()535 boolean isSearchExpanded(); 536 } 537 } 538