Skip to content

Commit

Permalink
DocumentsUI: Add a standalone File Manager
Browse files Browse the repository at this point in the history
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: I630467664b445d0d011555b84fa3d50abdb666f3
  • Loading branch information
xplodwild authored and maxwen committed Jan 14, 2015
1 parent ced24b1 commit 8ac9280
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 13 deletions.
8 changes: 7 additions & 1 deletion packages/DocumentsUI/AndroidManifest.xml
Expand Up @@ -7,12 +7,18 @@
<application
android:name=".DocumentsApplication"
android:label="@string/app_label"
android:icon="@drawable/ic_filemanager"
android:supportsRtl="true">

<activity
android:name=".DocumentsActivity"
android:theme="@style/DocumentsTheme"
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>
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
Expand Down
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.
5 changes: 5 additions & 0 deletions packages/DocumentsUI/res/menu/activity.xml
Expand Up @@ -20,6 +20,11 @@
android:title="@string/menu_create_dir"
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_material"
android:showAsAction="always" />
<item
android:id="@+id/menu_search"
android:title="@string/menu_search"
Expand Down
10 changes: 10 additions & 0 deletions packages/DocumentsUI/res/menu/mode_directory.xml
Expand Up @@ -19,6 +19,16 @@
android:id="@+id/menu_open"
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:icon="@*android:drawable/ic_menu_cut_material"
android:title="@string/menu_cut"
android:showAsAction="always" />
<item
android:id="@+id/menu_share"
android:icon="@drawable/ic_menu_share"
Expand Down
60 changes: 60 additions & 0 deletions packages/DocumentsUI/res/values/custom_strings.xml
@@ -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>
Expand Up @@ -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;
Expand All @@ -28,14 +43,17 @@
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;
Expand Down Expand Up @@ -84,7 +102,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.
Expand Down Expand Up @@ -463,11 +484,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;
}
Expand Down Expand Up @@ -501,6 +527,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;
}
Expand All @@ -517,16 +553,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);
}
Expand Down Expand Up @@ -585,7 +634,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();

Expand All @@ -601,6 +676,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);
Expand All @@ -610,9 +712,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) {
Expand Down Expand Up @@ -989,6 +1097,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;
Expand Down Expand Up @@ -1129,4 +1276,8 @@ private boolean isDocumentEnabled(String docMimeType, int docFlags) {

return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
}

public Executor getCurrentExecutor() {
return ((DocumentsActivity) getActivity()).getCurrentExecutor();
}
}

0 comments on commit 8ac9280

Please sign in to comment.