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