am 5b3f4aa8: Merge "Show loading, error, and info messages as footers." into klp-dev

* commit '5b3f4aa84fe3f97075f4ed0763c9ee8a3dd2065d':
  Show loading, error, and info messages as footers.
diff --git a/packages/DocumentsUI/res/layout/item_loading.xml b/packages/DocumentsUI/res/layout/item_loading.xml
new file mode 100644
index 0000000..7da71e3
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/item_loading.xml
@@ -0,0 +1,34 @@
+<?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:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingTop="8dip"
+    android:paddingBottom="8dip"
+    android:orientation="horizontal">
+
+    <ProgressBar
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:indeterminate="true"
+        style="?android:attr/progressBarStyle" />
+
+</FrameLayout>
diff --git a/packages/DocumentsUI/res/layout/item_message_grid.xml b/packages/DocumentsUI/res/layout/item_message_grid.xml
new file mode 100644
index 0000000..941340e
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/item_message_grid.xml
@@ -0,0 +1,59 @@
+<?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="180dip"
+    android:paddingBottom="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/chip"
+        android:foreground="@drawable/item_background"
+        android:duplicateParentState="true">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:paddingBottom="6dp"
+            android:orientation="vertical"
+            android:gravity="center">
+
+            <ImageView
+                android:id="@android:id/icon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null" />
+
+            <TextView
+                android:id="@android:id/title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:singleLine="true"
+                android:ellipsize="marquee"
+                android:paddingTop="6dp"
+                android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+                android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textAlignment="viewStart" />
+
+        </LinearLayout>
+
+    </FrameLayout>
+
+</FrameLayout>
diff --git a/packages/DocumentsUI/res/layout/item_message_list.xml b/packages/DocumentsUI/res/layout/item_message_list.xml
new file mode 100644
index 0000000..dda3c80
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/item_message_list.xml
@@ -0,0 +1,47 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@drawable/item_background"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingTop="8dip"
+    android:paddingBottom="8dip"
+    android:orientation="horizontal">
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="@android:dimen/app_icon_size"
+        android:layout_height="@android:dimen/app_icon_size"
+        android:layout_marginEnd="8dip"
+        android:layout_gravity="center_vertical"
+        android:scaleType="centerInside"
+        android:contentDescription="@null" />
+
+    <TextView
+        android:id="@android:id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:singleLine="true"
+        android:ellipsize="marquee"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:textAlignment="viewStart" />
+
+</LinearLayout>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 1220137..33d7d6af 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -412,11 +412,83 @@
         return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
     }
 
+    private interface Footer {
+        public View getView(View convertView, ViewGroup parent);
+    }
+
+    private static class LoadingFooter implements Footer {
+        @Override
+        public View getView(View convertView, ViewGroup parent) {
+            final Context context = parent.getContext();
+            if (convertView == null) {
+                final LayoutInflater inflater = LayoutInflater.from(context);
+                convertView = inflater.inflate(R.layout.item_loading, parent, false);
+            }
+            return convertView;
+        }
+    }
+
+    private class MessageFooter implements Footer {
+        private final int mIcon;
+        private final String mMessage;
+
+        public MessageFooter(int icon, String message) {
+            mIcon = icon;
+            mMessage = message;
+        }
+
+        @Override
+        public View getView(View convertView, ViewGroup parent) {
+            final Context context = parent.getContext();
+            final State state = getDisplayState(DirectoryFragment.this);
+
+            if (convertView == null) {
+                final LayoutInflater inflater = LayoutInflater.from(context);
+                if (state.mode == MODE_LIST) {
+                    convertView = inflater.inflate(R.layout.item_message_list, parent, false);
+                } else if (state.mode == MODE_GRID) {
+                    convertView = inflater.inflate(R.layout.item_message_grid, parent, false);
+                } else {
+                    throw new IllegalStateException();
+                }
+            }
+
+            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            icon.setImageResource(mIcon);
+            title.setText(mMessage);
+            return convertView;
+        }
+    }
+
     private class DocumentsAdapter extends BaseAdapter {
         private Cursor mCursor;
+        private int mCursorCount;
+
+        private List<Footer> mFooters = Lists.newArrayList();
 
         public void swapCursor(Cursor cursor) {
             mCursor = cursor;
+            mCursorCount = cursor != null ? cursor.getCount() : 0;
+
+            mFooters.clear();
+
+            final Bundle extras = cursor != null ? cursor.getExtras() : null;
+            if (extras != null) {
+                final String info = extras.getString(DocumentsContract.EXTRA_INFO);
+                if (info != null) {
+                    mFooters.add(new MessageFooter(
+                            com.android.internal.R.drawable.ic_menu_info_details, info));
+                }
+                final String error = extras.getString(DocumentsContract.EXTRA_ERROR);
+                if (error != null) {
+                    mFooters.add(new MessageFooter(
+                            com.android.internal.R.drawable.ic_dialog_alert, error));
+                }
+                if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) {
+                    mFooters.add(new LoadingFooter());
+                }
+            }
 
             if (isEmpty()) {
                 mEmptyView.setVisibility(View.VISIBLE);
@@ -429,6 +501,15 @@
 
         @Override
         public View getView(int position, View convertView, ViewGroup parent) {
+            if (position < mCursorCount) {
+                return getDocumentView(position, convertView, parent);
+            } else {
+                position -= mCursorCount;
+                return mFooters.get(position).getView(convertView, parent);
+            }
+        }
+
+        private View getDocumentView(int position, View convertView, ViewGroup parent) {
             final Context context = parent.getContext();
             final State state = getDisplayState(DirectoryFragment.this);
 
@@ -535,21 +616,42 @@
 
         @Override
         public int getCount() {
-            return mCursor != null ? mCursor.getCount() : 0;
+            return mCursorCount + mFooters.size();
         }
 
         @Override
         public Cursor getItem(int position) {
-            if (mCursor != null) {
+            if (position < mCursorCount) {
                 mCursor.moveToPosition(position);
+                return mCursor;
+            } else {
+                return null;
             }
-            return mCursor;
         }
 
         @Override
         public long getItemId(int position) {
             return position;
         }
+
+        @Override
+        public int getItemViewType(int position) {
+            if (position < mCursorCount) {
+                return 0;
+            } else {
+                return IGNORE_ITEM_VIEW_TYPE;
+            }
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return position < mCursorCount;
+        }
     }
 
     private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index 3f016b5..6ea57d7 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -77,11 +77,12 @@
                     .getContentResolver().acquireUnstableContentProviderClient(authority);
             final Cursor cursor = result.client.query(
                     mUri, null, null, null, getQuerySortOrder(mSortOrder), mSignal);
+            cursor.registerContentObserver(mObserver);
+
             final Cursor withRoot = new RootCursorWrapper(mUri.getAuthority(), mRootId, cursor, -1);
             final Cursor sorted = new SortingCursorWrapper(withRoot, mSortOrder);
 
             result.cursor = sorted;
-            result.cursor.registerContentObserver(mObserver);
         } catch (Exception e) {
             result.exception = e;
             ContentProviderClient.closeQuietly(result.client);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java b/packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java
index d0e5ff6..0b58218 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootCursorWrapper.java
@@ -18,6 +18,7 @@
 
 import android.database.AbstractCursor;
 import android.database.Cursor;
+import android.os.Bundle;
 
 /**
  * Cursor wrapper that adds columns to identify which root a document came from.
@@ -63,6 +64,11 @@
     }
 
     @Override
+    public Bundle getExtras() {
+        return mCursor.getExtras();
+    }
+
+    @Override
     public void close() {
         super.close();
         mCursor.close();
@@ -128,5 +134,4 @@
     public boolean isNull(int column) {
         return mCursor.isNull(column);
     }
-
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
index b434a35..19ad2e2 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
@@ -22,6 +22,7 @@
 
 import android.database.AbstractCursor;
 import android.database.Cursor;
+import android.os.Bundle;
 import android.provider.DocumentsContract.Document;
 
 /**
@@ -96,6 +97,11 @@
     }
 
     @Override
+    public Bundle getExtras() {
+        return mCursor.getExtras();
+    }
+
+    @Override
     public void close() {
         super.close();
         mCursor.close();
diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml
index 5272166..7094efc 100644
--- a/packages/ExternalStorageProvider/AndroidManifest.xml
+++ b/packages/ExternalStorageProvider/AndroidManifest.xml
@@ -13,7 +13,20 @@
             android:permission="android.permission.MANAGE_DOCUMENTS">
             <meta-data
                 android:name="android.content.DOCUMENT_PROVIDER"
-                android:resource="@xml/document_provider" />
+                android:value="true" />
+        </provider>
+
+        <!-- TODO: find a better place for tests to live -->
+        <provider
+            android:name=".TestDocumentsProvider"
+            android:authorities="com.example.documents"
+            android:grantUriPermissions="true"
+            android:exported="true"
+            android:permission="android.permission.MANAGE_DOCUMENTS"
+            android:enabled="false">
+            <meta-data
+                android:name="android.content.DOCUMENT_PROVIDER"
+                android:value="true" />
         </provider>
     </application>
 </manifest>
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
new file mode 100644
index 0000000..872974f
--- /dev/null
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
@@ -0,0 +1,244 @@
+/*
+ * 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.ContentResolver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.lang.ref.WeakReference;
+
+public class TestDocumentsProvider extends DocumentsProvider {
+    private static final String TAG = "TestDocuments";
+
+    private static final boolean CRASH_ROOTS = false;
+    private static final boolean CRASH_DOCUMENT = false;
+
+    private static final String MY_ROOT_ID = "myRoot";
+    private static final String MY_DOC_ID = "myDoc";
+    private static final String MY_DOC_NULL = "myNull";
+
+    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
+            Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_TYPE, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
+            Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
+            Root.COLUMN_AVAILABLE_BYTES,
+    };
+
+    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
+            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
+    };
+
+    private static String[] resolveRootProjection(String[] projection) {
+        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
+    }
+
+    private static String[] resolveDocumentProjection(String[] projection) {
+        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
+    }
+
+    @Override
+    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+        if (CRASH_ROOTS) System.exit(12);
+
+        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
+        final RowBuilder row = result.newRow();
+        row.offer(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
+        row.offer(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SERVICE);
+        row.offer(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS);
+        row.offer(Root.COLUMN_TITLE, "_Test title which is really long");
+        row.offer(Root.COLUMN_SUMMARY, "_Summary which is also super long text");
+        row.offer(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID);
+        row.offer(Root.COLUMN_AVAILABLE_BYTES, 1024);
+        return result;
+    }
+
+    @Override
+    public Cursor queryDocument(String documentId, String[] projection)
+            throws FileNotFoundException {
+        if (CRASH_DOCUMENT) System.exit(12);
+
+        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+        includeFile(result, documentId);
+        return result;
+    }
+
+    /**
+     * Holds any outstanding or finished "network" fetching.
+     */
+    private WeakReference<CloudTask> mTask;
+
+    private static class CloudTask implements Runnable {
+
+        private final ContentResolver mResolver;
+        private final Uri mNotifyUri;
+
+        private volatile boolean mFinished;
+
+        public CloudTask(ContentResolver resolver, Uri notifyUri) {
+            mResolver = resolver;
+            mNotifyUri = notifyUri;
+        }
+
+        @Override
+        public void run() {
+            // Pretend to do some network
+            Log.d(TAG, hashCode() + ": pretending to do some network!");
+            SystemClock.sleep(2000);
+            Log.d(TAG, hashCode() + ": network done!");
+
+            mFinished = true;
+
+            // Tell anyone remotely they should requery
+            mResolver.notifyChange(mNotifyUri, null, false);
+        }
+
+        public boolean includeIfFinished(MatrixCursor result) {
+            Log.d(TAG, hashCode() + ": includeIfFinished() found " + mFinished);
+            if (mFinished) {
+                includeFile(result, "_networkfile1");
+                includeFile(result, "_networkfile2");
+                includeFile(result, "_networkfile3");
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    private static class CloudCursor extends MatrixCursor {
+        public Object keepAlive;
+        public final Bundle extras = new Bundle();
+
+        public CloudCursor(String[] columnNames) {
+            super(columnNames);
+        }
+
+        @Override
+        public Bundle getExtras() {
+            return extras;
+        }
+    }
+
+    @Override
+    public Cursor queryChildDocuments(
+            String parentDocumentId, String[] projection, String sortOrder)
+            throws FileNotFoundException {
+
+        final ContentResolver resolver = getContext().getContentResolver();
+        final Uri notifyUri = DocumentsContract.buildDocumentUri(
+                "com.example.documents", parentDocumentId);
+
+        CloudCursor result = new CloudCursor(resolveDocumentProjection(projection));
+        result.setNotificationUri(resolver, notifyUri);
+
+        // Always include local results
+        includeFile(result, MY_DOC_NULL);
+        includeFile(result, "localfile1");
+        includeFile(result, "localfile2");
+
+        synchronized (this) {
+            // Try picking up an existing network fetch
+            CloudTask task = mTask != null ? mTask.get() : null;
+            if (task == null) {
+                Log.d(TAG, "No network task found; starting!");
+                task = new CloudTask(resolver, notifyUri);
+                mTask = new WeakReference<CloudTask>(task);
+                new Thread(task).start();
+
+                // Aggressively try freeing weak reference above
+                new Thread() {
+                    @Override
+                    public void run() {
+                        while (mTask.get() != null) {
+                            SystemClock.sleep(200);
+                            System.gc();
+                            System.runFinalization();
+                        }
+                        Log.d(TAG, "AHA! THE CLOUD TASK WAS GC'ED!");
+                    }
+                }.start();
+            }
+
+            // Blend in cloud results if ready
+            if (task.includeIfFinished(result)) {
+                result.extras.putString(DocumentsContract.EXTRA_INFO,
+                        "Everything Went Better Than Expected and this message is quite "
+                                + "long and verbose and maybe even too long");
+                result.extras.putString(DocumentsContract.EXTRA_ERROR,
+                        "But then again, maybe our server ran into an error, which means "
+                                + "we're going to have a bad time");
+            } else {
+                result.extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
+            }
+
+            // Tie the network fetch to the cursor GC lifetime
+            result.keepAlive = task;
+
+            return result;
+        }
+    }
+
+    @Override
+    public Cursor queryRecentDocuments(String rootId, String[] projection)
+            throws FileNotFoundException {
+        // Pretend to take a super long time to respond
+        SystemClock.sleep(3000);
+
+        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+        includeFile(result, "It was /worth/ the_wait for?the file:with the&incredibly long name");
+        return result;
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        throw new FileNotFoundException();
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    private static void includeFile(MatrixCursor result, String docId) {
+        final RowBuilder row = result.newRow();
+        row.offer(Document.COLUMN_DOCUMENT_ID, docId);
+        row.offer(Document.COLUMN_DISPLAY_NAME, docId);
+        row.offer(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
+
+        if (MY_DOC_ID.equals(docId)) {
+            row.offer(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
+        } else if (MY_DOC_NULL.equals(docId)) {
+            // No MIME type
+        } else {
+            row.offer(Document.COLUMN_MIME_TYPE, "application/octet-stream");
+        }
+    }
+}