• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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