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