1 /*
2  * Copyright (C) 2015 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.dirlist;
18 
19 import static com.android.documentsui.State.ACTION_BROWSE;
20 import static com.android.documentsui.State.ACTION_CREATE;
21 import static com.android.documentsui.State.ACTION_GET_CONTENT;
22 import static com.android.documentsui.State.ACTION_OPEN;
23 import static com.android.documentsui.State.ACTION_OPEN_TREE;
24 import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
25 
26 import android.content.Context;
27 import android.provider.DocumentsContract.Document;
28 import android.view.Menu;
29 import android.view.MenuItem;
30 
31 import com.android.documentsui.BaseActivity;
32 import com.android.documentsui.Menus;
33 import com.android.documentsui.MimePredicate;
34 import com.android.documentsui.R;
35 import com.android.documentsui.State;
36 import com.android.documentsui.dirlist.DirectoryFragment.ResultType;
37 
38 /**
39  * Providers support for specializing the DirectoryFragment to the "host" Activity.
40  * Feel free to expand the role of this class to handle other specializations.
41  */
42 public abstract class FragmentTuner {
43 
44     final Context mContext;
45     final State mState;
46 
FragmentTuner(Context context, State state)47     public FragmentTuner(Context context, State state) {
48         mContext = context;
49         mState = state;
50     }
51 
pick(Context context, State state)52     public static FragmentTuner pick(Context context, State state) {
53         switch (state.action) {
54             case ACTION_BROWSE:
55                 return new FilesTuner(context, state);
56             default:
57                 return new DocumentsTuner(context, state);
58         }
59     }
60 
61 
62     // Subtly different from isDocumentEnabled. The reason may be illuminated as follows.
63     // A folder is enabled such that it may be double clicked, even in settings
64     // when the folder itself cannot be selected. This may also be true of container types.
canSelectType(String docMimeType, int docFlags)65     public boolean canSelectType(String docMimeType, int docFlags) {
66         return true;
67     }
68 
isDocumentEnabled(String docMimeType, int docFlags)69     public boolean isDocumentEnabled(String docMimeType, int docFlags) {
70         return true;
71     }
72 
73     /**
74      * When managed mode is enabled, active downloads will be visible in the UI.
75      * Presumably this should only be true when in the downloads directory.
76      */
managedModeEnabled()77     boolean managedModeEnabled() {
78         return false;
79     }
80 
81     /**
82      * Whether drag n' drop is allowed in this context
83      */
dragAndDropEnabled()84     boolean dragAndDropEnabled() {
85         return false;
86     }
87 
updateActionMenu(Menu menu, SelectionDetails selection)88     abstract void updateActionMenu(Menu menu, SelectionDetails selection);
onModelLoaded(Model model, @ResultType int resultType, boolean isSearch)89     abstract void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch);
90 
91     /**
92      * Provides support for Platform specific specializations of DirectoryFragment.
93      */
94     private static final class DocumentsTuner extends FragmentTuner {
95 
96         // We use this to keep track of whether a model has been previously loaded or not so we can
97         // open the drawer on empty directories on first launch
98         private boolean mModelPreviousLoaded;
99 
DocumentsTuner(Context context, State state)100         public DocumentsTuner(Context context, State state) {
101             super(context, state);
102         }
103 
104         @Override
canSelectType(String docMimeType, int docFlags)105         public boolean canSelectType(String docMimeType, int docFlags) {
106             if (!isDocumentEnabled(docMimeType, docFlags)) {
107                 return false;
108             }
109 
110             if (MimePredicate.isDirectoryType(docMimeType)) {
111                 return false;
112             }
113 
114             if (mState.action == ACTION_OPEN_TREE
115                     || mState.action == ACTION_PICK_COPY_DESTINATION) {
116                 // In this case nothing *ever* is selectable...the expected user behavior is
117                 // they navigate *into* a folder, then click a confirmation button indicating
118                 // that the current directory is the directory they are picking.
119                 return false;
120             }
121 
122             return true;
123         }
124 
125         @Override
isDocumentEnabled(String mimeType, int docFlags)126         public boolean isDocumentEnabled(String mimeType, int docFlags) {
127             // Directories are always enabled.
128             if (MimePredicate.isDirectoryType(mimeType)) {
129                 return true;
130             }
131 
132             switch (mState.action) {
133                 case ACTION_CREATE:
134                     // Read-only files are disabled when creating.
135                     if ((docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
136                         return false;
137                     }
138                 case ACTION_OPEN:
139                 case ACTION_GET_CONTENT:
140                     final boolean isVirtual = (docFlags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
141                     if (isVirtual && mState.openableOnly) {
142                         return false;
143                     }
144             }
145 
146             return MimePredicate.mimeMatches(mState.acceptMimes, mimeType);
147         }
148 
149         @Override
updateActionMenu(Menu menu, SelectionDetails selection)150         public void updateActionMenu(Menu menu, SelectionDetails selection) {
151 
152             MenuItem open = menu.findItem(R.id.menu_open);
153             MenuItem share = menu.findItem(R.id.menu_share);
154             MenuItem delete = menu.findItem(R.id.menu_delete);
155             MenuItem rename = menu.findItem(R.id.menu_rename);
156             MenuItem selectAll = menu.findItem(R.id.menu_select_all);
157 
158             open.setVisible(mState.action == ACTION_GET_CONTENT
159                     || mState.action == ACTION_OPEN);
160             share.setVisible(false);
161             delete.setVisible(false);
162             rename.setVisible(false);
163             selectAll.setVisible(mState.allowMultiple);
164 
165             Menus.disableHiddenItems(menu);
166         }
167 
168         @Override
onModelLoaded(Model model, @ResultType int resultType, boolean isSearch)169         void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {
170             boolean showDrawer = false;
171 
172             if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
173                 showDrawer = false;
174             }
175             if (mState.external && mState.action == ACTION_GET_CONTENT) {
176                 showDrawer = true;
177             }
178             if (mState.action == ACTION_PICK_COPY_DESTINATION) {
179                 showDrawer = true;
180             }
181 
182             // When launched into empty root, open drawer.
183             if (model.isEmpty()) {
184                 showDrawer = true;
185             }
186 
187             if (showDrawer && !mState.hasInitialLocationChanged() && !isSearch
188                     && !mModelPreviousLoaded) {
189                 // This noops on layouts without drawer, so no need to guard.
190                 ((BaseActivity) mContext).setRootsDrawerOpen(true);
191             }
192             mModelPreviousLoaded = true;
193         }
194     }
195 
196     /**
197      * Provides support for Files activity specific specializations of DirectoryFragment.
198      */
199     private static final class FilesTuner extends FragmentTuner {
200 
201         // We use this to keep track of whether a model has been previously loaded or not so we can
202         // open the drawer on empty directories on first launch
203         private boolean mModelPreviousLoaded;
204 
FilesTuner(Context context, State state)205         public FilesTuner(Context context, State state) {
206             super(context, state);
207         }
208 
209         @Override
updateActionMenu(Menu menu, SelectionDetails selection)210         public void updateActionMenu(Menu menu, SelectionDetails selection) {
211 
212             menu.findItem(R.id.menu_open).setVisible(false);  // "open" is never used in Files.
213 
214             // Commands accessible only via keyboard...
215             MenuItem copy = menu.findItem(R.id.menu_copy_to_clipboard);
216             MenuItem paste = menu.findItem(R.id.menu_paste_from_clipboard);
217 
218             // Commands visible in the UI...
219             MenuItem rename = menu.findItem(R.id.menu_rename);
220             MenuItem moveTo = menu.findItem(R.id.menu_move_to);
221             MenuItem copyTo = menu.findItem(R.id.menu_copy_to);
222             MenuItem share = menu.findItem(R.id.menu_share);
223             MenuItem delete = menu.findItem(R.id.menu_delete);
224 
225             // copy is not visible, keyboard only
226             copy.setEnabled(!selection.containsPartialFiles());
227 
228             // Commands usually on action-bar, so we always manage visibility.
229             share.setVisible(!selection.containsDirectories() && !selection.containsPartialFiles());
230             delete.setVisible(selection.canDelete());
231 
232             share.setEnabled(!selection.containsDirectories() && !selection.containsPartialFiles());
233             delete.setEnabled(selection.canDelete());
234 
235             // Commands always in overflow, so we don't bother showing/hiding...
236             copyTo.setVisible(true);
237             moveTo.setVisible(true);
238             rename.setVisible(true);
239 
240             copyTo.setEnabled(!selection.containsPartialFiles());
241             moveTo.setEnabled(!selection.containsPartialFiles() && selection.canDelete());
242             rename.setEnabled(!selection.containsPartialFiles() && selection.canRename());
243 
244             Menus.disableHiddenItems(menu, copy, paste);
245         }
246 
247         @Override
onModelLoaded(Model model, @ResultType int resultType, boolean isSearch)248         void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {
249             // When launched into empty root, open drawer.
250             if (model.isEmpty() && !mState.hasInitialLocationChanged() && !isSearch
251                     && !mModelPreviousLoaded) {
252                 // This noops on layouts without drawer, so no need to guard.
253                 ((BaseActivity) mContext).setRootsDrawerOpen(true);
254             }
255             mModelPreviousLoaded = true;
256         }
257 
258         @Override
managedModeEnabled()259         public boolean managedModeEnabled() {
260             // When in downloads top level directory, we also show active downloads.
261             // And while we don't allow folders in Downloads, we do allow Zip files in
262             // downloads that themselves can be opened and viewed like directories.
263             // This method helps us understand when to kick in on those special behaviors.
264             return mState.stack.root != null
265                     && mState.stack.root.isDownloads()
266                     && mState.stack.size() == 1;
267         }
268 
269         @Override
dragAndDropEnabled()270         public boolean dragAndDropEnabled() {
271             return true;
272         }
273     }
274 
275     /**
276      * Access to meta data about the selection.
277      */
278     interface SelectionDetails {
containsDirectories()279         boolean containsDirectories();
containsPartialFiles()280         boolean containsPartialFiles();
281 
282         // TODO: Update these to express characteristics instead of answering concrete questions,
283         // since the answer to those questions is (or can be) activity specific.
canDelete()284         boolean canDelete();
canRename()285         boolean canRename();
286     }
287 }
288