DocumentsUI handles GET_CONTENT; hinting, errors.

Document browser now takes over all GET_CONTENT requests that request
openable Uris. It shows both storage backends and includes other apps
that respond to GET_CONTENT. Only grants transient read permissions.

Better guarding against throwing storage backends. Send sort order
and local-only hinting to backends.

Require that OPEN/CREATE_DOC users include openable category.

Bug: 10330112, 10329976, 10340741, 10331689, 10329971
Change-Id: Ieb8768a6d71201816046f4a4c48832061a313c28
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 5a6060a..e1b6a91 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -144,7 +144,7 @@
                 final DisplayState state = getDisplayState(DirectoryFragment.this);
                 mFilter = new MimePredicate(state.acceptMimes);
 
-                final Uri contentsUri;
+                Uri contentsUri;
                 if (mType == TYPE_NORMAL) {
                     contentsUri = DocumentsContract.buildContentsUri(uri);
                 } else if (mType == TYPE_RECENT_OPEN) {
@@ -153,6 +153,10 @@
                     contentsUri = uri;
                 }
 
+                if (state.localOnly) {
+                    contentsUri = DocumentsContract.setLocalOnly(contentsUri);
+                }
+
                 final Comparator<Document> sortOrder;
                 if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) {
                     sortOrder = new Document.DateComparator();
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index 94c2b61..98f9a4d 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -26,6 +26,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.CancellationSignal;
+import android.provider.DocumentsContract.DocumentColumns;
 import android.util.Log;
 
 import com.android.documentsui.model.Document;
@@ -38,6 +39,7 @@
 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>> {
@@ -46,6 +48,17 @@
     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);
@@ -56,11 +69,18 @@
 
     @Override
     public List<Document> loadInBackground(Uri uri, CancellationSignal signal) {
+        try {
+            return loadInBackgroundInternal(uri, signal);
+        } catch (Exception e) {
+            return new ExceptionResult(e);
+        }
+    }
+
+    private List<Document> loadInBackgroundInternal(Uri uri, CancellationSignal signal) {
         final ArrayList<Document> result = Lists.newArrayList();
 
-        // TODO: send selection and sorting hints to backend
         final ContentResolver resolver = getContext().getContentResolver();
-        final Cursor cursor = resolver.query(uri, null, null, null, null, signal);
+        final Cursor cursor = resolver.query(uri, null, null, null, getQuerySortOrder(), signal);
         try {
             while (cursor != null && cursor.moveToNext()) {
                 Document doc = null;
@@ -94,4 +114,16 @@
 
         return result;
     }
+
+    private String getQuerySortOrder() {
+        if (mSortOrder instanceof Document.DateComparator) {
+            return DocumentColumns.LAST_MODIFIED + " DESC";
+        } else if (mSortOrder instanceof Document.NameComparator) {
+            return DocumentColumns.DISPLAY_NAME + " ASC";
+        } else if (mSortOrder instanceof Document.SizeComparator) {
+            return DocumentColumns.SIZE + " DESC";
+        } else {
+            return null;
+        }
+    }
 }
diff --git a/src/com/android/documentsui/DocumentsActivity.java b/src/com/android/documentsui/DocumentsActivity.java
index a536acb..89ba66e 100644
--- a/src/com/android/documentsui/DocumentsActivity.java
+++ b/src/com/android/documentsui/DocumentsActivity.java
@@ -22,9 +22,11 @@
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.content.ClipData;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Intent;
+import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
@@ -60,6 +62,7 @@
 
     public static final int ACTION_OPEN = 1;
     public static final int ACTION_CREATE = 2;
+    public static final int ACTION_GET_CONTENT = 3;
 
     private int mAction;
 
@@ -84,11 +87,15 @@
         final String action = intent.getAction();
         if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
             mAction = ACTION_OPEN;
-            mDisplayState.allowMultiple = intent.getBooleanExtra(
-                    Intent.EXTRA_ALLOW_MULTIPLE, false);
         } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
             mAction = ACTION_CREATE;
-            mDisplayState.allowMultiple = false;
+        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
+            mAction = ACTION_GET_CONTENT;
+        }
+
+        if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
+            mDisplayState.allowMultiple = intent.getBooleanExtra(
+                    Intent.EXTRA_ALLOW_MULTIPLE, false);
         }
 
         if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
@@ -97,11 +104,7 @@
             mDisplayState.acceptMimes = new String[] { intent.getType() };
         }
 
-        if (MimePredicate.mimeMatches("image/*", mDisplayState.acceptMimes)) {
-            mDisplayState.mode = DisplayState.MODE_GRID;
-        } else {
-            mDisplayState.mode = DisplayState.MODE_LIST;
-        }
+        mDisplayState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
 
         setResult(Activity.RESULT_CANCELED);
         setContentView(R.layout.activity);
@@ -112,7 +115,14 @@
             SaveFragment.show(getFragmentManager(), mimeType, title);
         }
 
-        RootsFragment.show(getFragmentManager());
+        if (mAction == ACTION_GET_CONTENT) {
+            final Intent moreApps = new Intent(getIntent());
+            moreApps.setComponent(null);
+            moreApps.setPackage(null);
+            RootsFragment.show(getFragmentManager(), moreApps);
+        } else {
+            RootsFragment.show(getFragmentManager(), null);
+        }
 
         mRootsContainer = findViewById(R.id.container_roots);
 
@@ -186,7 +196,7 @@
             actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
             actionBar.setIcon(new ColorDrawable());
 
-            if (mAction == ACTION_OPEN) {
+            if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
                 actionBar.setTitle(R.string.title_open);
             } else if (mAction == ACTION_CREATE) {
                 actionBar.setTitle(R.string.title_save);
@@ -484,12 +494,21 @@
         }
     }
 
+    public void onAppPicked(ResolveInfo info) {
+        final Intent intent = new Intent(getIntent());
+        intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+        intent.setComponent(new ComponentName(
+                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
+        startActivity(intent);
+        finish();
+    }
+
     public void onDocumentPicked(Document doc) {
         final FragmentManager fm = getFragmentManager();
         if (doc.isDirectory()) {
             mStack.push(doc);
             onCurrentDirectoryChanged();
-        } else if (mAction == ACTION_OPEN) {
+        } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
             // Explicit file picked, return
             onFinished(doc.uri);
         } else if (mAction == ACTION_CREATE) {
@@ -538,7 +557,7 @@
             values.put(RecentsProvider.COL_PATH, rawStack);
             resolver.insert(RecentsProvider.buildRecentCreate(), values);
 
-        } else if (mAction == ACTION_OPEN) {
+        } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
             // Remember opened items
             for (Uri uri : uris) {
                 values.clear();
@@ -565,10 +584,13 @@
             intent.setClipData(clipData);
         }
 
-        // 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);
+        if (mAction == ACTION_GET_CONTENT) {
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        } else {
+            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();
@@ -580,6 +602,7 @@
         public int sortOrder = SORT_ORDER_NAME;
         public boolean allowMultiple = false;
         public boolean showSize = false;
+        public boolean localOnly = false;
 
         public static final int MODE_LIST = 0;
         public static final int MODE_GRID = 1;
diff --git a/src/com/android/documentsui/RecentsProvider.java b/src/com/android/documentsui/RecentsProvider.java
index 5268c1d..dbcb039 100644
--- a/src/com/android/documentsui/RecentsProvider.java
+++ b/src/com/android/documentsui/RecentsProvider.java
@@ -129,11 +129,11 @@
         switch (sMatcher.match(uri)) {
             case URI_RECENT_OPEN: {
                 return db.query(TABLE_RECENT_OPEN, projection,
-                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, sortOrder);
+                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
             }
             case URI_RECENT_CREATE: {
                 return db.query(TABLE_RECENT_CREATE, projection,
-                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, sortOrder);
+                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
             }
             case URI_RESUME: {
                 final String packageName = uri.getPathSegments().get(1);
diff --git a/src/com/android/documentsui/RootsCache.java b/src/com/android/documentsui/RootsCache.java
index b26db3b..ceab8fc 100644
--- a/src/com/android/documentsui/RootsCache.java
+++ b/src/com/android/documentsui/RootsCache.java
@@ -95,19 +95,24 @@
 
                 sProviders.put(info.providerInfo.authority, info);
 
-                // TODO: remove deprecated customRoots flag
-                // TODO: populate roots on background thread, and cache results
-                final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
-                final Cursor cursor = context.getContentResolver()
-                        .query(uri, null, null, null, null);
                 try {
-                    while (cursor.moveToNext()) {
-                        final Root root = Root.fromCursor(context, info, cursor);
-                        sRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
-                        sRootsList.add(root);
+                    // TODO: remove deprecated customRoots flag
+                    // TODO: populate roots on background thread, and cache results
+                    final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
+                    final Cursor cursor = context.getContentResolver()
+                            .query(uri, null, null, null, null);
+                    try {
+                        while (cursor.moveToNext()) {
+                            final Root root = Root.fromCursor(context, info, cursor);
+                            sRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
+                            sRootsList.add(root);
+                        }
+                    } finally {
+                        cursor.close();
                     }
-                } finally {
-                    cursor.close();
+                } catch (Exception e) {
+                    Log.w(TAG, "Failed to load some roots from " + info.providerInfo.authority
+                            + ": " + e);
                 }
             }
         }
diff --git a/src/com/android/documentsui/RootsFragment.java b/src/com/android/documentsui/RootsFragment.java
index 427ad42..e32414b 100644
--- a/src/com/android/documentsui/RootsFragment.java
+++ b/src/com/android/documentsui/RootsFragment.java
@@ -22,6 +22,9 @@
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
 import android.text.format.Formatter;
@@ -41,6 +44,8 @@
 import com.android.documentsui.model.Root.RootComparator;
 
 import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
 
 /**
  * Display list of known storage backend roots.
@@ -50,8 +55,14 @@
     private ListView mList;
     private SectionedRootsAdapter mAdapter;
 
-    public static void show(FragmentManager fm) {
+    private static final String EXTRA_INCLUDE_APPS = "includeApps";
+
+    public static void show(FragmentManager fm, Intent includeApps) {
+        final Bundle args = new Bundle();
+        args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
+
         final RootsFragment fragment = new RootsFragment();
+        fragment.setArguments(args);
 
         final FragmentTransaction ft = fm.beginTransaction();
         ft.replace(R.id.container_roots, fragment);
@@ -69,11 +80,11 @@
 
         final View view = inflater.inflate(R.layout.fragment_roots, container, false);
         mList = (ListView) view.findViewById(android.R.id.list);
-
-        mAdapter = new SectionedRootsAdapter(context, RootsCache.getRoots(context));
-        mList.setAdapter(mAdapter);
         mList.setOnItemClickListener(mItemListener);
 
+        final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
+        mAdapter = new SectionedRootsAdapter(context, RootsCache.getRoots(context), includeApps);
+
         return view;
     }
 
@@ -82,18 +93,26 @@
         super.onStart();
 
         final Context context = getActivity();
-        mAdapter.setShowAdvanced(SettingsActivity.getDisplayAdvancedDevices(context));
+        mAdapter.updateVisible(SettingsActivity.getDisplayAdvancedDevices(context));
+        mList.setAdapter(mAdapter);
     }
 
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            final Root root = (Root) mAdapter.getItem(position);
-            ((DocumentsActivity) getActivity()).onRootPicked(root, true);
+            final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
+            final Object item = mAdapter.getItem(position);
+            if (item instanceof Root) {
+                activity.onRootPicked((Root) item, true);
+            } else if (item instanceof ResolveInfo) {
+                activity.onAppPicked((ResolveInfo) item);
+            } else {
+                throw new IllegalStateException("Unknown root: " + item);
+            }
         }
     };
 
-    public static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
+    private static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
         private int mHeaderId;
 
         public RootsAdapter(Context context, int headerId) {
@@ -148,17 +167,61 @@
         }
     }
 
-    public static class SectionedRootsAdapter extends SectionedListAdapter {
+    private static class AppsAdapter extends ArrayAdapter<ResolveInfo> implements SectionAdapter {
+        public AppsAdapter(Context context) {
+            super(context, 0);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final Context context = parent.getContext();
+            final PackageManager pm = context.getPackageManager();
+            if (convertView == null) {
+                convertView = LayoutInflater.from(context)
+                        .inflate(R.layout.item_root, parent, false);
+            }
+
+            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
+
+            final ResolveInfo info = getItem(position);
+            icon.setImageDrawable(info.loadIcon(pm));
+            title.setText(info.loadLabel(pm));
+
+            // TODO: match existing summary behavior from disambig dialog
+            summary.setVisibility(View.GONE);
+
+            return convertView;
+        }
+
+        @Override
+        public View getHeaderView(View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.item_root_header, parent, false);
+            }
+
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            title.setText(R.string.root_type_apps);
+
+            return convertView;
+        }
+    }
+
+    private static class SectionedRootsAdapter extends SectionedListAdapter {
         private final RootsAdapter mServices;
         private final RootsAdapter mShortcuts;
         private final RootsAdapter mDevices;
         private final RootsAdapter mDevicesAdvanced;
+        private final AppsAdapter mApps;
 
-        public SectionedRootsAdapter(Context context, Collection<Root> roots) {
+        public SectionedRootsAdapter(Context context, Collection<Root> roots, Intent includeApps) {
             mServices = new RootsAdapter(context, R.string.root_type_service);
             mShortcuts = new RootsAdapter(context, R.string.root_type_shortcut);
             mDevices = new RootsAdapter(context, R.string.root_type_device);
             mDevicesAdvanced = new RootsAdapter(context, R.string.root_type_device);
+            mApps = new AppsAdapter(context);
 
             for (Root root : roots) {
                 Log.d(TAG, "Found rootType=" + root.rootType);
@@ -179,6 +242,19 @@
                 }
             }
 
+            if (includeApps != null) {
+                final PackageManager pm = context.getPackageManager();
+                final List<ResolveInfo> infos = pm.queryIntentActivities(
+                        includeApps, PackageManager.MATCH_DEFAULT_ONLY);
+
+                // Omit ourselves from the list
+                for (ResolveInfo info : infos) {
+                    if (!context.getPackageName().equals(info.activityInfo.packageName)) {
+                        mApps.add(info);
+                    }
+                }
+            }
+
             final RootComparator comp = new RootComparator();
             mServices.sort(comp);
             mShortcuts.sort(comp);
@@ -186,7 +262,7 @@
             mDevicesAdvanced.sort(comp);
         }
 
-        public void setShowAdvanced(boolean showAdvanced) {
+        public void updateVisible(boolean showAdvanced) {
             clearSections();
             if (mServices.getCount() > 0) {
                 addSection(mServices);
@@ -199,6 +275,10 @@
             if (devices.getCount() > 0) {
                 addSection(devices);
             }
+
+            if (mApps.getCount() > 0) {
+                addSection(mApps);
+            }
         }
     }
 }
diff --git a/src/com/android/documentsui/SectionedListAdapter.java b/src/com/android/documentsui/SectionedListAdapter.java
index aacce65..088e3fa 100644
--- a/src/com/android/documentsui/SectionedListAdapter.java
+++ b/src/com/android/documentsui/SectionedListAdapter.java
@@ -18,6 +18,7 @@
 
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.AdapterView;
 import android.widget.BaseAdapter;
 import android.widget.ListAdapter;
 
@@ -41,6 +42,11 @@
         notifyDataSetChanged();
     }
 
+    /**
+     * After mutating sections, you <em>must</em>
+     * {@link AdapterView#setAdapter(android.widget.Adapter)} to correctly
+     * recount view types.
+     */
     public void addSection(SectionAdapter adapter) {
         mSections.add(adapter);
         notifyDataSetChanged();
@@ -117,7 +123,7 @@
             if (position == 0) {
                 return false;
             } else if (position < sectionSize) {
-                return section.isEnabled(position);
+                return section.isEnabled(position - 1);
             }
 
             // Otherwise jump into next section
diff --git a/src/com/android/documentsui/TestActivity.java b/src/com/android/documentsui/TestActivity.java
index a086a43..f6548e8 100644
--- a/src/com/android/documentsui/TestActivity.java
+++ b/src/com/android/documentsui/TestActivity.java
@@ -60,6 +60,7 @@
             @Override
             public void onClick(View v) {
                 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                intent.addCategory(Intent.CATEGORY_OPENABLE);
                 intent.setType("*/*");
                 if (multiple.isChecked()) {
                     intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
@@ -75,6 +76,7 @@
             @Override
             public void onClick(View v) {
                 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                intent.addCategory(Intent.CATEGORY_OPENABLE);
                 intent.setType("image/*");
                 if (multiple.isChecked()) {
                     intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
@@ -90,6 +92,7 @@
             @Override
             public void onClick(View v) {
                 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                intent.addCategory(Intent.CATEGORY_OPENABLE);
                 intent.setType("*/*");
                 intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
                         "text/plain", "application/msword" });
@@ -107,6 +110,7 @@
             @Override
             public void onClick(View v) {
                 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+                intent.addCategory(Intent.CATEGORY_OPENABLE);
                 intent.setType("text/plain");
                 intent.putExtra(Intent.EXTRA_TITLE, "foobar.txt");
                 startActivityForResult(intent, 42);
@@ -114,6 +118,22 @@
         });
         view.addView(button);
 
+        button = new Button(context);
+        button.setText("GET_CONTENT */*");
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.addCategory(Intent.CATEGORY_OPENABLE);
+                intent.setType("*/*");
+                if (multiple.isChecked()) {
+                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+                }
+                startActivityForResult(Intent.createChooser(intent, "Kittens!"), 42);
+            }
+        });
+        view.addView(button);
+
         mResult = new TextView(context);
         view.addView(mResult);
 
@@ -131,7 +151,7 @@
                 is = getContentResolver().openInputStream(uri);
                 final int length = Streams.readFullyNoClose(is).length;
                 Log.d(TAG, "read length=" + length);
-            } catch (IOException e) {
+            } catch (Exception e) {
                 Log.w(TAG, "Failed to read " + uri, e);
             } finally {
                 IoUtils.closeQuietly(is);