1 /*
2  * Copyright (C) 2013 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.picker;
18 
19 import static com.android.documentsui.base.State.ACTION_CREATE;
20 import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
21 import static com.android.documentsui.base.State.ACTION_OPEN;
22 import static com.android.documentsui.base.State.ACTION_OPEN_TREE;
23 import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
24 
25 import android.content.Intent;
26 import android.graphics.Color;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.os.SystemClock;
30 import android.provider.DocumentsContract;
31 import android.view.KeyEvent;
32 import android.view.Menu;
33 import android.view.MenuItem;
34 import android.view.View;
35 
36 import androidx.annotation.CallSuper;
37 import androidx.fragment.app.Fragment;
38 import androidx.fragment.app.FragmentManager;
39 
40 import com.android.documentsui.ActionModeController;
41 import com.android.documentsui.BaseActivity;
42 import com.android.documentsui.DocsSelectionHelper;
43 import com.android.documentsui.DocumentsApplication;
44 import com.android.documentsui.FocusManager;
45 import com.android.documentsui.Injector;
46 import com.android.documentsui.MenuManager.DirectoryDetails;
47 import com.android.documentsui.Metrics;
48 import com.android.documentsui.ProviderExecutor;
49 import com.android.documentsui.R;
50 import com.android.documentsui.SharedInputHandler;
51 import com.android.documentsui.base.DocumentInfo;
52 import com.android.documentsui.base.Features;
53 import com.android.documentsui.base.MimeTypes;
54 import com.android.documentsui.base.RootInfo;
55 import com.android.documentsui.base.Shared;
56 import com.android.documentsui.base.State;
57 import com.android.documentsui.dirlist.AppsRowManager;
58 import com.android.documentsui.dirlist.DirectoryFragment;
59 import com.android.documentsui.prefs.ScopedPreferences;
60 import com.android.documentsui.services.FileOperationService;
61 import com.android.documentsui.sidebar.RootsFragment;
62 import com.android.documentsui.ui.DialogController;
63 import com.android.documentsui.ui.MessageBuilder;
64 
65 import java.util.Collection;
66 import java.util.List;
67 
68 public class PickActivity extends BaseActivity implements ActionHandler.Addons {
69 
70     static final String PREFERENCES_SCOPE = "picker";
71 
72     private static final String TAG = "PickActivity";
73 
74     private Injector<ActionHandler<PickActivity>> mInjector;
75     private SharedInputHandler mSharedInputHandler;
76 
PickActivity()77     public PickActivity() {
78         super(R.layout.documents_activity, TAG);
79     }
80 
81     // make these methods visible in this package to work around compiler bug http://b/62218600
focusSidebar()82     @Override protected boolean focusSidebar() { return super.focusSidebar(); }
popDir()83     @Override protected boolean popDir() { return super.popDir(); }
84 
85     @Override
onCreate(Bundle icicle)86     public void onCreate(Bundle icicle) {
87 
88         Features features = Features.create(this);
89         ScopedPreferences prefs = ScopedPreferences.create(this, PREFERENCES_SCOPE);
90 
91         mInjector = new Injector<>(
92                 features,
93                 new Config(),
94                 prefs,
95                 new MessageBuilder(this),
96                 DialogController.create(features, this, null),
97                 DocumentsApplication.getFileTypeLookup(this),
98                 (Collection<RootInfo> roots) -> {});
99 
100         super.onCreate(icicle);
101 
102         mInjector.selectionMgr = DocsSelectionHelper.create();
103 
104         mInjector.focusManager = new FocusManager(
105                 mInjector.features,
106                 mInjector.selectionMgr,
107                 mDrawer,
108                 this::focusSidebar,
109                 getColor(R.color.primary));
110 
111         mInjector.menuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
112 
113         mInjector.actionModeController = new ActionModeController(
114                 this,
115                 mInjector.selectionMgr,
116                 mInjector.menuManager,
117                 mInjector.messages);
118 
119         mInjector.pickResult = getPickResult(icicle);
120         mInjector.actions = new ActionHandler<>(
121                 this,
122                 mState,
123                 mProviders,
124                 mDocs,
125                 mSearchManager,
126                 ProviderExecutor::forAuthority,
127                 mInjector,
128                 LastAccessedStorage.create());
129 
130         mInjector.searchManager = mSearchManager;
131 
132         Intent intent = getIntent();
133 
134         mAppsRowManager = new AppsRowManager(mInjector.actions);
135         mInjector.appsRowManager = mAppsRowManager;
136 
137         mSharedInputHandler =
138                 new SharedInputHandler(
139                         mInjector.focusManager,
140                         mInjector.selectionMgr,
141                         mInjector.searchManager::cancelSearch,
142                         this::popDir,
143                         mInjector.features,
144                         mDrawer);
145         setupLayout(intent);
146         mInjector.actions.initLocation(intent);
147         Metrics.logPickerLaunchedFrom(Shared.getCallingPackageName(this));
148     }
149 
150     @Override
onBackPressed()151     public void onBackPressed() {
152         super.onBackPressed();
153         // log the case of user picking nothing.
154         mInjector.actions.getUpdatePickResultTask().execute();
155     }
156 
157     @Override
onSaveInstanceState(Bundle state)158     protected void onSaveInstanceState(Bundle state) {
159         super.onSaveInstanceState(state);
160         state.putParcelable(Shared.EXTRA_PICK_RESULT, mInjector.pickResult);
161     }
162 
163     @Override
onResume()164     protected void onResume() {
165         super.onResume();
166         mInjector.pickResult.setPickStartTime(SystemClock.uptimeMillis());
167     }
168 
169     @Override
onPause()170     protected void onPause() {
171         mInjector.pickResult.increaseDuration(SystemClock.uptimeMillis());
172         super.onPause();
173     }
174 
getPickResult(Bundle icicle)175     private static PickResult getPickResult(Bundle icicle) {
176         if (icicle != null) {
177             PickResult result = icicle.getParcelable(Shared.EXTRA_PICK_RESULT);
178             return result;
179         }
180 
181         return new PickResult();
182     }
183 
setupLayout(Intent intent)184     private void setupLayout(Intent intent) {
185         if (mState.action == ACTION_CREATE) {
186             final String mimeType = intent.getType();
187             final String title = intent.getStringExtra(Intent.EXTRA_TITLE);
188             SaveFragment.show(getSupportFragmentManager(), mimeType, title);
189         } else if (mState.action == ACTION_OPEN_TREE ||
190                    mState.action == ACTION_PICK_COPY_DESTINATION) {
191             PickFragment.show(getSupportFragmentManager());
192         } else {
193             // If PickFragment or SaveFragment does not show,
194             // Set save container background to transparent for edge to edge nav bar.
195             View saveContainer = findViewById(R.id.container_save);
196             saveContainer.setBackgroundColor(Color.TRANSPARENT);
197         }
198 
199         if (mState.action == ACTION_GET_CONTENT) {
200             final Intent moreApps = new Intent(intent);
201             moreApps.setComponent(null);
202             moreApps.setPackage(null);
203             RootsFragment.show(getSupportFragmentManager(), moreApps);
204         } else if (mState.action == ACTION_OPEN ||
205                    mState.action == ACTION_CREATE ||
206                    mState.action == ACTION_OPEN_TREE ||
207                    mState.action == ACTION_PICK_COPY_DESTINATION) {
208             RootsFragment.show(getSupportFragmentManager(), (Intent) null);
209         }
210     }
211 
212     @Override
includeState(State state)213     protected void includeState(State state) {
214         final Intent intent = getIntent();
215 
216         String defaultMimeType = (intent.getType() == null) ? "*/*" : intent.getType();
217         state.initAcceptMimes(intent, defaultMimeType);
218 
219         final String action = intent.getAction();
220         if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
221             state.action = ACTION_OPEN;
222         } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
223             state.action = ACTION_CREATE;
224         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
225             state.action = ACTION_GET_CONTENT;
226         } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) {
227             state.action = ACTION_OPEN_TREE;
228         } else if (Shared.ACTION_PICK_COPY_DESTINATION.equals(action)) {
229             state.action = ACTION_PICK_COPY_DESTINATION;
230         }
231 
232         if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) {
233             state.allowMultiple = intent.getBooleanExtra(
234                     Intent.EXTRA_ALLOW_MULTIPLE, false);
235         }
236 
237         if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT
238                 || state.action == ACTION_CREATE) {
239             state.openableOnly = intent.hasCategory(Intent.CATEGORY_OPENABLE);
240         }
241 
242         if (state.action == ACTION_PICK_COPY_DESTINATION) {
243             // Indicates that a copy operation (or move) includes a directory.
244             // Why? Directory creation isn't supported by some roots (like Downloads).
245             // This allows us to restrict available roots to just those with support.
246             state.directoryCopy = intent.getBooleanExtra(
247                     Shared.EXTRA_DIRECTORY_COPY, false);
248             state.copyOperationSubType = intent.getIntExtra(
249                     FileOperationService.EXTRA_OPERATION_TYPE,
250                     FileOperationService.OPERATION_COPY);
251         }
252     }
253 
254     @Override
onPostCreate(Bundle savedInstanceState)255     protected void onPostCreate(Bundle savedInstanceState) {
256         super.onPostCreate(savedInstanceState);
257         mDrawer.update();
258         mNavigator.update();
259     }
260 
261     @Override
getDrawerTitle()262     public String getDrawerTitle() {
263         String title = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT);
264         if (title == null) {
265             if (mState.action == ACTION_OPEN ||
266                 mState.action == ACTION_GET_CONTENT ||
267                 mState.action == ACTION_OPEN_TREE) {
268                 title = getResources().getString(R.string.title_open);
269             } else if (mState.action == ACTION_CREATE ||
270                        mState.action == ACTION_PICK_COPY_DESTINATION) {
271                 title = getResources().getString(R.string.title_save);
272             } else {
273                 // If all else fails, just call it "Documents".
274                 title = getResources().getString(R.string.app_label);
275             }
276         }
277 
278         return title;
279     }
280 
281     @Override
onPrepareOptionsMenu(Menu menu)282     public boolean onPrepareOptionsMenu(Menu menu) {
283         super.onPrepareOptionsMenu(menu);
284         mInjector.menuManager.updateOptionMenu(menu);
285 
286         final DocumentInfo cwd = getCurrentDirectory();
287 
288         if (mState.action == ACTION_CREATE) {
289             final FragmentManager fm = getSupportFragmentManager();
290             SaveFragment.get(fm).prepareForDirectory(cwd);
291         }
292 
293         return true;
294     }
295 
296     @Override
onOptionsItemSelected(MenuItem item)297     public boolean onOptionsItemSelected(MenuItem item) {
298         mInjector.pickResult.increaseActionCount();
299         return super.onOptionsItemSelected(item);
300     }
301 
302     @Override
refreshDirectory(int anim)303     protected void refreshDirectory(int anim) {
304         final FragmentManager fm = getSupportFragmentManager();
305         final RootInfo root = getCurrentRoot();
306         final DocumentInfo cwd = getCurrentDirectory();
307 
308         if (mState.stack.isRecents()) {
309             DirectoryFragment.showRecentsOpen(fm, anim);
310 
311             // In recents we pick layout mode based on the mimetype,
312             // picking GRID for visual types. We intentionally don't
313             // consult a user's saved preferences here since they are
314             // set per root (not per root and per mimetype).
315             boolean visualMimes = MimeTypes.mimeMatches(
316                     MimeTypes.VISUAL_MIMES, mState.acceptMimes);
317             mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
318         } else {
319                 // Normal boring directory
320                 DirectoryFragment.showDirectory(fm, root, cwd, anim);
321         }
322 
323         // Forget any replacement target
324         if (mState.action == ACTION_CREATE) {
325             final SaveFragment save = SaveFragment.get(fm);
326             if (save != null) {
327                 save.setReplaceTarget(null);
328             }
329         }
330 
331         if (mState.action == ACTION_OPEN_TREE ||
332             mState.action == ACTION_PICK_COPY_DESTINATION) {
333             final PickFragment pick = PickFragment.get(fm);
334             if (pick != null) {
335                 pick.setPickTarget(mState.action, mState.copyOperationSubType, cwd);
336             }
337         }
338     }
339 
340     @Override
onDirectoryCreated(DocumentInfo doc)341     protected void onDirectoryCreated(DocumentInfo doc) {
342         assert(doc.isDirectory());
343         mInjector.actions.openContainerDocument(doc);
344     }
345 
346     @Override
onDocumentPicked(DocumentInfo doc)347     public void onDocumentPicked(DocumentInfo doc) {
348         final FragmentManager fm = getSupportFragmentManager();
349         // Do not inline-open archives, as otherwise it would be impossible to pick
350         // archive files. Note, that picking files inside archives is not supported.
351         if (doc.isDirectory()) {
352             mInjector.actions.openContainerDocument(doc);
353             mSearchManager.recordHistory();
354         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
355             // Explicit file picked, return
356             mInjector.actions.finishPicking(doc.derivedUri);
357             mSearchManager.recordHistory();
358         } else if (mState.action == ACTION_CREATE) {
359             // Replace selected file
360             SaveFragment.get(fm).setReplaceTarget(doc);
361         }
362     }
363 
364     @Override
onDocumentsPicked(List<DocumentInfo> docs)365     public void onDocumentsPicked(List<DocumentInfo> docs) {
366         if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
367             final int size = docs.size();
368             final Uri[] uris = new Uri[size];
369             for (int i = 0; i < size; i++) {
370                 uris[i] = docs.get(i).derivedUri;
371             }
372             mInjector.actions.finishPicking(uris);
373             mSearchManager.recordHistory();
374         }
375     }
376 
377     @CallSuper
378     @Override
onKeyDown(int keyCode, KeyEvent event)379     public boolean onKeyDown(int keyCode, KeyEvent event) {
380         return mSharedInputHandler.onKeyDown(
381                 keyCode,
382                 event)
383                 || super.onKeyDown(keyCode, event);
384     }
385 
386     @Override
setResult(int resultCode, Intent intent, int notUsed)387     public void setResult(int resultCode, Intent intent, int notUsed) {
388         setResult(resultCode, intent);
389     }
390 
get(Fragment fragment)391     public static PickActivity get(Fragment fragment) {
392         return (PickActivity) fragment.getActivity();
393     }
394 
395     @Override
getInjector()396     public Injector<ActionHandler<PickActivity>> getInjector() {
397         return mInjector;
398     }
399 }
400