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