Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6398343e authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Return EXIF thumbnails when available.

Adds new ExifInterface method to extract the thumbnail range from
a larger image file, and use that to return an AssetFileDescriptor.

When decoding an AssetFileDescriptor thumbnail with offsets, read out
the raw data entirely, since Skia uses lseek() aggressively.

Bug: 10412208
Change-Id: I7906cdf82c0c3794cec7043c801a86f66efeb143
parent bd3b9025
Loading
Loading
Loading
Loading
+45 −11
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package android.provider;

import static android.net.TrafficStats.KB_IN_BYTES;
import static libcore.io.OsConstants.SEEK_SET;

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -36,7 +39,10 @@ import android.util.Log;

import com.google.android.collect.Lists;

import libcore.io.ErrnoException;
import libcore.io.IoBridge;
import libcore.io.IoUtils;
import libcore.io.Libcore;

import java.io.FileDescriptor;
import java.io.IOException;
@@ -527,25 +533,53 @@ public final class DocumentsContract {
     * @return decoded thumbnail, or {@code null} if problem was encountered.
     */
    public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) {
        final Bundle opts = new Bundle();
        opts.putParcelable(EXTRA_THUMBNAIL_SIZE, size);
        final Bundle openOpts = new Bundle();
        openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size);

        AssetFileDescriptor afd = null;
        try {
            afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts);
            afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts);

            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);
            final long offset = afd.getStartOffset();
            final long length = afd.getDeclaredLength();

            // Some thumbnails might be a region inside a larger file, such as
            // an EXIF thumbnail. Since BitmapFactory aggressively seeks around
            // the entire file, we read the region manually.
            byte[] region = null;
            if (offset > 0 && length <= 64 * KB_IN_BYTES) {
                region = new byte[(int) length];
                Libcore.os.lseek(fd, offset, SEEK_SET);
                if (IoBridge.read(fd, region, 0, region.length) != region.length) {
                    region = null;
                }
            }

            // We requested a rough thumbnail size, but the remote size may have
            // returned something giant, so defensively scale down as needed.
            final BitmapFactory.Options opts = new BitmapFactory.Options();
            opts.inJustDecodeBounds = true;
            if (region != null) {
                BitmapFactory.decodeByteArray(region, 0, region.length, opts);
            } else {
                BitmapFactory.decodeFileDescriptor(fd, null, opts);
            }

            final int widthSample = opts.outWidth / size.x;
            final int heightSample = opts.outHeight / size.y;

            opts.inJustDecodeBounds = false;
            opts.inSampleSize = Math.min(widthSample, heightSample);
            Log.d(TAG, "Decoding with sample size " + opts.inSampleSize);
            if (region != null) {
                return BitmapFactory.decodeByteArray(region, 0, region.length, opts);
            } else {
                return BitmapFactory.decodeFileDescriptor(fd, null, opts);
            }
        } catch (ErrnoException e) {
            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
            return null;
        } catch (IOException e) {
            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
            return null;
+16 −0
Original line number Diff line number Diff line
@@ -290,6 +290,20 @@ public class ExifInterface {
        }
    }

    /**
     * Returns the offset and length of thumbnail inside the JPEG file, or
     * {@code null} if there is no thumbnail.
     *
     * @return two-element array, the offset in the first value, and length in
     *         the second, or {@code null} if no thumbnail was found.
     * @hide
     */
    public long[] getThumbnailRange() {
        synchronized (sLock) {
            return getThumbnailRangeNative(mFilename);
        }
    }

    /**
     * Stores the latitude and longitude value in a float array. The first element is
     * the latitude, and the second element is the longitude. Returns false if the
@@ -416,4 +430,6 @@ public class ExifInterface {
    private native void commitChangesNative(String fileName);

    private native byte[] getThumbnailNative(String fileName);

    private native long[] getThumbnailRangeNative(String fileName);
}
+36 −1
Original line number Diff line number Diff line
@@ -20,10 +20,13 @@ import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
@@ -296,7 +299,6 @@ public class ExternalStorageProvider extends ContentProvider {
                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
                final String docId = DocumentsContract.getDocId(uri);

                // TODO: offer as thumbnail
                final File file = docIdToFile(root, docId);
                return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
            }
@@ -306,6 +308,39 @@ public class ExternalStorageProvider extends ContentProvider {
        }
    }

    @Override
    public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
            throws FileNotFoundException {
        if (opts == null || !opts.containsKey(DocumentsContract.EXTRA_THUMBNAIL_SIZE)) {
            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
        }

        switch (sMatcher.match(uri)) {
            case URI_DOCS_ID: {
                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
                final String docId = DocumentsContract.getDocId(uri);

                final File file = docIdToFile(root, docId);
                final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                        file, ParcelFileDescriptor.MODE_READ_ONLY);

                try {
                    final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
                    final long[] thumb = exif.getThumbnailRange();
                    if (thumb != null) {
                        return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
                    }
                } catch (IOException e) {
                }

                return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
            }
            default: {
                throw new UnsupportedOperationException("Unsupported Uri " + uri);
            }
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        switch (sMatcher.match(uri)) {