OSDN Git Service

Request more documents when EXTRA_HAS_MORE.
authorJeff Sharkey <jsharkey@android.com>
Mon, 19 Aug 2013 05:26:48 +0000 (22:26 -0700)
committerJeff Sharkey <jsharkey@android.com>
Mon, 19 Aug 2013 05:32:02 +0000 (22:32 -0700)
Implement EXTRA_HAS_MORE and EXTRA_REQUEST_MORE contract with
document providers.  Providers can include EXTRA_HAS_MORE when
additional data is available with additional cost, such as a network
request.

Listen to content changes based on returned cursor instead of
original Uri.  Include a test backend to exercise.  UX still under
development.

Bug: 10350207
Change-Id: Iaa8954df55a1a1c0aa96eb8a4fd288e12c2fbb01

packages/DocumentsUI/res/layout/fragment_directory.xml
packages/DocumentsUI/res/values/strings.xml
packages/DocumentsUI/res/xml/document_provider.xml [new file with mode: 0644]
packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java
packages/ExternalStorageProvider/AndroidManifest.xml
packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java [new file with mode: 0644]

index 8dbd1de..67c5954 100644 (file)
         android:paddingStart="?android:attr/listPreferredItemPaddingStart"
         android:visibility="gone" />
 
+    <Button
+        android:id="@+id/more"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:text="@string/more"
+        android:visibility="gone" />
+
 </FrameLayout>
index 2b83183..928ba85 100644 (file)
@@ -60,4 +60,7 @@
     <string name="toast_no_application">Can\'t open file</string>
     <string name="toast_failed_delete">Unable to delete some documents</string>
 
+    <string name="more">More</string>
+    <string name="loading">Loading\u2026</string>
+
 </resources>
diff --git a/packages/DocumentsUI/res/xml/document_provider.xml b/packages/DocumentsUI/res/xml/document_provider.xml
new file mode 100644 (file)
index 0000000..77891cb
--- /dev/null
@@ -0,0 +1,19 @@
+<?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.
+-->
+
+<documents-provider xmlns:android="http://schemas.android.com/apk/res/android"
+    android:customRoots="true">
+</documents-provider>
index d3421e7..dd9aee5 100644 (file)
@@ -32,6 +32,7 @@ import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Loader;
+import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.net.Uri;
@@ -54,6 +55,7 @@ import android.widget.AbsListView.MultiChoiceModeListener;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.BaseAdapter;
+import android.widget.Button;
 import android.widget.GridView;
 import android.widget.ImageView;
 import android.widget.ListView;
@@ -79,6 +81,7 @@ public class DirectoryFragment extends Fragment {
     private View mEmptyView;
     private ListView mListView;
     private GridView mGridView;
+    private Button mMoreView;
 
     private AbsListView mCurrentView;
 
@@ -93,7 +96,7 @@ public class DirectoryFragment extends Fragment {
     private Point mThumbSize;
 
     private DocumentsAdapter mAdapter;
-    private LoaderCallbacks<List<Document>> mCallbacks;
+    private LoaderCallbacks<DirectoryResult> mCallbacks;
 
     private static final String EXTRA_TYPE = "type";
     private static final String EXTRA_URI = "uri";
@@ -150,14 +153,16 @@ public class DirectoryFragment extends Fragment {
         mGridView.setOnItemClickListener(mItemListener);
         mGridView.setMultiChoiceModeListener(mMultiListener);
 
+        mMoreView = (Button) view.findViewById(R.id.more);
+
         mAdapter = new DocumentsAdapter();
 
         final Uri uri = getArguments().getParcelable(EXTRA_URI);
         mType = getArguments().getInt(EXTRA_TYPE);
 
-        mCallbacks = new LoaderCallbacks<List<Document>>() {
+        mCallbacks = new LoaderCallbacks<DirectoryResult>() {
             @Override
-            public Loader<List<Document>> onCreateLoader(int id, Bundle args) {
+            public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
                 final DisplayState state = getDisplayState(DirectoryFragment.this);
                 mFilter = new MimePredicate(state.acceptMimes);
 
@@ -189,12 +194,34 @@ public class DirectoryFragment extends Fragment {
             }
 
             @Override
-            public void onLoadFinished(Loader<List<Document>> loader, List<Document> data) {
-                mAdapter.swapDocuments(data);
+            public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
+                mAdapter.swapDocuments(result.contents);
+
+                final Cursor cursor = result.cursor;
+                if (cursor != null && cursor.getExtras()
+                        .getBoolean(DocumentsContract.EXTRA_HAS_MORE, false)) {
+                    mMoreView.setText(R.string.more);
+                    mMoreView.setVisibility(View.VISIBLE);
+                    mMoreView.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            mMoreView.setText(R.string.loading);
+                            final Bundle bundle = new Bundle();
+                            bundle.putBoolean(DocumentsContract.EXTRA_REQUEST_MORE, true);
+                            try {
+                                cursor.respond(bundle);
+                            } catch (Exception e) {
+                                Log.w(TAG, "Failed to respond: " + e);
+                            }
+                        }
+                    });
+                } else {
+                    mMoreView.setVisibility(View.GONE);
+                }
             }
 
             @Override
-            public void onLoaderReset(Loader<List<Document>> loader) {
+            public void onLoaderReset(Loader<DirectoryResult> loader) {
                 mAdapter.swapDocuments(null);
             }
         };
@@ -407,7 +434,7 @@ public class DirectoryFragment extends Fragment {
         public void swapDocuments(List<Document> documents) {
             mDocuments = documents;
 
-            if (documents != null && documents.isEmpty()) {
+            if (mDocuments != null && mDocuments.isEmpty()) {
                 mEmptyView.setVisibility(View.VISIBLE);
             } else {
                 mEmptyView.setVisibility(View.GONE);
index c99d6af..14d6fd5 100644 (file)
@@ -36,29 +36,27 @@ import com.google.android.collect.Lists;
 import libcore.io.IoUtils;
 
 import java.io.FileNotFoundException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.LinkedList;
 import java.util.List;
 
-public class DirectoryLoader extends UriDerivativeLoader<List<Document>> {
+class DirectoryResult implements AutoCloseable {
+    Cursor cursor;
+    List<Document> contents = Lists.newArrayList();
+    Exception e;
+
+    @Override
+    public void close() throws Exception {
+        IoUtils.closeQuietly(cursor);
+    }
+}
+
+public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> {
 
     private final int mType;
     private Predicate<Document> mFilter;
     private Comparator<Document> mSortOrder;
 
-    /**
-     * Stub result that represents an internal error.
-     */
-    public static class ExceptionResult extends LinkedList<Document> {
-        public final Exception e;
-
-        public ExceptionResult(Exception e) {
-            this.e = e;
-        }
-    }
-
     public DirectoryLoader(Context context, Uri uri, int type, Predicate<Document> filter,
             Comparator<Document> sortOrder) {
         super(context, uri);
@@ -68,53 +66,49 @@ public class DirectoryLoader extends UriDerivativeLoader<List<Document>> {
     }
 
     @Override
-    public List<Document> loadInBackground(Uri uri, CancellationSignal signal) {
+    public DirectoryResult loadInBackground(Uri uri, CancellationSignal signal) {
+        final DirectoryResult result = new DirectoryResult();
         try {
-            return loadInBackgroundInternal(uri, signal);
+            loadInBackgroundInternal(result, uri, signal);
         } catch (Exception e) {
-            return new ExceptionResult(e);
+            result.e = e;
         }
+        return result;
     }
 
-    private List<Document> loadInBackgroundInternal(Uri uri, CancellationSignal signal) {
-        final ArrayList<Document> result = Lists.newArrayList();
-
-        // TODO: subscribe to the notify uri from query
-
+    private void loadInBackgroundInternal(
+            DirectoryResult result, Uri uri, CancellationSignal signal) {
         final ContentResolver resolver = getContext().getContentResolver();
         final Cursor cursor = resolver.query(uri, null, null, null, getQuerySortOrder(), signal);
-        try {
-            while (cursor != null && cursor.moveToNext()) {
-                Document doc = null;
-                switch (mType) {
-                    case TYPE_NORMAL:
-                    case TYPE_SEARCH:
-                        doc = Document.fromDirectoryCursor(uri, cursor);
-                        break;
-                    case TYPE_RECENT_OPEN:
-                        try {
-                            doc = Document.fromRecentOpenCursor(resolver, cursor);
-                        } catch (FileNotFoundException e) {
-                            Log.w(TAG, "Failed to find recent: " + e);
-                        }
-                        break;
-                    default:
-                        throw new IllegalArgumentException("Unknown type");
-                }
-
-                if (doc != null && (mFilter == null || mFilter.apply(doc))) {
-                    result.add(doc);
-                }
+        result.cursor = cursor;
+        result.cursor.registerContentObserver(mObserver);
+
+        while (cursor.moveToNext()) {
+            Document doc = null;
+            switch (mType) {
+                case TYPE_NORMAL:
+                case TYPE_SEARCH:
+                    doc = Document.fromDirectoryCursor(uri, cursor);
+                    break;
+                case TYPE_RECENT_OPEN:
+                    try {
+                        doc = Document.fromRecentOpenCursor(resolver, cursor);
+                    } catch (FileNotFoundException e) {
+                        Log.w(TAG, "Failed to find recent: " + e);
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown type");
+            }
+
+            if (doc != null && (mFilter == null || mFilter.apply(doc))) {
+                result.contents.add(doc);
             }
-        } finally {
-            IoUtils.closeQuietly(cursor);
         }
 
         if (mSortOrder != null) {
-            Collections.sort(result, mSortOrder);
+            Collections.sort(result.contents, mSortOrder);
         }
-
-        return result;
     }
 
     private String getQuerySortOrder() {
index cd8adac..5466dbf 100644 (file)
@@ -124,7 +124,7 @@ public class RecentsCreateFragment extends Fragment {
         }
     };
 
-    public static class RecentsCreateLoader extends UriDerivativeLoader<List<DocumentStack>> {
+    public static class RecentsCreateLoader extends UriDerivativeLoader<Uri, List<DocumentStack>> {
         public RecentsCreateLoader(Context context) {
             super(context, RecentsProvider.buildRecentCreate());
         }
index 1b88af4..1a5bb0c 100644 (file)
@@ -19,7 +19,6 @@ package com.android.documentsui;
 import android.content.AsyncTaskLoader;
 import android.content.Context;
 import android.database.ContentObserver;
-import android.net.Uri;
 import android.os.CancellationSignal;
 import android.os.OperationCanceledException;
 
@@ -28,17 +27,16 @@ import android.os.OperationCanceledException;
  * changes while started, manages {@link CancellationSignal}, and caches
  * returned results.
  */
-public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
-    private final ForceLoadContentObserver mObserver;
-    private boolean mObserving;
+public abstract class UriDerivativeLoader<P, R> extends AsyncTaskLoader<R> {
+    final ForceLoadContentObserver mObserver;
 
-    private final Uri mUri;
+    private final P mParam;
 
-    private T mResult;
+    private R mResult;
     private CancellationSignal mCancellationSignal;
 
     @Override
-    public final T loadInBackground() {
+    public final R loadInBackground() {
         synchronized (this) {
             if (isLoadInBackgroundCanceled()) {
                 throw new OperationCanceledException();
@@ -46,7 +44,7 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
             mCancellationSignal = new CancellationSignal();
         }
         try {
-            return loadInBackground(mUri, mCancellationSignal);
+            return loadInBackground(mParam, mCancellationSignal);
         } finally {
             synchronized (this) {
                 mCancellationSignal = null;
@@ -54,7 +52,7 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
         }
     }
 
-    public abstract T loadInBackground(Uri uri, CancellationSignal signal);
+    public abstract R loadInBackground(P param, CancellationSignal signal);
 
     @Override
     public void cancelLoadInBackground() {
@@ -68,12 +66,12 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
     }
 
     @Override
-    public void deliverResult(T result) {
+    public void deliverResult(R result) {
         if (isReset()) {
             closeQuietly(result);
             return;
         }
-        T oldResult = mResult;
+        R oldResult = mResult;
         mResult = result;
 
         if (isStarted()) {
@@ -85,18 +83,14 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
         }
     }
 
-    public UriDerivativeLoader(Context context, Uri uri) {
+    public UriDerivativeLoader(Context context, P param) {
         super(context);
         mObserver = new ForceLoadContentObserver();
-        mUri = uri;
+        mParam = param;
     }
 
     @Override
     protected void onStartLoading() {
-        if (!mObserving) {
-            getContext().getContentResolver().registerContentObserver(mUri, false, mObserver);
-            mObserving = true;
-        }
         if (mResult != null) {
             deliverResult(mResult);
         }
@@ -111,7 +105,7 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
     }
 
     @Override
-    public void onCanceled(T result) {
+    public void onCanceled(R result) {
         closeQuietly(result);
     }
 
@@ -125,13 +119,10 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> {
         closeQuietly(mResult);
         mResult = null;
 
-        if (mObserving) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-            mObserving = false;
-        }
+        getContext().getContentResolver().unregisterContentObserver(mObserver);
     }
 
-    private void closeQuietly(T result) {
+    private void closeQuietly(R result) {
         if (result instanceof AutoCloseable) {
             try {
                 ((AutoCloseable) result).close();
index 5272166..8bd2a6d 100644 (file)
                 android:name="android.content.DOCUMENT_PROVIDER"
                 android:resource="@xml/document_provider" />
         </provider>
+
+        <!-- TODO: remove when we have real providers -->
+        <provider
+            android:name=".CloudTestDocumentsProvider"
+            android:authorities="com.android.externalstorage.cloudtest"
+            android:grantUriPermissions="true"
+            android:exported="true"
+            android:enabled="false"
+            android:permission="android.permission.MANAGE_DOCUMENTS">
+            <meta-data
+                android:name="android.content.DOCUMENT_PROVIDER"
+                android:resource="@xml/document_provider" />
+        </provider>
     </application>
 </manifest>
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java
new file mode 100644 (file)
index 0000000..119d92e
--- /dev/null
@@ -0,0 +1,253 @@
+/*
+ * 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.
+ */
+
+package com.android.externalstorage;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.Documents;
+import android.provider.DocumentsContract.RootColumns;
+import android.provider.DocumentsContract.Roots;
+import android.util.Log;
+
+import com.google.android.collect.Lists;
+
+import libcore.io.IoUtils;
+
+import java.io.FileNotFoundException;
+import java.util.List;
+
+public class CloudTestDocumentsProvider extends ContentProvider {
+    private static final String TAG = "CloudTest";
+
+    private static final String AUTHORITY = "com.android.externalstorage.cloudtest";
+
+    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    private static final int URI_ROOTS = 1;
+    private static final int URI_ROOTS_ID = 2;
+    private static final int URI_DOCS_ID = 3;
+    private static final int URI_DOCS_ID_CONTENTS = 4;
+    private static final int URI_DOCS_ID_SEARCH = 5;
+
+    static {
+        sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
+        sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
+        sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
+        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
+        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
+    }
+
+    private static final String[] ALL_ROOTS_COLUMNS = new String[] {
+            RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
+            RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
+    };
+
+    private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
+            DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
+            DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
+    };
+
+    private List<String> mKnownDocs = Lists.newArrayList("meow.png", "kittens.pdf");
+
+    private int mPage;
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        switch (sMatcher.match(uri)) {
+            case URI_ROOTS: {
+                final MatrixCursor result = new MatrixCursor(
+                        projection != null ? projection : ALL_ROOTS_COLUMNS);
+                includeDefaultRoot(result);
+                return result;
+            }
+            case URI_ROOTS_ID: {
+                final MatrixCursor result = new MatrixCursor(
+                        projection != null ? projection : ALL_ROOTS_COLUMNS);
+                includeDefaultRoot(result);
+                return result;
+            }
+            case URI_DOCS_ID: {
+                final String docId = DocumentsContract.getDocId(uri);
+                final MatrixCursor result = new MatrixCursor(
+                        projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
+                includeDoc(result, docId);
+                return result;
+            }
+            case URI_DOCS_ID_CONTENTS: {
+                final CloudCursor result = new CloudCursor(
+                        projection != null ? projection : ALL_DOCUMENTS_COLUMNS, uri);
+                for (String docId : mKnownDocs) {
+                    includeDoc(result, docId);
+                }
+                if (mPage < 3) {
+                    result.setHasMore();
+                }
+                result.setNotificationUri(getContext().getContentResolver(), uri);
+                return result;
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
+    }
+
+    private void includeDefaultRoot(MatrixCursor result) {
+        final RowBuilder row = result.newRow();
+        row.offer(RootColumns.ROOT_ID, "testroot");
+        row.offer(RootColumns.ROOT_TYPE, Roots.ROOT_TYPE_SERVICE);
+        row.offer(RootColumns.TITLE, "_TestTitle");
+        row.offer(RootColumns.SUMMARY, "_TestSummary");
+    }
+
+    private void includeDoc(MatrixCursor result, String docId) {
+        int flags = 0;
+
+        final String mimeType;
+        if (Documents.DOC_ID_ROOT.equals(docId)) {
+            mimeType = Documents.MIME_TYPE_DIR;
+        } else {
+            mimeType = "application/octet-stream";
+        }
+
+        final RowBuilder row = result.newRow();
+        row.offer(DocumentColumns.DOC_ID, docId);
+        row.offer(DocumentColumns.DISPLAY_NAME, docId);
+        row.offer(DocumentColumns.MIME_TYPE, mimeType);
+        row.offer(DocumentColumns.LAST_MODIFIED, System.currentTimeMillis());
+        row.offer(DocumentColumns.FLAGS, flags);
+    }
+
+    private class CloudCursor extends MatrixCursor {
+        private final Uri mUri;
+        private Bundle mExtras = new Bundle();
+
+        public CloudCursor(String[] columnNames, Uri uri) {
+            super(columnNames);
+            mUri = uri;
+        }
+
+        public void setHasMore() {
+            mExtras.putBoolean(DocumentsContract.EXTRA_HAS_MORE, true);
+        }
+
+        @Override
+        public Bundle getExtras() {
+            Log.d(TAG, "getExtras() " + mExtras);
+            return mExtras;
+        }
+
+        @Override
+        public Bundle respond(Bundle extras) {
+            extras.size();
+            Log.d(TAG, "respond() " + extras);
+            if (extras.getBoolean(DocumentsContract.EXTRA_REQUEST_MORE, false)) {
+                new CloudTask().execute(mUri);
+            }
+            return Bundle.EMPTY;
+        }
+    }
+
+    private class CloudTask extends AsyncTask<Uri, Void, Void> {
+        @Override
+        protected Void doInBackground(Uri... uris) {
+            final Uri uri = uris[0];
+
+            SystemClock.sleep(1000);
+
+            // Grab some files from the cloud
+            for (int i = 0; i < 5; i++) {
+                mKnownDocs.add("cloud-page" + mPage + "-file" + i);
+            }
+            mPage++;
+
+            Log.d(TAG, "Loaded more; notifying " + uri);
+            getContext().getContentResolver().notifyChange(uri, null, false);
+            return null;
+        }
+    }
+
+    private interface TypeQuery {
+        final String[] PROJECTION = {
+                DocumentColumns.MIME_TYPE };
+
+        final int MIME_TYPE = 0;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        switch (sMatcher.match(uri)) {
+            case URI_ROOTS: {
+                return Roots.MIME_TYPE_DIR;
+            }
+            case URI_ROOTS_ID: {
+                return Roots.MIME_TYPE_ITEM;
+            }
+            case URI_DOCS_ID: {
+                final Cursor cursor = query(uri, TypeQuery.PROJECTION, null, null, null);
+                try {
+                    if (cursor.moveToFirst()) {
+                        return cursor.getString(TypeQuery.MIME_TYPE);
+                    } else {
+                        return null;
+                    }
+                } finally {
+                    IoUtils.closeQuietly(cursor);
+                }
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        throw new UnsupportedOperationException("Unsupported Uri " + uri);
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException("Unsupported Uri " + uri);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Unsupported Uri " + uri);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Unsupported Uri " + uri);
+    }
+}