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

This commit is contained in:
Jeff Sharkey
2013-09-04 04:34:17 +00:00
committed by Android (Google) Code Review
9 changed files with 517 additions and 6 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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> {

View File

@@ -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);

View File

@@ -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.
@@ -62,6 +63,11 @@ public class RootCursorWrapper extends AbstractCursor {
mColumnNames[mRootIdIndex] = COLUMN_ROOT_ID;
}
@Override
public Bundle getExtras() {
return mCursor.getExtras();
}
@Override
public void close() {
super.close();
@@ -128,5 +134,4 @@ public class RootCursorWrapper extends AbstractCursor {
public boolean isNull(int column) {
return mCursor.isNull(column);
}
}

View File

@@ -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;
/**
@@ -95,6 +96,11 @@ public class SortingCursorWrapper extends AbstractCursor {
}
}
@Override
public Bundle getExtras() {
return mCursor.getExtras();
}
@Override
public void close() {
super.close();

View File

@@ -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>

View File

@@ -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");
}
}
}