MtpDatabase.java revision 2837eefc5459427138c080d445bb491c75630163
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media;
18
19import android.content.Context;
20import android.content.ContentValues;
21import android.content.IContentProvider;
22import android.content.Intent;
23import android.database.Cursor;
24import android.net.Uri;
25import android.os.RemoteException;
26import android.provider.MediaStore.Audio;
27import android.provider.MediaStore.MediaColumns;
28import android.provider.MediaStore.MtpObjects;
29import android.provider.Mtp;
30import android.util.Log;
31
32/**
33 * {@hide}
34 */
35public class MtpDatabase {
36
37    private static final String TAG = "MtpDatabase";
38
39    private final Context mContext;
40    private final IContentProvider mMediaProvider;
41    private final String mVolumeName;
42    private final Uri mObjectsUri;
43
44    // true if the database has been modified in the current MTP session
45    private boolean mDatabaseModified;
46
47    // FIXME - this should be passed in via the constructor
48    private final int mStorageID = 0x00010001;
49
50    private static final String[] ID_PROJECTION = new String[] {
51            MtpObjects.ObjectColumns._ID, // 0
52    };
53    private static final String[] PATH_SIZE_PROJECTION = new String[] {
54            MtpObjects.ObjectColumns._ID, // 0
55            MtpObjects.ObjectColumns.DATA, // 1
56            MtpObjects.ObjectColumns.SIZE, // 2
57    };
58    private static final String[] OBJECT_INFO_PROJECTION = new String[] {
59            MtpObjects.ObjectColumns._ID, // 0
60            MtpObjects.ObjectColumns.DATA, // 1
61            MtpObjects.ObjectColumns.FORMAT, // 2
62            MtpObjects.ObjectColumns.PARENT, // 3
63            MtpObjects.ObjectColumns.SIZE, // 4
64            MtpObjects.ObjectColumns.DATE_MODIFIED, // 5
65    };
66    private static final String ID_WHERE = MtpObjects.ObjectColumns._ID + "=?";
67    private static final String PATH_WHERE = MtpObjects.ObjectColumns.DATA + "=?";
68    private static final String PARENT_WHERE = MtpObjects.ObjectColumns.PARENT + "=?";
69    private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND "
70                                            + MtpObjects.ObjectColumns.FORMAT + "=?";
71
72    private final MediaScanner mMediaScanner;
73
74    static {
75        System.loadLibrary("media_jni");
76    }
77
78    public MtpDatabase(Context context, String volumeName) {
79        native_setup();
80
81        mContext = context;
82        mMediaProvider = context.getContentResolver().acquireProvider("media");
83        mVolumeName = volumeName;
84        mObjectsUri = MtpObjects.getContentUri(volumeName);
85        mMediaScanner = new MediaScanner(context);
86    }
87
88    @Override
89    protected void finalize() throws Throwable {
90        try {
91            native_finalize();
92        } finally {
93            super.finalize();
94        }
95    }
96
97    private int beginSendObject(String path, int format, int parent,
98                         int storage, long size, long modified) {
99        mDatabaseModified = true;
100        ContentValues values = new ContentValues();
101        values.put(MtpObjects.ObjectColumns.DATA, path);
102        values.put(MtpObjects.ObjectColumns.FORMAT, format);
103        values.put(MtpObjects.ObjectColumns.PARENT, parent);
104        // storage is ignored for now
105        values.put(MtpObjects.ObjectColumns.SIZE, size);
106        values.put(MtpObjects.ObjectColumns.DATE_MODIFIED, modified);
107
108        try {
109            Uri uri = mMediaProvider.insert(mObjectsUri, values);
110            if (uri != null) {
111                return Integer.parseInt(uri.getPathSegments().get(2));
112            } else {
113                return -1;
114            }
115        } catch (RemoteException e) {
116            Log.e(TAG, "RemoteException in beginSendObject", e);
117            return -1;
118        }
119    }
120
121    private void endSendObject(String path, int handle, int format, boolean succeeded) {
122        if (succeeded) {
123            // handle abstract playlists separately
124            // they do not exist in the file system so don't use the media scanner here
125            if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
126                // Strip Windows Media Player file extension
127                if (path.endsWith(".pla")) {
128                    path = path.substring(0, path.length() - 4);
129                }
130
131                // extract name from path
132                String name = path;
133                int lastSlash = name.lastIndexOf('/');
134                if (lastSlash >= 0) {
135                    name = name.substring(lastSlash + 1);
136                }
137
138                ContentValues values = new ContentValues(1);
139                values.put(Audio.Playlists.DATA, path);
140                values.put(Audio.Playlists.NAME, name);
141                values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
142                try {
143                    Uri uri = mMediaProvider.insert(Audio.Playlists.EXTERNAL_CONTENT_URI, values);
144                } catch (RemoteException e) {
145                    Log.e(TAG, "RemoteException in endSendObject", e);
146                }
147            } else {
148                Uri uri = mMediaScanner.scanMtpFile(path, mVolumeName, handle, format);
149            }
150        } else {
151            deleteFile(handle);
152        }
153    }
154
155    private int[] getObjectList(int storageID, int format, int parent) {
156        // we can ignore storageID until we support multiple storages
157        Log.d(TAG, "getObjectList parent: " + parent);
158        Cursor c = null;
159        try {
160            if (format != 0) {
161                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
162                            PARENT_FORMAT_WHERE,
163                            new String[] { Integer.toString(parent), Integer.toString(format) },
164                             null);
165            } else {
166                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
167                            PARENT_WHERE, new String[] { Integer.toString(parent) }, null);
168            }
169            if (c == null) {
170                Log.d(TAG, "null cursor");
171                return null;
172            }
173            int count = c.getCount();
174            if (count > 0) {
175                int[] result = new int[count];
176                for (int i = 0; i < count; i++) {
177                    c.moveToNext();
178                    result[i] = c.getInt(0);
179                }
180                Log.d(TAG, "returning " + result);
181                return result;
182            }
183        } catch (RemoteException e) {
184            Log.e(TAG, "RemoteException in getObjectList", e);
185        } finally {
186            if (c != null) {
187                c.close();
188            }
189        }
190        return null;
191    }
192
193    private int getNumObjects(int storageID, int format, int parent) {
194        // we can ignore storageID until we support multiple storages
195        Log.d(TAG, "getObjectList parent: " + parent);
196        Cursor c = null;
197        try {
198            if (format != 0) {
199                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
200                            PARENT_FORMAT_WHERE,
201                            new String[] { Integer.toString(parent), Integer.toString(format) },
202                             null);
203            } else {
204                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
205                            PARENT_WHERE, new String[] { Integer.toString(parent) }, null);
206            }
207            if (c != null) {
208                return c.getCount();
209            }
210        } catch (RemoteException e) {
211            Log.e(TAG, "RemoteException in getNumObjects", e);
212        } finally {
213            if (c != null) {
214                c.close();
215            }
216        }
217        return -1;
218    }
219
220    private int[] getSupportedPlaybackFormats() {
221        return new int[] {
222            MtpConstants.FORMAT_ASSOCIATION,
223            MtpConstants.FORMAT_MP3,
224            MtpConstants.FORMAT_MPEG,
225            MtpConstants.FORMAT_EXIF_JPEG,
226            MtpConstants.FORMAT_TIFF_EP,
227            MtpConstants.FORMAT_GIF,
228            MtpConstants.FORMAT_JFIF,
229            MtpConstants.FORMAT_PNG,
230            MtpConstants.FORMAT_TIFF,
231            MtpConstants.FORMAT_WMA,
232            MtpConstants.FORMAT_OGG,
233            MtpConstants.FORMAT_AAC,
234            MtpConstants.FORMAT_MP4_CONTAINER,
235            MtpConstants.FORMAT_MP2,
236            MtpConstants.FORMAT_3GP_CONTAINER,
237            MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
238            MtpConstants.FORMAT_WPL_PLAYLIST,
239            MtpConstants.FORMAT_M3U_PLAYLIST,
240            MtpConstants.FORMAT_PLS_PLAYLIST,
241        };
242    }
243
244    private int[] getSupportedCaptureFormats() {
245        // no capture formats yet
246        return null;
247    }
248
249    private int[] getSupportedObjectProperties(int handle) {
250        return new int[] {
251            MtpConstants.PROPERTY_STORAGE_ID,
252            MtpConstants.PROPERTY_OBJECT_FORMAT,
253            MtpConstants.PROPERTY_OBJECT_SIZE,
254            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
255            MtpConstants.PROPERTY_PARENT_OBJECT,
256        };
257    }
258
259    private int[] getSupportedDeviceProperties() {
260        // no device properties yet
261        return null;
262    }
263
264    private int getObjectProperty(int handle, int property,
265                            long[] outIntValue, char[] outStringValue) {
266        Log.d(TAG, "getObjectProperty: " + property);
267        String column = null;
268        boolean isString = false;
269
270        switch (property) {
271            case MtpConstants.PROPERTY_STORAGE_ID:
272                outIntValue[0] = mStorageID;
273                return MtpConstants.RESPONSE_OK;
274            case MtpConstants.PROPERTY_OBJECT_FORMAT:
275                column = MtpObjects.ObjectColumns.FORMAT;
276                break;
277            case MtpConstants.PROPERTY_PROTECTION_STATUS:
278                // protection status is always 0
279                outIntValue[0] = 0;
280                return MtpConstants.RESPONSE_OK;
281            case MtpConstants.PROPERTY_OBJECT_SIZE:
282                column = MtpObjects.ObjectColumns.SIZE;
283                break;
284            case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
285                column = MtpObjects.ObjectColumns.DATA;
286                isString = true;
287                break;
288            case MtpConstants.PROPERTY_DATE_MODIFIED:
289                column = MtpObjects.ObjectColumns.DATE_MODIFIED;
290                break;
291            case MtpConstants.PROPERTY_PARENT_OBJECT:
292                column = MtpObjects.ObjectColumns.PARENT;
293                break;
294            case MtpConstants.PROPERTY_PERSISTENT_UID:
295                // PUID is concatenation of storageID and object handle
296                long puid = mStorageID;
297                puid <<= 32;
298                puid += handle;
299                outIntValue[0] = puid;
300                return MtpConstants.RESPONSE_OK;
301            default:
302                return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
303        }
304
305        Cursor c = null;
306        try {
307            // for now we are only reading properties from the "objects" table
308            c = mMediaProvider.query(mObjectsUri,
309                            new String [] { MtpObjects.ObjectColumns._ID, column },
310                            ID_WHERE, new String[] { Integer.toString(handle) }, null);
311            if (c != null && c.moveToNext()) {
312                if (isString) {
313                    String value = c.getString(1);
314                    int start = 0;
315
316                    if (property == MtpConstants.PROPERTY_OBJECT_FILE_NAME) {
317                        // extract name from full path
318                        int lastSlash = value.lastIndexOf('/');
319                        if (lastSlash >= 0) {
320                            start = lastSlash + 1;
321                        }
322                    }
323                    int end = value.length();
324                    if (end - start > 255) {
325                        end = start + 255;
326                    }
327                    value.getChars(start, end, outStringValue, 0);
328                    outStringValue[end - start] = 0;
329                } else {
330                    outIntValue[0] = c.getLong(1);
331                }
332                return MtpConstants.RESPONSE_OK;
333            }
334        } catch (Exception e) {
335            return MtpConstants.RESPONSE_GENERAL_ERROR;
336        } finally {
337            if (c != null) {
338                c.close();
339            }
340        }
341        // query failed if we get here
342        return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
343    }
344
345    private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
346                        char[] outName, long[] outSizeModified) {
347        Log.d(TAG, "getObjectInfo: " + handle);
348        Cursor c = null;
349        try {
350            c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION,
351                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
352            if (c != null && c.moveToNext()) {
353                outStorageFormatParent[0] = mStorageID;
354                outStorageFormatParent[1] = c.getInt(2);
355                outStorageFormatParent[2] = c.getInt(3);
356
357                // extract name from path
358                String path = c.getString(1);
359                int lastSlash = path.lastIndexOf('/');
360                int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
361                int end = path.length();
362                if (end - start > 255) {
363                    end = start + 255;
364                }
365                path.getChars(start, end, outName, 0);
366                outName[end - start] = 0;
367
368                outSizeModified[0] = c.getLong(4);
369                outSizeModified[1] = c.getLong(5);
370                return true;
371            }
372        } catch (RemoteException e) {
373            Log.e(TAG, "RemoteException in getObjectProperty", e);
374        } finally {
375            if (c != null) {
376                c.close();
377            }
378        }
379        return false;
380    }
381
382    private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLength) {
383        Log.d(TAG, "getObjectFilePath: " + handle);
384        Cursor c = null;
385        try {
386            c = mMediaProvider.query(mObjectsUri, PATH_SIZE_PROJECTION,
387                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
388            if (c != null && c.moveToNext()) {
389                String path = c.getString(1);
390                path.getChars(0, path.length(), outFilePath, 0);
391                outFilePath[path.length()] = 0;
392                outFileLength[0] = c.getLong(2);
393                return MtpConstants.RESPONSE_OK;
394            } else {
395                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
396            }
397        } catch (RemoteException e) {
398            Log.e(TAG, "RemoteException in getObjectFilePath", e);
399            return MtpConstants.RESPONSE_GENERAL_ERROR;
400        } finally {
401            if (c != null) {
402                c.close();
403            }
404        }
405    }
406
407    private int deleteFile(int handle) {
408        Log.d(TAG, "deleteFile: " + handle);
409        mDatabaseModified = true;
410        Uri uri = MtpObjects.getContentUri(mVolumeName, handle);
411        try {
412            if (mMediaProvider.delete(uri, null, null) == 1) {
413                return MtpConstants.RESPONSE_OK;
414            } else {
415                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
416            }
417        } catch (RemoteException e) {
418            Log.e(TAG, "RemoteException in deleteFile", e);
419            return MtpConstants.RESPONSE_GENERAL_ERROR;
420        }
421    }
422
423    private int[] getObjectReferences(int handle) {
424        Log.d(TAG, "getObjectReferences for: " + handle);
425        Uri uri = MtpObjects.getReferencesUri(mVolumeName, handle);
426        Cursor c = null;
427        try {
428            c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null);
429            if (c == null) {
430                return null;
431            }
432            int count = c.getCount();
433            if (count > 0) {
434                int[] result = new int[count];
435                for (int i = 0; i < count; i++) {
436                    c.moveToNext();
437                    result[i] = c.getInt(0);
438                }
439                return result;
440            }
441        } catch (RemoteException e) {
442            Log.e(TAG, "RemoteException in getObjectList", e);
443        } finally {
444            if (c != null) {
445                c.close();
446            }
447        }
448        return null;
449    }
450
451    private int setObjectReferences(int handle, int[] references) {
452        mDatabaseModified = true;
453        Uri uri = MtpObjects.getReferencesUri(mVolumeName, handle);
454        int count = references.length;
455        ContentValues[] valuesList = new ContentValues[count];
456        for (int i = 0; i < count; i++) {
457            ContentValues values = new ContentValues();
458            values.put(MtpObjects.ObjectColumns._ID, references[i]);
459            valuesList[i] = values;
460        }
461        try {
462            if (count == mMediaProvider.bulkInsert(uri, valuesList)) {
463                return MtpConstants.RESPONSE_OK;
464            }
465        } catch (RemoteException e) {
466            Log.e(TAG, "RemoteException in setObjectReferences", e);
467        }
468        return MtpConstants.RESPONSE_GENERAL_ERROR;
469    }
470
471    private void sessionStarted() {
472        Log.d(TAG, "sessionStarted");
473        mDatabaseModified = false;
474    }
475
476    private void sessionEnded() {
477        Log.d(TAG, "sessionEnded");
478        if (mDatabaseModified) {
479            Log.d(TAG, "sending ACTION_MTP_SESSION_END");
480            mContext.sendBroadcast(new Intent(Mtp.ACTION_MTP_SESSION_END));
481            mDatabaseModified = false;
482        }
483    }
484
485    // used by the JNI code
486    private int mNativeContext;
487
488    private native final void native_setup();
489    private native final void native_finalize();
490}
491