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