diff options
9 files changed, 517 insertions, 6 deletions
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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { @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 @@ public class DirectoryFragment extends Fragment { @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 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { .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 @@ package com.android.documentsui; 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 @@ public class RootCursorWrapper extends AbstractCursor { } @Override + public Bundle getExtras() { + return mCursor.getExtras(); + } + + @Override public void close() { super.close(); mCursor.close(); @@ -128,5 +134,4 @@ public class RootCursorWrapper extends AbstractCursor { 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 static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE; import android.database.AbstractCursor; import android.database.Cursor; +import android.os.Bundle; import android.provider.DocumentsContract.Document; /** @@ -96,6 +97,11 @@ public class SortingCursorWrapper extends AbstractCursor { } @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"); + } + } +} |