Add metadata support to MTP docs provider.
Remove convenience method from DocumentsProvider, clients to use MetadataReader directly. Concentrate mimetype checking in MetadataReader.isSupportedType. Update FileSystemProvider to use MetadataReader directly. Test: Updated mtp tests. Other functioanlity manually verified. Change-Id: Ie1e3d3092b53107f6c980c18b1451290dd2a9653
This commit is contained in:
@@ -66,8 +66,6 @@ import android.util.Log;
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -634,30 +632,6 @@ public abstract class DocumentsProvider extends ContentProvider {
|
||||
throw new UnsupportedOperationException("Metadata not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns metadata for arbitrary file given its stream and mimetype.
|
||||
*
|
||||
* <p><b>Note: Providers should only call this with streams for locally cached resources.
|
||||
* Use of network backed streams is inadvisable for performance reasons.
|
||||
*
|
||||
* @param stream The input stream. Should be backed by locally cached content.
|
||||
* Client retains ownership of the stream.
|
||||
* @param mimeType The mime type of the file.
|
||||
* @param tags The list of tags to load, if known. Pass null to get a default set.
|
||||
* @return Bundle containing any metadata found.
|
||||
* @throws IOException in the event of an error reading metadata.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
protected Bundle getDocumentMetadataFromStream(InputStream stream, String mimeType)
|
||||
throws IOException {
|
||||
Bundle metadata = new Bundle();
|
||||
// TODO: Remove the last null arg from MetadataReader. It was the "tags" value,
|
||||
// the has been removed from the getDocumentMetadata method.
|
||||
MetadataReader.getMetadata(metadata, stream, mimeType, null);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return concrete MIME type of the requested document. Must match the value
|
||||
* of {@link Document#COLUMN_MIME_TYPE} for this document. The default
|
||||
|
||||
@@ -36,29 +36,31 @@ import java.util.Map;
|
||||
*/
|
||||
public final class MetadataReader {
|
||||
|
||||
private MetadataReader() {
|
||||
}
|
||||
private MetadataReader() {}
|
||||
|
||||
private static final String[] DEFAULT_EXIF_TAGS = {
|
||||
ExifInterface.TAG_IMAGE_WIDTH,
|
||||
ExifInterface.TAG_IMAGE_LENGTH,
|
||||
ExifInterface.TAG_APERTURE,
|
||||
ExifInterface.TAG_COPYRIGHT,
|
||||
ExifInterface.TAG_DATETIME,
|
||||
ExifInterface.TAG_EXPOSURE_TIME,
|
||||
ExifInterface.TAG_F_NUMBER,
|
||||
ExifInterface.TAG_GPS_LATITUDE,
|
||||
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||
ExifInterface.TAG_GPS_LONGITUDE,
|
||||
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||
ExifInterface.TAG_IMAGE_WIDTH,
|
||||
ExifInterface.TAG_IMAGE_LENGTH,
|
||||
ExifInterface.TAG_MAKE,
|
||||
ExifInterface.TAG_MODEL,
|
||||
ExifInterface.TAG_APERTURE,
|
||||
ExifInterface.TAG_SHUTTER_SPEED_VALUE
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
|
||||
};
|
||||
|
||||
private static final Map<String, Integer> TYPE_MAPPING = new HashMap<>();
|
||||
private static final String[] ALL_KNOWN_EXIF_KEYS;
|
||||
private static final int TYPE_INT = 0;
|
||||
private static final int TYPE_DOUBLE = 1;
|
||||
private static final int TYPE_STRING = 2;
|
||||
|
||||
private static final Map<String, Integer> TYPE_MAPPING = new HashMap<>();
|
||||
static {
|
||||
// TODO: Move this over to ExifInterface.java
|
||||
// Since each ExifInterface item has a type, and there's currently no way to get the type
|
||||
@@ -198,11 +200,19 @@ public final class MetadataReader {
|
||||
TYPE_MAPPING.put(ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, TYPE_INT);
|
||||
TYPE_MAPPING.put(ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, TYPE_INT);
|
||||
TYPE_MAPPING.put(ExifInterface.TAG_RW2_ISO, TYPE_INT);
|
||||
ALL_KNOWN_EXIF_KEYS = TYPE_MAPPING.keySet().toArray(new String[TYPE_MAPPING.size()]);
|
||||
}
|
||||
private static final String JPG_MIME_TYPE = "image/jpg";
|
||||
private static final String JPEG_MIME_TYPE = "image/jpeg";
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if caller can generally expect to get metadata results
|
||||
* for the supplied mimetype.
|
||||
*/
|
||||
public static boolean isSupportedMimeType(String mimeType) {
|
||||
return JPG_MIME_TYPE.equals(mimeType) || JPEG_MIME_TYPE.equals(mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic metadata retrieval method that can retrieve any available metadata from a given doc
|
||||
* Currently only functions for exifdata
|
||||
@@ -228,10 +238,9 @@ public final class MetadataReader {
|
||||
*/
|
||||
public static void getMetadata(Bundle metadata, InputStream stream, String mimeType,
|
||||
@Nullable String[] tags) throws IOException {
|
||||
List<String> metadataTypes = new ArrayList();
|
||||
if (mimeType.equals(JPG_MIME_TYPE) || mimeType.equals(JPEG_MIME_TYPE)) {
|
||||
ExifInterface exifInterface = new ExifInterface(stream);
|
||||
Bundle exifData = getExifData(exifInterface, tags);
|
||||
List<String> metadataTypes = new ArrayList<>();
|
||||
if (isSupportedMimeType(mimeType)) {
|
||||
Bundle exifData = getExifData(stream, tags);
|
||||
if (exifData.size() > 0) {
|
||||
metadata.putBundle(DocumentsContract.METADATA_EXIF, exifData);
|
||||
metadataTypes.add(DocumentsContract.METADATA_EXIF);
|
||||
@@ -246,8 +255,7 @@ public final class MetadataReader {
|
||||
/**
|
||||
* Helper method that is called if getMetadata is called for an image mimeType.
|
||||
*
|
||||
* @param exif the bundle to which we add exif data.
|
||||
* @param exifInterface an ExifInterface for an image
|
||||
* @param stream the input stream from which to extra data.
|
||||
* @param tags a list of ExifInterface tags that are used to retrieve data.
|
||||
* if null, returns a default set of data from the following keys:
|
||||
* ExifInterface.TAG_IMAGE_WIDTH,
|
||||
@@ -262,11 +270,13 @@ public final class MetadataReader {
|
||||
* ExifInterface.TAG_APERTURE,
|
||||
* ExifInterface.TAG_SHUTTER_SPEED_VALUE
|
||||
*/
|
||||
private static Bundle getExifData(ExifInterface exifInterface, @Nullable String[] tags)
|
||||
private static Bundle getExifData(InputStream stream, @Nullable String[] tags)
|
||||
throws IOException {
|
||||
if (tags == null) {
|
||||
tags = DEFAULT_EXIF_TAGS;
|
||||
}
|
||||
|
||||
ExifInterface exifInterface = new ExifInterface(stream);
|
||||
Bundle exif = new Bundle();
|
||||
for (String tag : tags) {
|
||||
if (TYPE_MAPPING.get(tag).equals(TYPE_INT)) {
|
||||
|
||||
@@ -28,7 +28,6 @@ import android.database.MatrixCursor.RowBuilder;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.FileObserver;
|
||||
@@ -39,6 +38,7 @@ import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.MetadataReader;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
@@ -52,6 +52,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -120,26 +121,31 @@ public abstract class FileSystemProvider extends DocumentsProvider {
|
||||
|
||||
if (!file.isFile()) {
|
||||
Log.w(TAG, "Can't stream non-regular file. Returning empty metadata.");
|
||||
return Bundle.EMPTY;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!file.canRead()) {
|
||||
Log.w(TAG, "Can't stream non-readable file. Returning empty metadata.");
|
||||
return Bundle.EMPTY;
|
||||
return null;
|
||||
}
|
||||
|
||||
String filePath = file.getAbsolutePath();
|
||||
FileInputStream stream = new FileInputStream(filePath);
|
||||
String mimeType = getTypeForFile(file);
|
||||
if (!MetadataReader.isSupportedMimeType(mimeType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream stream = null;
|
||||
try {
|
||||
return getDocumentMetadataFromStream(stream, getTypeForFile(file));
|
||||
Bundle metadata = new Bundle();
|
||||
stream = new FileInputStream(file.getAbsolutePath());
|
||||
MetadataReader.getMetadata(metadata, stream, mimeType, null);
|
||||
return metadata;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "An error occurred retrieving the metadata", e);
|
||||
return null;
|
||||
} finally {
|
||||
IoUtils.closeQuietly(stream);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected final List<String> findDocumentPath(File parent, File doc)
|
||||
@@ -502,7 +508,7 @@ public abstract class FileSystemProvider extends DocumentsProvider {
|
||||
}
|
||||
|
||||
protected boolean typeSupportsMetadata(String mimeType) {
|
||||
return MIMETYPE_JPG.equals(mimeType) || MIMETYPE_JPEG.equals(mimeType);
|
||||
return MetadataReader.isSupportedMimeType(mimeType);
|
||||
}
|
||||
|
||||
private static String getTypeForName(String name) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import android.mtp.MtpConstants;
|
||||
import android.mtp.MtpObjectInfo;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MetadataReader;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
|
||||
@@ -900,6 +901,9 @@ class MtpDatabase {
|
||||
protectionState == MtpConstants.PROTECTION_STATUS_NONE) {
|
||||
flag |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
}
|
||||
if (MetadataReader.isSupportedMimeType(mimeType)) {
|
||||
flag |= Document.FLAG_SUPPORTS_METADATA;
|
||||
}
|
||||
if (thumbnailSize > 0) {
|
||||
flag |= Document.FLAG_SUPPORTS_THUMBNAIL;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.mtp;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
@@ -37,11 +38,12 @@ import android.os.FileUtils;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ProxyFileDescriptorCallback;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Path;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.provider.MetadataReader;
|
||||
import android.provider.Settings;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.OsConstants;
|
||||
@@ -50,14 +52,16 @@ import android.util.Log;
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
/**
|
||||
* DocumentsProvider for MTP devices.
|
||||
@@ -107,7 +111,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
mResources = getContext().getResources();
|
||||
mMtpManager = new MtpManager(getContext());
|
||||
mResolver = getContext().getContentResolver();
|
||||
mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
|
||||
mDeviceToolkits = new HashMap<>();
|
||||
mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
|
||||
mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
|
||||
mIntentSender = new ServiceIntentSender(getContext());
|
||||
@@ -151,7 +155,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
mResources = resources;
|
||||
mMtpManager = mtpManager;
|
||||
mResolver = resolver;
|
||||
mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
|
||||
mDeviceToolkits = new HashMap<>();
|
||||
mDatabase = database;
|
||||
mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
|
||||
mIntentSender = intentSender;
|
||||
@@ -548,6 +552,29 @@ public class MtpDocumentsProvider extends DocumentsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException {
|
||||
String mimeType = getDocumentType(docId);
|
||||
|
||||
if (!MetadataReader.isSupportedMimeType(mimeType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = new ParcelFileDescriptor.AutoCloseInputStream(
|
||||
openDocument(docId, "r", null));
|
||||
Bundle metadata = new Bundle();
|
||||
MetadataReader.getMetadata(metadata, stream, mimeType, null);
|
||||
return metadata;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "An error occurred retrieving the metadata", e);
|
||||
return null;
|
||||
} finally {
|
||||
IoUtils.closeQuietly(stream);
|
||||
}
|
||||
}
|
||||
|
||||
void openDevice(int deviceId) throws IOException {
|
||||
synchronized (mDeviceListLock) {
|
||||
if (mDeviceToolkits.containsKey(deviceId)) {
|
||||
|
||||
@@ -237,7 +237,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
|
||||
assertEquals(
|
||||
COLUMN_FLAGS,
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_WRITE,
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_METADATA,
|
||||
cursor.getInt(9));
|
||||
assertEquals(2 * 1024 * 1024, getInt(cursor, COLUMN_SIZE));
|
||||
assertEquals(
|
||||
|
||||
@@ -329,7 +329,8 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
|
||||
assertEquals(
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL,
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_METADATA,
|
||||
cursor.getInt(4));
|
||||
assertEquals(1024 * 1024 * 5, cursor.getInt(5));
|
||||
}
|
||||
@@ -474,7 +475,8 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
|
||||
assertEquals("image/jpeg", cursor.getString(1));
|
||||
assertEquals("image.jpg", cursor.getString(2));
|
||||
assertEquals(0, cursor.getLong(3));
|
||||
assertEquals(Document.FLAG_SUPPORTS_THUMBNAIL, cursor.getInt(4));
|
||||
assertEquals(Document.FLAG_SUPPORTS_THUMBNAIL
|
||||
| Document.FLAG_SUPPORTS_METADATA, cursor.getInt(4));
|
||||
assertEquals(1024 * 1024 * 5, cursor.getInt(5));
|
||||
|
||||
cursor.close();
|
||||
|
||||
Reference in New Issue
Block a user