Permalink
Browse files

DocumentsUI: Add a standalone File Manager

This commit adds a standalone mode to the DocumentsUI, to let
it act as a file manager/explorer.
You can browse your storage contents, open files, and delete them,
as well as do everything that the DocumentsUI can do (ie. sort
by name/size/type, list and grid view, recents, search, ...).

This gives us a consistent file browsing UX accross all apps
that uses the new documents API.

Change-Id: If73a3c100f010bdb766c843976d4fd3573ea805c
  • Loading branch information...
1 parent 72e4c3b commit 4e2845272d7eebde9e18f02e27602dfff70b280b @xplodwild xplodwild committed with xplodwild Nov 11, 2013
@@ -7,13 +7,19 @@
<application
android:name=".DocumentsApplication"
android:label="@string/app_label"
+ android:icon="@drawable/ic_filemanager"
android:supportsRtl="true">
<!-- TODO: allow rotation when state saving is in better shape -->
<activity
android:name=".DocumentsActivity"
android:theme="@style/Theme"
- android:icon="@drawable/ic_doc_text">
+ android:icon="@drawable/ic_filemanager">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
<intent-filter android:priority="100">
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -21,6 +21,11 @@
android:icon="@drawable/ic_menu_new_folder"
android:showAsAction="always" />
<item
+ android:id="@+id/menu_paste"
+ android:title="@string/menu_paste"
+ android:icon="@*android:drawable/ic_menu_paste_holo_light"
+ android:showAsAction="always" />
+ <item
android:id="@+id/menu_search"
android:title="@string/menu_search"
android:icon="@drawable/ic_menu_search"
@@ -20,6 +20,16 @@
android:title="@string/menu_open"
android:showAsAction="always" />
<item
+ android:id="@+id/menu_copy"
+ android:icon="@drawable/ic_menu_copy"
+ android:title="@string/menu_copy"
+ android:showAsAction="always" />
+ <item
+ android:id="@+id/menu_cut"
+ android:title="@string/menu_cut"
+ android:icon="@drawable/ic_menu_cut_holo_light"
+ android:showAsAction="always" />
+ <item
android:id="@+id/menu_share"
android:icon="@drawable/ic_menu_share"
android:title="@string/menu_share"
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The OmniROM Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Action bar title when the app is in standalone (file manager) mode [CHAR LIMIT=32] -->
+ <string name="title_standalone">Files</string>
+
+ <!-- Toast shown after successfully deleting files -->
+ <string name="toast_success_delete">Files deleted</string>
+
+ <!-- Title of the Delete confirmation dialog -->
+ <string name="dialog_delete_confirm_title">Delete files?</string>
+
+ <!-- Menu item title that copies the selected documents [CHAR LIMIT=24] -->
+ <string name="menu_copy">Copy</string>
+ <!-- Menu item title that cuts the selected documents [CHAR LIMIT=24] -->
+ <string name="menu_cut">Cut</string>
+ <!-- Menu item title that pastes the selected documents [CHAR LIMIT=24] -->
+ <string name="menu_paste">Paste</string>
+
+
+ <!-- Text shown in a toast when the user copies files -->
+ <plurals name="files_copied">
+ <item quantity="one">The file has been put in the clipboard</item>
+ <item quantity="other">%d files have been put in the clipboard</item>
+ </plurals>
+
+ <!-- Text shown in a toast when the user pastes files -->
+ <plurals name="files_pasted">
+ <item quantity="one">The file has been pasted. Upload might be finishing in background.</item>
+ <item quantity="other">%d files have been pasted. Upload might be finishing in background.</item>
+ </plurals>
+
+ <!-- Text show in the progress dialog while files are being copied -->
+ <string name="copy_in_progress">Copying...</string>
+
+ <!-- Text show in the progress dialog while files are being deleted -->
+ <string name="delete_in_progress">Deleting...</string>
+
+ <plurals name="dialog_delete_confirm_message">
+ <item quantity="one">This file will be permanently deleted!</item>
+ <item quantity="other">This will permanently delete the %d files selected!</item>
+ </plurals>
+
+ <string name="ok">OK</string>
+ <string name="cancel">Cancel</string>
+</resources>
@@ -12,13 +12,28 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
+ *
+ * Per article 5 of the Apache 2.0 License, some modifications to this code
+ * were made by the OmniROM Project.
+ *
+ * Modifications Copyright (C) 2013 The OmniROM Project
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.android.documentsui;
import static com.android.documentsui.DocumentsActivity.TAG;
import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_STANDALONE;
import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
@@ -28,16 +43,20 @@
import static com.android.documentsui.model.DocumentInfo.getCursorString;
import android.app.ActivityManager;
+import android.app.AlertDialog;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.LoaderManager.LoaderCallbacks;
+import android.app.ProgressDialog;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
+import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
@@ -84,7 +103,10 @@
import com.google.android.collect.Lists;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.Executor;
/**
* Display the documents inside a single directory.
@@ -446,11 +468,16 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
final MenuItem open = menu.findItem(R.id.menu_open);
final MenuItem share = menu.findItem(R.id.menu_share);
final MenuItem delete = menu.findItem(R.id.menu_delete);
+ final MenuItem copy = menu.findItem(R.id.menu_copy);
+ final MenuItem cut = menu.findItem(R.id.menu_cut);
final boolean manageMode = state.action == ACTION_MANAGE;
- open.setVisible(!manageMode);
- share.setVisible(manageMode);
- delete.setVisible(manageMode);
+ final boolean stdMode = state.action == ACTION_STANDALONE;
+ open.setVisible(!manageMode && !stdMode);
+ share.setVisible(manageMode || stdMode);
+ delete.setVisible(manageMode || stdMode);
+ copy.setVisible(stdMode);
+ cut.setVisible(stdMode);
return true;
}
@@ -484,6 +511,16 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
mode.finish();
return true;
+ } else if (id == R.id.menu_copy) {
+ onCopyDocuments(docs);
+ mode.finish();
+ return true;
+
+ } else if (id == R.id.menu_cut) {
+ onCutDocuments(docs);
+ mode.finish();
+ return true;
+
} else {
return false;
}
@@ -500,16 +537,29 @@ public void onItemCheckedStateChanged(
if (checked) {
// Directories and footer items cannot be checked
boolean valid = false;
+ boolean hasFolder = false;
final Cursor cursor = mAdapter.getItem(position);
if (cursor != null) {
final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
- if (!Document.MIME_TYPE_DIR.equals(docMimeType)) {
+ final State state = getDisplayState(DirectoryFragment.this);
+ if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
+ hasFolder = true;
+ }
+ if (!Document.MIME_TYPE_DIR.equals(docMimeType) || state.action == ACTION_STANDALONE) {
valid = isDocumentEnabled(docMimeType, docFlags);
}
}
+ if (hasFolder) {
+ final Menu menu = mode.getMenu();
+ final MenuItem copy = menu.findItem(R.id.menu_copy);
+ final MenuItem cut = menu.findItem(R.id.menu_cut);
+ copy.setVisible(false);
+ cut.setVisible(false);
+ }
+
if (!valid) {
mCurrentView.setItemChecked(position, false);
}
@@ -568,7 +618,33 @@ private void onShareDocuments(List<DocumentInfo> docs) {
startActivity(intent);
}
- private void onDeleteDocuments(List<DocumentInfo> docs) {
+ private void onDeleteDocuments(final List<DocumentInfo> docs) {
+ final Context context = getActivity();
+ final ContentResolver resolver = context.getContentResolver();
+ final Resources resources = context.getResources();
+
+ // Open a confirmation dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ new DeleteFilesTask(docs.toArray(new DocumentInfo[0])).executeOnExecutor(getCurrentExecutor());
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User cancelled the dialog, ignore actions
+ }
+ });
+
+ builder.setTitle(R.string.dialog_delete_confirm_title)
+ .setMessage(resources.getQuantityString(R.plurals.dialog_delete_confirm_message, docs.size(), docs.size()));
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private boolean onDeleteDocumentsImpl(final List<DocumentInfo> docs) {
final Context context = getActivity();
final ContentResolver resolver = context.getContentResolver();
@@ -584,6 +660,33 @@ private void onDeleteDocuments(List<DocumentInfo> docs) {
try {
client = DocumentsApplication.acquireUnstableProviderOrThrow(
resolver, doc.derivedUri.getAuthority());
+
+ if (Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
+ // In order to delete a directory, we must delete its contents first. We
+ // recursively do so.
+ Uri contentsUri = DocumentsContract.buildChildDocumentsUri(
+ doc.authority, doc.documentId);
+ final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
+
+ // We get the contents of the directory
+ DirectoryLoader loader = new DirectoryLoader(
+ context, mType, root, doc, contentsUri, SORT_ORDER_UNKNOWN);
+
+ DirectoryResult result = loader.loadInBackground();
+ Cursor cursor = result.cursor;
+
+ // Build a list of the docs to delete, and delete them
+ ArrayList<DocumentInfo> docsToDelete = new ArrayList<DocumentInfo>();
+ for (int i = 0; i < cursor.getCount(); i++) {
+ cursor.moveToPosition(i);
+ final DocumentInfo subDoc = DocumentInfo.fromDirectoryCursor(cursor);
+ docsToDelete.add(subDoc);
+ }
+
+ onDeleteDocumentsImpl(docsToDelete);
+ }
+
+
DocumentsContract.deleteDocument(client, doc.derivedUri);
} catch (Exception e) {
Log.w(TAG, "Failed to delete " + doc);
@@ -593,9 +696,15 @@ private void onDeleteDocuments(List<DocumentInfo> docs) {
}
}
- if (hadTrouble) {
- Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show();
- }
+ return !hadTrouble;
+ }
+
+ private void onCopyDocuments(final List<DocumentInfo> docs) {
+ ((DocumentsActivity) getActivity()).setClipboardDocuments(docs, true);
+ }
+
+ private void onCutDocuments(final List<DocumentInfo> docs) {
+ ((DocumentsActivity) getActivity()).setClipboardDocuments(docs, false);
}
private static State getDisplayState(Fragment fragment) {
@@ -987,6 +1096,45 @@ public int getItemViewType(int position) {
}
}
+ private class DeleteFilesTask extends AsyncTask<Void, Integer, Boolean> {
+ private final DocumentInfo[] mDocs;
+ private ProgressDialog mProgressDialog;
+
+ public DeleteFilesTask(DocumentInfo... docs) {
+ mDocs = docs;
+ mProgressDialog = new ProgressDialog(getActivity());
+ mProgressDialog.setMessage(getString(R.string.delete_in_progress));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCanceledOnTouchOutside(false);
+
+ mProgressDialog.show();
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ ArrayList<DocumentInfo> docs = new ArrayList<DocumentInfo>();
+ Collections.addAll(docs, mDocs);
+ boolean result = onDeleteDocumentsImpl(docs);
+
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mProgressDialog.dismiss();
+
+ if (result == false) {
+ Toast.makeText(getActivity(), R.string.toast_failed_delete, Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(getActivity(), R.string.toast_success_delete, Toast.LENGTH_SHORT).show();
+ }
+
+ // Reload files in the current folder
+ getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
+ updateDisplayState();
+ }
+ }
+
private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap>
implements Preemptable {
private final Uri mUri;
@@ -1126,4 +1274,8 @@ private boolean isDocumentEnabled(String docMimeType, int docFlags) {
return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
}
+
+ public Executor getCurrentExecutor() {
+ return ((DocumentsActivity) getActivity()).getCurrentExecutor();
+ }
}
Oops, something went wrong. Retry.

0 comments on commit 4e28452

Please sign in to comment.