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; 18 19 import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_UNKNOWN; 20 import static com.android.documentsui.Shared.DEBUG; 21 22 import android.app.Activity; 23 import android.app.FragmentManager; 24 import android.content.ActivityNotFoundException; 25 import android.content.ClipData; 26 import android.content.ContentResolver; 27 import android.content.ContentValues; 28 import android.content.Intent; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.Parcelable; 32 import android.provider.DocumentsContract; 33 import android.support.design.widget.Snackbar; 34 import android.util.Log; 35 import android.view.KeyEvent; 36 import android.view.Menu; 37 import android.view.MenuItem; 38 39 import com.android.documentsui.OperationDialogFragment.DialogType; 40 import com.android.documentsui.RecentsProvider.ResumeColumns; 41 import com.android.documentsui.dirlist.AnimationView; 42 import com.android.documentsui.dirlist.DirectoryFragment; 43 import com.android.documentsui.dirlist.Model; 44 import com.android.documentsui.model.DocumentInfo; 45 import com.android.documentsui.model.DocumentStack; 46 import com.android.documentsui.model.DurableUtils; 47 import com.android.documentsui.model.RootInfo; 48 import com.android.documentsui.services.FileOperationService; 49 50 import java.io.FileNotFoundException; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.Collection; 54 import java.util.List; 55 56 /** 57 * Standalone file management activity. 58 */ 59 public class FilesActivity extends BaseActivity { 60 61 public static final String TAG = "FilesActivity"; 62 63 // See comments where this const is referenced for details. 64 private static final int DRAWER_NO_FIDDLE_DELAY = 1500; 65 66 // Track the time we opened the drawer in response to back being pressed. 67 // We use the time gap to figure out whether to close app or reopen the drawer. 68 private long mDrawerLastFiddled; 69 private DocumentClipper mClipper; 70 FilesActivity()71 public FilesActivity() { 72 super(R.layout.files_activity, TAG); 73 } 74 75 @Override onCreate(Bundle icicle)76 public void onCreate(Bundle icicle) { 77 super.onCreate(icicle); 78 79 mClipper = new DocumentClipper(this); 80 81 RootsFragment.show(getFragmentManager(), null); 82 83 final Intent intent = getIntent(); 84 final Uri uri = intent.getData(); 85 86 if (mState.restored) { 87 if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData()); 88 } else if (!mState.stack.isEmpty()) { 89 // If a non-empty stack is present in our state, it was read (presumably) 90 // from EXTRA_STACK intent extra. In this case, we'll skip other means of 91 // loading or restoring the stack (like URI). 92 // 93 // When restoring from a stack, if a URI is present, it should only ever be: 94 // -- a launch URI: Launch URIs support sensible activity management, 95 // but don't specify a real content target) 96 // -- a fake Uri from notifications. These URIs have no authority (TODO: details). 97 // 98 // Any other URI is *sorta* unexpected...except when browsing an archive 99 // in downloads. 100 if(uri != null 101 && uri.getAuthority() != null 102 && !uri.equals(mState.stack.peek()) 103 && !LauncherActivity.isLaunchUri(uri)) { 104 if (DEBUG) Log.w(TAG, 105 "Launching with non-empty stack. Ignoring unexpected uri: " + uri); 106 } else { 107 if (DEBUG) Log.d(TAG, "Launching with non-empty stack."); 108 } 109 refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 110 } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { 111 assert(uri != null); 112 new OpenUriForViewTask(this).executeOnExecutor( 113 ProviderExecutor.forAuthority(uri.getAuthority()), uri); 114 } else if (DocumentsContract.isRootUri(this, uri)) { 115 if (DEBUG) Log.d(TAG, "Launching with root URI."); 116 // If we've got a specific root to display, restore that root using a dedicated 117 // authority. That way a misbehaving provider won't result in an ANR. 118 loadRoot(uri); 119 } else { 120 if (DEBUG) Log.d(TAG, "All other means skipped. Launching into default directory."); 121 loadRoot(getDefaultRoot()); 122 } 123 124 final @DialogType int dialogType = intent.getIntExtra( 125 FileOperationService.EXTRA_DIALOG_TYPE, DIALOG_TYPE_UNKNOWN); 126 // DialogFragment takes care of restoring the dialog on configuration change. 127 // Only show it manually for the first time (icicle is null). 128 if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) { 129 final int opType = intent.getIntExtra( 130 FileOperationService.EXTRA_OPERATION, 131 FileOperationService.OPERATION_COPY); 132 final ArrayList<DocumentInfo> srcList = 133 intent.getParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST); 134 OperationDialogFragment.show( 135 getFragmentManager(), 136 dialogType, 137 srcList, 138 mState.stack, 139 opType); 140 } 141 } 142 143 @Override includeState(State state)144 void includeState(State state) { 145 final Intent intent = getIntent(); 146 147 state.action = State.ACTION_BROWSE; 148 state.allowMultiple = true; 149 150 // Options specific to the DocumentsActivity. 151 assert(!intent.hasExtra(Intent.EXTRA_LOCAL_ONLY)); 152 153 final DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK); 154 if (stack != null) { 155 state.stack = stack; 156 } 157 } 158 159 @Override onPostCreate(Bundle savedInstanceState)160 protected void onPostCreate(Bundle savedInstanceState) { 161 super.onPostCreate(savedInstanceState); 162 // This check avoids a flicker from "Recents" to "Home". 163 // Only update action bar at this point if there is an active 164 // serach. Why? Because this avoid an early (undesired) load of 165 // the recents root...which is the default root in other activities. 166 // In Files app "Home" is the default, but it is loaded async. 167 // update will be called once Home root is loaded. 168 // Except while searching we need this call to ensure the 169 // search bits get layed out correctly. 170 if (mSearchManager.isSearching()) { 171 mNavigator.update(); 172 } 173 } 174 175 @Override onResume()176 public void onResume() { 177 super.onResume(); 178 179 final RootInfo root = getCurrentRoot(); 180 181 // If we're browsing a specific root, and that root went away, then we 182 // have no reason to hang around. 183 // TODO: Rather than just disappearing, maybe we should inform 184 // the user what has happened, let them close us. Less surprising. 185 if (mRoots.getRootBlocking(root.authority, root.rootId) == null) { 186 finish(); 187 } 188 } 189 190 @Override getDrawerTitle()191 public String getDrawerTitle() { 192 Intent intent = getIntent(); 193 return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE)) 194 ? intent.getStringExtra(Intent.EXTRA_TITLE) 195 : getTitle().toString(); 196 } 197 198 @Override onPrepareOptionsMenu(Menu menu)199 public boolean onPrepareOptionsMenu(Menu menu) { 200 super.onPrepareOptionsMenu(menu); 201 202 final RootInfo root = getCurrentRoot(); 203 204 final MenuItem createDir = menu.findItem(R.id.menu_create_dir); 205 final MenuItem pasteFromCb = menu.findItem(R.id.menu_paste_from_clipboard); 206 final MenuItem settings = menu.findItem(R.id.menu_settings); 207 final MenuItem newWindow = menu.findItem(R.id.menu_new_window); 208 209 createDir.setVisible(true); 210 createDir.setEnabled(canCreateDirectory()); 211 pasteFromCb.setEnabled(mClipper.hasItemsToPaste()); 212 settings.setVisible(root.hasSettings()); 213 newWindow.setVisible(Shared.shouldShowFancyFeatures(this)); 214 215 Menus.disableHiddenItems(menu, pasteFromCb); 216 // It hides icon if searching in progress 217 mSearchManager.updateMenu(); 218 return true; 219 } 220 221 @Override onOptionsItemSelected(MenuItem item)222 public boolean onOptionsItemSelected(MenuItem item) { 223 switch (item.getItemId()) { 224 case R.id.menu_create_dir: 225 assert(canCreateDirectory()); 226 showCreateDirectoryDialog(); 227 break; 228 case R.id.menu_new_window: 229 createNewWindow(); 230 break; 231 case R.id.menu_paste_from_clipboard: 232 DirectoryFragment dir = getDirectoryFragment(); 233 if (dir != null) { 234 dir.pasteFromClipboard(); 235 } 236 break; 237 default: 238 return super.onOptionsItemSelected(item); 239 } 240 return true; 241 } 242 createNewWindow()243 private void createNewWindow() { 244 Metrics.logUserAction(this, Metrics.USER_ACTION_NEW_WINDOW); 245 246 Intent intent = LauncherActivity.createLaunchIntent(this); 247 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); 248 249 // With new multi-window mode we have to pick how we are launched. 250 // By default we'd be launched in-place above the existing app. 251 // By setting launch-to-side ActivityManager will open us to side. 252 if (isInMultiWindowMode()) { 253 intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT); 254 } 255 256 startActivity(intent); 257 } 258 259 @Override refreshDirectory(int anim)260 void refreshDirectory(int anim) { 261 final FragmentManager fm = getFragmentManager(); 262 final RootInfo root = getCurrentRoot(); 263 final DocumentInfo cwd = getCurrentDirectory(); 264 265 assert(!mSearchManager.isSearching()); 266 267 if (cwd == null) { 268 DirectoryFragment.showRecentsOpen(fm, anim); 269 } else { 270 // Normal boring directory 271 DirectoryFragment.showDirectory(fm, root, cwd, anim); 272 } 273 } 274 275 @Override onRootPicked(RootInfo root)276 void onRootPicked(RootInfo root) { 277 super.onRootPicked(root); 278 mDrawer.setOpen(false); 279 } 280 281 @Override onDocumentsPicked(List<DocumentInfo> docs)282 public void onDocumentsPicked(List<DocumentInfo> docs) { 283 throw new UnsupportedOperationException(); 284 } 285 286 @Override onDocumentPicked(DocumentInfo doc, Model model)287 public void onDocumentPicked(DocumentInfo doc, Model model) { 288 // Anything on downloads goes through the back through downloads manager 289 // (that's the MANAGE_DOCUMENT bit). 290 // This is done for two reasons: 291 // 1) The file in question might be a failed/queued or otherwise have some 292 // specialized download handling. 293 // 2) For APKs, the download manager will add on some important security stuff 294 // like origin URL. 295 // All other files not on downloads, event APKs, would get no benefit from this 296 // treatment, thusly the "isDownloads" check. 297 298 // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for 299 // files in archives. Also, if the activity is already browsing a ZIP from downloads, 300 // then skip MANAGE_DOCUMENTS. 301 final boolean isViewing = Intent.ACTION_VIEW.equals(getIntent().getAction()); 302 final boolean isInArchive = mState.stack.size() > 1; 303 if (getCurrentRoot().isDownloads() && !isInArchive && !isViewing) { 304 // First try managing the document; we expect manager to filter 305 // based on authority, so we don't grant. 306 final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT); 307 manage.setData(doc.derivedUri); 308 309 try { 310 startActivity(manage); 311 return; 312 } catch (ActivityNotFoundException ex) { 313 // fall back to regular handling below. 314 } 315 } 316 317 if (doc.isContainer()) { 318 openContainerDocument(doc); 319 } else { 320 openDocument(doc, model); 321 } 322 } 323 324 /** 325 * Launches an intent to view the specified document. 326 */ openDocument(DocumentInfo doc, Model model)327 private void openDocument(DocumentInfo doc, Model model) { 328 Intent intent = new QuickViewIntentBuilder( 329 getPackageManager(), getResources(), doc, model).build(); 330 331 if (intent != null) { 332 // TODO: un-work around issue b/24963914. Should be fixed soon. 333 try { 334 startActivity(intent); 335 return; 336 } catch (SecurityException e) { 337 // Carry on to regular view mode. 338 Log.e(TAG, "Caught security error: " + e.getLocalizedMessage()); 339 } 340 } 341 342 // Fall back to traditional VIEW action... 343 intent = new Intent(Intent.ACTION_VIEW); 344 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 345 intent.setData(doc.derivedUri); 346 347 if (DEBUG && intent.getClipData() != null) { 348 Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData()); 349 } 350 351 try { 352 startActivity(intent); 353 } catch (ActivityNotFoundException e) { 354 Snackbars.makeSnackbar( 355 this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show(); 356 } 357 } 358 359 @Override onKeyShortcut(int keyCode, KeyEvent event)360 public boolean onKeyShortcut(int keyCode, KeyEvent event) { 361 DirectoryFragment dir; 362 // TODO: All key events should be statically bound using alphabeticShortcut. 363 // But not working. 364 switch (keyCode) { 365 case KeyEvent.KEYCODE_A: 366 dir = getDirectoryFragment(); 367 if (dir != null) { 368 dir.selectAllFiles(); 369 } 370 return true; 371 case KeyEvent.KEYCODE_C: 372 dir = getDirectoryFragment(); 373 if (dir != null) { 374 dir.copySelectedToClipboard(); 375 } 376 return true; 377 case KeyEvent.KEYCODE_V: 378 dir = getDirectoryFragment(); 379 if (dir != null) { 380 dir.pasteFromClipboard(); 381 } 382 return true; 383 default: 384 return super.onKeyShortcut(keyCode, event); 385 } 386 } 387 388 // Do some "do what a I want" drawer fiddling, but don't 389 // do it if user already hit back recently and we recently 390 // did some fiddling. 391 @Override onBeforePopDir()392 boolean onBeforePopDir() { 393 int size = mState.stack.size(); 394 395 if (mDrawer.isPresent() 396 && (System.currentTimeMillis() - mDrawerLastFiddled) > DRAWER_NO_FIDDLE_DELAY) { 397 // Close drawer if it is open. 398 if (mDrawer.isOpen()) { 399 mDrawer.setOpen(false); 400 mDrawerLastFiddled = System.currentTimeMillis(); 401 return true; 402 } 403 404 // Open the Close drawer if it is closed and we're at the top of a root. 405 if (size <= 1) { 406 mDrawer.setOpen(true); 407 // Remember so we don't just close it again if back is pressed again. 408 mDrawerLastFiddled = System.currentTimeMillis(); 409 return true; 410 } 411 } 412 413 return false; 414 } 415 416 // Turns out only DocumentsActivity was ever calling saveStackBlocking. 417 // There may be a case where we want to contribute entries from 418 // Behavior here in FilesActivity, but it isn't yet obvious. 419 // TODO: Contribute to recents, or remove this. writeStackToRecentsBlocking()420 void writeStackToRecentsBlocking() { 421 final ContentResolver resolver = getContentResolver(); 422 final ContentValues values = new ContentValues(); 423 424 final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack); 425 426 // Remember location for next app launch 427 final String packageName = getCallingPackageMaybeExtra(); 428 values.clear(); 429 values.put(ResumeColumns.STACK, rawStack); 430 values.put(ResumeColumns.EXTERNAL, 0); 431 resolver.insert(RecentsProvider.buildResume(packageName), values); 432 } 433 434 @Override onTaskFinished(Uri... uris)435 void onTaskFinished(Uri... uris) { 436 if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris)); 437 438 final Intent intent = new Intent(); 439 if (uris.length == 1) { 440 intent.setData(uris[0]); 441 } else if (uris.length > 1) { 442 final ClipData clipData = new ClipData( 443 null, mState.acceptMimes, new ClipData.Item(uris[0])); 444 for (int i = 1; i < uris.length; i++) { 445 clipData.addItem(new ClipData.Item(uris[i])); 446 } 447 intent.setClipData(clipData); 448 } 449 450 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 451 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 452 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 453 454 setResult(Activity.RESULT_OK, intent); 455 finish(); 456 } 457 458 /** 459 * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible 460 * to know which root to select. Also, the stack doesn't contain intermediate directories. 461 * It's primarly used for opening ZIP archives from Downloads app. 462 */ 463 private static final class OpenUriForViewTask extends PairedTask<FilesActivity, Uri, Void> { 464 465 private final State mState; OpenUriForViewTask(FilesActivity activity)466 public OpenUriForViewTask(FilesActivity activity) { 467 super(activity); 468 mState = activity.mState; 469 } 470 471 @Override run(Uri... params)472 protected Void run(Uri... params) { 473 final Uri uri = params[0]; 474 475 final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner); 476 final String authority = uri.getAuthority(); 477 478 final Collection<RootInfo> roots = 479 rootsCache.getRootsForAuthorityBlocking(authority); 480 if (roots.isEmpty()) { 481 Log.e(TAG, "Failed to find root for the requested Uri: " + uri); 482 return null; 483 } 484 485 final RootInfo root = roots.iterator().next(); 486 mState.stack.root = root; 487 try { 488 mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri)); 489 } catch (FileNotFoundException e) { 490 Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + uri); 491 } 492 mState.stack.add(mOwner.getRootDocumentBlocking(root)); 493 return null; 494 } 495 496 @Override finish(Void result)497 protected void finish(Void result) { 498 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 499 } 500 } 501 } 502