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 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.graphics.Outline; 24 import android.graphics.drawable.ColorDrawable; 25 import android.graphics.drawable.Drawable; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.ViewOutlineProvider; 29 import android.view.Window; 30 import android.view.WindowManager; 31 import android.widget.FrameLayout; 32 33 import androidx.annotation.ColorRes; 34 import androidx.annotation.Nullable; 35 import androidx.appcompat.widget.Toolbar; 36 import androidx.core.content.ContextCompat; 37 38 import com.android.documentsui.base.RootInfo; 39 import com.android.documentsui.base.State; 40 import com.android.documentsui.base.UserId; 41 import com.android.documentsui.dirlist.AnimationView; 42 import com.android.documentsui.util.VersionUtils; 43 import com.android.modules.utils.build.SdkLevel; 44 45 import com.google.android.material.appbar.AppBarLayout; 46 import com.google.android.material.appbar.CollapsingToolbarLayout; 47 48 import java.util.function.IntConsumer; 49 50 /** 51 * A facade over the portions of the app and drawer toolbars. 52 */ 53 public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListener { 54 55 private static final String TAG = "NavigationViewManager"; 56 57 private final DrawerController mDrawer; 58 private final Toolbar mToolbar; 59 private final BaseActivity mActivity; 60 private final View mHeader; 61 private final State mState; 62 private final NavigationViewManager.Environment mEnv; 63 private final Breadcrumb mBreadcrumb; 64 private final ProfileTabs mProfileTabs; 65 private final View mSearchBarView; 66 private final CollapsingToolbarLayout mCollapsingBarLayout; 67 private final Drawable mDefaultActionBarBackground; 68 private final ViewOutlineProvider mDefaultOutlineProvider; 69 private final ViewOutlineProvider mSearchBarOutlineProvider; 70 private final boolean mShowSearchBar; 71 private final ConfigStore mConfigStore; 72 73 private boolean mIsActionModeActivated = false; 74 @ColorRes 75 private int mDefaultStatusBarColorResId; 76 NavigationViewManager( BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, Breadcrumb breadcrumb, View tabLayoutContainer, UserIdManager userIdManager, ConfigStore configStore)77 public NavigationViewManager( 78 BaseActivity activity, 79 DrawerController drawer, 80 State state, 81 NavigationViewManager.Environment env, 82 Breadcrumb breadcrumb, 83 View tabLayoutContainer, 84 UserIdManager userIdManager, 85 ConfigStore configStore) { 86 this(activity, drawer, state, env, breadcrumb, tabLayoutContainer, userIdManager, null, 87 configStore); 88 } 89 NavigationViewManager( BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, Breadcrumb breadcrumb, View tabLayoutContainer, UserManagerState userManagerState, ConfigStore configStore)90 public NavigationViewManager( 91 BaseActivity activity, 92 DrawerController drawer, 93 State state, 94 NavigationViewManager.Environment env, 95 Breadcrumb breadcrumb, 96 View tabLayoutContainer, 97 UserManagerState userManagerState, 98 ConfigStore configStore) { 99 this(activity, drawer, state, env, breadcrumb, tabLayoutContainer, null, userManagerState, 100 configStore); 101 } 102 NavigationViewManager( BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, Breadcrumb breadcrumb, View tabLayoutContainer, UserIdManager userIdManager, UserManagerState userManagerState, ConfigStore configStore)103 public NavigationViewManager( 104 BaseActivity activity, 105 DrawerController drawer, 106 State state, 107 NavigationViewManager.Environment env, 108 Breadcrumb breadcrumb, 109 View tabLayoutContainer, 110 UserIdManager userIdManager, 111 UserManagerState userManagerState, 112 ConfigStore configStore) { 113 114 mActivity = activity; 115 mToolbar = activity.findViewById(R.id.toolbar); 116 mHeader = activity.findViewById(R.id.directory_header); 117 mDrawer = drawer; 118 mState = state; 119 mEnv = env; 120 mBreadcrumb = breadcrumb; 121 mBreadcrumb.setup(env, state, this::onNavigationItemSelected); 122 mConfigStore = configStore; 123 mProfileTabs = 124 getProfileTabs(tabLayoutContainer, userIdManager, userManagerState, activity); 125 126 mToolbar.setNavigationOnClickListener( 127 new View.OnClickListener() { 128 @Override 129 public void onClick(View v) { 130 onNavigationIconClicked(); 131 } 132 }); 133 mSearchBarView = activity.findViewById(R.id.searchbar_title); 134 mCollapsingBarLayout = activity.findViewById(R.id.collapsing_toolbar); 135 mDefaultActionBarBackground = mToolbar.getBackground(); 136 mDefaultOutlineProvider = mToolbar.getOutlineProvider(); 137 mShowSearchBar = activity.getResources().getBoolean(R.bool.show_search_bar); 138 139 final int[] styledAttrs = {android.R.attr.statusBarColor}; 140 TypedArray a = mActivity.obtainStyledAttributes(styledAttrs); 141 mDefaultStatusBarColorResId = a.getResourceId(0, -1); 142 if (mDefaultStatusBarColorResId == -1) { 143 Log.w(TAG, "Retrieve statusBarColorResId from theme failed, assigned default"); 144 mDefaultStatusBarColorResId = R.color.app_background_color; 145 } 146 a.recycle(); 147 148 final Resources resources = mToolbar.getResources(); 149 final int radius = resources.getDimensionPixelSize(R.dimen.search_bar_radius); 150 final int marginStart = 151 resources.getDimensionPixelSize(R.dimen.search_bar_background_margin_start); 152 final int marginEnd = 153 resources.getDimensionPixelSize(R.dimen.search_bar_background_margin_end); 154 mSearchBarOutlineProvider = new ViewOutlineProvider() { 155 @Override 156 public void getOutline(View view, Outline outline) { 157 outline.setRoundRect(marginStart, 0, 158 view.getWidth() - marginEnd, view.getHeight(), radius); 159 } 160 }; 161 } 162 getProfileTabs(View tabLayoutContainer, UserIdManager userIdManager, UserManagerState userManagerState, BaseActivity activity)163 private ProfileTabs getProfileTabs(View tabLayoutContainer, UserIdManager userIdManager, 164 UserManagerState userManagerState, BaseActivity activity) { 165 return mConfigStore.isPrivateSpaceInDocsUIEnabled() 166 ? new ProfileTabs(tabLayoutContainer, mState, userManagerState, mEnv, activity, 167 mConfigStore) 168 : new ProfileTabs(tabLayoutContainer, mState, userIdManager, mEnv, activity, 169 mConfigStore); 170 } 171 172 @Override onOffsetChanged(AppBarLayout appBarLayout, int offset)173 public void onOffsetChanged(AppBarLayout appBarLayout, int offset) { 174 if (!VersionUtils.isAtLeastS()) { 175 return; 176 } 177 178 // For S+ Only. Change toolbar color dynamically based on scroll offset. 179 // Usually this can be done in xml using app:contentScrim and app:statusBarScrim, however 180 // in our case since we also put directory_header.xml inside the CollapsingToolbarLayout, 181 // the scrim will also cover the directory header. Long term need to think about how to 182 // move directory_header out of the AppBarLayout. 183 184 Window window = mActivity.getWindow(); 185 View actionBar = 186 window.getDecorView().findViewById(androidx.appcompat.R.id.action_mode_bar); 187 int dynamicHeaderColor = ContextCompat.getColor(mActivity, 188 offset == 0 ? mDefaultStatusBarColorResId : R.color.color_surface_header); 189 if (actionBar != null) { 190 // Action bar needs to be updated separately for selection mode. 191 actionBar.setBackgroundColor(dynamicHeaderColor); 192 } 193 194 window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 195 window.setStatusBarColor(dynamicHeaderColor); 196 if (shouldShowSearchBar()) { 197 // Do not change search bar background. 198 } else { 199 mToolbar.setBackground(new ColorDrawable(dynamicHeaderColor)); 200 } 201 } 202 setSearchBarClickListener(View.OnClickListener listener)203 public void setSearchBarClickListener(View.OnClickListener listener) { 204 mSearchBarView.setOnClickListener(listener); 205 if (SdkLevel.isAtLeastU()) { 206 try { 207 mSearchBarView.setHandwritingDelegatorCallback( 208 () -> listener.onClick(mSearchBarView)); 209 } catch (LinkageError e) { 210 // Running on a device with an older build of Android U 211 // TODO(b/274154553): Remove try/catch block after Android U Beta 1 is released 212 } 213 } 214 } 215 getProfileTabsAddons()216 public ProfileTabsAddons getProfileTabsAddons() { 217 return mProfileTabs; 218 } 219 220 /** 221 * Sets a listener to the profile tabs. 222 */ setProfileTabsListener(ProfileTabs.Listener listener)223 public void setProfileTabsListener(ProfileTabs.Listener listener) { 224 mProfileTabs.setListener(listener); 225 } 226 onNavigationIconClicked()227 private void onNavigationIconClicked() { 228 if (mDrawer.isPresent()) { 229 mDrawer.setOpen(true); 230 } 231 } 232 onNavigationItemSelected(int position)233 void onNavigationItemSelected(int position) { 234 boolean changed = false; 235 while (mState.stack.size() > position + 1) { 236 changed = true; 237 mState.stack.pop(); 238 } 239 if (changed) { 240 mEnv.refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE); 241 } 242 } 243 getSelectedUser()244 public UserId getSelectedUser() { 245 return mProfileTabs.getSelectedUser(); 246 } 247 setActionModeActivated(boolean actionModeActivated)248 public void setActionModeActivated(boolean actionModeActivated) { 249 mIsActionModeActivated = actionModeActivated; 250 update(); 251 } 252 update()253 public void update() { 254 updateScrollFlag(); 255 updateToolbar(); 256 mProfileTabs.updateView(); 257 258 // TODO: Looks to me like this block is never getting hit. 259 if (mEnv.isSearchExpanded()) { 260 mToolbar.setTitle(null); 261 mBreadcrumb.show(false); 262 return; 263 } 264 265 mDrawer.setTitle(mEnv.getDrawerTitle()); 266 267 mToolbar.setNavigationIcon(getActionBarIcon()); 268 mToolbar.setNavigationContentDescription(R.string.drawer_open); 269 270 if (shouldShowSearchBar()) { 271 mBreadcrumb.show(false); 272 mToolbar.setTitle(null); 273 mSearchBarView.setVisibility(View.VISIBLE); 274 } else { 275 mSearchBarView.setVisibility(View.GONE); 276 String title = mState.stack.size() <= 1 277 ? mEnv.getCurrentRoot().title : mState.stack.getTitle(); 278 if (VERBOSE) Log.v(TAG, "New toolbar title is: " + title); 279 mToolbar.setTitle(title); 280 mBreadcrumb.show(true); 281 mBreadcrumb.postUpdate(); 282 } 283 } 284 updateScrollFlag()285 private void updateScrollFlag() { 286 if (mCollapsingBarLayout == null) { 287 return; 288 } 289 290 AppBarLayout.LayoutParams lp = 291 (AppBarLayout.LayoutParams) mCollapsingBarLayout.getLayoutParams(); 292 lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 293 | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); 294 mCollapsingBarLayout.setLayoutParams(lp); 295 } 296 updateToolbar()297 private void updateToolbar() { 298 if (mCollapsingBarLayout == null) { 299 // Tablet mode does not use CollapsingBarLayout 300 // (res/layout-sw720dp/directory_app_bar.xml or res/layout/fixed_layout.xml) 301 if (shouldShowSearchBar()) { 302 mToolbar.setBackgroundResource(R.drawable.search_bar_background); 303 mToolbar.setOutlineProvider(mSearchBarOutlineProvider); 304 } else { 305 mToolbar.setBackground(mDefaultActionBarBackground); 306 mToolbar.setOutlineProvider(null); 307 } 308 return; 309 } 310 311 CollapsingToolbarLayout.LayoutParams toolbarLayoutParams = 312 (CollapsingToolbarLayout.LayoutParams) mToolbar.getLayoutParams(); 313 314 int headerTopOffset = 0; 315 if (shouldShowSearchBar() && !mIsActionModeActivated) { 316 mToolbar.setBackgroundResource(R.drawable.search_bar_background); 317 mToolbar.setOutlineProvider(mSearchBarOutlineProvider); 318 int searchBarMargin = mToolbar.getResources().getDimensionPixelSize( 319 R.dimen.search_bar_margin); 320 toolbarLayoutParams.setMargins(searchBarMargin, searchBarMargin, searchBarMargin, 321 searchBarMargin); 322 mToolbar.setLayoutParams(toolbarLayoutParams); 323 mToolbar.setElevation( 324 mToolbar.getResources().getDimensionPixelSize(R.dimen.search_bar_elevation)); 325 headerTopOffset = toolbarLayoutParams.height + searchBarMargin * 2; 326 } else { 327 mToolbar.setBackground(mDefaultActionBarBackground); 328 mToolbar.setOutlineProvider(mDefaultOutlineProvider); 329 int actionBarMargin = mToolbar.getResources().getDimensionPixelSize( 330 R.dimen.action_bar_margin); 331 toolbarLayoutParams.setMargins(0, 0, 0, /* bottom= */ actionBarMargin); 332 mToolbar.setLayoutParams(toolbarLayoutParams); 333 mToolbar.setElevation( 334 mToolbar.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation)); 335 headerTopOffset = toolbarLayoutParams.height + actionBarMargin; 336 } 337 338 if (!mIsActionModeActivated) { 339 FrameLayout.LayoutParams headerLayoutParams = 340 (FrameLayout.LayoutParams) mHeader.getLayoutParams(); 341 headerLayoutParams.setMargins(0, /* top= */ headerTopOffset, 0, 0); 342 mHeader.setLayoutParams(headerLayoutParams); 343 } 344 } 345 shouldShowSearchBar()346 private boolean shouldShowSearchBar() { 347 return mState.stack.isRecents() && !mEnv.isSearchExpanded() && mShowSearchBar; 348 } 349 350 // Hamburger if drawer is present, else sad nullness. 351 private @Nullable getActionBarIcon()352 Drawable getActionBarIcon() { 353 if (mDrawer.isPresent()) { 354 return mToolbar.getContext().getDrawable(R.drawable.ic_hamburger); 355 } else { 356 return null; 357 } 358 } 359 revealRootsDrawer(boolean open)360 void revealRootsDrawer(boolean open) { 361 mDrawer.setOpen(open); 362 } 363 364 interface Breadcrumb { setup(Environment env, State state, IntConsumer listener)365 void setup(Environment env, State state, IntConsumer listener); 366 show(boolean visibility)367 void show(boolean visibility); 368 postUpdate()369 void postUpdate(); 370 } 371 372 interface Environment { 373 @Deprecated 374 // Use CommonAddones#getCurrentRoot getCurrentRoot()375 RootInfo getCurrentRoot(); 376 getDrawerTitle()377 String getDrawerTitle(); 378 379 @Deprecated 380 // Use CommonAddones#refreshCurrentRootAndDirectory refreshCurrentRootAndDirectory(int animation)381 void refreshCurrentRootAndDirectory(int animation); 382 isSearchExpanded()383 boolean isSearchExpanded(); 384 } 385 } 386