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