diff options
Diffstat (limited to 'media/java/android')
| -rw-r--r-- | media/java/android/media/AudioFormat.java | 4 | ||||
| -rw-r--r-- | media/java/android/media/AudioManager.java | 18 | ||||
| -rw-r--r-- | media/java/android/media/AudioRecord.java | 10 | ||||
| -rw-r--r-- | media/java/android/media/MediaFile.java | 91 | ||||
| -rw-r--r-- | media/java/android/media/MediaRecorder.java | 32 | ||||
| -rw-r--r-- | media/java/android/media/MediaScanner.java | 12 | ||||
| -rw-r--r-- | media/java/android/media/MtpClient.java | 104 | ||||
| -rw-r--r-- | media/java/android/media/MtpCursor.java | 223 | ||||
| -rw-r--r-- | media/java/android/media/MtpDatabase.java | 466 | ||||
| -rw-r--r-- | media/java/android/media/MtpServer.java | 67 |
10 files changed, 996 insertions, 31 deletions
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index f1fa1e8..31e4631 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -31,9 +31,9 @@ public class AudioFormat { public static final int ENCODING_INVALID = 0; /** Default audio data format */ public static final int ENCODING_DEFAULT = 1; - /** Audio data format: PCM 16 bit per sample */ + /** Audio data format: PCM 16 bit per sample. Guaranteed to be supported by devices. */ public static final int ENCODING_PCM_16BIT = 2; // accessed by native code - /** Audio data format: PCM 8 bit per sample */ + /** Audio data format: PCM 8 bit per sample. Not guaranteed to be supported by devices. */ public static final int ENCODING_PCM_8BIT = 3; // accessed by native code /** Invalid audio channel configuration */ diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index bbbba74..b23dcde 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -1525,4 +1525,22 @@ public class AudioManager { * {@hide} */ private IBinder mICallBack = new Binder(); + + /** + * Checks whether the phone is in silent mode, with or without vibrate. + * + * @return true if phone is in silent mode, with or without vibrate. + * + * @see #getRingerMode() + * + * @hide pending API Council approval + */ + public boolean isSilentMode() { + int ringerMode = getRingerMode(); + boolean silentMode = + (ringerMode == RINGER_MODE_SILENT) || + (ringerMode == RINGER_MODE_VIBRATE); + return silentMode; + } + } diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index c48eaad..c567a6e 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -194,11 +194,13 @@ public class AudioRecord * Class constructor. * @param audioSource the recording source. See {@link MediaRecorder.AudioSource} for * recording source definitions. - * @param sampleRateInHz the sample rate expressed in Hertz. Examples of rates are (but - * not limited to) 44100, 22050 and 11025. + * @param sampleRateInHz the sample rate expressed in Hertz. 44100Hz is currently the only + * rate that is guaranteed to work on all devices, but other rates such as 22050, + * 16000, and 11025 may work on some devices. * @param channelConfig describes the configuration of the audio channels. * See {@link AudioFormat#CHANNEL_IN_MONO} and - * {@link AudioFormat#CHANNEL_IN_STEREO} + * {@link AudioFormat#CHANNEL_IN_STEREO}. {@link AudioFormat#CHANNEL_IN_MONO} is guaranteed + * to work on all devices. * @param audioFormat the format in which the audio data is represented. * See {@link AudioFormat#ENCODING_PCM_16BIT} and * {@link AudioFormat#ENCODING_PCM_8BIT} @@ -444,6 +446,8 @@ public class AudioRecord * or {@link #ERROR} if the implementation was unable to query the hardware for its * output properties, * or the minimum buffer size expressed in bytes. + * @see #AudioRecord(int, int, int, int, int) for more information on valid + * configuration values. */ static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) { int channelCount = 0; diff --git a/media/java/android/media/MediaFile.java b/media/java/android/media/MediaFile.java index 6e527d9..a346ae4 100644 --- a/media/java/android/media/MediaFile.java +++ b/media/java/android/media/MediaFile.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.provider.MediaStore.Audio; import android.provider.MediaStore.Images; import android.provider.MediaStore.Video; +import android.provider.Mtp; import android.media.DecoderCapabilities; import android.media.DecoderCapabilities.VideoDecoder; import android.media.DecoderCapabilities.AudioDecoder; @@ -96,15 +97,32 @@ public class MediaFile { } } - private static HashMap<String, MediaFileType> sFileTypeMap + private static HashMap<String, MediaFileType> sFileTypeMap = new HashMap<String, MediaFileType>(); - private static HashMap<String, Integer> sMimeTypeMap - = new HashMap<String, Integer>(); + private static HashMap<String, Integer> sMimeTypeMap + = new HashMap<String, Integer>(); + // maps file extension to MTP format code + private static HashMap<String, Integer> sFileTypeToFormatMap + = new HashMap<String, Integer>(); + // maps mime type to MTP format code + private static HashMap<String, Integer> sMimeTypeToFormatMap + = new HashMap<String, Integer>(); + // maps MTP format code to mime type + private static HashMap<Integer, String> sFormatToMimeTypeMap + = new HashMap<Integer, String>(); + static void addFileType(String extension, int fileType, String mimeType) { sFileTypeMap.put(extension, new MediaFileType(fileType, mimeType)); sMimeTypeMap.put(mimeType, Integer.valueOf(fileType)); } + static void addFileType(String extension, int fileType, String mimeType, int mtpFormatCode) { + addFileType(extension, fileType, mimeType); + sFileTypeToFormatMap.put(extension, Integer.valueOf(mtpFormatCode)); + sMimeTypeToFormatMap.put(mimeType, Integer.valueOf(mtpFormatCode)); + sFormatToMimeTypeMap.put(mtpFormatCode, mimeType); + } + private static boolean isWMAEnabled() { List<AudioDecoder> decoders = DecoderCapabilities.getAudioDecoders(); for (AudioDecoder decoder: decoders) { @@ -126,17 +144,17 @@ public class MediaFile { } static { - addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg"); - addFileType("M4A", FILE_TYPE_M4A, "audio/mp4"); - addFileType("WAV", FILE_TYPE_WAV, "audio/x-wav"); + addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg", Mtp.Object.FORMAT_MP3); + addFileType("M4A", FILE_TYPE_M4A, "audio/mp4", Mtp.Object.FORMAT_MPEG); + addFileType("WAV", FILE_TYPE_WAV, "audio/x-wav", Mtp.Object.FORMAT_WAV); addFileType("AMR", FILE_TYPE_AMR, "audio/amr"); addFileType("AWB", FILE_TYPE_AWB, "audio/amr-wb"); if (isWMAEnabled()) { - addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma"); + addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma", Mtp.Object.FORMAT_WMA); } - addFileType("OGG", FILE_TYPE_OGG, "application/ogg"); - addFileType("OGA", FILE_TYPE_OGG, "application/ogg"); - addFileType("AAC", FILE_TYPE_AAC, "audio/aac"); + addFileType("OGG", FILE_TYPE_OGG, "application/ogg", Mtp.Object.FORMAT_OGG); + addFileType("OGA", FILE_TYPE_OGG, "application/ogg", Mtp.Object.FORMAT_OGG); + addFileType("AAC", FILE_TYPE_AAC, "audio/aac", Mtp.Object.FORMAT_AAC); addFileType("MKA", FILE_TYPE_MKA, "audio/x-matroska"); addFileType("MID", FILE_TYPE_MID, "audio/midi"); @@ -148,32 +166,32 @@ public class MediaFile { addFileType("RTX", FILE_TYPE_MID, "audio/midi"); addFileType("OTA", FILE_TYPE_MID, "audio/midi"); - addFileType("MPEG", FILE_TYPE_MP4, "video/mpeg"); - addFileType("MP4", FILE_TYPE_MP4, "video/mp4"); - addFileType("M4V", FILE_TYPE_M4V, "video/mp4"); - addFileType("3GP", FILE_TYPE_3GPP, "video/3gpp"); - addFileType("3GPP", FILE_TYPE_3GPP, "video/3gpp"); - addFileType("3G2", FILE_TYPE_3GPP2, "video/3gpp2"); - addFileType("3GPP2", FILE_TYPE_3GPP2, "video/3gpp2"); + addFileType("MPEG", FILE_TYPE_MP4, "video/mpeg", Mtp.Object.FORMAT_MPEG); + addFileType("MP4", FILE_TYPE_MP4, "video/mp4", Mtp.Object.FORMAT_MPEG); + addFileType("M4V", FILE_TYPE_M4V, "video/mp4", Mtp.Object.FORMAT_MPEG); + addFileType("3GP", FILE_TYPE_3GPP, "video/3gpp", Mtp.Object.FORMAT_3GP_CONTAINER); + addFileType("3GPP", FILE_TYPE_3GPP, "video/3gpp", Mtp.Object.FORMAT_3GP_CONTAINER); + addFileType("3G2", FILE_TYPE_3GPP2, "video/3gpp2", Mtp.Object.FORMAT_3GP_CONTAINER); + addFileType("3GPP2", FILE_TYPE_3GPP2, "video/3gpp2", Mtp.Object.FORMAT_3GP_CONTAINER); addFileType("MKV", FILE_TYPE_MKV, "video/x-matroska"); addFileType("WEBM", FILE_TYPE_MKV, "video/x-matroska"); addFileType("TS", FILE_TYPE_MP2TS, "video/mp2ts"); if (isWMVEnabled()) { - addFileType("WMV", FILE_TYPE_WMV, "video/x-ms-wmv"); + addFileType("WMV", FILE_TYPE_WMV, "video/x-ms-wmv", Mtp.Object.FORMAT_WMV); addFileType("ASF", FILE_TYPE_ASF, "video/x-ms-asf"); } - addFileType("JPG", FILE_TYPE_JPEG, "image/jpeg"); - addFileType("JPEG", FILE_TYPE_JPEG, "image/jpeg"); - addFileType("GIF", FILE_TYPE_GIF, "image/gif"); - addFileType("PNG", FILE_TYPE_PNG, "image/png"); - addFileType("BMP", FILE_TYPE_BMP, "image/x-ms-bmp"); + addFileType("JPG", FILE_TYPE_JPEG, "image/jpeg", Mtp.Object.FORMAT_EXIF_JPEG); + addFileType("JPEG", FILE_TYPE_JPEG, "image/jpeg", Mtp.Object.FORMAT_EXIF_JPEG); + addFileType("GIF", FILE_TYPE_GIF, "image/gif", Mtp.Object.FORMAT_GIF); + addFileType("PNG", FILE_TYPE_PNG, "image/png", Mtp.Object.FORMAT_PNG); + addFileType("BMP", FILE_TYPE_BMP, "image/x-ms-bmp", Mtp.Object.FORMAT_BMP); addFileType("WBMP", FILE_TYPE_WBMP, "image/vnd.wap.wbmp"); - addFileType("M3U", FILE_TYPE_M3U, "audio/x-mpegurl"); - addFileType("PLS", FILE_TYPE_PLS, "audio/x-scpls"); - addFileType("WPL", FILE_TYPE_WPL, "application/vnd.ms-wpl"); + addFileType("M3U", FILE_TYPE_M3U, "audio/x-mpegurl", Mtp.Object.FORMAT_M3U_PLAYLIST); + addFileType("PLS", FILE_TYPE_PLS, "audio/x-scpls", Mtp.Object.FORMAT_PLS_PLAYLIST); + addFileType("WPL", FILE_TYPE_WPL, "application/vnd.ms-wpl", Mtp.Object.FORMAT_WPL_PLAYLIST); // compute file extensions list for native Media Scanner StringBuilder builder = new StringBuilder(); @@ -222,4 +240,25 @@ public class MediaFile { return (value == null ? 0 : value.intValue()); } + public static int getFormatCode(String fileName, String mimeType) { + if (mimeType != null) { + Integer value = sMimeTypeToFormatMap.get(mimeType); + if (value != null) { + return value.intValue(); + } + } + int lastDot = fileName.lastIndexOf('.'); + if (lastDot > 0) { + String extension = fileName.substring(lastDot + 1); + Integer value = sFileTypeToFormatMap.get(extension); + if (value != null) { + return value.intValue(); + } + } + return Mtp.Object.FORMAT_UNDEFINED; + } + + public static String getMimeTypeForFormatCode(int formatCode) { + return sFormatToMimeTypeMap.get(formatCode); + } } diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java index 34a86ec..94f5c7a 100644 --- a/media/java/android/media/MediaRecorder.java +++ b/media/java/android/media/MediaRecorder.java @@ -282,6 +282,28 @@ public class MediaRecorder } /** + * Enables/Disables time lapse capture and sets its parameters. This method should + * be called after setProfile(). + * + * @param enableTimeLapse Pass true to enable time lapse capture, false to disable it. + * @param useStillCameraForTimeLapse Pass true to use still camera for capturing time lapse + * frames, false to use the video camera. + * @param timeBetweenTimeLapseFrameCaptureMs time between two captures of time lapse frames. + * @param encoderLevel the video encoder level. + */ + public void setTimeLapseParameters(boolean enableTimeLapse, + boolean useStillCameraForTimeLapse, + int timeBetweenTimeLapseFrameCaptureMs, int encoderLevel) { + setParameter(String.format("time-lapse-enable=%d", + (enableTimeLapse) ? 1 : 0)); + setParameter(String.format("use-still-camera-for-time-lapse=%d", + (useStillCameraForTimeLapse) ? 1 : 0)); + setParameter(String.format("time-between-time-lapse-frame-capture=%d", + timeBetweenTimeLapseFrameCaptureMs)); + setVideoEncoderLevel(encoderLevel); + } + + /** * Sets the format of the output file produced during recording. Call this * after setAudioSource()/setVideoSource() but before prepare(). * @@ -448,6 +470,16 @@ public class MediaRecorder } /** + * Sets the level of the encoder. Call this before prepare(). + * + * @param encoderLevel the video encoder level. + * @hide + */ + public void setVideoEncoderLevel(int encoderLevel) { + setParameter(String.format("video-param-encoder-level=%d", encoderLevel)); + } + + /** * Pass in the file descriptor of the file to be written. Call this after * setOutputFormat() but before prepare(). * diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java index 3333268..7f91c22 100644 --- a/media/java/android/media/MediaScanner.java +++ b/media/java/android/media/MediaScanner.java @@ -305,6 +305,7 @@ public class MediaScanner private Uri mGenresUri; private Uri mPlaylistsUri; private boolean mProcessPlaylists, mProcessGenres; + private int mMtpObjectHandle; // used when scanning the image database so we know whether we have to prune // old thumbnail files @@ -625,6 +626,9 @@ public class MediaScanner map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); map.put(MediaStore.MediaColumns.SIZE, mFileSize); map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); + if (mMtpObjectHandle != 0) { + map.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle); + } if (MediaFile.isVideoFileType(mFileType)) { map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaStore.UNKNOWN_STRING)); @@ -1227,6 +1231,14 @@ public class MediaScanner } } + public Uri scanMtpFile(String path, String volumeName, int objectHandle, int format) { + String mimeType = MediaFile.getMimeTypeForFormatCode(format); + mMtpObjectHandle = objectHandle; + Uri result = scanSingleFile(path, volumeName, mimeType); + mMtpObjectHandle = 0; + return result; + } + // returns the number of matching file/directory names, starting from the right private int matchPaths(String path1, String path2) { int result = 0; diff --git a/media/java/android/media/MtpClient.java b/media/java/android/media/MtpClient.java new file mode 100644 index 0000000..1aebcb8 --- /dev/null +++ b/media/java/android/media/MtpClient.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010 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 android.media; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +/** + * {@hide} + */ +public class MtpClient { + + private static final String TAG = "MtpClient"; + + private final Listener mListener; + + static { + System.loadLibrary("media_jni"); + } + + public MtpClient(Listener listener) { + native_setup(); + if (listener == null) { + throw new NullPointerException("MtpClient: listener is null"); + } + mListener = listener; + } + + @Override + protected void finalize() { + native_finalize(); + } + + public boolean start() { + return native_start(); + } + + public void stop() { + native_stop(); + } + + public boolean deleteObject(int deviceID, int objectID) { + return native_delete_object(deviceID, objectID); + } + + public int getParent(int deviceID, int objectID) { + return native_get_parent(deviceID, objectID); + } + + public int getStorageID(int deviceID, int objectID) { + return native_get_storage_id(deviceID, objectID); + } + + // create a file descriptor for reading the contents of an object over MTP + public ParcelFileDescriptor openFile(int deviceID, int objectID) { + return native_open_file(deviceID, objectID); + } + + public interface Listener { + // called when a new MTP device has been discovered + void deviceAdded(int id); + + // called when an MTP device has been removed + void deviceRemoved(int id); + } + + // called from native code + private void deviceAdded(int id) { + Log.d(TAG, "deviceAdded " + id); + mListener.deviceAdded(id); + } + + // called from native code + private void deviceRemoved(int id) { + Log.d(TAG, "deviceRemoved " + id); + mListener.deviceRemoved(id); + } + + // used by the JNI code + private int mNativeContext; + + private native final void native_setup(); + private native final void native_finalize(); + private native boolean native_start(); + private native void native_stop(); + private native boolean native_delete_object(int deviceID, int objectID); + private native int native_get_parent(int deviceID, int objectID); + private native int native_get_storage_id(int deviceID, int objectID); + private native ParcelFileDescriptor native_open_file(int deviceID, int objectID); +} diff --git a/media/java/android/media/MtpCursor.java b/media/java/android/media/MtpCursor.java new file mode 100644 index 0000000..6ecfd0d --- /dev/null +++ b/media/java/android/media/MtpCursor.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2010 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 android.media; + +import android.database.AbstractWindowedCursor; +import android.database.CursorWindow; +import android.provider.Mtp; +import android.util.Log; + +import java.util.HashMap; + +/** + * Cursor class for MTP content provider + * @hide + */ +public final class MtpCursor extends AbstractWindowedCursor { + static final String TAG = "MtpCursor"; + static final int NO_COUNT = -1; + + /* constants for mQueryType */ + public static final int DEVICE = 1; + public static final int DEVICE_ID = 2; + public static final int STORAGE = 3; + public static final int STORAGE_ID = 4; + public static final int OBJECT = 5; + public static final int OBJECT_ID = 6; + public static final int STORAGE_CHILDREN = 7; + public static final int OBJECT_CHILDREN = 8; + + private int mQueryType; + private int mDeviceID; + private int mStorageID; + private int mQbjectID; + + /** The names of the columns in the projection */ + private String[] mColumns; + + /** The number of rows in the cursor */ + private int mCount = NO_COUNT; + + private final MtpClient mClient; + + public MtpCursor(MtpClient client, int queryType, int deviceID, int storageID, int objectID, + String[] projection) { + + mClient = client; + mQueryType = queryType; + mDeviceID = deviceID; + mStorageID = storageID; + mQbjectID = objectID; + mColumns = projection; + + HashMap<String, Integer> map; + switch (queryType) { + case DEVICE: + case DEVICE_ID: + map = sDeviceProjectionMap; + break; + case STORAGE: + case STORAGE_ID: + map = sStorageProjectionMap; + break; + case OBJECT: + case OBJECT_ID: + case STORAGE_CHILDREN: + case OBJECT_CHILDREN: + map = sObjectProjectionMap; + break; + default: + throw new IllegalArgumentException("unknown query type " + queryType); + } + + int[] columns = new int[projection.length]; + for (int i = 0; i < projection.length; i++) { + Integer id = map.get(projection[i]); + if (id == null) { + throw new IllegalArgumentException("unknown column " + projection[i]); + } + columns[i] = id.intValue(); + } + native_setup(client, queryType, deviceID, storageID, objectID, columns); + } + + @Override + protected void finalize() { + native_finalize(); + } + + @Override + public int getCount() { + if (mCount == NO_COUNT) { + fillWindow(0); + } + return mCount; + } + + @Override + public boolean requery() { + Log.d(TAG, "requery"); + mCount = NO_COUNT; + if (mWindow != null) { + mWindow.clear(); + } + return super.requery(); + } + + private void fillWindow(int startPos) { + if (mWindow == null) { + // If there isn't a window set already it will only be accessed locally + mWindow = new CursorWindow(true /* the window is local only */); + } else { + mWindow.clear(); + } + mWindow.setStartPosition(startPos); + mCount = native_fill_window(mWindow, startPos); + } + + @Override + public String[] getColumnNames() { + Log.d(TAG, "getColumnNames returning " + mColumns); + return mColumns; + } + + /* Device Column IDs */ + /* These must match the values in MtpCursor.cpp */ + private static final int DEVICE_ROW_ID = 1; + private static final int DEVICE_MANUFACTURER = 2; + private static final int DEVICE_MODEL = 3; + + /* Storage Column IDs */ + /* These must match the values in MtpCursor.cpp */ + private static final int STORAGE_ROW_ID = 101; + private static final int STORAGE_IDENTIFIER = 102; + private static final int STORAGE_DESCRIPTION = 103; + + /* Object Column IDs */ + /* These must match the values in MtpCursor.cpp */ + private static final int OBJECT_ROW_ID = 201; + private static final int OBJECT_STORAGE_ID = 202; + private static final int OBJECT_FORMAT = 203; + private static final int OBJECT_PROTECTION_STATUS = 204; + private static final int OBJECT_SIZE = 205; + private static final int OBJECT_THUMB_FORMAT = 206; + private static final int OBJECT_THUMB_SIZE = 207; + private static final int OBJECT_THUMB_WIDTH = 208; + private static final int OBJECT_THUMB_HEIGHT = 209; + private static final int OBJECT_IMAGE_WIDTH = 210; + private static final int OBJECT_IMAGE_HEIGHT = 211; + private static final int OBJECT_IMAGE_DEPTH = 212; + private static final int OBJECT_PARENT = 213; + private static final int OBJECT_ASSOCIATION_TYPE = 214; + private static final int OBJECT_ASSOCIATION_DESC = 215; + private static final int OBJECT_SEQUENCE_NUMBER = 216; + private static final int OBJECT_NAME = 217; + private static final int OBJECT_DATE_CREATED = 218; + private static final int OBJECT_DATE_MODIFIED = 219; + private static final int OBJECT_KEYWORDS = 220; + private static final int OBJECT_THUMB = 221; + + private static HashMap<String, Integer> sDeviceProjectionMap; + private static HashMap<String, Integer> sStorageProjectionMap; + private static HashMap<String, Integer> sObjectProjectionMap; + + static { + sDeviceProjectionMap = new HashMap<String, Integer>(); + sDeviceProjectionMap.put(Mtp.Device._ID, new Integer(DEVICE_ROW_ID)); + sDeviceProjectionMap.put(Mtp.Device.MANUFACTURER, new Integer(DEVICE_MANUFACTURER)); + sDeviceProjectionMap.put(Mtp.Device.MODEL, new Integer(DEVICE_MODEL)); + + sStorageProjectionMap = new HashMap<String, Integer>(); + sStorageProjectionMap.put(Mtp.Storage._ID, new Integer(STORAGE_ROW_ID)); + sStorageProjectionMap.put(Mtp.Storage.IDENTIFIER, new Integer(STORAGE_IDENTIFIER)); + sStorageProjectionMap.put(Mtp.Storage.DESCRIPTION, new Integer(STORAGE_DESCRIPTION)); + + sObjectProjectionMap = new HashMap<String, Integer>(); + sObjectProjectionMap.put(Mtp.Object._ID, new Integer(OBJECT_ROW_ID)); + sObjectProjectionMap.put(Mtp.Object.STORAGE_ID, new Integer(OBJECT_STORAGE_ID)); + sObjectProjectionMap.put(Mtp.Object.FORMAT, new Integer(OBJECT_FORMAT)); + sObjectProjectionMap.put(Mtp.Object.PROTECTION_STATUS, new Integer(OBJECT_PROTECTION_STATUS)); + sObjectProjectionMap.put(Mtp.Object.SIZE, new Integer(OBJECT_SIZE)); + sObjectProjectionMap.put(Mtp.Object.THUMB_FORMAT, new Integer(OBJECT_THUMB_FORMAT)); + sObjectProjectionMap.put(Mtp.Object.THUMB_SIZE, new Integer(OBJECT_THUMB_SIZE)); + sObjectProjectionMap.put(Mtp.Object.THUMB_WIDTH, new Integer(OBJECT_THUMB_WIDTH)); + sObjectProjectionMap.put(Mtp.Object.THUMB_HEIGHT, new Integer(OBJECT_THUMB_HEIGHT)); + sObjectProjectionMap.put(Mtp.Object.IMAGE_WIDTH, new Integer(OBJECT_IMAGE_WIDTH)); + sObjectProjectionMap.put(Mtp.Object.IMAGE_HEIGHT, new Integer(OBJECT_IMAGE_HEIGHT)); + sObjectProjectionMap.put(Mtp.Object.IMAGE_DEPTH, new Integer(OBJECT_IMAGE_DEPTH)); + sObjectProjectionMap.put(Mtp.Object.PARENT, new Integer(OBJECT_PARENT)); + sObjectProjectionMap.put(Mtp.Object.ASSOCIATION_TYPE, new Integer(OBJECT_ASSOCIATION_TYPE)); + sObjectProjectionMap.put(Mtp.Object.ASSOCIATION_DESC, new Integer(OBJECT_ASSOCIATION_DESC)); + sObjectProjectionMap.put(Mtp.Object.SEQUENCE_NUMBER, new Integer(OBJECT_SEQUENCE_NUMBER)); + sObjectProjectionMap.put(Mtp.Object.NAME, new Integer(OBJECT_NAME)); + sObjectProjectionMap.put(Mtp.Object.DATE_CREATED, new Integer(OBJECT_DATE_CREATED)); + sObjectProjectionMap.put(Mtp.Object.DATE_MODIFIED, new Integer(OBJECT_DATE_MODIFIED)); + sObjectProjectionMap.put(Mtp.Object.KEYWORDS, new Integer(OBJECT_KEYWORDS)); + sObjectProjectionMap.put(Mtp.Object.THUMB, new Integer(OBJECT_THUMB)); + + sObjectProjectionMap.put(Mtp.Object.NAME, new Integer(OBJECT_NAME)); + } + + // used by the JNI code + private int mNativeContext; + + private native final void native_setup(MtpClient client, int queryType, + int deviceID, int storageID, int objectID, int[] columns); + private native final void native_finalize(); + private native void native_wait_for_event(); + private native int native_fill_window(CursorWindow window, int startPos); +} diff --git a/media/java/android/media/MtpDatabase.java b/media/java/android/media/MtpDatabase.java new file mode 100644 index 0000000..88cce46 --- /dev/null +++ b/media/java/android/media/MtpDatabase.java @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2010 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 android.media; + +import android.content.Context; +import android.content.ContentValues; +import android.content.IContentProvider; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.MediaStore.Audio; +import android.provider.MediaStore.MediaColumns; +import android.provider.MediaStore.MtpObjects; +import android.provider.Mtp; +import android.util.Log; + +/** + * {@hide} + */ +public class MtpDatabase { + + private static final String TAG = "MtpDatabase"; + + private final IContentProvider mMediaProvider; + private final String mVolumeName; + private final Uri mObjectsUri; + + // FIXME - this should be passed in via the constructor + private final int mStorageID = 0x00010001; + + private static final String[] ID_PROJECTION = new String[] { + MtpObjects.ObjectColumns._ID, // 0 + }; + private static final String[] PATH_SIZE_PROJECTION = new String[] { + MtpObjects.ObjectColumns._ID, // 0 + MtpObjects.ObjectColumns.DATA, // 1 + MtpObjects.ObjectColumns.SIZE, // 2 + }; + private static final String[] OBJECT_INFO_PROJECTION = new String[] { + MtpObjects.ObjectColumns._ID, // 0 + MtpObjects.ObjectColumns.DATA, // 1 + MtpObjects.ObjectColumns.FORMAT, // 2 + MtpObjects.ObjectColumns.PARENT, // 3 + MtpObjects.ObjectColumns.SIZE, // 4 + MtpObjects.ObjectColumns.DATE_MODIFIED, // 5 + }; + private static final String ID_WHERE = MtpObjects.ObjectColumns._ID + "=?"; + private static final String PATH_WHERE = MtpObjects.ObjectColumns.DATA + "=?"; + private static final String PARENT_WHERE = MtpObjects.ObjectColumns.PARENT + "=?"; + private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + + MtpObjects.ObjectColumns.FORMAT + "=?"; + + private final MediaScanner mMediaScanner; + + // MTP property codes + private static final int MTP_PROPERTY_STORAGE_ID = 0xDC01; + private static final int MTP_PROPERTY_OBJECT_FORMAT = 0xDC02; + private static final int MTP_PROPERTY_OBJECT_SIZE = 0xDC04; + private static final int MTP_PROPERTY_OBJECT_FILE_NAME = 0xDC07; + private static final int MTP_PROPERTY_DATE_MODIFIED = 0xDC09; + private static final int MTP_PROPERTY_PARENT_OBJECT = 0xDC0B; + + // MTP response codes + private static final int MTP_RESPONSE_OK = 0x2001; + private static final int MTP_RESPONSE_GENERAL_ERROR = 0x2002; + private static final int MTP_RESPONSE_INVALID_OBJECT_HANDLE = 0x2009; + private static final int MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED = 0xA80A; + + static { + System.loadLibrary("media_jni"); + } + + public MtpDatabase(Context context, String volumeName) { + native_setup(); + + mMediaProvider = context.getContentResolver().acquireProvider("media"); + mVolumeName = volumeName; + mObjectsUri = MtpObjects.getContentUri(volumeName); + mMediaScanner = new MediaScanner(context); + } + + @Override + protected void finalize() { + native_finalize(); + } + + private int beginSendObject(String path, int format, int parent, + int storage, long size, long modified) { + ContentValues values = new ContentValues(); + values.put(MtpObjects.ObjectColumns.DATA, path); + values.put(MtpObjects.ObjectColumns.FORMAT, format); + values.put(MtpObjects.ObjectColumns.PARENT, parent); + // storage is ignored for now + values.put(MtpObjects.ObjectColumns.SIZE, size); + values.put(MtpObjects.ObjectColumns.DATE_MODIFIED, modified); + + try { + Uri uri = mMediaProvider.insert(mObjectsUri, values); + if (uri != null) { + return Integer.parseInt(uri.getPathSegments().get(2)); + } else { + return -1; + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in beginSendObject", e); + return -1; + } + } + + private void endSendObject(String path, int handle, int format, boolean succeeded) { + if (succeeded) { + // handle abstract playlists separately + // they do not exist in the file system so don't use the media scanner here + if (format == Mtp.Object.FORMAT_ABSTRACT_AV_PLAYLIST) { + // Strip Windows Media Player file extension + if (path.endsWith(".pla")) { + path = path.substring(0, path.length() - 4); + } + + // extract name from path + String name = path; + int lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0) { + name = name.substring(lastSlash + 1); + } + + ContentValues values = new ContentValues(1); + values.put(Audio.Playlists.DATA, path); + values.put(Audio.Playlists.NAME, name); + values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle); + try { + Uri uri = mMediaProvider.insert(Audio.Playlists.EXTERNAL_CONTENT_URI, values); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in endSendObject", e); + } + } else { + Uri uri = mMediaScanner.scanMtpFile(path, mVolumeName, handle, format); + } + } else { + deleteFile(handle); + } + } + + private int[] getObjectList(int storageID, int format, int parent) { + // we can ignore storageID until we support multiple storages + Log.d(TAG, "getObjectList parent: " + parent); + Cursor c = null; + try { + if (format != 0) { + c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, + PARENT_FORMAT_WHERE, + new String[] { Integer.toString(parent), Integer.toString(format) }, + null); + } else { + c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, + PARENT_WHERE, new String[] { Integer.toString(parent) }, null); + } + if (c == null) { + Log.d(TAG, "null cursor"); + return null; + } + int count = c.getCount(); + if (count > 0) { + int[] result = new int[count]; + for (int i = 0; i < count; i++) { + c.moveToNext(); + result[i] = c.getInt(0); + } + Log.d(TAG, "returning " + result); + return result; + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in getObjectList", e); + } finally { + if (c != null) { + c.close(); + } + } + return null; + } + + private int getNumObjects(int storageID, int format, int parent) { + // we can ignore storageID until we support multiple storages + Log.d(TAG, "getObjectList parent: " + parent); + Cursor c = null; + try { + if (format != 0) { + c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, + PARENT_FORMAT_WHERE, + new String[] { Integer.toString(parent), Integer.toString(format) }, + null); + } else { + c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, + PARENT_WHERE, new String[] { Integer.toString(parent) }, null); + } + if (c != null) { + return c.getCount(); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in getNumObjects", e); + } finally { + if (c != null) { + c.close(); + } + } + return -1; + } + + private int[] getSupportedPlaybackFormats() { + return new int[] { + Mtp.Object.FORMAT_ASSOCIATION, + Mtp.Object.FORMAT_MP3, + Mtp.Object.FORMAT_MPEG, + Mtp.Object.FORMAT_EXIF_JPEG, + Mtp.Object.FORMAT_TIFF_EP, + Mtp.Object.FORMAT_GIF, + Mtp.Object.FORMAT_JFIF, + Mtp.Object.FORMAT_PNG, + Mtp.Object.FORMAT_TIFF, + Mtp.Object.FORMAT_WMA, + Mtp.Object.FORMAT_OGG, + Mtp.Object.FORMAT_AAC, + Mtp.Object.FORMAT_MP4_CONTAINER, + Mtp.Object.FORMAT_MP2, + Mtp.Object.FORMAT_3GP_CONTAINER, + Mtp.Object.FORMAT_ABSTRACT_AV_PLAYLIST, + Mtp.Object.FORMAT_WPL_PLAYLIST, + Mtp.Object.FORMAT_M3U_PLAYLIST, + Mtp.Object.FORMAT_PLS_PLAYLIST, + }; + } + + private int[] getSupportedCaptureFormats() { + // no capture formats yet + return null; + } + + private int[] getSupportedObjectProperties(int handle) { + return new int[] { + Mtp.Object.PROPERTY_STORAGE_ID, + Mtp.Object.PROPERTY_OBJECT_FORMAT, + Mtp.Object.PROPERTY_OBJECT_SIZE, + Mtp.Object.PROPERTY_OBJECT_FILE_NAME, + Mtp.Object.PROPERTY_PARENT_OBJECT, + }; + } + + private int[] getSupportedDeviceProperties() { + // no device properties yet + return null; + } + + private int getObjectProperty(int handle, int property, + long[] outIntValue, char[] outStringValue) { + Log.d(TAG, "getObjectProperty: " + property); + String column = null; + boolean isString = false; + + switch (property) { + case MTP_PROPERTY_STORAGE_ID: + outIntValue[0] = mStorageID; + return MTP_RESPONSE_OK; + case MTP_PROPERTY_OBJECT_FORMAT: + column = MtpObjects.ObjectColumns.FORMAT; + break; + case MTP_PROPERTY_OBJECT_SIZE: + column = MtpObjects.ObjectColumns.SIZE; + break; + case MTP_PROPERTY_OBJECT_FILE_NAME: + column = MtpObjects.ObjectColumns.DATA; + isString = true; + break; + case MTP_PROPERTY_DATE_MODIFIED: + column = MtpObjects.ObjectColumns.DATE_MODIFIED; + break; + case MTP_PROPERTY_PARENT_OBJECT: + column = MtpObjects.ObjectColumns.PARENT; + break; + default: + return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED; + } + + Cursor c = null; + try { + // for now we are only reading properties from the "objects" table + c = mMediaProvider.query(mObjectsUri, + new String [] { MtpObjects.ObjectColumns._ID, column }, + ID_WHERE, new String[] { Integer.toString(handle) }, null); + if (c != null && c.moveToNext()) { + if (isString) { + String value = c.getString(1); + int start = 0; + + if (property == MTP_PROPERTY_OBJECT_FILE_NAME) { + // extract name from full path + int lastSlash = value.lastIndexOf('/'); + if (lastSlash >= 0) { + start = lastSlash + 1; + } + } + int end = value.length(); + if (end - start > 255) { + end = start + 255; + } + value.getChars(start, end, outStringValue, 0); + outStringValue[end - start] = 0; + } else { + outIntValue[0] = c.getLong(1); + } + return MTP_RESPONSE_OK; + } + } catch (Exception e) { + return MTP_RESPONSE_GENERAL_ERROR; + } finally { + if (c != null) { + c.close(); + } + } + // query failed if we get here + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + private boolean getObjectInfo(int handle, int[] outStorageFormatParent, + char[] outName, long[] outSizeModified) { + Log.d(TAG, "getObjectInfo: " + handle); + Cursor c = null; + try { + c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION, + ID_WHERE, new String[] { Integer.toString(handle) }, null); + if (c != null && c.moveToNext()) { + outStorageFormatParent[0] = mStorageID; + outStorageFormatParent[1] = c.getInt(2); + outStorageFormatParent[2] = c.getInt(3); + + // extract name from path + String path = c.getString(1); + int lastSlash = path.lastIndexOf('/'); + int start = (lastSlash >= 0 ? lastSlash + 1 : 0); + int end = path.length(); + if (end - start > 255) { + end = start + 255; + } + path.getChars(start, end, outName, 0); + outName[end - start] = 0; + + outSizeModified[0] = c.getLong(4); + outSizeModified[1] = c.getLong(5); + return true; + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in getObjectProperty", e); + } finally { + if (c != null) { + c.close(); + } + } + return false; + } + + private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLength) { + Log.d(TAG, "getObjectFilePath: " + handle); + Cursor c = null; + try { + c = mMediaProvider.query(mObjectsUri, PATH_SIZE_PROJECTION, + ID_WHERE, new String[] { Integer.toString(handle) }, null); + if (c != null && c.moveToNext()) { + String path = c.getString(1); + path.getChars(0, path.length(), outFilePath, 0); + outFilePath[path.length()] = 0; + outFileLength[0] = c.getLong(2); + return MTP_RESPONSE_OK; + } else { + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in getObjectFilePath", e); + return MTP_RESPONSE_GENERAL_ERROR; + } finally { + if (c != null) { + c.close(); + } + } + } + + private int deleteFile(int handle) { + Log.d(TAG, "deleteFile: " + handle); + Uri uri = MtpObjects.getContentUri(mVolumeName, handle); + try { + if (mMediaProvider.delete(uri, null, null) == 1) { + return MTP_RESPONSE_OK; + } else { + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in deleteFile", e); + return MTP_RESPONSE_GENERAL_ERROR; + } + } + + private int[] getObjectReferences(int handle) { + Log.d(TAG, "getObjectReferences for: " + handle); + Uri uri = MtpObjects.getReferencesUri(mVolumeName, handle); + Cursor c = null; + try { + c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null); + if (c == null) { + return null; + } + int count = c.getCount(); + if (count > 0) { + int[] result = new int[count]; + for (int i = 0; i < count; i++) { + c.moveToNext(); + result[i] = c.getInt(0); + } + return result; + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in getObjectList", e); + } finally { + if (c != null) { + c.close(); + } + } + return null; + } + + private int setObjectReferences(int handle, int[] references) { + Uri uri = MtpObjects.getReferencesUri(mVolumeName, handle); + int count = references.length; + ContentValues[] valuesList = new ContentValues[count]; + for (int i = 0; i < count; i++) { + ContentValues values = new ContentValues(); + values.put(MtpObjects.ObjectColumns._ID, references[i]); + valuesList[i] = values; + } + try { + if (count == mMediaProvider.bulkInsert(uri, valuesList)) { + return MTP_RESPONSE_OK; + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in setObjectReferences", e); + } + return MTP_RESPONSE_GENERAL_ERROR; + } + + // used by the JNI code + private int mNativeContext; + + private native final void native_setup(); + private native final void native_finalize(); +} diff --git a/media/java/android/media/MtpServer.java b/media/java/android/media/MtpServer.java new file mode 100644 index 0000000..b0945a5 --- /dev/null +++ b/media/java/android/media/MtpServer.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 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 android.media; + +import android.util.Log; + +/** + * Java wrapper for MTP/PTP support as USB responder. + * {@hide} + */ +public class MtpServer { + + private static final String TAG = "MtpServer"; + + static { + System.loadLibrary("media_jni"); + } + + public MtpServer(MtpDatabase database, String storagePath) { + native_setup(database, storagePath); + } + + @Override + protected void finalize() { + native_finalize(); + } + + public void start() { + native_start(); + } + + public void stop() { + native_stop(); + } + + public void sendObjectAdded(int handle) { + native_send_object_added(handle); + } + + public void sendObjectRemoved(int handle) { + native_send_object_removed(handle); + } + + // used by the JNI code + private int mNativeContext; + + private native final void native_setup(MtpDatabase database, String storagePath); + private native final void native_finalize(); + private native final void native_start(); + private native final void native_stop(); + private native final void native_send_object_added(int handle); + private native final void native_send_object_removed(int handle); +} |
