Query specific collections for properties.

We're now enforcing the public API schema, so clients like MTP need
to query specific collections when asking for properties that are
specific to those media types.

Also refactor MediaScanner calls to go through MediaProvider, so
they can be handled by ModernMediaScanner.  We no longer directly
try inserting items, and instead rely completely on the scanner.

Bug: 127625964, 122263824
Test: atest MediaProviderTests
Test: atest cts/tests/tests/provider/src/android/provider/cts/MediaStore*
Change-Id: Ic86982766606a619afafe725f9c60013a5671840
This commit is contained in:
Jeff Sharkey
2019-03-24 12:50:51 -06:00
parent 1c3ef21ab8
commit ff200957fd
4 changed files with 106 additions and 185 deletions

View File

@@ -136,6 +136,7 @@ public final class MediaStore {
* removing nomedia files
* @hide
*/
@Deprecated
public static final String UNHIDE_CALL = "unhide";
/**
@@ -3536,4 +3537,17 @@ public final class MediaStore {
throw new IOException("User " + user + " must be unlocked and running");
}
}
/** @hide */
public static Uri scanFile(Context context, File file) {
final ContentResolver resolver = context.getContentResolver();
try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
final Bundle in = new Bundle();
in.putParcelable(Intent.EXTRA_STREAM, Uri.fromFile(file));
final Bundle out = client.call(SCAN_FILE_CALL, null, in);
return out.getParcelable(Intent.EXTRA_STREAM);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2018 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.internal.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Denotes that the class, method or field has its visibility relaxed so
* that native code can access it.
*/
@Retention(RetentionPolicy.CLASS)
public @interface VisibleForNative {
}

View File

@@ -25,16 +25,13 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.media.MediaScanner;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Files;
import android.provider.MediaStore.MediaColumns;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
@@ -42,6 +39,8 @@ import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import com.android.internal.annotations.VisibleForNative;
import dalvik.system.CloseGuard;
import com.google.android.collect.Sets;
@@ -72,7 +71,6 @@ public class MtpDatabase implements AutoCloseable {
private final ContentProviderClient mMediaProvider;
private final String mVolumeName;
private final Uri mObjectsUri;
private final MediaScanner mMediaScanner;
private final AtomicBoolean mClosed = new AtomicBoolean();
private final CloseGuard mCloseGuard = CloseGuard.get();
@@ -159,7 +157,6 @@ public class MtpDatabase implements AutoCloseable {
MtpConstants.PROPERTY_TRACK,
MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
MtpConstants.PROPERTY_DURATION,
MtpConstants.PROPERTY_GENRE,
MtpConstants.PROPERTY_COMPOSER,
MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
MtpConstants.PROPERTY_BITRATE_TYPE,
@@ -187,6 +184,7 @@ public class MtpDatabase implements AutoCloseable {
MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
};
@VisibleForNative
private int[] getSupportedObjectProperties(int format) {
switch (format) {
case MtpConstants.FORMAT_MP3:
@@ -214,14 +212,41 @@ public class MtpDatabase implements AutoCloseable {
}
}
public static Uri getObjectPropertiesUri(int format, String volumeName) {
switch (format) {
case MtpConstants.FORMAT_MP3:
case MtpConstants.FORMAT_WAV:
case MtpConstants.FORMAT_WMA:
case MtpConstants.FORMAT_OGG:
case MtpConstants.FORMAT_AAC:
return MediaStore.Audio.Media.getContentUri(volumeName);
case MtpConstants.FORMAT_MPEG:
case MtpConstants.FORMAT_3GP_CONTAINER:
case MtpConstants.FORMAT_WMV:
return MediaStore.Video.Media.getContentUri(volumeName);
case MtpConstants.FORMAT_EXIF_JPEG:
case MtpConstants.FORMAT_GIF:
case MtpConstants.FORMAT_PNG:
case MtpConstants.FORMAT_BMP:
case MtpConstants.FORMAT_DNG:
case MtpConstants.FORMAT_HEIF:
return MediaStore.Images.Media.getContentUri(volumeName);
default:
return MediaStore.Files.getContentUri(volumeName);
}
}
@VisibleForNative
private int[] getSupportedDeviceProperties() {
return DEVICE_PROPERTIES;
}
@VisibleForNative
private int[] getSupportedPlaybackFormats() {
return PLAYBACK_FORMATS;
}
@VisibleForNative
private int[] getSupportedCaptureFormats() {
// no capture formats yet
return null;
@@ -254,7 +279,6 @@ public class MtpDatabase implements AutoCloseable {
.acquireContentProviderClient(MediaStore.AUTHORITY);
mVolumeName = volumeName;
mObjectsUri = Files.getMtpObjectsUri(volumeName);
mMediaScanner = new MediaScanner(context, mVolumeName);
mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
@Override
public void sendObjectAdded(int id) {
@@ -304,7 +328,6 @@ public class MtpDatabase implements AutoCloseable {
mManager.close();
mCloseGuard.close();
if (mClosed.compareAndSet(false, true)) {
mMediaScanner.close();
if (mMediaProvider != null) {
mMediaProvider.close();
}
@@ -380,6 +403,7 @@ public class MtpDatabase implements AutoCloseable {
}
}
@VisibleForNative
private int beginSendObject(String path, int format, int parent, int storageId) {
MtpStorageManager.MtpObject parentObj =
parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
@@ -391,6 +415,7 @@ public class MtpDatabase implements AutoCloseable {
return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
}
@VisibleForNative
private void endSendObject(int handle, boolean succeeded) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
if (obj == null || !mManager.endSendObject(obj, succeeded)) {
@@ -399,69 +424,16 @@ public class MtpDatabase implements AutoCloseable {
}
// Add the new file to MediaProvider
if (succeeded) {
String path = obj.getPath().toString();
int format = obj.getFormat();
// Get parent info from MediaProvider, since the id is different from MTP's
ContentValues values = new ContentValues();
values.put(Files.FileColumns.DATA, path);
values.put(Files.FileColumns.FORMAT, format);
values.put(Files.FileColumns.SIZE, obj.getSize());
values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
try {
if (obj.getParent().isRoot()) {
values.put(Files.FileColumns.PARENT, 0);
} else {
int parentId = findInMedia(obj.getParent().getPath());
if (parentId != -1) {
values.put(Files.FileColumns.PARENT, parentId);
} else {
// The parent isn't in MediaProvider. Don't add the new file.
return;
}
}
Uri uri = mMediaProvider.insert(mObjectsUri, values);
if (uri != null) {
rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
}
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in beginSendObject", e);
}
MediaStore.scanFile(mContext, obj.getPath().toFile());
}
}
@VisibleForNative
private void rescanFile(String path, int handle, int format) {
// handle abstract playlists separately
// they do not exist in the file system so don't use the media scanner here
if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
// extract name from path
String name = path;
int lastSlash = name.lastIndexOf('/');
if (lastSlash >= 0) {
name = name.substring(lastSlash + 1);
}
// strip trailing ".pla" from the name
if (name.endsWith(".pla")) {
name = name.substring(0, name.length() - 4);
}
ContentValues values = new ContentValues(1);
values.put(Audio.Playlists.DATA, path);
values.put(Audio.Playlists.NAME, name);
values.put(Files.FileColumns.FORMAT, format);
values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
try {
mMediaProvider.insert(
Audio.Playlists.EXTERNAL_CONTENT_URI, values);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in endSendObject", e);
}
} else {
mMediaScanner.scanMtpFile(path, handle, format);
}
MediaStore.scanFile(mContext, new File(path));
}
@VisibleForNative
private int[] getObjectList(int storageID, int format, int parent) {
List<MtpStorageManager.MtpObject> objs = mManager.getObjects(parent,
format, storageID);
@@ -475,6 +447,7 @@ public class MtpDatabase implements AutoCloseable {
return ret;
}
@VisibleForNative
private int getNumObjects(int storageID, int format, int parent) {
List<MtpStorageManager.MtpObject> objs = mManager.getObjects(parent,
format, storageID);
@@ -484,6 +457,7 @@ public class MtpDatabase implements AutoCloseable {
return objs.size();
}
@VisibleForNative
private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
int groupCode, int depth) {
// FIXME - implement group support
@@ -551,16 +525,16 @@ public class MtpDatabase implements AutoCloseable {
// format should be the same between get & put
propertyGroup = mPropertyGroupsByFormat.get(format);
if (propertyGroup == null) {
int[] propertyList = getSupportedObjectProperties(format);
final int[] propertyList = getSupportedObjectProperties(format);
propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
propertyList);
mPropertyGroupsByFormat.put(format, propertyGroup);
}
} else {
// Get this property value
final int[] propertyList = new int[]{property};
propertyGroup = mPropertyGroupsByProperty.get(property);
if (propertyGroup == null) {
final int[] propertyList = new int[]{property};
propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
propertyList);
mPropertyGroupsByProperty.put(property, propertyGroup);
@@ -616,28 +590,19 @@ public class MtpDatabase implements AutoCloseable {
if (obj.isDir()) {
// for directories, check if renamed from something hidden to something non-hidden
if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) {
// directory was unhidden
try {
mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null);
} catch (RemoteException e) {
Log.e(TAG, "failed to unhide/rescan for " + newPath);
}
MediaStore.scanFile(mContext, newPath.toFile());
}
} else {
// for files, check if renamed from .nomedia to something else
if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)
&& !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) {
try {
mMediaProvider.call(MediaStore.UNHIDE_CALL,
oldPath.getParent().toString(), null);
} catch (RemoteException e) {
Log.e(TAG, "failed to unhide/rescan for " + newPath);
}
MediaStore.scanFile(mContext, newPath.getParent().toFile());
}
}
return MtpConstants.RESPONSE_OK;
}
@VisibleForNative
private int beginMoveObject(int handle, int newParent, int newStorage) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
MtpStorageManager.MtpObject parent = newParent == 0 ?
@@ -649,6 +614,7 @@ public class MtpDatabase implements AutoCloseable {
return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
}
@VisibleForNative
private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
int objId, boolean success) {
MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
@@ -698,20 +664,14 @@ public class MtpDatabase implements AutoCloseable {
mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
} else {
// Old parent doesn't exist - add the object
values.put(Files.FileColumns.FORMAT, obj.getFormat());
values.put(Files.FileColumns.SIZE, obj.getSize());
values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
Uri uri = mMediaProvider.insert(mObjectsUri, values);
if (uri != null) {
rescanFile(path.toString(),
Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat());
}
MediaStore.scanFile(mContext, path.toFile());
}
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in mMediaProvider.update", e);
}
}
@VisibleForNative
private int beginCopyObject(int handle, int newParent, int newStorage) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
MtpStorageManager.MtpObject parent = newParent == 0 ?
@@ -721,6 +681,7 @@ public class MtpDatabase implements AutoCloseable {
return mManager.beginCopyObject(obj, parent);
}
@VisibleForNative
private void endCopyObject(int handle, boolean success) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
if (obj == null || !mManager.endCopyObject(obj, success)) {
@@ -730,39 +691,10 @@ public class MtpDatabase implements AutoCloseable {
if (!success) {
return;
}
String path = obj.getPath().toString();
int format = obj.getFormat();
// Get parent info from MediaProvider, since the id is different from MTP's
ContentValues values = new ContentValues();
values.put(Files.FileColumns.DATA, path);
values.put(Files.FileColumns.FORMAT, format);
values.put(Files.FileColumns.SIZE, obj.getSize());
values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
try {
if (obj.getParent().isRoot()) {
values.put(Files.FileColumns.PARENT, 0);
} else {
int parentId = findInMedia(obj.getParent().getPath());
if (parentId != -1) {
values.put(Files.FileColumns.PARENT, parentId);
} else {
// The parent isn't in MediaProvider. Don't add the new file.
return;
}
}
if (obj.isDir()) {
mMediaScanner.scanDirectories(new String[]{path});
} else {
Uri uri = mMediaProvider.insert(mObjectsUri, values);
if (uri != null) {
rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
}
}
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in beginSendObject", e);
}
MediaStore.scanFile(mContext, obj.getPath().toFile());
}
@VisibleForNative
private int setObjectProperty(int handle, int property,
long intValue, String stringValue) {
switch (property) {
@@ -774,6 +706,7 @@ public class MtpDatabase implements AutoCloseable {
}
}
@VisibleForNative
private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
switch (property) {
case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
@@ -809,6 +742,7 @@ public class MtpDatabase implements AutoCloseable {
}
}
@VisibleForNative
private int setDeviceProperty(int property, long intValue, String stringValue) {
switch (property) {
case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
@@ -823,6 +757,7 @@ public class MtpDatabase implements AutoCloseable {
return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
}
@VisibleForNative
private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
char[] outName, long[] outCreatedModified) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
@@ -842,6 +777,7 @@ public class MtpDatabase implements AutoCloseable {
return true;
}
@VisibleForNative
private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
if (obj == null) {
@@ -866,6 +802,7 @@ public class MtpDatabase implements AutoCloseable {
return obj.getFormat();
}
@VisibleForNative
private int beginDeleteObject(int handle) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
if (obj == null) {
@@ -877,6 +814,7 @@ public class MtpDatabase implements AutoCloseable {
return MtpConstants.RESPONSE_OK;
}
@VisibleForNative
private void endDeleteObject(int handle, boolean success) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
if (obj == null) {
@@ -922,12 +860,7 @@ public class MtpDatabase implements AutoCloseable {
String[] whereArgs = new String[]{path.toString()};
if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) {
if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) {
try {
String parentPath = path.getParent().toString();
mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null);
} catch (RemoteException e) {
Log.e(TAG, "failed to unhide/rescan for " + path);
}
MediaStore.scanFile(mContext, path.getParent().toFile());
}
} else {
Log.i(TAG, "Mediaprovider didn't delete " + path);
@@ -937,6 +870,7 @@ public class MtpDatabase implements AutoCloseable {
}
}
@VisibleForNative
private int[] getObjectReferences(int handle) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
if (obj == null)
@@ -972,6 +906,7 @@ public class MtpDatabase implements AutoCloseable {
return null;
}
@VisibleForNative
private int setObjectReferences(int handle, int[] references) {
MtpStorageManager.MtpObject obj = mManager.getObject(handle);
if (obj == null)
@@ -1004,7 +939,7 @@ public class MtpDatabase implements AutoCloseable {
return MtpConstants.RESPONSE_GENERAL_ERROR;
}
// used by the JNI code
@VisibleForNative
private long mNativeContext;
private native final void native_setup();

View File

@@ -48,7 +48,6 @@ class MtpPropertyGroup {
private final ContentProviderClient mProvider;
private final String mVolumeName;
private final Uri mUri;
// list of all properties in this group
private final Property[] mProperties;
@@ -62,7 +61,6 @@ class MtpPropertyGroup {
public MtpPropertyGroup(ContentProviderClient provider, String volumeName, int[] properties) {
mProvider = provider;
mVolumeName = volumeName;
mUri = Files.getMtpObjectsUri(volumeName);
int count = properties.length;
ArrayList<String> columns = new ArrayList<>(count);
@@ -139,10 +137,6 @@ class MtpPropertyGroup {
column = Audio.AudioColumns.ALBUM_ARTIST;
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_GENRE:
// genre requires a special query
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_COMPOSER:
column = Audio.AudioColumns.COMPOSER;
type = MtpConstants.TYPE_STR;
@@ -176,46 +170,6 @@ class MtpPropertyGroup {
}
}
private String queryAudio(String path, String column) {
Cursor c = null;
try {
c = mProvider.query(Audio.Media.getContentUri(mVolumeName),
new String [] { column },
PATH_WHERE, new String[] {path}, null, null);
if (c != null && c.moveToNext()) {
return c.getString(0);
} else {
return "";
}
} catch (Exception e) {
return "";
} finally {
if (c != null) {
c.close();
}
}
}
private String queryGenre(String path) {
Cursor c = null;
try {
c = mProvider.query(Audio.Genres.getContentUri(mVolumeName),
new String [] { Audio.GenresColumns.NAME },
PATH_WHERE, new String[] {path}, null, null);
if (c != null && c.moveToNext()) {
return c.getString(0);
} else {
return "";
}
} catch (Exception e) {
return "";
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Gets the values of the properties represented by this property group for the given
* object and adds them to the given property list.
@@ -229,12 +183,16 @@ class MtpPropertyGroup {
if (property.column != -1 && c == null) {
try {
// Look up the entry in MediaProvider only if one of those properties is needed.
c = mProvider.query(mUri, mColumns,
final Uri uri = MtpDatabase.getObjectPropertiesUri(object.getFormat(),
mVolumeName);
c = mProvider.query(uri, mColumns,
PATH_WHERE, new String[] {path}, null, null);
if (c != null && !c.moveToNext()) {
c.close();
c = null;
}
} catch (IllegalArgumentException e) {
return MtpConstants.RESPONSE_INVALID_OBJECT_PROP_CODE;
} catch (RemoteException e) {
Log.e(TAG, "Mediaprovider lookup failed");
}
@@ -290,20 +248,6 @@ class MtpPropertyGroup {
list.append(id, property.code, MtpConstants.TYPE_UINT16,
track % 1000);
break;
case MtpConstants.PROPERTY_ARTIST:
list.append(id, property.code,
queryAudio(path, Audio.AudioColumns.ARTIST));
break;
case MtpConstants.PROPERTY_ALBUM_NAME:
list.append(id, property.code,
queryAudio(path, Audio.AudioColumns.ALBUM));
break;
case MtpConstants.PROPERTY_GENRE:
String genre = queryGenre(path);
if (genre != null) {
list.append(id, property.code, genre);
}
break;
case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
case MtpConstants.PROPERTY_AUDIO_BITRATE:
case MtpConstants.PROPERTY_SAMPLE_RATE: