Merge "Resized thumbnails; async; extend MatrixCursor." into klp-dev

This commit is contained in:
Jeff Sharkey
2013-08-18 20:53:29 +00:00
committed by Android (Google) Code Review
9 changed files with 307 additions and 70 deletions

View File

@@ -8052,6 +8052,7 @@ package android.database {
public class MatrixCursor.RowBuilder {
method public android.database.MatrixCursor.RowBuilder add(java.lang.Object);
method public android.database.MatrixCursor.RowBuilder offer(java.lang.String, java.lang.Object);
}
public class MergeCursor extends android.database.AbstractCursor {

View File

@@ -83,11 +83,10 @@ public class MatrixCursor extends AbstractCursor {
* row
*/
public RowBuilder newRow() {
rowCount++;
int endIndex = rowCount * columnCount;
final int row = rowCount++;
final int endIndex = rowCount * columnCount;
ensureCapacity(endIndex);
int start = endIndex - columnCount;
return new RowBuilder(start, endIndex);
return new RowBuilder(row);
}
/**
@@ -180,18 +179,29 @@ public class MatrixCursor extends AbstractCursor {
}
/**
* Builds a row, starting from the left-most column and adding one column
* value at a time. Follows the same ordering as the column names specified
* at cursor construction time.
* Builds a row of values using either of these approaches:
* <ul>
* <li>Values can be added with explicit column ordering using
* {@link #add(Object)}, which starts from the left-most column and adds one
* column value at a time. This follows the same ordering as the column
* names specified at cursor construction time.
* <li>Column and value pairs can be offered for possible inclusion using
* {@link #offer(String, Object)}. If the cursor includes the given column,
* the value will be set for that column, otherwise the value is ignored.
* This approach is useful when matching data to a custom projection.
* </ul>
* Undefined values are left as {@code null}.
*/
public class RowBuilder {
private int index;
private final int row;
private final int endIndex;
RowBuilder(int index, int endIndex) {
this.index = index;
this.endIndex = endIndex;
private int index;
RowBuilder(int row) {
this.row = row;
this.index = row * columnCount;
this.endIndex = index + columnCount;
}
/**
@@ -210,6 +220,21 @@ public class MatrixCursor extends AbstractCursor {
data[index++] = columnValue;
return this;
}
/**
* Offer value for possible inclusion if this cursor defines the given
* column. Columns not defined by the cursor are silently ignored.
*
* @return this builder to support chaining
*/
public RowBuilder offer(String columnName, Object value) {
for (int i = 0; i < columnNames.length; i++) {
if (columnName.equals(columnNames[i])) {
data[(row * columnCount) + i] = value;
}
}
return this;
}
}
// AbstractCursor implementation.

View File

@@ -36,6 +36,7 @@ import com.google.android.collect.Lists;
import libcore.io.IoUtils;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@@ -461,16 +462,27 @@ public final class DocumentsContract {
final Bundle opts = new Bundle();
opts.putParcelable(EXTRA_THUMBNAIL_SIZE, size);
InputStream is = null;
AssetFileDescriptor afd = null;
try {
is = new AssetFileDescriptor.AutoCloseInputStream(
resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts));
return BitmapFactory.decodeStream(is);
afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts);
final FileDescriptor fd = afd.getFileDescriptor();
final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options();
bitmapOpts.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts);
final int widthSample = bitmapOpts.outWidth / size.x;
final int heightSample = bitmapOpts.outHeight / size.y;
bitmapOpts.inJustDecodeBounds = false;
bitmapOpts.inSampleSize = Math.min(widthSample, heightSample);
return BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts);
} catch (IOException e) {
Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
return null;
} finally {
IoUtils.closeQuietly(is);
IoUtils.closeQuietly(afd);
}
}

View File

@@ -19,8 +19,8 @@ package android.provider;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
@@ -532,7 +532,8 @@ public final class MediaStore {
private static final Object sThumbBufLock = new Object();
private static byte[] sThumbBuf;
private static Bitmap getMiniThumbFromFile(Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) {
private static Bitmap getMiniThumbFromFile(
Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) {
Bitmap bitmap = null;
Uri thumbUri = null;
try {
@@ -577,6 +578,7 @@ public final class MediaStore {
if (c != null) c.close();
}
}
/**
* This method ensure thumbnails associated with origId are generated and decode the byte
* stream from database (MICRO_KIND) or file (MINI_KIND).

View File

@@ -128,6 +128,56 @@ public class MatrixCursorTest extends TestCase {
} catch (IllegalArgumentException e) { /* expected */ }
}
public void testRowBuilderOffer() {
MatrixCursor cursor = newMatrixCursor();
cursor.newRow()
.offer("float", 4.2f)
.offer("string", "foobar")
.offer("blob", new byte[] {(byte) 0xaa, (byte) 0x55})
.offer("lolwat", "kittens");
cursor.newRow();
cursor.newRow()
.offer("string", "zero")
.offer("string", "one")
.offer("string", "two")
.offer("lolwat", "kittens");
assertTrue(cursor.moveToFirst());
assertEquals("foobar", cursor.getString(0));
assertEquals(null, cursor.getString(1));
assertEquals(0, cursor.getShort(1));
assertEquals(0, cursor.getInt(2));
assertEquals(0, cursor.getLong(3));
assertEquals(4.2f, cursor.getFloat(4));
assertEquals(0.0d, cursor.getDouble(5));
MoreAsserts.assertEquals(new byte[] {(byte) 0xaa, (byte) 0x55}, cursor.getBlob(6));
assertTrue(cursor.moveToNext());
assertEquals(null, cursor.getString(0));
assertEquals(0, cursor.getShort(1));
assertEquals(0, cursor.getInt(2));
assertEquals(0, cursor.getLong(3));
assertEquals(0.0f, cursor.getFloat(4));
assertEquals(0.0d, cursor.getDouble(5));
assertEquals(null, cursor.getBlob(6));
assertTrue(cursor.moveToNext());
assertEquals("two", cursor.getString(0));
assertEquals(0, cursor.getShort(1));
assertEquals(0, cursor.getInt(2));
assertEquals(0, cursor.getLong(3));
assertEquals(0.0f, cursor.getFloat(4));
assertEquals(0.0d, cursor.getDouble(5));
assertEquals(null, cursor.getBlob(6));
assertTrue(cursor.isLast());
assertFalse(cursor.moveToNext());
assertTrue(cursor.isAfterLast());
}
static class NonIterableArrayList<T> extends ArrayList<T> {
NonIterableArrayList() {}

View File

@@ -16,17 +16,24 @@
package com.android.documentsui;
import static com.android.documentsui.DocumentsActivity.TAG;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Loader;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.text.format.DateUtils;
import android.text.format.Formatter;
import android.text.format.Time;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.LayoutInflater;
@@ -75,6 +82,8 @@ public class DirectoryFragment extends Fragment {
private int mType = TYPE_NORMAL;
private Point mThumbSize;
private DocumentsAdapter mAdapter;
private LoaderCallbacks<List<Document>> mCallbacks;
@@ -217,7 +226,9 @@ public class DirectoryFragment extends Fragment {
choiceMode = ListView.CHOICE_MODE_NONE;
}
final int thumbSize;
if (state.mode == DisplayState.MODE_GRID) {
thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
mListView.setAdapter(null);
mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
mGridView.setAdapter(mAdapter);
@@ -226,6 +237,7 @@ public class DirectoryFragment extends Fragment {
mGridView.setChoiceMode(choiceMode);
mCurrentView = mGridView;
} else if (state.mode == DisplayState.MODE_LIST) {
thumbSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
mGridView.setAdapter(null);
mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
mListView.setAdapter(mAdapter);
@@ -234,6 +246,8 @@ public class DirectoryFragment extends Fragment {
} else {
throw new IllegalStateException();
}
mThumbSize = new Point(thumbSize, thumbSize);
}
private OnItemClickListener mItemListener = new OnItemClickListener() {
@@ -349,9 +363,21 @@ public class DirectoryFragment extends Fragment {
final TextView date = (TextView) convertView.findViewById(R.id.date);
final TextView size = (TextView) convertView.findViewById(R.id.size);
final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) icon.getTag();
if (oldTask != null) {
oldTask.cancel(false);
}
if (doc.isThumbnailSupported()) {
// TODO: load thumbnails async
icon.setImageURI(doc.uri);
final Bitmap cachedResult = ThumbnailCache.get(context).get(doc.uri);
if (cachedResult != null) {
icon.setImageBitmap(cachedResult);
} else {
final ThumbnailAsyncTask task = new ThumbnailAsyncTask(icon, mThumbSize);
icon.setImageBitmap(null);
icon.setTag(task);
task.execute(doc.uri);
}
} else {
icon.setImageDrawable(RootsCache.resolveDocumentIcon(
context, doc.uri.getAuthority(), doc.mimeType));
@@ -380,10 +406,11 @@ public class DirectoryFragment extends Fragment {
(summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE);
}
// TODO: omit year from format
date.setText(DateUtils.formatSameDayTime(
doc.lastModified, System.currentTimeMillis(), DateFormat.SHORT,
DateFormat.SHORT));
if (doc.lastModified == -1) {
date.setText(null);
} else {
date.setText(formatTime(context, doc.lastModified));
}
if (state.showSize) {
size.setVisibility(View.VISIBLE);
@@ -414,4 +441,66 @@ public class DirectoryFragment extends Fragment {
return getItem(position).uri.hashCode();
}
}
private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> {
private final ImageView mTarget;
private final Point mSize;
public ThumbnailAsyncTask(ImageView target, Point size) {
mTarget = target;
mSize = size;
}
@Override
protected void onPreExecute() {
mTarget.setTag(this);
}
@Override
protected Bitmap doInBackground(Uri... params) {
final Context context = mTarget.getContext();
final Uri uri = params[0];
Bitmap result = null;
try {
result = DocumentsContract.getThumbnail(
context.getContentResolver(), uri, mSize);
if (result != null) {
ThumbnailCache.get(context).put(uri, result);
}
} catch (Exception e) {
Log.w(TAG, "Failed to load thumbnail: " + e);
}
return result;
}
@Override
protected void onPostExecute(Bitmap result) {
if (mTarget.getTag() == this) {
mTarget.setImageBitmap(result);
mTarget.setTag(null);
}
}
}
private static String formatTime(Context context, long when) {
// TODO: DateUtils should make this easier
Time then = new Time();
then.set(when);
Time now = new Time();
now.setToNow();
int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
| DateUtils.FORMAT_ABBREV_ALL;
if (then.year != now.year) {
flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
} else if (then.yearDay != now.yearDay) {
flags |= DateUtils.FORMAT_SHOW_DATE;
} else {
flags |= DateUtils.FORMAT_SHOW_TIME;
}
return DateUtils.formatDateTime(context, when, flags);
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.documentsui;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.LruCache;
public class ThumbnailCache extends LruCache<Uri, Bitmap> {
private static ThumbnailCache sCache;
public static ThumbnailCache get(Context context) {
if (sCache == null) {
final ActivityManager am = (ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE);
final int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;
sCache = new ThumbnailCache(memoryClassBytes / 4);
}
return sCache;
}
public ThumbnailCache(int maxSizeBytes) {
super(maxSizeBytes);
}
@Override
protected int sizeOf(Uri key, Bitmap value) {
return value.getByteCount();
}
}

View File

@@ -7,7 +7,7 @@
<application android:label="@string/app_label">
<provider
android:name=".ExternalStorageProvider"
android:authorities="com.android.externalstorage"
android:authorities="com.android.externalstorage.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">

View File

@@ -22,10 +22,10 @@ import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.provider.DocumentsContract.Documents;
@@ -45,7 +45,7 @@ import java.util.LinkedList;
public class ExternalStorageProvider extends ContentProvider {
private static final String TAG = "ExternalStorage";
private static final String AUTHORITY = "com.android.externalstorage";
private static final String AUTHORITY = "com.android.externalstorage.documents";
// TODO: support multiple storage devices
@@ -57,6 +57,14 @@ public class ExternalStorageProvider extends ContentProvider {
private static final int URI_DOCS_ID_CONTENTS = 4;
private static final int URI_DOCS_ID_SEARCH = 5;
static {
sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
}
private HashMap<String, Root> mRoots = Maps.newHashMap();
private static class Root {
@@ -68,13 +76,15 @@ public class ExternalStorageProvider extends ContentProvider {
public File path;
}
static {
sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
}
private static final String[] ALL_ROOTS_COLUMNS = new String[] {
RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
};
private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
};
@Override
public boolean onCreate() {
@@ -93,64 +103,59 @@ public class ExternalStorageProvider extends ContentProvider {
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// TODO: support custom projections
final String[] rootsProjection = new String[] {
BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
final String[] docsProjection = new String[] {
BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
DocumentColumns.FLAGS };
switch (sMatcher.match(uri)) {
case URI_ROOTS: {
final MatrixCursor cursor = new MatrixCursor(rootsProjection);
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_ROOTS_COLUMNS);
for (Root root : mRoots.values()) {
includeRoot(cursor, root);
includeRoot(result, root);
}
return cursor;
return result;
}
case URI_ROOTS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final MatrixCursor cursor = new MatrixCursor(rootsProjection);
includeRoot(cursor, root);
return cursor;
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_ROOTS_COLUMNS);
includeRoot(result, root);
return result;
}
case URI_DOCS_ID: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final MatrixCursor cursor = new MatrixCursor(docsProjection);
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
final File file = docIdToFile(root, docId);
includeFile(cursor, root, file);
return cursor;
includeFile(result, root, file);
return result;
}
case URI_DOCS_ID_CONTENTS: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final MatrixCursor cursor = new MatrixCursor(docsProjection);
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
final File parent = docIdToFile(root, docId);
for (File file : parent.listFiles()) {
includeFile(cursor, root, file);
includeFile(result, root, file);
}
return cursor;
return result;
}
case URI_DOCS_ID_SEARCH: {
final Root root = mRoots.get(DocumentsContract.getRootId(uri));
final String docId = DocumentsContract.getDocId(uri);
final String query = DocumentsContract.getSearchQuery(uri).toLowerCase();
final MatrixCursor cursor = new MatrixCursor(docsProjection);
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
final File parent = docIdToFile(root, docId);
final LinkedList<File> pending = new LinkedList<File>();
pending.add(parent);
while (!pending.isEmpty() && cursor.getCount() < 20) {
while (!pending.isEmpty() && result.getCount() < 20) {
final File file = pending.removeFirst();
if (file.isDirectory()) {
for (File child : file.listFiles()) {
@@ -158,12 +163,12 @@ public class ExternalStorageProvider extends ContentProvider {
}
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(cursor, root, file);
includeFile(result, root, file);
}
}
}
return cursor;
return result;
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
@@ -196,13 +201,17 @@ public class ExternalStorageProvider extends ContentProvider {
}
}
private void includeRoot(MatrixCursor cursor, Root root) {
cursor.addRow(new Object[] {
root.name.hashCode(), root.name, root.rootType, root.icon, root.title, root.summary,
root.path.getFreeSpace() });
private void includeRoot(MatrixCursor result, Root root) {
final RowBuilder row = result.newRow();
row.offer(RootColumns.ROOT_ID, root.name);
row.offer(RootColumns.ROOT_TYPE, root.rootType);
row.offer(RootColumns.ICON, root.icon);
row.offer(RootColumns.TITLE, root.title);
row.offer(RootColumns.SUMMARY, root.summary);
row.offer(RootColumns.AVAILABLE_BYTES, root.path.getFreeSpace());
}
private void includeFile(MatrixCursor cursor, Root root, File file) {
private void includeFile(MatrixCursor result, Root root, File file) {
int flags = 0;
if (file.isDirectory()) {
@@ -223,8 +232,6 @@ public class ExternalStorageProvider extends ContentProvider {
}
final String docId = fileToDocId(root, file);
final long id = docId.hashCode();
final String displayName;
if (Documents.DOC_ID_ROOT.equals(docId)) {
displayName = root.title;
@@ -232,8 +239,13 @@ public class ExternalStorageProvider extends ContentProvider {
displayName = file.getName();
}
cursor.addRow(new Object[] {
id, displayName, file.length(), docId, mimeType, file.lastModified(), flags });
final RowBuilder row = result.newRow();
row.offer(DocumentColumns.DOC_ID, docId);
row.offer(DocumentColumns.DISPLAY_NAME, displayName);
row.offer(DocumentColumns.SIZE, file.length());
row.offer(DocumentColumns.MIME_TYPE, mimeType);
row.offer(DocumentColumns.LAST_MODIFIED, file.lastModified());
row.offer(DocumentColumns.FLAGS, flags);
}
@Override