diff options
Diffstat (limited to 'packages/DocumentsUI/src/com/android')
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("• %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]; + } + }; } |
