Loading res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -336,4 +336,7 @@ <!-- Text shown on button to open an application --> <string name="open_app">Open <xliff:g id="name" example="Cloud Storage">%1$s</xliff:g></string> <!-- Dialog text shown when confirming if they want to overwrite a file --> <string name="overwrite_file_confirmation_message">Overwrite <xliff:g id="name" example="foobar.txt">%1$s</xliff:g>?</string> </resources> src/com/android/documentsui/BaseActivity.java +0 −9 Original line number Diff line number Diff line Loading @@ -545,15 +545,6 @@ public abstract class BaseActivity return mState.stack.peek(); } public Executor getExecutorForCurrentDirectory() { final DocumentInfo cwd = getCurrentDirectory(); if (cwd != null && cwd.authority != null) { return ProviderExecutor.forAuthority(cwd.authority); } else { return AsyncTask.THREAD_POOL_EXECUTOR; } } @Override public void onBackPressed() { // While action bar is expanded, the state stack UI is hidden. Loading src/com/android/documentsui/picker/ActionHandler.java +124 −6 Original line number Diff line number Diff line Loading @@ -17,11 +17,19 @@ package com.android.documentsui.picker; import static com.android.documentsui.base.Shared.DEBUG; import static com.android.documentsui.base.State.ACTION_CREATE; import static com.android.documentsui.base.State.ACTION_GET_CONTENT; import static com.android.documentsui.base.State.ACTION_OPEN_TREE; import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; import android.app.Activity; import android.app.FragmentManager; import android.content.ClipData; import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Parcelable; import android.provider.DocumentsContract; import android.provider.Settings; import android.util.Log; Loading @@ -31,6 +39,7 @@ import com.android.documentsui.ActivityConfig; import com.android.documentsui.DocumentsAccess; import com.android.documentsui.Injector; import com.android.documentsui.Metrics; import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.Features; Loading @@ -44,8 +53,10 @@ import com.android.documentsui.Model; import com.android.documentsui.picker.ActionHandler.Addons; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.RootsAccess; import com.android.documentsui.services.FileOperationService; import com.android.internal.annotations.VisibleForTesting; import java.util.Arrays; import java.util.concurrent.Executor; import javax.annotation.Nullable; Loading @@ -59,7 +70,8 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T private final Features mFeatures; private final ActivityConfig mConfig; private @Nullable Model mModel; private final Model mModel; private final LastAccessedStorage mLastAccessed; ActionHandler( T activity, Loading @@ -68,13 +80,15 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector injector) { Injector injector, LastAccessedStorage lastAccessed) { super(activity, state, roots, docs, searchMgr, executors, injector); mConfig = injector.config; mFeatures = injector.features; mModel = injector.getModel(); mLastAccessed = lastAccessed; } @Override Loading Loading @@ -136,7 +150,8 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T private void loadLastAccessedStack() { if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); new LoadLastAccessedStackTask<>(mActivity, mState, mRoots, this::onLastAccessedStackLoaded) new LoadLastAccessedStackTask<>( mActivity, mLastAccessed, mState, mRoots, this::onLastAccessedStackLoaded) .execute(); } Loading @@ -152,13 +167,13 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T private void loadDefaultLocation() { switch (mState.action) { case State.ACTION_PICK_COPY_DESTINATION: case ACTION_PICK_COPY_DESTINATION: case State.ACTION_CREATE: loadHomeDir(); break; case State.ACTION_GET_CONTENT: case ACTION_GET_CONTENT: case State.ACTION_OPEN: case State.ACTION_OPEN_TREE: case ACTION_OPEN_TREE: mState.stack.changeRoot(mRoots.getRecentsRoot()); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); break; Loading Loading @@ -221,6 +236,109 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T return false; } void pickDocument(DocumentInfo pickTarget) { assert(pickTarget != null); Uri result; switch (mState.action) { case ACTION_OPEN_TREE: result = DocumentsContract.buildTreeDocumentUri( pickTarget.authority, pickTarget.documentId); break; case ACTION_PICK_COPY_DESTINATION: result = pickTarget.derivedUri; break; default: // Should not be reached throw new IllegalStateException("Invalid mState.action"); } finishPicking(result); } void saveDocument( String mimeType, String displayName, BooleanConsumer inProgressStateListener) { assert(mState.action == ACTION_CREATE); new CreatePickedDocumentTask( mActivity, mLastAccessed, mState.stack, mimeType, displayName, inProgressStateListener, this::onPickFinished) .executeOnExecutor(getExecutorForCurrentDirectory()); } // User requested to overwrite a target. If confirmed by user #finishPicking() will be // called. void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) { assert(mState.action == ACTION_CREATE); assert(replaceTarget != null); mInjector.dialogs.confirmOverwrite(fm, replaceTarget); } void finishPicking(Uri... docs) { new SetLastAccessedStackTask( mActivity, mLastAccessed, mState.stack, () -> { onPickFinished(docs); } ) .executeOnExecutor(getExecutorForCurrentDirectory()); } private void onPickFinished(Uri... uris) { if (DEBUG) 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); } // TODO: Separate this piece of logic per action. // We don't instantiate different objects for different actions at the first place, so it's // not a easy task to separate this logic cleanly. // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its // inheritance structure. if (mState.action == ACTION_GET_CONTENT) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else if (mState.action == ACTION_OPEN_TREE) { 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); } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { // Picking a copy destination is only used internally by us, so we // don't need to extend permissions to the caller. intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType); } else { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); } mActivity.setResult(Activity.RESULT_OK, intent); mActivity.finish(); } private Executor getExecutorForCurrentDirectory() { final DocumentInfo cwd = mActivity.getCurrentDirectory(); if (cwd != null && cwd.authority != null) { return mExecutors.lookup(cwd.authority); } else { return AsyncTask.THREAD_POOL_EXECUTOR; } } public interface Addons extends CommonAddons { void onAppPicked(ResolveInfo info); void onDocumentPicked(DocumentInfo doc); Loading src/com/android/documentsui/picker/CreatePickedDocumentTask.java 0 → 100644 +108 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 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.picker; import android.app.Activity; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.net.Uri; import android.provider.DocumentsContract; import android.support.design.widget.Snackbar; import android.util.Log; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.R; import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.PairedTask; import com.android.documentsui.ui.Snackbars; import java.util.function.Consumer; /** * Task that creates a new document in the background. */ class CreatePickedDocumentTask extends PairedTask<Activity, Void, Uri> { private static final String TAG = "CreatePickedDocumentTas"; private final LastAccessedStorage mLastAccessed; private final DocumentStack mStack; private final String mMimeType; private final String mDisplayName; private final BooleanConsumer mInProgressStateListener; private final Consumer<Uri> mCallback; CreatePickedDocumentTask( Activity activity, LastAccessedStorage lastAccessed, DocumentStack stack, String mimeType, String displayName, BooleanConsumer inProgressStateListener, Consumer<Uri> callback) { super(activity); mLastAccessed = lastAccessed; mStack = stack; mMimeType = mimeType; mDisplayName = displayName; mInProgressStateListener = inProgressStateListener; mCallback = callback; } @Override protected void prepare() { mInProgressStateListener.accept(true); } @Override protected Uri run(Void... params) { DocumentInfo cwd = mStack.peek(); final ContentResolver resolver = mOwner.getContentResolver(); 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) { mLastAccessed.setLastAccessed(mOwner, mStack); } return childUri; } @Override protected void finish(Uri result) { if (result != null) { mCallback.accept(result); } else { Snackbars.makeSnackbar( mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show(); } mInProgressStateListener.accept(false); } } src/com/android/documentsui/picker/LastAccessedProvider.java +7 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.documentsui.picker; import static com.android.documentsui.base.DocumentInfo.getCursorString; import android.app.Activity; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; Loading Loading @@ -114,7 +115,12 @@ public class LastAccessedProvider extends ContentProvider { } } public static void setLastAccessed( /** * Rather than concretely depending on LastAccessedProvider, consider using * {@link LastAccessedStorage#setLastAccessed(Activity, DocumentStack)}. */ @Deprecated static void setLastAccessed( ContentResolver resolver, String packageName, DocumentStack stack) { final ContentValues values = new ContentValues(); final byte[] rawStack = DurableUtils.writeToArrayOrNull(stack); Loading Loading
res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -336,4 +336,7 @@ <!-- Text shown on button to open an application --> <string name="open_app">Open <xliff:g id="name" example="Cloud Storage">%1$s</xliff:g></string> <!-- Dialog text shown when confirming if they want to overwrite a file --> <string name="overwrite_file_confirmation_message">Overwrite <xliff:g id="name" example="foobar.txt">%1$s</xliff:g>?</string> </resources>
src/com/android/documentsui/BaseActivity.java +0 −9 Original line number Diff line number Diff line Loading @@ -545,15 +545,6 @@ public abstract class BaseActivity return mState.stack.peek(); } public Executor getExecutorForCurrentDirectory() { final DocumentInfo cwd = getCurrentDirectory(); if (cwd != null && cwd.authority != null) { return ProviderExecutor.forAuthority(cwd.authority); } else { return AsyncTask.THREAD_POOL_EXECUTOR; } } @Override public void onBackPressed() { // While action bar is expanded, the state stack UI is hidden. Loading
src/com/android/documentsui/picker/ActionHandler.java +124 −6 Original line number Diff line number Diff line Loading @@ -17,11 +17,19 @@ package com.android.documentsui.picker; import static com.android.documentsui.base.Shared.DEBUG; import static com.android.documentsui.base.State.ACTION_CREATE; import static com.android.documentsui.base.State.ACTION_GET_CONTENT; import static com.android.documentsui.base.State.ACTION_OPEN_TREE; import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; import android.app.Activity; import android.app.FragmentManager; import android.content.ClipData; import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Parcelable; import android.provider.DocumentsContract; import android.provider.Settings; import android.util.Log; Loading @@ -31,6 +39,7 @@ import com.android.documentsui.ActivityConfig; import com.android.documentsui.DocumentsAccess; import com.android.documentsui.Injector; import com.android.documentsui.Metrics; import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.Features; Loading @@ -44,8 +53,10 @@ import com.android.documentsui.Model; import com.android.documentsui.picker.ActionHandler.Addons; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.RootsAccess; import com.android.documentsui.services.FileOperationService; import com.android.internal.annotations.VisibleForTesting; import java.util.Arrays; import java.util.concurrent.Executor; import javax.annotation.Nullable; Loading @@ -59,7 +70,8 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T private final Features mFeatures; private final ActivityConfig mConfig; private @Nullable Model mModel; private final Model mModel; private final LastAccessedStorage mLastAccessed; ActionHandler( T activity, Loading @@ -68,13 +80,15 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector injector) { Injector injector, LastAccessedStorage lastAccessed) { super(activity, state, roots, docs, searchMgr, executors, injector); mConfig = injector.config; mFeatures = injector.features; mModel = injector.getModel(); mLastAccessed = lastAccessed; } @Override Loading Loading @@ -136,7 +150,8 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T private void loadLastAccessedStack() { if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); new LoadLastAccessedStackTask<>(mActivity, mState, mRoots, this::onLastAccessedStackLoaded) new LoadLastAccessedStackTask<>( mActivity, mLastAccessed, mState, mRoots, this::onLastAccessedStackLoaded) .execute(); } Loading @@ -152,13 +167,13 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T private void loadDefaultLocation() { switch (mState.action) { case State.ACTION_PICK_COPY_DESTINATION: case ACTION_PICK_COPY_DESTINATION: case State.ACTION_CREATE: loadHomeDir(); break; case State.ACTION_GET_CONTENT: case ACTION_GET_CONTENT: case State.ACTION_OPEN: case State.ACTION_OPEN_TREE: case ACTION_OPEN_TREE: mState.stack.changeRoot(mRoots.getRecentsRoot()); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); break; Loading Loading @@ -221,6 +236,109 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T return false; } void pickDocument(DocumentInfo pickTarget) { assert(pickTarget != null); Uri result; switch (mState.action) { case ACTION_OPEN_TREE: result = DocumentsContract.buildTreeDocumentUri( pickTarget.authority, pickTarget.documentId); break; case ACTION_PICK_COPY_DESTINATION: result = pickTarget.derivedUri; break; default: // Should not be reached throw new IllegalStateException("Invalid mState.action"); } finishPicking(result); } void saveDocument( String mimeType, String displayName, BooleanConsumer inProgressStateListener) { assert(mState.action == ACTION_CREATE); new CreatePickedDocumentTask( mActivity, mLastAccessed, mState.stack, mimeType, displayName, inProgressStateListener, this::onPickFinished) .executeOnExecutor(getExecutorForCurrentDirectory()); } // User requested to overwrite a target. If confirmed by user #finishPicking() will be // called. void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) { assert(mState.action == ACTION_CREATE); assert(replaceTarget != null); mInjector.dialogs.confirmOverwrite(fm, replaceTarget); } void finishPicking(Uri... docs) { new SetLastAccessedStackTask( mActivity, mLastAccessed, mState.stack, () -> { onPickFinished(docs); } ) .executeOnExecutor(getExecutorForCurrentDirectory()); } private void onPickFinished(Uri... uris) { if (DEBUG) 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); } // TODO: Separate this piece of logic per action. // We don't instantiate different objects for different actions at the first place, so it's // not a easy task to separate this logic cleanly. // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its // inheritance structure. if (mState.action == ACTION_GET_CONTENT) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else if (mState.action == ACTION_OPEN_TREE) { 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); } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { // Picking a copy destination is only used internally by us, so we // don't need to extend permissions to the caller. intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType); } else { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); } mActivity.setResult(Activity.RESULT_OK, intent); mActivity.finish(); } private Executor getExecutorForCurrentDirectory() { final DocumentInfo cwd = mActivity.getCurrentDirectory(); if (cwd != null && cwd.authority != null) { return mExecutors.lookup(cwd.authority); } else { return AsyncTask.THREAD_POOL_EXECUTOR; } } public interface Addons extends CommonAddons { void onAppPicked(ResolveInfo info); void onDocumentPicked(DocumentInfo doc); Loading
src/com/android/documentsui/picker/CreatePickedDocumentTask.java 0 → 100644 +108 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 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.picker; import android.app.Activity; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.net.Uri; import android.provider.DocumentsContract; import android.support.design.widget.Snackbar; import android.util.Log; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.R; import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.PairedTask; import com.android.documentsui.ui.Snackbars; import java.util.function.Consumer; /** * Task that creates a new document in the background. */ class CreatePickedDocumentTask extends PairedTask<Activity, Void, Uri> { private static final String TAG = "CreatePickedDocumentTas"; private final LastAccessedStorage mLastAccessed; private final DocumentStack mStack; private final String mMimeType; private final String mDisplayName; private final BooleanConsumer mInProgressStateListener; private final Consumer<Uri> mCallback; CreatePickedDocumentTask( Activity activity, LastAccessedStorage lastAccessed, DocumentStack stack, String mimeType, String displayName, BooleanConsumer inProgressStateListener, Consumer<Uri> callback) { super(activity); mLastAccessed = lastAccessed; mStack = stack; mMimeType = mimeType; mDisplayName = displayName; mInProgressStateListener = inProgressStateListener; mCallback = callback; } @Override protected void prepare() { mInProgressStateListener.accept(true); } @Override protected Uri run(Void... params) { DocumentInfo cwd = mStack.peek(); final ContentResolver resolver = mOwner.getContentResolver(); 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) { mLastAccessed.setLastAccessed(mOwner, mStack); } return childUri; } @Override protected void finish(Uri result) { if (result != null) { mCallback.accept(result); } else { Snackbars.makeSnackbar( mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show(); } mInProgressStateListener.accept(false); } }
src/com/android/documentsui/picker/LastAccessedProvider.java +7 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.documentsui.picker; import static com.android.documentsui.base.DocumentInfo.getCursorString; import android.app.Activity; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; Loading Loading @@ -114,7 +115,12 @@ public class LastAccessedProvider extends ContentProvider { } } public static void setLastAccessed( /** * Rather than concretely depending on LastAccessedProvider, consider using * {@link LastAccessedStorage#setLastAccessed(Activity, DocumentStack)}. */ @Deprecated static void setLastAccessed( ContentResolver resolver, String packageName, DocumentStack stack) { final ContentValues values = new ContentValues(); final byte[] rawStack = DurableUtils.writeToArrayOrNull(stack); Loading