Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 20d96d8a authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Define storage roots, external GUIDs, creation.

Allow storage backends to publish multiple roots into the UI, which
are defined by a directory GUID, type, and label details.  Update
external provider to surface a primary external storage root, and
switch to burning file path into the returned GUIDs so they remain
durable.

Added insert, update, and delete support to external provider. Adds
file extensions to display names when needed to match MIME type.

Add flags for searching and deletion, and extras for Cursor
pagination. Add directory creation dialog to UI. Opening a document
always gives write access.

Change-Id: I9bea1aa0dcde909a5ab86aefeece7451ab920cf1
parent 5259ffba
Loading
Loading
Loading
Loading
+21 −2
Original line number Diff line number Diff line
@@ -20245,15 +20245,25 @@ package android.provider {
    ctor public DocumentsContract();
    method public static android.net.Uri buildContentsUri(android.net.Uri);
    method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
    method public static android.net.Uri buildSearchUri(java.lang.String, java.lang.String);
    method public static android.net.Uri buildRootsUri(java.lang.String);
    method public static android.net.Uri buildSearchUri(android.net.Uri, java.lang.String);
    method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point);
    method public static boolean renameDocument(android.content.ContentResolver, android.net.Uri, java.lang.String);
    field public static final java.lang.String EXTRA_HAS_MORE = "has_more";
    field public static final java.lang.String EXTRA_REQUEST_MORE = "request_more";
    field public static final java.lang.String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
    field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
    field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4
    field public static final int FLAG_SUPPORTS_RENAME = 2; // 0x2
    field public static final int FLAG_SUPPORTS_THUMBNAIL = 4; // 0x4
    field public static final int FLAG_SUPPORTS_SEARCH = 16; // 0x10
    field public static final int FLAG_SUPPORTS_THUMBNAIL = 8; // 0x8
    field public static final java.lang.String MIME_TYPE_DIRECTORY = "vnd.android.cursor.dir/doc";
    field public static final java.lang.String PARAM_QUERY = "query";
    field public static final java.lang.String ROOT_GUID = "0";
    field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
    field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
    field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
    field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
  }
  public static abstract interface DocumentsContract.DocumentColumns implements android.provider.OpenableColumns {
@@ -20263,6 +20273,15 @@ package android.provider {
    field public static final java.lang.String MIME_TYPE = "mime_type";
  }
  public static abstract interface DocumentsContract.RootColumns {
    field public static final java.lang.String AVAILABLE_BYTES = "available_bytes";
    field public static final java.lang.String GUID = "guid";
    field public static final java.lang.String ICON = "icon";
    field public static final java.lang.String ROOT_TYPE = "root_type";
    field public static final java.lang.String SUMMARY = "summary";
    field public static final java.lang.String TITLE = "title";
  }
  public final deprecated class LiveFolders implements android.provider.BaseColumns {
    field public static final java.lang.String ACTION_CREATE_LIVE_FOLDER = "android.intent.action.CREATE_LIVE_FOLDER";
    field public static final java.lang.String DESCRIPTION = "description";
+127 −11
Original line number Diff line number Diff line
@@ -16,10 +16,13 @@

package android.provider;

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
@@ -39,9 +42,10 @@ import java.io.InputStream;
public final class DocumentsContract {
    private static final String TAG = "Documents";

    // content://com.example/roots/
    // content://com.example/docs/0/
    // content://com.example/docs/0/contents/
    // content://com.example/search/?query=pony
    // content://com.example/docs/0/search/?query=pony

    /**
     * MIME type of a document which is a directory that may contain additional
@@ -77,26 +81,70 @@ public final class DocumentsContract {
     */
    public static final int FLAG_SUPPORTS_RENAME = 1 << 1;

    /**
     * Flag indicating that a document is deletable.
     *
     * @see DocumentColumns#FLAGS
     */
    public static final int FLAG_SUPPORTS_DELETE = 1 << 2;

    /**
     * Flag indicating that a document can be represented as a thumbnail.
     *
     * @see DocumentColumns#FLAGS
     * @see #getThumbnail(ContentResolver, Uri, Point)
     */
    public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 2;
    public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3;

    /**
     * Flag indicating that a document is a directory that supports search.
     *
     * @see DocumentColumns#FLAGS
     */
    public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;

    /**
     * Optimal dimensions for a document thumbnail request, stored as a
     * {@link Point} object. This is only a hint, and the returned thumbnail may
     * have different dimensions.
     *
     * @see ContentProvider#openTypedAssetFile(Uri, String, Bundle)
     */
    public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";

    /**
     * Extra boolean flag included in a directory {@link Cursor#getExtras()}
     * indicating that the backend can provide additional data if requested,
     * such as additional search results.
     */
    public static final String EXTRA_HAS_MORE = "has_more";

    /**
     * Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a
     * directory to request that additional data should be fetched. When
     * requested data is ready, the provider should send a change notification
     * to cause a requery.
     *
     * @see Cursor#respond(Bundle)
     * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
     *      boolean)
     */
    public static final String EXTRA_REQUEST_MORE = "request_more";

    private static final String PATH_ROOTS = "roots";
    private static final String PATH_DOCS = "docs";
    private static final String PATH_CONTENTS = "contents";
    private static final String PATH_SEARCH = "search";

    private static final String PARAM_QUERY = "query";
    public static final String PARAM_QUERY = "query";

    /**
     * Build URI representing the custom roots in a storage backend.
     */
    public static Uri buildRootsUri(String authority) {
        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                .authority(authority).appendPath(PATH_ROOTS).build();
    }

    /**
     * Build URI representing the given {@link DocumentColumns#GUID} in a
@@ -108,11 +156,14 @@ public final class DocumentsContract {
    }

    /**
     * Build URI representing a search for matching documents in a storage
     * backend.
     * Build URI representing a search for matching documents under a directory
     * in a storage backend.
     *
     * @param documentUri directory to search under, which must have
     *            {@link #FLAG_SUPPORTS_SEARCH}.
     */
    public static Uri buildSearchUri(String authority, String query) {
        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
    public static Uri buildSearchUri(Uri documentUri, String query) {
        return documentUri.buildUpon()
                .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
    }

@@ -134,7 +185,8 @@ public final class DocumentsContract {
    public interface DocumentColumns extends OpenableColumns {
        /**
         * The globally unique ID for a document within a storage backend.
         * Values <em>must</em> never change once returned.
         * Values <em>must</em> never change once returned. This field is
         * read-only to document clients.
         * <p>
         * Type: STRING
         *
@@ -144,7 +196,9 @@ public final class DocumentsContract {

        /**
         * MIME type of a document, matching the value returned by
         * {@link ContentResolver#getType(android.net.Uri)}.
         * {@link ContentResolver#getType(android.net.Uri)}. This field must be
         * provided when a new document is created, but after that the field is
         * read-only.
         * <p>
         * Type: STRING
         *
@@ -154,7 +208,8 @@ public final class DocumentsContract {

        /**
         * Timestamp when a document was last modified, in milliseconds since
         * January 1, 1970 00:00:00.0 UTC.
         * January 1, 1970 00:00:00.0 UTC. This field is read-only to document
         * clients.
         * <p>
         * Type: INTEGER (long)
         *
@@ -163,13 +218,74 @@ public final class DocumentsContract {
        public static final String LAST_MODIFIED = "last_modified";

        /**
         * Flags that apply to a specific document.
         * Flags that apply to a specific document. This field is read-only to
         * document clients.
         * <p>
         * Type: INTEGER (int)
         */
        public static final String FLAGS = "flags";
    }

    public static final int ROOT_TYPE_SERVICE = 1;
    public static final int ROOT_TYPE_SHORTCUT = 2;
    public static final int ROOT_TYPE_DEVICE = 3;
    public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;

    /**
     * These are standard columns for the roots URI.
     *
     * @see DocumentsContract#buildRootsUri(String)
     */
    public interface RootColumns {
        /**
         * Storage root type, use for clustering.
         * <p>
         * Type: INTEGER (int)
         *
         * @see DocumentsContract#ROOT_TYPE_SERVICE
         * @see DocumentsContract#ROOT_TYPE_DEVICE
         */
        public static final String ROOT_TYPE = "root_type";

        /**
         * GUID of directory entry for this storage root.
         * <p>
         * Type: STRING
         */
        public static final String GUID = "guid";

        /**
         * Icon resource ID for this storage root, or {@code 0} to use the
         * default {@link ProviderInfo#icon}.
         * <p>
         * Type: INTEGER (int)
         */
        public static final String ICON = "icon";

        /**
         * Title for this storage root, or {@code null} to use the default
         * {@link ProviderInfo#labelRes}.
         * <p>
         * Type: STRING
         */
        public static final String TITLE = "title";

        /**
         * Summary for this storage root, or {@code null} to omit.
         * <p>
         * Type: STRING
         */
        public static final String SUMMARY = "summary";

        /**
         * Number of free bytes of available in this storage root, or -1 if
         * unknown or unbounded.
         * <p>
         * Type: INTEGER (long)
         */
        public static final String AVAILABLE_BYTES = "available_bytes";
    }

    /**
     * Return thumbnail representing the document at the given URI. Callers are
     * responsible for their own caching. Given document must have
+27 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 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.
-->

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="?android:attr/listPreferredItemPaddingEnd">

    <EditText
        android:id="@android:id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</FrameLayout>
+69 −13
Original line number Diff line number Diff line
@@ -21,9 +21,15 @@ import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.RootColumns;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -39,11 +45,11 @@ import com.google.android.collect.Lists;
import java.util.List;

/**
 * Display all known storage backends.
 * Display all known storage roots.
 */
public class BackendFragment extends Fragment {

    // TODO: handle multiple accounts from single backend
    // TODO: cluster backends by type

    private GridView mGridView;
    private BackendAdapter mAdapter;
@@ -57,19 +63,69 @@ public class BackendFragment extends Fragment {
        ft.commitAllowingStateLoss();
    }

    public static class Root {
        public int rootType;
        public Uri uri;
        public Drawable icon;
        public String title;
        public String summary;

        public static Root fromCursor(Context context, ProviderInfo info, Cursor cursor) {
            final Root root = new Root();

            root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
            root.uri = DocumentsContract.buildDocumentUri(
                    info.authority, cursor.getString(cursor.getColumnIndex(RootColumns.GUID)));

            final PackageManager pm = context.getPackageManager();
            final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
            if (icon != 0) {
                try {
                    root.icon = pm.getResourcesForApplication(info.applicationInfo)
                            .getDrawable(icon);
                } catch (NotFoundException e) {
                    throw new RuntimeException(e);
                } catch (NameNotFoundException e) {
                    throw new RuntimeException(e);
                }
            } else {
                root.icon = info.loadIcon(pm);
            }

            root.title = cursor.getString(cursor.getColumnIndex(RootColumns.TITLE));
            if (root.title == null) {
                root.title = info.loadLabel(pm).toString();
            }

            root.summary = cursor.getString(cursor.getColumnIndex(RootColumns.SUMMARY));

            return root;
        }
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final Context context = inflater.getContext();

        // Gather known storage providers
        // Gather roots from known storage providers
        final List<ProviderInfo> providers = context.getPackageManager()
                .queryContentProviders(null, -1, PackageManager.GET_META_DATA);
        final List<ProviderInfo> backends = Lists.newArrayList();
        final List<Root> roots = Lists.newArrayList();
        for (ProviderInfo info : providers) {
            if (info.metaData != null
                    && info.metaData.containsKey(DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
                backends.add(info);
                // TODO: populate roots on background thread, and cache results
                final Uri uri = DocumentsContract.buildRootsUri(info.authority);
                final Cursor cursor = context.getContentResolver()
                        .query(uri, null, null, null, null);
                try {
                    while (cursor.moveToNext()) {
                        roots.add(Root.fromCursor(context, info, cursor));
                    }
                } finally {
                    cursor.close();
                }
            }
        }

@@ -78,7 +134,7 @@ public class BackendFragment extends Fragment {
        mGridView = (GridView) view.findViewById(R.id.grid);
        mGridView.setOnItemClickListener(mItemListener);

        mAdapter = new BackendAdapter(context, backends);
        mAdapter = new BackendAdapter(context, roots);
        mGridView.setAdapter(mAdapter);
        mGridView.setNumColumns(GridView.AUTO_FIT);

@@ -88,13 +144,13 @@ public class BackendFragment extends Fragment {
    private OnItemClickListener mItemListener = new OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            final ProviderInfo info = mAdapter.getItem(position);
            ((DocumentsActivity) getActivity()).onBackendPicked(info);
            final Root root = mAdapter.getItem(position);
            ((DocumentsActivity) getActivity()).onRootPicked(root);
        }
    };

    public static class BackendAdapter extends ArrayAdapter<ProviderInfo> {
        public BackendAdapter(Context context, List<ProviderInfo> list) {
    public static class BackendAdapter extends ArrayAdapter<Root> {
        public BackendAdapter(Context context, List<Root> list) {
            super(context, android.R.layout.simple_list_item_1, list);
        }

@@ -109,9 +165,9 @@ public class BackendFragment extends Fragment {
            final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);

            final PackageManager pm = parent.getContext().getPackageManager();
            final ProviderInfo info = getItem(position);
            icon.setImageDrawable(info.loadIcon(pm));
            text1.setText(info.loadLabel(pm));
            final Root root = getItem(position);
            icon.setImageDrawable(root.icon);
            text1.setText(root.title);

            return convertView;
        }
+86 −14
Original line number Diff line number Diff line
@@ -21,15 +21,20 @@ import static com.android.documentsui.DirectoryFragment.getCursorString;
import android.app.ActionBar;
import android.app.ActionBar.OnNavigationListener;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.app.FragmentManager.BackStackEntry;
import android.app.FragmentManager.OnBackStackChangedListener;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
@@ -44,8 +49,11 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.TextView;

import com.android.documentsui.BackendFragment.Root;

import java.util.Arrays;
import java.util.List;

@@ -160,7 +168,7 @@ public class DocumentsActivity extends Activity {
            getFragmentManager().popBackStack();
            updateActionBar();
        } else if (id == R.id.menu_create_dir) {
            // TODO: show dialog to create directory
            CreateDirectoryFragment.show(getFragmentManager());
        }
        return super.onOptionsItemSelected(item);
    }
@@ -232,11 +240,8 @@ public class DocumentsActivity extends Activity {
        invalidateOptionsMenu();
    }

    public void onBackendPicked(ProviderInfo info) {
        final Uri uri = DocumentsContract.buildDocumentUri(
                info.authority, DocumentsContract.ROOT_GUID);
        final CharSequence displayName = info.loadLabel(getPackageManager());
        DirectoryFragment.show(getFragmentManager(), uri, displayName.toString());
    public void onRootPicked(Root root) {
        DirectoryFragment.show(getFragmentManager(), root.uri, root.title);
    }

    public void onDocumentPicked(Document doc) {
@@ -263,8 +268,12 @@ public class DocumentsActivity extends Activity {
    }

    public void onSaveRequested(String mimeType, String displayName) {
        // TODO: create file, confirming before overwriting
        final Uri uri = null;
        final ContentValues values = new ContentValues();
        values.put(DocumentColumns.MIME_TYPE, mimeType);
        values.put(DocumentColumns.DISPLAY_NAME, displayName);

        // TODO: handle errors from remote side
        final Uri uri = getContentResolver().insert(mCurrentDir, values);
        onFinished(uri);
    }

@@ -283,11 +292,10 @@ public class DocumentsActivity extends Activity {
            intent.setClipData(clipData);
        }

        intent.addFlags(
                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
        if (mAction == ACTION_CREATE) {
            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        }
        // TODO: omit WRITE and PERSIST for GET_CONTENT
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);

        setResult(Activity.RESULT_OK, intent);
        finish();
@@ -318,6 +326,24 @@ public class DocumentsActivity extends Activity {
            doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
            return doc;
        }

        public static Document fromUri(ContentResolver resolver, Uri uri) {
            final Document doc = new Document();
            doc.uri = uri;

            final Cursor cursor = resolver.query(uri, null, null, null, null);
            try {
                if (!cursor.moveToFirst()) {
                    throw new IllegalArgumentException("Missing details for " + uri);
                }
                doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
                doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
            } finally {
                cursor.close();
            }

            return doc;
        }
    }

    public static boolean mimeMatches(String filter, String[] tests) {
@@ -359,4 +385,50 @@ public class DocumentsActivity extends Activity {
            }
        }
    }

    private static final String TAG_CREATE_DIRECTORY = "create_directory";

    public static class CreateDirectoryFragment extends DialogFragment {
        public static void show(FragmentManager fm) {
            final CreateDirectoryFragment dialog = new CreateDirectoryFragment();
            dialog.show(fm, TAG_CREATE_DIRECTORY);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final Context context = getActivity();
            final ContentResolver resolver = context.getContentResolver();

            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
            final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());

            final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
            final EditText text1 = (EditText)view.findViewById(android.R.id.text1);

            builder.setTitle(R.string.menu_create_dir);
            builder.setView(view);

            builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    final String displayName = text1.getText().toString();

                    final ContentValues values = new ContentValues();
                    values.put(DocumentColumns.MIME_TYPE, DocumentsContract.MIME_TYPE_DIRECTORY);
                    values.put(DocumentColumns.DISPLAY_NAME, displayName);

                    // TODO: handle errors from remote side
                    final DocumentsActivity activity = (DocumentsActivity) getActivity();
                    final Uri uri = resolver.insert(activity.mCurrentDir, values);

                    // Navigate into newly created child
                    final Document doc = Document.fromUri(resolver, uri);
                    activity.onDocumentPicked(doc);
                }
            });
            builder.setNegativeButton(android.R.string.cancel, null);

            return builder.create();
        }
    }
}
Loading