summaryrefslogtreecommitdiffstats
path: root/packages/DocumentsUI/src/com/android
diff options
context:
space:
mode:
Diffstat (limited to 'packages/DocumentsUI/src/com/android')
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java172
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/CopyService.java467
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java125
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java12
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryView.java3
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java239
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java99
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/IconUtils.java3
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/PickFragment.java48
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java4
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java8
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsCache.java10
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java14
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java2
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java2
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java937
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java3
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java28
20 files changed, 1979 insertions, 209 deletions
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
new file mode 100644
index 0000000..8039b71
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 The Android Open Source 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.
+ */
+
+package com.android.documentsui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.pm.ResolveInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.model.DurableUtils;
+import com.android.documentsui.model.RootInfo;
+import com.google.common.collect.Maps;
+
+abstract class BaseActivity extends Activity {
+ public abstract State getDisplayState();
+ public abstract RootInfo getCurrentRoot();
+ public abstract void onStateChanged();
+ public abstract void setRootsDrawerOpen(boolean open);
+ public abstract void onDocumentPicked(DocumentInfo doc);
+ public abstract void onDocumentsPicked(List<DocumentInfo> docs);
+ public abstract DocumentInfo getCurrentDirectory();
+ public abstract void setPending(boolean pending);
+ public abstract void onStackPicked(DocumentStack stack);
+ public abstract void onPickRequested(DocumentInfo pickTarget);
+ public abstract void onAppPicked(ResolveInfo info);
+ public abstract void onRootPicked(RootInfo root, boolean closeDrawer);
+ public abstract void onSaveRequested(DocumentInfo replaceTarget);
+ public abstract void onSaveRequested(String mimeType, String displayName);
+
+ public static BaseActivity get(Fragment fragment) {
+ return (BaseActivity) fragment.getActivity();
+ }
+
+ public static abstract class DocumentsIntent {
+ /** Intent action name to open copy destination. */
+ public static String ACTION_OPEN_COPY_DESTINATION =
+ "com.android.documentsui.OPEN_COPY_DESTINATION";
+
+ /**
+ * Extra boolean flag for ACTION_OPEN_COPY_DESTINATION_STRING, which
+ * specifies if the destination directory needs to create new directory or not.
+ */
+ public static String EXTRA_DIRECTORY_COPY = "com.android.documentsui.DIRECTORY_COPY";
+ }
+
+ public static class State implements android.os.Parcelable {
+ public int action;
+ public String[] acceptMimes;
+
+ /** Explicit user choice */
+ public int userMode = MODE_UNKNOWN;
+ /** Derived after loader */
+ public int derivedMode = MODE_LIST;
+
+ /** Explicit user choice */
+ public int userSortOrder = SORT_ORDER_UNKNOWN;
+ /** Derived after loader */
+ public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
+
+ public boolean allowMultiple = false;
+ public boolean showSize = false;
+ public boolean localOnly = false;
+ public boolean forceAdvanced = false;
+ public boolean showAdvanced = false;
+ public boolean stackTouched = false;
+ public boolean restored = false;
+ public boolean directoryCopy = false;
+
+ /** Current user navigation stack; empty implies recents. */
+ public DocumentStack stack = new DocumentStack();
+ /** Currently active search, overriding any stack. */
+ public String currentSearch;
+
+ /** Instance state for every shown directory */
+ public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap();
+
+ /** Currently copying file */
+ public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<DocumentInfo>();
+
+ public static final int ACTION_OPEN = 1;
+ public static final int ACTION_CREATE = 2;
+ public static final int ACTION_GET_CONTENT = 3;
+ public static final int ACTION_OPEN_TREE = 4;
+ public static final int ACTION_MANAGE = 5;
+ public static final int ACTION_BROWSE = 6;
+ public static final int ACTION_BROWSE_ALL = 7;
+ public static final int ACTION_OPEN_COPY_DESTINATION = 8;
+
+ public static final int MODE_UNKNOWN = 0;
+ public static final int MODE_LIST = 1;
+ public static final int MODE_GRID = 2;
+
+ public static final int SORT_ORDER_UNKNOWN = 0;
+ public static final int SORT_ORDER_DISPLAY_NAME = 1;
+ public static final int SORT_ORDER_LAST_MODIFIED = 2;
+ public static final int SORT_ORDER_SIZE = 3;
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(action);
+ out.writeInt(userMode);
+ out.writeStringArray(acceptMimes);
+ out.writeInt(userSortOrder);
+ out.writeInt(allowMultiple ? 1 : 0);
+ out.writeInt(showSize ? 1 : 0);
+ out.writeInt(localOnly ? 1 : 0);
+ out.writeInt(forceAdvanced ? 1 : 0);
+ out.writeInt(showAdvanced ? 1 : 0);
+ out.writeInt(stackTouched ? 1 : 0);
+ out.writeInt(restored ? 1 : 0);
+ DurableUtils.writeToParcel(out, stack);
+ out.writeString(currentSearch);
+ out.writeMap(dirState);
+ out.writeList(selectedDocumentsForCopy);
+ }
+
+ public static final Creator<State> CREATOR = new Creator<State>() {
+ @Override
+ public State createFromParcel(Parcel in) {
+ final State state = new State();
+ state.action = in.readInt();
+ state.userMode = in.readInt();
+ state.acceptMimes = in.readStringArray();
+ state.userSortOrder = in.readInt();
+ state.allowMultiple = in.readInt() != 0;
+ state.showSize = in.readInt() != 0;
+ state.localOnly = in.readInt() != 0;
+ state.forceAdvanced = in.readInt() != 0;
+ state.showAdvanced = in.readInt() != 0;
+ state.stackTouched = in.readInt() != 0;
+ state.restored = in.readInt() != 0;
+ DurableUtils.readFromParcel(in, state.stack);
+ state.currentSearch = in.readString();
+ in.readMap(state.dirState, null);
+ in.readList(state.selectedDocumentsForCopy, null);
+ return state;
+ }
+
+ @Override
+ public State[] newArray(int size) {
+ return new State[size];
+ }
+ };
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java
new file mode 100644
index 0000000..a9f03b6
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2015 The Android Open Source 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.
+ */
+
+package com.android.documentsui;
+
+import static com.android.documentsui.model.DocumentInfo.getCursorLong;
+import static com.android.documentsui.model.DocumentInfo.getCursorString;
+
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+
+import libcore.io.IoUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class CopyService extends IntentService {
+ public static final String TAG = "CopyService";
+
+ private static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
+ public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
+ public static final String EXTRA_STACK = "com.android.documentsui.STACK";
+ public static final String EXTRA_FAILURE = "com.android.documentsui.FAILURE";
+
+ // TODO: Move it to a shared file when more operations are implemented.
+ public static final int FAILURE_COPY = 1;
+
+ private NotificationManager mNotificationManager;
+ private Notification.Builder mProgressBuilder;
+
+ // Jobs are serialized but a job ID is used, to avoid mixing up cancellation requests.
+ private String mJobId;
+ private volatile boolean mIsCancelled;
+ // Parameters of the copy job. Requests to an IntentService are serialized so this code only
+ // needs to deal with one job at a time.
+ private final ArrayList<Uri> mFailedFiles;
+ private long mBatchSize;
+ private long mBytesCopied;
+ private long mStartTime;
+ private long mLastNotificationTime;
+ // Speed estimation
+ private long mBytesCopiedSample;
+ private long mSampleTime;
+ private long mSpeed;
+ private long mRemainingTime;
+ // Provider clients are acquired for the duration of each copy job. Note that there is an
+ // implicit assumption that all srcs come from the same authority.
+ private ContentProviderClient mSrcClient;
+ private ContentProviderClient mDstClient;
+
+ public CopyService() {
+ super("CopyService");
+
+ mFailedFiles = new ArrayList<Uri>();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent.hasExtra(EXTRA_CANCEL)) {
+ handleCancel(intent);
+ }
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent.hasExtra(EXTRA_CANCEL)) {
+ handleCancel(intent);
+ return;
+ }
+
+ final ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
+ final DocumentStack stack = intent.getParcelableExtra(EXTRA_STACK);
+
+ try {
+ // Acquire content providers.
+ mSrcClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(),
+ srcs.get(0).authority);
+ mDstClient = DocumentsApplication.acquireUnstableProviderOrThrow(getContentResolver(),
+ stack.peek().authority);
+
+ setupCopyJob(srcs, stack);
+
+ for (int i = 0; i < srcs.size() && !mIsCancelled; ++i) {
+ copy(srcs.get(i), stack.peek());
+ }
+ } catch (Exception e) {
+ // Catch-all to prevent any copy errors from wedging the app.
+ Log.e(TAG, "Exceptions occurred during copying", e);
+ } finally {
+ ContentProviderClient.releaseQuietly(mSrcClient);
+ ContentProviderClient.releaseQuietly(mDstClient);
+
+ // Dismiss the ongoing copy notification when the copy is done.
+ mNotificationManager.cancel(mJobId, 0);
+
+ if (mFailedFiles.size() > 0) {
+ final Context context = getApplicationContext();
+ final Intent navigateIntent = new Intent(context, StandaloneActivity.class);
+ navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack);
+ navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY);
+ navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, mFailedFiles);
+
+ final Notification.Builder errorBuilder = new Notification.Builder(this)
+ .setContentTitle(context.getResources().
+ getQuantityString(R.plurals.copy_error_notification_title,
+ mFailedFiles.size(), mFailedFiles.size()))
+ .setContentText(getString(R.string.notification_touch_for_details))
+ .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
+ .setCategory(Notification.CATEGORY_ERROR)
+ .setSmallIcon(R.drawable.ic_menu_copy)
+ .setAutoCancel(true);
+ mNotificationManager.notify(mJobId, 0, errorBuilder.build());
+ }
+
+ // TODO: Display a toast if the copy was cancelled.
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ /**
+ * Sets up the CopyService to start tracking and sending notifications for the given batch of
+ * files.
+ *
+ * @param srcs A list of src files to copy.
+ * @param stack The copy destination stack.
+ * @throws RemoteException
+ */
+ private void setupCopyJob(ArrayList<DocumentInfo> srcs, DocumentStack stack)
+ throws RemoteException {
+ // Create an ID for this copy job. Use the timestamp.
+ mJobId = String.valueOf(SystemClock.elapsedRealtime());
+ // Reset the cancellation flag.
+ mIsCancelled = false;
+
+ final Context context = getApplicationContext();
+ final Intent navigateIntent = new Intent(context, StandaloneActivity.class);
+ navigateIntent.putExtra(EXTRA_STACK, (Parcelable) stack);
+
+ mProgressBuilder = new Notification.Builder(this)
+ .setContentTitle(getString(R.string.copy_notification_title))
+ .setContentIntent(PendingIntent.getActivity(context, 0, navigateIntent, 0))
+ .setCategory(Notification.CATEGORY_PROGRESS)
+ .setSmallIcon(R.drawable.ic_menu_copy)
+ .setOngoing(true);
+
+ final Intent cancelIntent = new Intent(this, CopyService.class);
+ cancelIntent.putExtra(EXTRA_CANCEL, mJobId);
+ mProgressBuilder.addAction(R.drawable.ic_cab_cancel,
+ getString(android.R.string.cancel), PendingIntent.getService(this, 0,
+ cancelIntent,
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
+
+ // Send an initial progress notification.
+ mProgressBuilder.setProgress(0, 0, true); // Indeterminate progress while setting up.
+ mProgressBuilder.setContentText(getString(R.string.copy_preparing));
+ mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
+
+ // Reset batch parameters.
+ mFailedFiles.clear();
+ mBatchSize = calculateFileSizes(srcs);
+ mBytesCopied = 0;
+ mStartTime = SystemClock.elapsedRealtime();
+ mLastNotificationTime = 0;
+ mBytesCopiedSample = 0;
+ mSampleTime = 0;
+ mSpeed = 0;
+ mRemainingTime = 0;
+
+ // TODO: Check preconditions for copy.
+ // - check that the destination has enough space and is writeable?
+ // - check MIME types?
+ }
+
+ /**
+ * Calculates the cumulative size of all the documents in the list. Directories are recursed
+ * into and totaled up.
+ *
+ * @param srcs
+ * @return Size in bytes.
+ * @throws RemoteException
+ */
+ private long calculateFileSizes(List<DocumentInfo> srcs) throws RemoteException {
+ long result = 0;
+ for (DocumentInfo src : srcs) {
+ if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
+ // Directories need to be recursed into.
+ result += calculateFileSizesHelper(src.derivedUri);
+ } else {
+ result += src.size;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Calculates (recursively) the cumulative size of all the files under the given directory.
+ *
+ * @throws RemoteException
+ */
+ private long calculateFileSizesHelper(Uri uri) throws RemoteException {
+ final String authority = uri.getAuthority();
+ final Uri queryUri = DocumentsContract.buildChildDocumentsUri(authority,
+ DocumentsContract.getDocumentId(uri));
+ final String queryColumns[] = new String[] {
+ Document.COLUMN_DOCUMENT_ID,
+ Document.COLUMN_MIME_TYPE,
+ Document.COLUMN_SIZE
+ };
+
+ long result = 0;
+ Cursor cursor = null;
+ try {
+ cursor = mSrcClient.query(queryUri, queryColumns, null, null, null);
+ while (cursor.moveToNext()) {
+ if (Document.MIME_TYPE_DIR.equals(
+ getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
+ // Recurse into directories.
+ final Uri subdirUri = DocumentsContract.buildDocumentUri(authority,
+ getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
+ result += calculateFileSizesHelper(subdirUri);
+ } else {
+ // This may return -1 if the size isn't defined. Ignore those cases.
+ long size = getCursorLong(cursor, Document.COLUMN_SIZE);
+ result += size > 0 ? size : 0;
+ }
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ }
+
+ return result;
+ }
+
+ /**
+ * Cancels the current copy job, if its ID matches the given ID.
+ *
+ * @param intent The cancellation intent.
+ */
+ private void handleCancel(Intent intent) {
+ final String cancelledId = intent.getStringExtra(EXTRA_CANCEL);
+ // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
+ // cancellation requests from affecting unrelated copy jobs.
+ if (Objects.equals(mJobId, cancelledId)) {
+ // Set the cancel flag. This causes the copy loops to exit.
+ mIsCancelled = true;
+ // Dismiss the progress notification here rather than in the copy loop. This preserves
+ // interactivity for the user in case the copy loop is stalled.
+ mNotificationManager.cancel(mJobId, 0);
+ }
+ }
+
+ /**
+ * Logs progress on the current copy operation. Displays/Updates the progress notification.
+ *
+ * @param bytesCopied
+ */
+ private void makeProgress(long bytesCopied) {
+ mBytesCopied += bytesCopied;
+ double done = (double) mBytesCopied / mBatchSize;
+ String percent = NumberFormat.getPercentInstance().format(done);
+
+ // Update time estimate
+ long currentTime = SystemClock.elapsedRealtime();
+ long elapsedTime = currentTime - mStartTime;
+
+ // Send out progress notifications once a second.
+ if (currentTime - mLastNotificationTime > 1000) {
+ updateRemainingTimeEstimate(elapsedTime);
+ mProgressBuilder.setProgress(100, (int) (done * 100), false);
+ mProgressBuilder.setContentInfo(percent);
+ if (mRemainingTime > 0) {
+ mProgressBuilder.setContentText(getString(R.string.copy_remaining,
+ DateUtils.formatDuration(mRemainingTime)));
+ } else {
+ mProgressBuilder.setContentText(null);
+ }
+ mNotificationManager.notify(mJobId, 0, mProgressBuilder.build());
+ mLastNotificationTime = currentTime;
+ }
+ }
+
+ /**
+ * Generates an estimate of the remaining time in the copy.
+ *
+ * @param elapsedTime The time elapsed so far.
+ */
+ private void updateRemainingTimeEstimate(long elapsedTime) {
+ final long sampleDuration = elapsedTime - mSampleTime;
+ final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
+ if (mSpeed == 0) {
+ mSpeed = sampleSpeed;
+ } else {
+ mSpeed = ((3 * mSpeed) + sampleSpeed) / 4;
+ }
+
+ if (mSampleTime > 0 && mSpeed > 0) {
+ mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
+ } else {
+ mRemainingTime = 0;
+ }
+
+ mSampleTime = elapsedTime;
+ mBytesCopiedSample = mBytesCopied;
+ }
+
+ /**
+ * Copies a the given documents to the given location.
+ *
+ * @param srcInfo DocumentInfos for the documents to copy.
+ * @param dstDirInfo The destination directory.
+ * @throws RemoteException
+ */
+ private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
+ final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirInfo.derivedUri,
+ srcInfo.mimeType, srcInfo.displayName);
+ if (dstUri == null) {
+ // If this is a directory, the entire subdir will not be copied over.
+ Log.e(TAG, "Error while copying " + srcInfo.displayName);
+ mFailedFiles.add(srcInfo.derivedUri);
+ return;
+ }
+
+ if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) {
+ copyDirectoryHelper(srcInfo.derivedUri, dstUri);
+ } else {
+ copyFileHelper(srcInfo.derivedUri, dstUri);
+ }
+ }
+
+ /**
+ * Handles recursion into a directory and copying its contents. Note that in linux terms, this
+ * does the equivalent of "cp src/* dst", not "cp -r src dst".
+ *
+ * @param srcDirUri URI of the directory to copy from. The routine will copy the directory's
+ * contents, not the directory itself.
+ * @param dstDirUri URI of the directory to copy to. Must be created beforehand.
+ * @throws RemoteException
+ */
+ private void copyDirectoryHelper(Uri srcDirUri, Uri dstDirUri) throws RemoteException {
+ // Recurse into directories. Copy children into the new subdirectory.
+ final String queryColumns[] = new String[] {
+ Document.COLUMN_DISPLAY_NAME,
+ Document.COLUMN_DOCUMENT_ID,
+ Document.COLUMN_MIME_TYPE,
+ Document.COLUMN_SIZE
+ };
+ final Uri queryUri = DocumentsContract.buildChildDocumentsUri(srcDirUri.getAuthority(),
+ DocumentsContract.getDocumentId(srcDirUri));
+ Cursor cursor = null;
+ try {
+ // Iterate over srcs in the directory; copy to the destination directory.
+ cursor = mSrcClient.query(queryUri, queryColumns, null, null, null);
+ while (cursor.moveToNext()) {
+ final String childMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+ final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirUri,
+ childMimeType, getCursorString(cursor, Document.COLUMN_DISPLAY_NAME));
+ final Uri childUri = DocumentsContract.buildDocumentUri(srcDirUri.getAuthority(),
+ getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
+ if (Document.MIME_TYPE_DIR.equals(childMimeType)) {
+ copyDirectoryHelper(childUri, dstUri);
+ } else {
+ copyFileHelper(childUri, dstUri);
+ }
+ }
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ }
+ }
+
+ /**
+ * Handles copying a single file.
+ *
+ * @param srcUri URI of the file to copy from.
+ * @param dstUri URI of the *file* to copy to. Must be created beforehand.
+ * @throws RemoteException
+ */
+ private void copyFileHelper(Uri srcUri, Uri dstUri) throws RemoteException {
+ // Copy an individual file.
+ CancellationSignal canceller = new CancellationSignal();
+ ParcelFileDescriptor srcFile = null;
+ ParcelFileDescriptor dstFile = null;
+ InputStream src = null;
+ OutputStream dst = null;
+
+ boolean errorOccurred = false;
+ try {
+ srcFile = mSrcClient.openFile(srcUri, "r", canceller);
+ dstFile = mDstClient.openFile(dstUri, "w", canceller);
+ src = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
+ dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
+
+ byte[] buffer = new byte[8192];
+ int len;
+ while (!mIsCancelled && ((len = src.read(buffer)) != -1)) {
+ dst.write(buffer, 0, len);
+ makeProgress(len);
+ }
+ srcFile.checkError();
+ dstFile.checkError();
+ } catch (IOException e) {
+ errorOccurred = true;
+ Log.e(TAG, "Error while copying " + srcUri.toString(), e);
+ mFailedFiles.add(srcUri);
+ } finally {
+ // This also ensures the file descriptors are closed.
+ IoUtils.closeQuietly(src);
+ IoUtils.closeQuietly(dst);
+ }
+
+ if (errorOccurred || mIsCancelled) {
+ // Clean up half-copied files.
+ canceller.cancel();
+ try {
+ DocumentsContract.deleteDocument(mDstClient, dstUri);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to clean up: " + srcUri, e);
+ // RemoteExceptions usually signal that the connection is dead, so there's no point
+ // attempting to continue. Propagate the exception up so the copy job is cancelled.
+ throw e;
+ }
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
index ba8c35f..1a17ee0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -70,7 +70,7 @@ public class CreateDirectoryFragment extends DialogFragment {
public void onClick(DialogInterface dialog, int which) {
final String displayName = text1.getText().toString();
- final DocumentsActivity activity = (DocumentsActivity) getActivity();
+ final BaseActivity activity = (BaseActivity) getActivity();
final DocumentInfo cwd = activity.getCurrentDirectory();
new CreateDirectoryTask(activity, cwd, displayName).executeOnExecutor(
@@ -83,12 +83,12 @@ public class CreateDirectoryFragment extends DialogFragment {
}
private class CreateDirectoryTask extends AsyncTask<Void, Void, DocumentInfo> {
- private final DocumentsActivity mActivity;
+ private final BaseActivity mActivity;
private final DocumentInfo mCwd;
private final String mDisplayName;
public CreateDirectoryTask(
- DocumentsActivity activity, DocumentInfo cwd, String displayName) {
+ BaseActivity activity, DocumentInfo cwd, String displayName) {
mActivity = activity;
mCwd = cwd;
mDisplayName = displayName;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 39c2252..37a14c6 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -16,17 +16,20 @@
package com.android.documentsui;
+import static com.android.documentsui.BaseActivity.State.ACTION_BROWSE;
+import static com.android.documentsui.BaseActivity.State.ACTION_BROWSE_ALL;
+import static com.android.documentsui.BaseActivity.State.ACTION_CREATE;
+import static com.android.documentsui.BaseActivity.State.ACTION_MANAGE;
+import static com.android.documentsui.BaseActivity.State.MODE_GRID;
+import static com.android.documentsui.BaseActivity.State.MODE_LIST;
+import static com.android.documentsui.BaseActivity.State.MODE_UNKNOWN;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_UNKNOWN;
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.MODE_GRID;
-import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
-import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN;
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
+import android.app.Activity;
import android.app.ActivityManager;
import android.app.Fragment;
import android.app.FragmentManager;
@@ -76,7 +79,7 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
-import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.ProviderExecutor.Preemptable;
import com.android.documentsui.RecentsProvider.StateColumns;
import com.android.documentsui.model.DocumentInfo;
@@ -106,6 +109,8 @@ public class DirectoryFragment extends Fragment {
public static final int ANIM_DOWN = 3;
public static final int ANIM_UP = 4;
+ public static final int REQUEST_COPY_DESTINATION = 1;
+
private int mType = TYPE_NORMAL;
private String mStateKey;
@@ -301,13 +306,13 @@ public class DirectoryFragment extends Fragment {
state.derivedMode = result.mode;
}
state.derivedSortOrder = result.sortOrder;
- ((DocumentsActivity) context).onStateChanged();
+ ((BaseActivity) context).onStateChanged();
updateDisplayState();
// When launched into empty recents, show drawer
if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) {
- ((DocumentsActivity) context).setRootsDrawerOpen(true);
+ ((BaseActivity) context).setRootsDrawerOpen(true);
}
// Restore any previous instance state
@@ -335,6 +340,33 @@ public class DirectoryFragment extends Fragment {
}
@Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ final Context context = getActivity();
+ final Resources res = context.getResources();
+
+ // There's only one request code right now. Replace this with a switch statement or
+ // something more scalable when more codes are added.
+ if (requestCode != REQUEST_COPY_DESTINATION) {
+ return;
+ }
+ if (resultCode == Activity.RESULT_CANCELED || data == null) {
+ // User pressed the back button or otherwise cancelled the destination pick. Don't
+ // proceed with the copy.
+ return;
+ }
+
+ final List<DocumentInfo> docs = getDisplayState(this).selectedDocumentsForCopy;
+ final Intent copyIntent = new Intent(context, CopyService.class);
+ copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST, new ArrayList<DocumentInfo>(docs));
+ copyIntent.putExtra(CopyService.EXTRA_STACK, data.getParcelableExtra(CopyService.EXTRA_STACK));
+
+ Toast.makeText(context,
+ res.getQuantityString(R.plurals.copy_begin, docs.size(), docs.size()),
+ Toast.LENGTH_SHORT).show();
+ context.startService(copyIntent);
+ }
+
+ @Override
public void onStop() {
super.onStop();
@@ -386,7 +418,7 @@ public class DirectoryFragment extends Fragment {
// Mode change is just visual change; no need to kick loader, and
// deliver change event immediately.
state.derivedMode = state.userMode;
- ((DocumentsActivity) getActivity()).onStateChanged();
+ ((BaseActivity) getActivity()).onStateChanged();
updateDisplayState();
}
@@ -441,7 +473,7 @@ public class DirectoryFragment extends Fragment {
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
if (isDocumentEnabled(docMimeType, docFlags)) {
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
- ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
+ ((BaseActivity) getActivity()).onDocumentPicked(doc);
}
}
}
@@ -463,11 +495,15 @@ public class DirectoryFragment extends Fragment {
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 boolean manageOrBrowse = (state.action == ACTION_MANAGE
+ || state.action == ACTION_BROWSE || state.action == ACTION_BROWSE_ALL);
- final boolean manageMode = state.action == ACTION_MANAGE;
- open.setVisible(!manageMode);
- share.setVisible(manageMode);
- delete.setVisible(manageMode);
+ open.setVisible(!manageOrBrowse);
+ share.setVisible(manageOrBrowse);
+ delete.setVisible(manageOrBrowse);
+ copy.setVisible(manageOrBrowse);
return true;
}
@@ -487,7 +523,7 @@ public class DirectoryFragment extends Fragment {
final int id = item.getItemId();
if (id == R.id.menu_open) {
- DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
+ BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
mode.finish();
return true;
@@ -501,6 +537,19 @@ public class DirectoryFragment extends Fragment {
mode.finish();
return true;
+ } else if (id == R.id.menu_copy) {
+ onCopyDocuments(docs);
+ mode.finish();
+ return true;
+
+ } else if (id == R.id.menu_select_all) {
+ int count = mCurrentView.getCount();
+ for (int i = 0; i < count; i++) {
+ mCurrentView.setItemChecked(i, true);
+ }
+ updateDisplayState();
+ return true;
+
} else {
return false;
}
@@ -522,9 +571,7 @@ public class DirectoryFragment extends Fragment {
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)) {
- valid = isDocumentEnabled(docMimeType, docFlags);
- }
+ valid = isDocumentEnabled(docMimeType, docFlags);
}
if (!valid) {
@@ -553,8 +600,17 @@ public class DirectoryFragment extends Fragment {
private void onShareDocuments(List<DocumentInfo> docs) {
Intent intent;
- if (docs.size() == 1) {
- final DocumentInfo doc = docs.get(0);
+
+ // Filter out directories - those can't be shared.
+ List<DocumentInfo> docsForSend = Lists.newArrayList();
+ for (DocumentInfo doc: docs) {
+ if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
+ docsForSend.add(doc);
+ }
+ }
+
+ if (docsForSend.size() == 1) {
+ final DocumentInfo doc = docsForSend.get(0);
intent = new Intent(Intent.ACTION_SEND);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -562,14 +618,14 @@ public class DirectoryFragment extends Fragment {
intent.setType(doc.mimeType);
intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
- } else if (docs.size() > 1) {
+ } else if (docsForSend.size() > 1) {
intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_DEFAULT);
final ArrayList<String> mimeTypes = Lists.newArrayList();
final ArrayList<Uri> uris = Lists.newArrayList();
- for (DocumentInfo doc : docs) {
+ for (DocumentInfo doc : docsForSend) {
mimeTypes.add(doc.mimeType);
uris.add(doc.derivedUri);
}
@@ -615,8 +671,29 @@ public class DirectoryFragment extends Fragment {
}
}
+ private void onCopyDocuments(List<DocumentInfo> docs) {
+ getDisplayState(this).selectedDocumentsForCopy = docs;
+
+ // Pop up a dialog to pick a destination. This is inadequate but works for now.
+ // TODO: Implement a picker that is to spec.
+ final Intent intent = new Intent(
+ BaseActivity.DocumentsIntent.ACTION_OPEN_COPY_DESTINATION,
+ Uri.EMPTY,
+ getActivity(),
+ DocumentsActivity.class);
+ boolean directoryCopy = false;
+ for (DocumentInfo info : docs) {
+ if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
+ directoryCopy = true;
+ break;
+ }
+ }
+ intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy);
+ startActivityForResult(intent, REQUEST_COPY_DESTINATION);
+ }
+
private static State getDisplayState(Fragment fragment) {
- return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
+ return ((BaseActivity) fragment.getActivity()).getDisplayState();
}
private static abstract class Footer {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index 163615d..8e4ec8c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -17,11 +17,11 @@
package com.android.documentsui;
import static com.android.documentsui.DocumentsActivity.TAG;
-import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_DISPLAY_NAME;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN;
+import static com.android.documentsui.BaseActivity.State.MODE_UNKNOWN;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_DISPLAY_NAME;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_LAST_MODIFIED;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_SIZE;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_UNKNOWN;
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import android.content.AsyncTaskLoader;
@@ -36,7 +36,7 @@ import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;
-import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.RecentsProvider.StateColumns;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryView.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryView.java
index 4f52a03..4893652 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryView.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryView.java
@@ -17,9 +17,6 @@
package com.android.documentsui;
import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
import android.util.AttributeSet;
import android.widget.FrameLayout;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 8778f11..a2a789f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -16,20 +16,21 @@
package com.android.documentsui;
+import static com.android.documentsui.BaseActivity.State.ACTION_BROWSE;
+import static com.android.documentsui.BaseActivity.State.ACTION_CREATE;
+import static com.android.documentsui.BaseActivity.State.ACTION_GET_CONTENT;
+import static com.android.documentsui.BaseActivity.State.ACTION_MANAGE;
+import static com.android.documentsui.BaseActivity.State.ACTION_OPEN;
+import static com.android.documentsui.BaseActivity.State.ACTION_OPEN_TREE;
+import static com.android.documentsui.BaseActivity.State.ACTION_OPEN_COPY_DESTINATION;
+import static com.android.documentsui.BaseActivity.State.MODE_GRID;
+import static com.android.documentsui.BaseActivity.State.MODE_LIST;
import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
import static com.android.documentsui.DirectoryFragment.ANIM_SIDE;
import static com.android.documentsui.DirectoryFragment.ANIM_UP;
-import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
-import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
-import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
-import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN;
-import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN_TREE;
-import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
-import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
import android.app.Activity;
-import android.app.Fragment;
import android.app.FragmentManager;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
@@ -46,7 +47,6 @@ import android.graphics.Point;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Root;
@@ -54,7 +54,6 @@ import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.DrawerLayout.DrawerListener;
import android.util.Log;
-import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@@ -73,25 +72,23 @@ import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
+import libcore.io.IoUtils;
+
import com.android.documentsui.RecentsProvider.RecentColumns;
import com.android.documentsui.RecentsProvider.ResumeColumns;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
import com.android.documentsui.model.RootInfo;
-import com.google.common.collect.Maps;
-
-import libcore.io.IoUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executor;
-public class DocumentsActivity extends Activity {
+public class DocumentsActivity extends BaseActivity {
public static final String TAG = "Documents";
private static final String EXTRA_STATE = "state";
@@ -182,7 +179,7 @@ public class DocumentsActivity extends Activity {
setActionBar(mToolbar);
// Hide roots when we're managing a specific root
- if (mState.action == ACTION_MANAGE) {
+ if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
if (mShowAsDialog) {
findViewById(R.id.container_roots).setVisibility(View.GONE);
} else {
@@ -194,7 +191,8 @@ public class DocumentsActivity extends Activity {
final String mimeType = getIntent().getType();
final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
SaveFragment.show(getFragmentManager(), mimeType, title);
- } else if (mState.action == ACTION_OPEN_TREE) {
+ } else if (mState.action == ACTION_OPEN_TREE ||
+ mState.action == ACTION_OPEN_COPY_DESTINATION) {
PickFragment.show(getFragmentManager());
}
@@ -203,13 +201,15 @@ public class DocumentsActivity extends Activity {
moreApps.setComponent(null);
moreApps.setPackage(null);
RootsFragment.show(getFragmentManager(), moreApps);
- } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE
- || mState.action == ACTION_OPEN_TREE) {
+ } else if (mState.action == ACTION_OPEN ||
+ mState.action == ACTION_CREATE ||
+ mState.action == ACTION_OPEN_TREE ||
+ mState.action == ACTION_OPEN_COPY_DESTINATION) {
RootsFragment.show(getFragmentManager(), null);
}
if (!mState.restored) {
- if (mState.action == ACTION_MANAGE) {
+ if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
final Uri rootUri = getIntent().getData();
new RestoreRootTask(rootUri).executeOnExecutor(getCurrentExecutor());
} else {
@@ -235,6 +235,10 @@ public class DocumentsActivity extends Activity {
mState.action = ACTION_OPEN_TREE;
} else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) {
mState.action = ACTION_MANAGE;
+ } else if (DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT.equals(action)) {
+ mState.action = ACTION_BROWSE;
+ } else if (DocumentsIntent.ACTION_OPEN_COPY_DESTINATION.equals(action)) {
+ mState.action = ACTION_OPEN_COPY_DESTINATION;
}
if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
@@ -242,7 +246,7 @@ public class DocumentsActivity extends Activity {
Intent.EXTRA_ALLOW_MULTIPLE, false);
}
- if (mState.action == ACTION_MANAGE) {
+ if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
mState.acceptMimes = new String[] { "*/*" };
mState.allowMultiple = true;
} else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
@@ -256,11 +260,15 @@ public class DocumentsActivity extends Activity {
mState.showAdvanced = mState.forceAdvanced
| LocalPreferences.getDisplayAdvancedDevices(this);
- if (mState.action == ACTION_MANAGE) {
+ if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
mState.showSize = true;
} else {
mState.showSize = LocalPreferences.getDisplayFileSize(this);
}
+ if (mState.action == ACTION_OPEN_COPY_DESTINATION) {
+ mState.directoryCopy = intent.getBooleanExtra(
+ BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, false);
+ }
}
private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
@@ -389,6 +397,7 @@ public class DocumentsActivity extends Activity {
updateActionBar();
}
+ @Override
public void setRootsDrawerOpen(boolean open) {
if (!mShowAsDialog) {
if (open) {
@@ -409,16 +418,19 @@ public class DocumentsActivity extends Activity {
public void updateActionBar() {
if (mRootsToolbar != null) {
- if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT
- || mState.action == ACTION_OPEN_TREE) {
+ if (mState.action == ACTION_OPEN ||
+ mState.action == ACTION_GET_CONTENT ||
+ mState.action == ACTION_OPEN_TREE) {
mRootsToolbar.setTitle(R.string.title_open);
- } else if (mState.action == ACTION_CREATE) {
+ } else if (mState.action == ACTION_CREATE ||
+ mState.action == ACTION_OPEN_COPY_DESTINATION) {
mRootsToolbar.setTitle(R.string.title_save);
}
}
final RootInfo root = getCurrentRoot();
- final boolean showRootIcon = mShowAsDialog || (mState.action == ACTION_MANAGE);
+ final boolean showRootIcon = mShowAsDialog
+ || (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE);
if (showRootIcon) {
mToolbar.setNavigationIcon(
root != null ? root.loadToolbarIcon(mToolbar.getContext()) : null);
@@ -549,6 +561,7 @@ public class DocumentsActivity extends Activity {
final MenuItem list = menu.findItem(R.id.menu_list);
final MenuItem advanced = menu.findItem(R.id.menu_advanced);
final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
+ final MenuItem settings = menu.findItem(R.id.menu_settings);
sort.setVisible(cwd != null);
grid.setVisible(mState.derivedMode != MODE_GRID);
@@ -576,7 +589,8 @@ public class DocumentsActivity extends Activity {
sortSize.setVisible(mState.showSize);
boolean searchVisible;
- boolean fileSizeVisible = mState.action != ACTION_MANAGE;
+ boolean fileSizeVisible = !(mState.action == ACTION_MANAGE
+ || mState.action == ACTION_BROWSE);
if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
createDir.setVisible(cwd != null && cwd.isCreateSupported());
searchVisible = false;
@@ -606,9 +620,12 @@ public class DocumentsActivity extends Activity {
fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
? R.string.menu_file_size_hide : R.string.menu_file_size_show);
- advanced.setVisible(mState.action != ACTION_MANAGE);
+ advanced.setVisible(!(mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE));
fileSize.setVisible(fileSizeVisible);
+ settings.setVisible((mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE)
+ && (root.flags & Root.FLAG_HAS_SETTINGS) != 0);
+
return true;
}
@@ -648,6 +665,13 @@ public class DocumentsActivity extends Activity {
} else if (id == R.id.menu_file_size) {
setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
return true;
+ } else if (id == R.id.menu_settings) {
+ final RootInfo root = getCurrentRoot();
+ final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
+ intent.setDataAndType(DocumentsContract.buildRootUri(root.authority, root.rootId),
+ DocumentsContract.Root.MIME_TYPE_ITEM);
+ startActivity(intent);
+ return true;
} else {
return super.onOptionsItemSelected(item);
}
@@ -667,9 +691,7 @@ public class DocumentsActivity extends Activity {
invalidateOptionsMenu();
}
- /**
- * Update UI to reflect internal state changes not from user.
- */
+ @Override
public void onStateChanged() {
invalidateOptionsMenu();
}
@@ -690,6 +712,7 @@ public class DocumentsActivity extends Activity {
DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
}
+ @Override
public void setPending(boolean pending) {
final SaveFragment save = SaveFragment.get(getFragmentManager());
if (save != null) {
@@ -808,6 +831,7 @@ public class DocumentsActivity extends Activity {
}
};
+ @Override
public RootInfo getCurrentRoot() {
if (mState.stack.root != null) {
return mState.stack.root;
@@ -816,6 +840,7 @@ public class DocumentsActivity extends Activity {
}
}
+ @Override
public DocumentInfo getCurrentDirectory() {
return mState.stack.peek();
}
@@ -834,6 +859,7 @@ public class DocumentsActivity extends Activity {
}
}
+ @Override
public State getDisplayState() {
return mState;
}
@@ -847,7 +873,9 @@ public class DocumentsActivity extends Activity {
if (cwd == null) {
// No directory means recents
- if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
+ if (mState.action == ACTION_CREATE ||
+ mState.action == ACTION_OPEN_TREE ||
+ mState.action == ACTION_OPEN_COPY_DESTINATION) {
RecentsCreateFragment.show(fm);
} else {
DirectoryFragment.showRecentsOpen(fm, anim);
@@ -876,12 +904,13 @@ public class DocumentsActivity extends Activity {
}
}
- if (mState.action == ACTION_OPEN_TREE) {
+ if (mState.action == ACTION_OPEN_TREE ||
+ mState.action == ACTION_OPEN_COPY_DESTINATION) {
final PickFragment pick = PickFragment.get(fm);
if (pick != null) {
final CharSequence displayName = (mState.stack.size() <= 1) ? root.title
: cwd.displayName;
- pick.setPickTarget(cwd, displayName);
+ pick.setPickTarget(mState.action, cwd, displayName);
}
}
@@ -895,6 +924,7 @@ public class DocumentsActivity extends Activity {
dumpStack();
}
+ @Override
public void onStackPicked(DocumentStack stack) {
try {
// Update the restored stack to ensure we have freshest data
@@ -909,6 +939,7 @@ public class DocumentsActivity extends Activity {
}
}
+ @Override
public void onRootPicked(RootInfo root, boolean closeDrawer) {
// Clear entire backstack and start in new root
mState.stack.root = root;
@@ -955,6 +986,7 @@ public class DocumentsActivity extends Activity {
}
}
+ @Override
public void onAppPicked(ResolveInfo info) {
final Intent intent = new Intent(getIntent());
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
@@ -985,6 +1017,7 @@ public class DocumentsActivity extends Activity {
}
}
+ @Override
public void onDocumentPicked(DocumentInfo doc) {
final FragmentManager fm = getFragmentManager();
if (doc.isDirectory()) {
@@ -1017,9 +1050,21 @@ public class DocumentsActivity extends Activity {
Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
}
}
+ } else if (mState.action == ACTION_BROWSE) {
+ // Go straight to viewing
+ final Intent view = new Intent(Intent.ACTION_VIEW);
+ view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ view.setData(doc.derivedUri);
+
+ try {
+ startActivity(view);
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
+ }
}
}
+ @Override
public void onDocumentsPicked(List<DocumentInfo> docs) {
if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
final int size = docs.size();
@@ -1031,18 +1076,29 @@ public class DocumentsActivity extends Activity {
}
}
+ @Override
public void onSaveRequested(DocumentInfo replaceTarget) {
new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
}
+ @Override
public void onSaveRequested(String mimeType, String displayName) {
new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
}
+ @Override
public void onPickRequested(DocumentInfo pickTarget) {
- final Uri viaUri = DocumentsContract.buildTreeDocumentUri(pickTarget.authority,
- pickTarget.documentId);
- new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor());
+ Uri result;
+ if (mState.action == ACTION_OPEN_TREE) {
+ result = DocumentsContract.buildTreeDocumentUri(
+ pickTarget.authority, pickTarget.documentId);
+ } else if (mState.action == ACTION_OPEN_COPY_DESTINATION) {
+ result = pickTarget.derivedUri;
+ } else {
+ // Should not be reached.
+ throw new IllegalStateException("Invalid mState.action.");
+ }
+ new PickFinishTask(result).executeOnExecutor(getCurrentExecutor());
}
private void saveStackBlocking() {
@@ -1050,7 +1106,9 @@ public class DocumentsActivity extends Activity {
final ContentValues values = new ContentValues();
final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
- if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
+ if (mState.action == ACTION_CREATE ||
+ mState.action == ACTION_OPEN_TREE ||
+ mState.action == ACTION_OPEN_COPY_DESTINATION) {
// Remember stack for last create
values.clear();
values.put(RecentColumns.KEY, mState.stack.buildKey());
@@ -1083,11 +1141,14 @@ public class DocumentsActivity extends Activity {
if (mState.action == ACTION_GET_CONTENT) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- } else if (mState.action == ACTION_OPEN_TREE) {
+ } else if (mState.action == ACTION_OPEN_TREE ||
+ mState.action == ACTION_OPEN_COPY_DESTINATION) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ // TODO: Move passing the stack to the separate ACTION_COPY action once it's implemented.
+ intent.putExtra(CopyService.EXTRA_STACK, (Parcelable)mState.stack);
} else {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -1188,102 +1249,6 @@ public class DocumentsActivity extends Activity {
}
}
- public static class State implements android.os.Parcelable {
- public int action;
- public String[] acceptMimes;
-
- /** Explicit user choice */
- public int userMode = MODE_UNKNOWN;
- /** Derived after loader */
- public int derivedMode = MODE_LIST;
-
- /** Explicit user choice */
- public int userSortOrder = SORT_ORDER_UNKNOWN;
- /** Derived after loader */
- public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
-
- public boolean allowMultiple = false;
- public boolean showSize = false;
- public boolean localOnly = false;
- public boolean forceAdvanced = false;
- public boolean showAdvanced = false;
- public boolean stackTouched = false;
- public boolean restored = false;
-
- /** Current user navigation stack; empty implies recents. */
- public DocumentStack stack = new DocumentStack();
- /** Currently active search, overriding any stack. */
- public String currentSearch;
-
- /** Instance state for every shown directory */
- public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap();
-
- public static final int ACTION_OPEN = 1;
- public static final int ACTION_CREATE = 2;
- public static final int ACTION_GET_CONTENT = 3;
- public static final int ACTION_OPEN_TREE = 4;
- public static final int ACTION_MANAGE = 5;
-
- public static final int MODE_UNKNOWN = 0;
- public static final int MODE_LIST = 1;
- public static final int MODE_GRID = 2;
-
- public static final int SORT_ORDER_UNKNOWN = 0;
- public static final int SORT_ORDER_DISPLAY_NAME = 1;
- public static final int SORT_ORDER_LAST_MODIFIED = 2;
- public static final int SORT_ORDER_SIZE = 3;
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(action);
- out.writeInt(userMode);
- out.writeStringArray(acceptMimes);
- out.writeInt(userSortOrder);
- out.writeInt(allowMultiple ? 1 : 0);
- out.writeInt(showSize ? 1 : 0);
- out.writeInt(localOnly ? 1 : 0);
- out.writeInt(forceAdvanced ? 1 : 0);
- out.writeInt(showAdvanced ? 1 : 0);
- out.writeInt(stackTouched ? 1 : 0);
- out.writeInt(restored ? 1 : 0);
- DurableUtils.writeToParcel(out, stack);
- out.writeString(currentSearch);
- out.writeMap(dirState);
- }
-
- public static final Creator<State> CREATOR = new Creator<State>() {
- @Override
- public State createFromParcel(Parcel in) {
- final State state = new State();
- state.action = in.readInt();
- state.userMode = in.readInt();
- state.acceptMimes = in.readStringArray();
- state.userSortOrder = in.readInt();
- state.allowMultiple = in.readInt() != 0;
- state.showSize = in.readInt() != 0;
- state.localOnly = in.readInt() != 0;
- state.forceAdvanced = in.readInt() != 0;
- state.showAdvanced = in.readInt() != 0;
- state.stackTouched = in.readInt() != 0;
- state.restored = in.readInt() != 0;
- DurableUtils.readFromParcel(in, state.stack);
- state.currentSearch = in.readString();
- in.readMap(state.dirState, null);
- return state;
- }
-
- @Override
- public State[] newArray(int size) {
- return new State[size];
- }
- };
- }
-
private void dumpStack() {
Log.d(TAG, "Current stack: ");
Log.d(TAG, " * " + mState.stack.root);
@@ -1291,8 +1256,4 @@ public class DocumentsActivity extends Activity {
Log.d(TAG, " +-- " + doc);
}
}
-
- public static DocumentsActivity get(Fragment fragment) {
- return (DocumentsActivity) fragment.getActivity();
- }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java b/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java
new file mode 100644
index 0000000..1748c9c
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2015 The Android Open Source 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.
+ */
+
+package com.android.documentsui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+
+import com.android.documentsui.model.DocumentInfo;
+
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+
+/**
+ * Alert dialog for failed operations.
+ */
+public class FailureDialogFragment extends DialogFragment
+ implements DialogInterface.OnClickListener {
+ private static final String TAG = "FailureDialogFragment";
+
+ private int mFailure;
+ private ArrayList<Uri> mFailedSrcList;
+
+ public static void show(FragmentManager fm, int failure, ArrayList<Uri> failedSrcList) {
+ // TODO: Add support for other failures than copy.
+ if (failure != CopyService.FAILURE_COPY) {
+ return;
+ }
+
+ final Bundle args = new Bundle();
+ args.putInt(CopyService.EXTRA_FAILURE, failure);
+ args.putParcelableArrayList(CopyService.EXTRA_SRC_LIST, failedSrcList);
+
+ final FragmentTransaction ft = fm.beginTransaction();
+ final FailureDialogFragment fragment = new FailureDialogFragment();
+ fragment.setArguments(args);
+
+ ft.add(fragment, TAG);
+ ft.commitAllowingStateLoss();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // TODO: Pass mFailure and mFailedSrcList to the parent fragment.
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle inState) {
+ super.onCreate(inState);
+
+ mFailure = getArguments().getInt(CopyService.EXTRA_FAILURE);
+ mFailedSrcList = getArguments().getParcelableArrayList(CopyService.EXTRA_SRC_LIST);
+
+ final StringBuilder list = new StringBuilder("<p>");
+ for (Uri documentUri : mFailedSrcList) {
+ try {
+ final DocumentInfo documentInfo = DocumentInfo.fromUri(
+ getActivity().getContentResolver(), documentUri);
+ list.append(String.format("&#8226; %s<br>", documentInfo.displayName));
+ }
+ catch (FileNotFoundException ignore) {
+ // Source file most probably gone.
+ }
+ }
+ list.append("</p>");
+ final String message = String.format(getString(R.string.copy_failure_alert_content),
+ list.toString());
+
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(getString(R.string.copy_failure_alert_title))
+ .setMessage(Html.fromHtml(message))
+ // TODO: Implement retrying the copy operation.
+ .setPositiveButton(R.string.retry, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .create();
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/IconUtils.java b/packages/DocumentsUI/src/com/android/documentsui/IconUtils.java
index 416aeb0..b43fedf 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/IconUtils.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/IconUtils.java
@@ -19,7 +19,6 @@ package com.android.documentsui;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
-import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.provider.DocumentsContract.Document;
import android.util.TypedValue;
@@ -269,7 +268,7 @@ public class IconUtils {
public static Drawable applyTintColor(Context context, int drawableId, int tintColorId) {
final Drawable icon = context.getDrawable(drawableId);
icon.mutate();
- icon.setTintList(context.getResources().getColorStateList(tintColorId));
+ icon.setTintList(context.getColorStateList(tintColorId));
return icon;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
index 5112c92..7ea51b9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
@@ -16,6 +16,8 @@
package com.android.documentsui;
+import android.R.string;
+import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
@@ -40,6 +42,7 @@ public class PickFragment extends Fragment {
private View mContainer;
private Button mPick;
+ private Button mCancel;
public static void show(FragmentManager fm) {
final PickFragment fragment = new PickFragment();
@@ -61,7 +64,10 @@ public class PickFragment extends Fragment {
mPick = (Button) mContainer.findViewById(android.R.id.button1);
mPick.setOnClickListener(mPickListener);
- setPickTarget(null, null);
+ mCancel = (Button) mContainer.findViewById(android.R.id.button2);
+ mCancel.setOnClickListener(mCancelListener);
+
+ setPickTarget(0, null, null);
return mContainer;
}
@@ -69,23 +75,49 @@ public class PickFragment extends Fragment {
private View.OnClickListener mPickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
- final DocumentsActivity activity = DocumentsActivity.get(PickFragment.this);
+ final BaseActivity activity = BaseActivity.get(PickFragment.this);
activity.onPickRequested(mPickTarget);
}
};
- public void setPickTarget(DocumentInfo pickTarget, CharSequence displayName) {
- mPickTarget = pickTarget;
+ private View.OnClickListener mCancelListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final BaseActivity activity = BaseActivity.get(PickFragment.this);
+ activity.setResult(Activity.RESULT_CANCELED);
+ activity.finish();
+ }
+ };
+ /**
+ * @param action Which action defined in BaseActivity.State is the picker shown for.
+ */
+ public void setPickTarget(int action,
+ DocumentInfo pickTarget,
+ CharSequence displayName) {
if (mContainer != null) {
- if (mPickTarget != null) {
- mContainer.setVisibility(View.VISIBLE);
+ if (pickTarget != null) {
final Locale locale = getResources().getConfiguration().locale;
- final String raw = getString(R.string.menu_select).toUpperCase(locale);
- mPick.setText(TextUtils.expandTemplate(raw, displayName));
+ switch (action) {
+ case BaseActivity.State.ACTION_OPEN_TREE:
+ final String raw = getString(R.string.menu_select).toUpperCase(locale);
+ mPick.setText(TextUtils.expandTemplate(raw, displayName));
+ mCancel.setVisibility(View.GONE);
+ break;
+ case BaseActivity.State.ACTION_OPEN_COPY_DESTINATION:
+ mPick.setText(getString(R.string.button_copy).toUpperCase(locale));
+ mCancel.setVisibility(View.VISIBLE);
+ break;
+ default:
+ throw new IllegalArgumentException("Illegal action for PickFragment.");
+ }
+ }
+ if (pickTarget != null && pickTarget.isCreateSupported()) {
+ mContainer.setVisibility(View.VISIBLE);
} else {
mContainer.setVisibility(View.GONE);
}
}
+ mPickTarget = pickTarget;
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
index 34ce42d..f5908c5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
@@ -17,7 +17,7 @@
package com.android.documentsui;
import static com.android.documentsui.DocumentsActivity.TAG;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_LAST_MODIFIED;
import android.app.ActivityManager;
import android.content.AsyncTaskLoader;
@@ -34,7 +34,7 @@ import android.provider.DocumentsContract.Root;
import android.text.format.DateUtils;
import android.util.Log;
-import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.model.RootInfo;
import com.google.android.collect.Maps;
import com.google.common.collect.Lists;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
index dd75dbd..26aecc5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
@@ -45,7 +45,7 @@ import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
-import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.RecentsProvider.RecentColumns;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
@@ -95,7 +95,7 @@ public class RecentsCreateFragment extends Fragment {
mListView.setAdapter(mAdapter);
final RootsCache roots = DocumentsApplication.getRootsCache(context);
- final State state = ((DocumentsActivity) getActivity()).getDisplayState();
+ final State state = ((BaseActivity) getActivity()).getDisplayState();
mCallbacks = new LoaderCallbacks<List<DocumentStack>>() {
@Override
@@ -110,7 +110,7 @@ public class RecentsCreateFragment extends Fragment {
// When launched into empty recents, show drawer
if (mAdapter.isEmpty() && !state.stackTouched) {
- ((DocumentsActivity) context).setRootsDrawerOpen(true);
+ ((BaseActivity) context).setRootsDrawerOpen(true);
}
}
@@ -139,7 +139,7 @@ public class RecentsCreateFragment extends Fragment {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final DocumentStack stack = mAdapter.getItem(position);
- ((DocumentsActivity) getActivity()).onStackPicked(stack);
+ ((BaseActivity) getActivity()).onStackPicked(stack);
}
};
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index d72db1d..27e8f20 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -36,7 +36,7 @@ import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Root;
import android.util.Log;
-import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.model.RootInfo;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -360,14 +360,20 @@ public class RootsCache {
// Exclude read-only devices when creating
if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
+ if (state.action == State.ACTION_OPEN_COPY_DESTINATION && !supportsCreate) continue;
// Exclude roots that don't support directory picking
if (state.action == State.ACTION_OPEN_TREE && !supportsIsChild) continue;
// Exclude advanced devices when not requested
if (!state.showAdvanced && advanced) continue;
// Exclude non-local devices when local only
if (state.localOnly && !localOnly) continue;
+ // Exclude downloads roots that don't support directory creation
+ // TODO: Add flag to check the root supports directory creation or not.
+ if (state.directoryCopy && root.isDownloads()) continue;
// Only show empty roots when creating
- if (state.action != State.ACTION_CREATE && empty) continue;
+ if ((state.action != State.ACTION_CREATE ||
+ state.action != State.ACTION_OPEN_TREE ||
+ state.action != State.ACTION_OPEN_COPY_DESTINATION) && empty) continue;
// Only include roots that serve requested content
final boolean overlap =
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index 884cf31..ed5e123 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -16,8 +16,6 @@
package com.android.documentsui;
-import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
-
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
@@ -43,7 +41,7 @@ import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
-import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import com.google.common.collect.Lists;
@@ -101,7 +99,7 @@ public class RootsFragment extends Fragment {
final Context context = getActivity();
final RootsCache roots = DocumentsApplication.getRootsCache(context);
- final State state = ((DocumentsActivity) context).getDisplayState();
+ final State state = ((BaseActivity) context).getDisplayState();
mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
@Override
@@ -138,9 +136,9 @@ public class RootsFragment extends Fragment {
public void onDisplayStateChanged() {
final Context context = getActivity();
- final State state = ((DocumentsActivity) context).getDisplayState();
+ final State state = ((BaseActivity) context).getDisplayState();
- if (state.action == ACTION_GET_CONTENT) {
+ if (state.action == State.ACTION_GET_CONTENT) {
mList.setOnItemLongClickListener(mItemLongClickListener);
} else {
mList.setOnItemLongClickListener(null);
@@ -153,7 +151,7 @@ public class RootsFragment extends Fragment {
public void onCurrentRootChanged() {
if (mAdapter == null) return;
- final RootInfo root = ((DocumentsActivity) getActivity()).getCurrentRoot();
+ final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot();
for (int i = 0; i < mAdapter.getCount(); i++) {
final Object item = mAdapter.getItem(i);
if (item instanceof RootItem) {
@@ -176,7 +174,7 @@ public class RootsFragment extends Fragment {
private OnItemClickListener mItemListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
+ final BaseActivity activity = BaseActivity.get(RootsFragment.this);
final Item item = mAdapter.getItem(position);
if (item instanceof RootItem) {
activity.onRootPicked(((RootItem) item).root, true);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java
index 8d37cdf..49651b4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java
@@ -19,7 +19,7 @@ package com.android.documentsui;
import android.content.AsyncTaskLoader;
import android.content.Context;
-import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.model.RootInfo;
import java.util.Collection;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index ce98db2..a13fccc 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -113,7 +113,7 @@ public class SaveFragment extends Fragment {
private View.OnClickListener mSaveListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
- final DocumentsActivity activity = DocumentsActivity.get(SaveFragment.this);
+ final BaseActivity activity = BaseActivity.get(SaveFragment.this);
if (mReplaceTarget != null) {
activity.onSaveRequested(mReplaceTarget);
} else {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
index 6c8ca20..3ec3d1c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
@@ -16,9 +16,9 @@
package com.android.documentsui;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_DISPLAY_NAME;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
-import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_DISPLAY_NAME;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_LAST_MODIFIED;
+import static com.android.documentsui.BaseActivity.State.SORT_ORDER_SIZE;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
new file mode 100644
index 0000000..976f21d
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
@@ -0,0 +1,937 @@
+/*
+ * Copyright (C) 2015 The Android Open Source 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.
+ */
+
+package com.android.documentsui;
+
+
+import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
+import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
+import static com.android.documentsui.DirectoryFragment.ANIM_SIDE;
+import static com.android.documentsui.DirectoryFragment.ANIM_UP;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Debug;
+import android.provider.DocumentsContract;
+import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v4.widget.DrawerLayout.DrawerListener;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnActionExpandListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.SearchView;
+import android.widget.SearchView.OnQueryTextListener;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.Toolbar;
+
+import com.android.documentsui.FailureDialogFragment;
+import com.android.documentsui.RecentsProvider.ResumeColumns;
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.model.DurableUtils;
+import com.android.documentsui.model.RootInfo;
+
+import libcore.io.IoUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class StandaloneActivity extends BaseActivity {
+ public static final String TAG = "StandaloneFileManagement";
+
+ private static final String EXTRA_STATE = "state";
+
+ private static final int CODE_FORWARD = 42;
+
+ private SearchView mSearchView;
+
+ private Toolbar mToolbar;
+ private Spinner mToolbarStack;
+
+ private Toolbar mRootsToolbar;
+
+ private ActionBarDrawerToggle mDrawerToggle;
+
+ private DirectoryContainerView mDirectoryContainer;
+
+ private boolean mIgnoreNextNavigation;
+ private boolean mIgnoreNextClose;
+ private boolean mIgnoreNextCollapse;
+
+ private boolean mSearchExpanded;
+
+ private RootsCache mRoots;
+ private State mState;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ // Debug.waitForDebugger();
+ super.onCreate(icicle);
+
+ mRoots = DocumentsApplication.getRootsCache(this);
+
+ setResult(Activity.RESULT_CANCELED);
+ setContentView(R.layout.activity);
+
+ final Context context = this;
+ final Resources res = getResources();
+
+ // Strongly define our horizontal dimension; we leave vertical as
+ final WindowManager.LayoutParams a = getWindow().getAttributes();
+
+ final Point size = new Point();
+ getWindowManager().getDefaultDisplay().getSize(size);
+ // a.width = (int) res.getFraction(R.dimen.dialog_width, size.x, size.x);
+
+ getWindow().setAttributes(a);
+
+ mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory);
+
+ if (icicle != null) {
+ mState = icicle.getParcelable(EXTRA_STATE);
+ } else {
+ buildDefaultState();
+ }
+
+ mToolbar = (Toolbar) findViewById(R.id.toolbar);
+ mToolbar.setTitleTextAppearance(context,
+ android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
+
+ mToolbarStack = (Spinner) findViewById(R.id.stack);
+ mToolbarStack.setOnItemSelectedListener(mStackListener);
+
+ mRootsToolbar = (Toolbar) findViewById(R.id.roots_toolbar);
+ if (mRootsToolbar != null) {
+ mRootsToolbar.setTitleTextAppearance(context,
+ android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
+ }
+
+ setActionBar(mToolbar);
+
+ RootsFragment.show(getFragmentManager(), null);
+ if (!mState.restored) {
+ new RestoreStackTask().execute();
+ final Intent intent = getIntent();
+ final int failure = intent.getIntExtra(CopyService.EXTRA_FAILURE, 0);
+ if (failure != 0) {
+ final ArrayList<Uri> failedSrcList = intent.getParcelableArrayListExtra(
+ CopyService.EXTRA_SRC_LIST);
+ FailureDialogFragment.show(getFragmentManager(), failure, failedSrcList);
+ }
+ } else {
+ onCurrentDirectoryChanged(ANIM_NONE);
+ }
+ }
+
+ private void buildDefaultState() {
+ mState = new State();
+
+ final Intent intent = getIntent();
+ mState.action = State.ACTION_BROWSE_ALL;
+ mState.acceptMimes = new String[] { "*/*" };
+ mState.allowMultiple = true;
+ mState.acceptMimes = new String[] { intent.getType() };
+ mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
+ mState.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
+ mState.showAdvanced = mState.forceAdvanced
+ | LocalPreferences.getDisplayAdvancedDevices(this);
+ mState.showSize = true;
+ final DocumentStack stack = intent.getParcelableExtra(CopyService.EXTRA_STACK);
+ if (stack != null)
+ mState.stack = stack;
+ }
+
+ private class RestoreStackTask extends AsyncTask<Void, Void, Void> {
+ private volatile boolean mRestoredStack;
+ private volatile boolean mExternal;
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ // Restore last stack for calling package
+ final String packageName = getCallingPackageMaybeExtra();
+ final Cursor cursor = getContentResolver()
+ .query(RecentsProvider.buildResume(packageName), null, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
+ final byte[] rawStack = cursor.getBlob(
+ cursor.getColumnIndex(ResumeColumns.STACK));
+ DurableUtils.readFromArray(rawStack, mState.stack);
+ mRestoredStack = true;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to resume: " + e);
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ }
+
+ if (mRestoredStack) {
+ // Update the restored stack to ensure we have freshest data
+ final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
+ try {
+ mState.stack.updateRoot(matchingRoots);
+ mState.stack.updateDocuments(getContentResolver());
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Failed to restore stack: " + e);
+ mState.stack.reset();
+ mRestoredStack = false;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ if (isDestroyed()) return;
+ mState.restored = true;
+ onCurrentDirectoryChanged(ANIM_NONE);
+ }
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ if (mDrawerToggle != null) {
+ mDrawerToggle.syncState();
+ }
+ updateActionBar();
+ }
+
+ @Override
+ public void setRootsDrawerOpen(boolean open) {
+ Log.w(TAG, "Trying to change state of roots drawer to > " + (open ? "open" : "closed"));
+ // throw new UnsupportedOperationException();
+ }
+
+ public void updateActionBar() {
+ final RootInfo root = getCurrentRoot();
+ mToolbar.setNavigationIcon(
+ root != null ? root.loadToolbarIcon(mToolbar.getContext()) : null);
+ mToolbar.setNavigationContentDescription(R.string.drawer_open);
+ mToolbar.setNavigationOnClickListener(null);
+
+ if (mSearchExpanded) {
+ mToolbar.setTitle(null);
+ mToolbarStack.setVisibility(View.GONE);
+ mToolbarStack.setAdapter(null);
+ } else {
+ if (mState.stack.size() <= 1) {
+ mToolbar.setTitle(root.title);
+ mToolbarStack.setVisibility(View.GONE);
+ mToolbarStack.setAdapter(null);
+ } else {
+ mToolbar.setTitle(null);
+ mToolbarStack.setVisibility(View.VISIBLE);
+ mToolbarStack.setAdapter(mStackAdapter);
+
+ mIgnoreNextNavigation = true;
+ mToolbarStack.setSelection(mStackAdapter.getCount() - 1);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.activity, menu);
+
+ for (int i = 0; i < menu.size(); i++) {
+ final MenuItem item = menu.getItem(i);
+ switch (item.getItemId()) {
+ case R.id.menu_advanced:
+ case R.id.menu_file_size:
+ break;
+ default:
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+ }
+
+ final MenuItem searchMenu = menu.findItem(R.id.menu_search);
+ mSearchView = (SearchView) searchMenu.getActionView();
+ mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ mSearchExpanded = true;
+ mState.currentSearch = query;
+ mSearchView.clearFocus();
+ onCurrentDirectoryChanged(ANIM_NONE);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ return false;
+ }
+ });
+
+ searchMenu.setOnActionExpandListener(new OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ mSearchExpanded = true;
+ updateActionBar();
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ mSearchExpanded = false;
+ if (mIgnoreNextCollapse) {
+ mIgnoreNextCollapse = false;
+ return true;
+ }
+
+ mState.currentSearch = null;
+ onCurrentDirectoryChanged(ANIM_NONE);
+ return true;
+ }
+ });
+
+ mSearchView.setOnCloseListener(new SearchView.OnCloseListener() {
+ @Override
+ public boolean onClose() {
+ mSearchExpanded = false;
+ if (mIgnoreNextClose) {
+ mIgnoreNextClose = false;
+ return false;
+ }
+
+ mState.currentSearch = null;
+ onCurrentDirectoryChanged(ANIM_NONE);
+ return false;
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ final FragmentManager fm = getFragmentManager();
+
+ final RootInfo root = getCurrentRoot();
+ final DocumentInfo cwd = getCurrentDirectory();
+
+ final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
+ final MenuItem search = menu.findItem(R.id.menu_search);
+ final MenuItem sort = menu.findItem(R.id.menu_sort);
+ final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
+ final MenuItem grid = menu.findItem(R.id.menu_grid);
+ final MenuItem list = menu.findItem(R.id.menu_list);
+ final MenuItem advanced = menu.findItem(R.id.menu_advanced);
+ final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
+
+ sort.setVisible(cwd != null);
+ grid.setVisible(mState.derivedMode != State.MODE_GRID);
+ list.setVisible(mState.derivedMode != State.MODE_LIST);
+
+ if (mState.currentSearch != null) {
+ // Search uses backend ranking; no sorting
+ sort.setVisible(false);
+
+ search.expandActionView();
+
+ mSearchView.setIconified(false);
+ mSearchView.clearFocus();
+ mSearchView.setQuery(mState.currentSearch, false);
+ } else {
+ mIgnoreNextClose = true;
+ mSearchView.setIconified(true);
+ mSearchView.clearFocus();
+
+ mIgnoreNextCollapse = true;
+ search.collapseActionView();
+ }
+
+ // Only sort by size when visible
+ sortSize.setVisible(mState.showSize);
+
+ fileSize.setVisible(true);
+ search.setVisible(true);
+ createDir.setVisible(true);
+ advanced.setVisible(true);
+
+ advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this)
+ ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
+ fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
+ ? R.string.menu_file_size_hide : R.string.menu_file_size_show);
+
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ final int id = item.getItemId();
+ if (id == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else if (id == R.id.menu_create_dir) {
+ CreateDirectoryFragment.show(getFragmentManager());
+ return true;
+ } else if (id == R.id.menu_search) {
+ return false;
+ } else if (id == R.id.menu_sort_name) {
+ setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
+ return true;
+ } else if (id == R.id.menu_sort_date) {
+ setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
+ return true;
+ } else if (id == R.id.menu_sort_size) {
+ setUserSortOrder(State.SORT_ORDER_SIZE);
+ return true;
+ } else if (id == R.id.menu_grid) {
+ setUserMode(State.MODE_GRID);
+ return true;
+ } else if (id == R.id.menu_list) {
+ setUserMode(State.MODE_LIST);
+ return true;
+ } else if (id == R.id.menu_advanced) {
+ setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this));
+ return true;
+ } else if (id == R.id.menu_file_size) {
+ setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void setDisplayAdvancedDevices(boolean display) {
+ LocalPreferences.setDisplayAdvancedDevices(this, display);
+ mState.showAdvanced = mState.forceAdvanced | display;
+ RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
+ invalidateOptionsMenu();
+ }
+
+ private void setDisplayFileSize(boolean display) {
+ LocalPreferences.setDisplayFileSize(this, display);
+ mState.showSize = display;
+ DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged();
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onStateChanged() {
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * Set state sort order based on explicit user action.
+ */
+ private void setUserSortOrder(int sortOrder) {
+ mState.userSortOrder = sortOrder;
+ DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
+ }
+
+ /**
+ * Set state mode based on explicit user action.
+ */
+ private void setUserMode(int mode) {
+ mState.userMode = mode;
+ DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
+ }
+
+ @Override
+ public void setPending(boolean pending) {
+ final SaveFragment save = SaveFragment.get(getFragmentManager());
+ if (save != null) {
+ save.setPending(pending);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!mState.stackTouched) {
+ super.onBackPressed();
+ return;
+ }
+
+ final int size = mState.stack.size();
+ if (size > 1) {
+ mState.stack.pop();
+ onCurrentDirectoryChanged(ANIM_UP);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ super.onSaveInstanceState(state);
+ state.putParcelable(EXTRA_STATE, mState);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ super.onRestoreInstanceState(state);
+ }
+
+ private BaseAdapter mStackAdapter = new BaseAdapter() {
+ @Override
+ public int getCount() {
+ return mState.stack.size();
+ }
+
+ @Override
+ public DocumentInfo getItem(int position) {
+ return mState.stack.get(mState.stack.size() - position - 1);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_subdir_title, parent, false);
+ }
+
+ final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+ final DocumentInfo doc = getItem(position);
+
+ if (position == 0) {
+ final RootInfo root = getCurrentRoot();
+ title.setText(root.title);
+ } else {
+ title.setText(doc.displayName);
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_subdir, parent, false);
+ }
+
+ final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir);
+ final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+ final DocumentInfo doc = getItem(position);
+
+ if (position == 0) {
+ final RootInfo root = getCurrentRoot();
+ title.setText(root.title);
+ subdir.setVisibility(View.GONE);
+ } else {
+ title.setText(doc.displayName);
+ subdir.setVisibility(View.VISIBLE);
+ }
+
+ return convertView;
+ }
+ };
+
+ private OnItemSelectedListener mStackListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (mIgnoreNextNavigation) {
+ mIgnoreNextNavigation = false;
+ return;
+ }
+
+ while (mState.stack.size() > position + 1) {
+ mState.stackTouched = true;
+ mState.stack.pop();
+ }
+ onCurrentDirectoryChanged(ANIM_UP);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // Ignored
+ }
+ };
+
+ @Override
+ public RootInfo getCurrentRoot() {
+ if (mState.stack.root != null) {
+ return mState.stack.root;
+ } else {
+ return mRoots.getRecentsRoot();
+ }
+ }
+
+ public DocumentInfo getCurrentDirectory() {
+ return mState.stack.peek();
+ }
+
+ private String getCallingPackageMaybeExtra() {
+ final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
+ return (extra != null) ? extra : getCallingPackage();
+ }
+
+ public Executor getCurrentExecutor() {
+ final DocumentInfo cwd = getCurrentDirectory();
+ if (cwd != null && cwd.authority != null) {
+ return ProviderExecutor.forAuthority(cwd.authority);
+ } else {
+ return AsyncTask.THREAD_POOL_EXECUTOR;
+ }
+ }
+
+ @Override
+ public State getDisplayState() {
+ return mState;
+ }
+
+ private void onCurrentDirectoryChanged(int anim) {
+ final FragmentManager fm = getFragmentManager();
+ final RootInfo root = getCurrentRoot();
+ final DocumentInfo cwd = getCurrentDirectory();
+
+ mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN);
+
+ if (cwd == null) {
+ DirectoryFragment.showRecentsOpen(fm, anim);
+
+ // Start recents in grid when requesting visual things
+ final boolean visualMimes = MimePredicate.mimeMatches(
+ MimePredicate.VISUAL_MIMES, mState.acceptMimes);
+ mState.userMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
+ mState.derivedMode = mState.userMode;
+ } else {
+ if (mState.currentSearch != null) {
+ // Ongoing search
+ DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
+ } else {
+ // Normal boring directory
+ DirectoryFragment.showNormal(fm, root, cwd, anim);
+ }
+ }
+
+ final RootsFragment roots = RootsFragment.get(fm);
+ if (roots != null) {
+ roots.onCurrentRootChanged();
+ }
+
+ updateActionBar();
+ invalidateOptionsMenu();
+ dumpStack();
+ }
+
+ @Override
+ public void onStackPicked(DocumentStack stack) {
+ try {
+ // Update the restored stack to ensure we have freshest data
+ stack.updateDocuments(getContentResolver());
+
+ mState.stack = stack;
+ mState.stackTouched = true;
+ onCurrentDirectoryChanged(ANIM_SIDE);
+
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Failed to restore stack: " + e);
+ }
+ }
+
+ @Override
+ public void onRootPicked(RootInfo root, boolean closeDrawer) {
+ // Clear entire backstack and start in new root
+ mState.stack.root = root;
+ mState.stack.clear();
+ mState.stackTouched = true;
+
+ if (!mRoots.isRecentsRoot(root)) {
+ new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
+ } else {
+ onCurrentDirectoryChanged(ANIM_SIDE);
+ }
+ }
+
+ private class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
+ private RootInfo mRoot;
+
+ public PickRootTask(RootInfo root) {
+ mRoot = root;
+ }
+
+ @Override
+ protected DocumentInfo doInBackground(Void... params) {
+ try {
+ final Uri uri = DocumentsContract.buildDocumentUri(
+ mRoot.authority, mRoot.documentId);
+ return DocumentInfo.fromUri(getContentResolver(), uri);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Failed to find root", e);
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(DocumentInfo result) {
+ if (result != null) {
+ mState.stack.push(result);
+ mState.stackTouched = true;
+ onCurrentDirectoryChanged(ANIM_SIDE);
+ }
+ }
+ }
+
+ @Override
+ public void onAppPicked(ResolveInfo info) {
+ final Intent intent = new Intent(getIntent());
+ intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ intent.setComponent(new ComponentName(
+ info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
+ startActivityForResult(intent, CODE_FORWARD);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.d(TAG, "onActivityResult() code=" + resultCode);
+
+ // Only relay back results when not canceled; otherwise stick around to
+ // let the user pick another app/backend.
+ if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
+
+ // Remember that we last picked via external app
+ final String packageName = getCallingPackageMaybeExtra();
+ final ContentValues values = new ContentValues();
+ values.put(ResumeColumns.EXTERNAL, 1);
+ getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
+
+ // Pass back result to original caller
+ setResult(resultCode, data);
+ finish();
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ public void onDocumentPicked(DocumentInfo doc) {
+ final FragmentManager fm = getFragmentManager();
+ if (doc.isDirectory()) {
+ mState.stack.push(doc);
+ mState.stackTouched = true;
+ onCurrentDirectoryChanged(ANIM_DOWN);
+ } else {
+ // Fall back to viewing
+ final Intent view = new Intent(Intent.ACTION_VIEW);
+ view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ view.setData(doc.derivedUri);
+
+ try {
+ startActivity(view);
+ } catch (ActivityNotFoundException ex2) {
+ Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ public void onDocumentsPicked(List<DocumentInfo> docs) {
+ // TODO
+ }
+
+ @Override
+ public void onSaveRequested(DocumentInfo replaceTarget) {
+ new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
+ }
+
+ @Override
+ public void onSaveRequested(String mimeType, String displayName) {
+ new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
+ }
+
+ @Override
+ public void onPickRequested(DocumentInfo pickTarget) {
+ final Uri viaUri = DocumentsContract.buildTreeDocumentUri(pickTarget.authority,
+ pickTarget.documentId);
+ new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor());
+ }
+
+ private void saveStackBlocking() {
+ final ContentResolver resolver = getContentResolver();
+ final ContentValues values = new ContentValues();
+
+ final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
+
+ // Remember location for next app launch
+ final String packageName = getCallingPackageMaybeExtra();
+ values.clear();
+ values.put(ResumeColumns.STACK, rawStack);
+ values.put(ResumeColumns.EXTERNAL, 0);
+ resolver.insert(RecentsProvider.buildResume(packageName), values);
+ }
+
+ private void onFinished(Uri... uris) {
+ Log.d(TAG, "onFinished() " + Arrays.toString(uris));
+
+ final Intent intent = new Intent();
+ if (uris.length == 1) {
+ intent.setData(uris[0]);
+ } else if (uris.length > 1) {
+ final ClipData clipData = new ClipData(
+ null, mState.acceptMimes, new ClipData.Item(uris[0]));
+ for (int i = 1; i < uris.length; i++) {
+ clipData.addItem(new ClipData.Item(uris[i]));
+ }
+ intent.setClipData(clipData);
+ }
+
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
+
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+ }
+
+ private class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
+ private final String mMimeType;
+ private final String mDisplayName;
+
+ public CreateFinishTask(String mimeType, String displayName) {
+ mMimeType = mimeType;
+ mDisplayName = displayName;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ setPending(true);
+ }
+
+ @Override
+ protected Uri doInBackground(Void... params) {
+ final ContentResolver resolver = getContentResolver();
+ final DocumentInfo cwd = getCurrentDirectory();
+
+ ContentProviderClient client = null;
+ Uri childUri = null;
+ try {
+ client = DocumentsApplication.acquireUnstableProviderOrThrow(
+ resolver, cwd.derivedUri.getAuthority());
+ childUri = DocumentsContract.createDocument(
+ client, cwd.derivedUri, mMimeType, mDisplayName);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to create document", e);
+ } finally {
+ ContentProviderClient.releaseQuietly(client);
+ }
+
+ if (childUri != null) {
+ saveStackBlocking();
+ }
+
+ return childUri;
+ }
+
+ @Override
+ protected void onPostExecute(Uri result) {
+ if (result != null) {
+ onFinished(result);
+ } else {
+ Toast.makeText(StandaloneActivity.this, R.string.save_error, Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ setPending(false);
+ }
+ }
+
+ private class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
+ private final Uri[] mUris;
+
+ public ExistingFinishTask(Uri... uris) {
+ mUris = uris;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ saveStackBlocking();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ onFinished(mUris);
+ }
+ }
+
+ private class PickFinishTask extends AsyncTask<Void, Void, Void> {
+ private final Uri mUri;
+
+ public PickFinishTask(Uri uri) {
+ mUri = uri;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ saveStackBlocking();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ onFinished(mUri);
+ }
+ }
+
+ private void dumpStack() {
+ Log.d(TAG, "Current stack: ");
+ Log.d(TAG, " * " + mState.stack.root);
+ for (DocumentInfo doc : mState.stack) {
+ Log.d(TAG, " +-- " + doc);
+ }
+ }
+
+ public static BaseActivity get(Fragment fragment) {
+ return (BaseActivity) fragment.getActivity();
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
index 1c5ca86..5d5f2eb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
@@ -24,6 +24,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.text.TextUtils;
@@ -161,8 +162,6 @@ public class DocumentInfo implements Durable, Parcelable {
this.authority = authority;
this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
- this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
- this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
this.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
this.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
this.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
index 28bab6c..34bd696 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
@@ -17,6 +17,8 @@
package com.android.documentsui.model;
import android.content.ContentResolver;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.provider.DocumentsProvider;
import java.io.DataInputStream;
@@ -31,7 +33,7 @@ import java.util.LinkedList;
* Representation of a stack of {@link DocumentInfo}, usually the result of a
* user-driven traversal.
*/
-public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
+public class DocumentStack extends LinkedList<DocumentInfo> implements Durable, Parcelable {
private static final int VERSION_INIT = 1;
private static final int VERSION_ADD_ROOT = 2;
@@ -135,4 +137,28 @@ public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
doc.write(out);
}
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ DurableUtils.writeToParcel(dest, this);
+ }
+
+ public static final Creator<DocumentStack> CREATOR = new Creator<DocumentStack>() {
+ @Override
+ public DocumentStack createFromParcel(Parcel in) {
+ final DocumentStack stack = new DocumentStack();
+ DurableUtils.readFromParcel(in, stack);
+ return stack;
+ }
+
+ @Override
+ public DocumentStack[] newArray(int size) {
+ return new DocumentStack[size];
+ }
+ };
}