Create unique files, root ordering, UI bugs.

When a file already exists on disk, try adding a counter suffix to
make a unique name.  Move services near top of roots list, just below
recents.  Remove "Documents" root.

Increase number of recents allowed from single provider, and add more
logging to diagnose wedged loaders.

When launching GET_CONTENT apps, wait for successful result before
relaying result; canceled requests now return to DocumentsUI.

Add CloseGuard to ContentProviderClients, since leaked instances can
keep the remote process alive.

Fix UI bug around trailing breadcrumbs.  Fix bug that dropped Recents
from roots list.  Add up action to Settings activity.  Give our
activity a default icon while waiting for async roots to load.

Bug: 10818683, 10819461, 10819461, 10819196, 10860199
Change-Id: I7b9e26b1cf8353dd3175458b23da2b4bda6c5831
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index 39b453d..0650798 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -26,6 +26,8 @@
 import android.os.ParcelFileDescriptor;
 import android.content.res.AssetFileDescriptor;
 
+import dalvik.system.CloseGuard;
+
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 
@@ -49,6 +51,8 @@
     private final boolean mStable;
     private boolean mReleased;
 
+    private final CloseGuard mGuard = CloseGuard.get();
+
     /**
      * @hide
      */
@@ -58,6 +62,7 @@
         mContentResolver = contentResolver;
         mPackageName = contentResolver.mPackageName;
         mStable = stable;
+        mGuard.open("release");
     }
 
     /** See {@link ContentProvider#query ContentProvider.query} */
@@ -324,6 +329,7 @@
                 throw new IllegalStateException("Already released");
             }
             mReleased = true;
+            mGuard.close();
             if (mStable) {
                 return mContentResolver.releaseProvider(mContentProvider);
             } else {
@@ -332,6 +338,13 @@
         }
     }
 
+    @Override
+    protected void finalize() throws Throwable {
+        if (mGuard != null) {
+            mGuard.warnIfOpen();
+        }
+    }
+
     /**
      * Get a reference to the {@link ContentProvider} that is associated with this
      * client. If the {@link ContentProvider} is running in a different process then
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 19a29f2..71a0567 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -11,7 +11,8 @@
         <!-- TODO: allow rotation when state saving is in better shape -->
         <activity
             android:name=".DocumentsActivity"
-            android:theme="@style/Theme">
+            android:theme="@style/Theme"
+            android:icon="@drawable/ic_doc_text">
             <intent-filter android:priority="100">
                 <action android:name="android.intent.action.OPEN_DOCUMENT" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 457bb19..6d2f9b9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -87,8 +87,8 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 
 public class DocumentsActivity extends Activity {
@@ -96,6 +96,8 @@
 
     private static final String EXTRA_STATE = "state";
 
+    private static final int CODE_FORWARD = 42;
+
     private boolean mShowAsDialog;
 
     private SearchView mSearchView;
@@ -843,11 +845,24 @@
 
     public void onAppPicked(ResolveInfo info) {
         final Intent intent = new Intent(getIntent());
-        intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
         intent.setComponent(new ComponentName(
                 info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
-        startActivity(intent);
-        finish();
+        startActivityForResult(intent, CODE_FORWARD);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        Log.d(TAG, "onActivityResult() code=" + resultCode);
+
+        // Only relay back results when not canceled; otherwise stick around to
+        // let the user pick another app/backend.
+        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
+            setResult(resultCode, data);
+            finish();
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
     }
 
     public void onDocumentPicked(DocumentInfo doc) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
index 3659c6e..e390456 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
@@ -52,6 +52,7 @@
 import java.util.concurrent.TimeUnit;
 
 public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
+    private static final boolean LOGD = true;
 
     public static final int MAX_OUTSTANDING_RECENTS = 2;
 
@@ -63,7 +64,7 @@
     /**
      * Maximum documents from a single root.
      */
-    public static final int MAX_DOCS_FROM_ROOT = 24;
+    public static final int MAX_DOCS_FROM_ROOT = 64;
 
     private static final ExecutorService sExecutor = buildExecutor();
 
@@ -194,6 +195,11 @@
             }
         }
 
+        if (LOGD) {
+            Log.d(TAG, "Found " + cursors.size() + " of " + mTasks.size() + " recent queries done");
+            Log.d(TAG, sExecutor.toString());
+        }
+
         final DirectoryResult result = new DirectoryResult();
         result.sortOrder = SORT_ORDER_LAST_MODIFIED;
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
index 5076370..a396f79 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
@@ -195,12 +195,9 @@
 
             final SpannableStringBuilder builder = new SpannableStringBuilder();
             builder.append(stack.root.title);
-            appendDrawable(builder, crumb);
             for (int i = stack.size() - 2; i >= 0; i--) {
+                appendDrawable(builder, crumb);
                 builder.append(stack.get(i).displayName);
-                if (i > 0) {
-                    appendDrawable(builder, crumb);
-                }
             }
             title.setText(builder);
             title.setEllipsize(TruncateAt.MIDDLE);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index 52d6cc8..15af8aa 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -179,6 +179,8 @@
             final Multimap<String, RootInfo> roots = ArrayListMultimap.create();
             final HashSet<String> stoppedAuthorities = Sets.newHashSet();
 
+            roots.put(mRecentsRoot.authority, mRecentsRoot);
+
             final ContentResolver resolver = mContext.getContentResolver();
             final PackageManager pm = mContext.getPackageManager();
             final List<ProviderInfo> providers = pm.queryContentProviders(
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index df9bce1..d602622 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -253,6 +253,7 @@
     }
 
     private static class SectionedRootsAdapter extends SectionedListAdapter {
+        private final RootsAdapter mRecent;
         private final RootsAdapter mServices;
         private final RootsAdapter mShortcuts;
         private final RootsAdapter mDevices;
@@ -260,12 +261,18 @@
 
         public SectionedRootsAdapter(
                 Context context, Collection<RootInfo> roots, Intent includeApps) {
+            mRecent = new RootsAdapter(context);
             mServices = new RootsAdapter(context);
             mShortcuts = new RootsAdapter(context);
             mDevices = new RootsAdapter(context);
             mApps = new AppsAdapter(context);
 
             for (RootInfo root : roots) {
+                if (root.authority == null) {
+                    mRecent.add(root);
+                    continue;
+                }
+
                 switch (root.rootType) {
                     case Root.ROOT_TYPE_SERVICE:
                         mServices.add(root);
@@ -297,15 +304,18 @@
             mShortcuts.sort(comp);
             mDevices.sort(comp);
 
+            if (mRecent.getCount() > 0) {
+                addSection(mRecent);
+            }
+            if (mServices.getCount() > 0) {
+                addSection(mServices);
+            }
             if (mShortcuts.getCount() > 0) {
                 addSection(mShortcuts);
             }
             if (mDevices.getCount() > 0) {
                 addSection(mDevices);
             }
-            if (mServices.getCount() > 0) {
-                addSection(mServices);
-            }
             if (mApps.getCount() > 0) {
                 addSection(mApps);
             }
@@ -315,12 +325,6 @@
     public static class RootComparator implements Comparator<RootInfo> {
         @Override
         public int compare(RootInfo lhs, RootInfo rhs) {
-            if (lhs.authority == null) {
-                return -1;
-            } else if (rhs.authority == null) {
-                return 1;
-            }
-
             final int score = DocumentInfo.compareToIgnoreCaseNullable(lhs.title, rhs.title);
             if (score != 0) {
                 return score;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SettingsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/SettingsActivity.java
index a85f6a9..d423e3f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SettingsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SettingsActivity.java
@@ -22,6 +22,7 @@
 import android.os.Bundle;
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceManager;
+import android.view.MenuItem;
 
 public class SettingsActivity extends Activity {
     private static final String KEY_ADVANCED_DEVICES = "advancedDevices";
@@ -47,9 +48,19 @@
         final ActionBar bar = getActionBar();
         if (bar != null) {
             bar.setDisplayShowHomeEnabled(false);
+            bar.setDisplayHomeAsUpEnabled(true);
         }
     }
 
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
     public static class SettingsFragment extends PreferenceFragment {
         @Override
         public void onCreate(Bundle savedInstanceState) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
index 681cc9b..08a8c13 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
@@ -181,7 +181,7 @@
 
     @Override
     public String toString() {
-        return "Document{name=" + displayName + ", docId=" + documentId + "}";
+        return "Document{docId=" + documentId + ", name=" + displayName + "}";
     }
 
     public boolean isCreateSupported() {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
index a870c7b..014901a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
@@ -185,7 +185,7 @@
 
     @Override
     public String toString() {
-        return "Root{title=" + title + ", rootId=" + rootId + "}";
+        return "Root{authority=" + authority + ", rootId=" + rootId + ", title=" + title + "}";
     }
 
     public Drawable loadIcon(Context context) {
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 3e2cd15..d6f477d 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -96,25 +96,6 @@
             throw new IllegalStateException(e);
         }
 
-        try {
-            final String rootId = "documents";
-            final File path = Environment.getExternalStoragePublicDirectory(
-                    Environment.DIRECTORY_DOCUMENTS);
-            mIdToPath.put(rootId, path);
-
-            final RootInfo root = new RootInfo();
-            root.rootId = rootId;
-            root.rootType = Root.ROOT_TYPE_SHORTCUT;
-            root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY
-                    | Root.FLAG_SUPPORTS_SEARCH;
-            root.title = getContext().getString(R.string.root_documents);
-            root.docId = getDocIdForFile(path);
-            mRoots.add(root);
-            mIdToRoot.put(rootId, root);
-        } catch (FileNotFoundException e) {
-            throw new IllegalStateException(e);
-        }
-
         return true;
     }
 
@@ -230,14 +211,23 @@
     public String createDocument(String docId, String mimeType, String displayName)
             throws FileNotFoundException {
         final File parent = getFileForDocId(docId);
-        displayName = validateDisplayName(mimeType, displayName);
+        File file;
 
-        final File file = new File(parent, displayName);
         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+            file = new File(parent, displayName);
             if (!file.mkdir()) {
                 throw new IllegalStateException("Failed to mkdir " + file);
             }
         } else {
+            displayName = removeExtension(mimeType, displayName);
+            file = new File(parent, addExtension(mimeType, displayName));
+
+            // If conflicting file, try adding counter suffix
+            int n = 0;
+            while (file.exists() && n++ < 32) {
+                file = new File(parent, addExtension(mimeType, displayName + " (" + n + ")"));
+            }
+
             try {
                 if (!file.createNewFile()) {
                     throw new IllegalStateException("Failed to touch " + file);
@@ -354,20 +344,31 @@
         return "application/octet-stream";
     }
 
-    private static String validateDisplayName(String mimeType, String displayName) {
-        if (Document.MIME_TYPE_DIR.equals(mimeType)) {
-            return displayName;
-        } else {
-            // Try appending meaningful extension if needed
-            if (!mimeType.equals(getTypeForName(displayName))) {
-                final String extension = MimeTypeMap.getSingleton()
-                        .getExtensionFromMimeType(mimeType);
-                if (extension != null) {
-                    displayName += "." + extension;
-                }
+    /**
+     * Remove file extension from name, but only if exact MIME type mapping
+     * exists. This means we can reapply the extension later.
+     */
+    private static String removeExtension(String mimeType, String name) {
+        final int lastDot = name.lastIndexOf('.');
+        if (lastDot >= 0) {
+            final String extension = name.substring(lastDot + 1);
+            final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+            if (mimeType.equals(nameMime)) {
+                return name.substring(0, lastDot);
             }
-
-            return displayName;
         }
+        return name;
+    }
+
+    /**
+     * Add file extension to name, but only if exact MIME type mapping exists.
+     */
+    private static String addExtension(String mimeType, String name) {
+        final String extension = MimeTypeMap.getSingleton()
+                .getExtensionFromMimeType(mimeType);
+        if (extension != null) {
+            return name + "." + extension;
+        }
+        return name;
     }
 }