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