Home | History | Annotate | Download | only in mtp
      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 
     17 package android.mtp;
     18 
     19 import android.content.Context;
     20 import android.content.ContentValues;
     21 import android.content.IContentProvider;
     22 import android.content.Intent;
     23 import android.content.SharedPreferences;
     24 import android.database.Cursor;
     25 import android.database.sqlite.SQLiteDatabase;
     26 import android.media.MediaScanner;
     27 import android.net.Uri;
     28 import android.os.Environment;
     29 import android.os.RemoteException;
     30 import android.provider.MediaStore;
     31 import android.provider.MediaStore.Audio;
     32 import android.provider.MediaStore.Files;
     33 import android.provider.MediaStore.Images;
     34 import android.provider.MediaStore.MediaColumns;
     35 import android.util.Log;
     36 import android.view.Display;
     37 import android.view.WindowManager;
     38 
     39 import java.io.File;
     40 import java.util.HashMap;
     41 import java.util.Locale;
     42 
     43 /**
     44  * {@hide}
     45  */
     46 public class MtpDatabase {
     47 
     48     private static final String TAG = "MtpDatabase";
     49 
     50     private final Context mContext;
     51     private final IContentProvider mMediaProvider;
     52     private final String mVolumeName;
     53     private final Uri mObjectsUri;
     54     // path to primary storage
     55     private final String mMediaStoragePath;
     56     // if not null, restrict all queries to these subdirectories
     57     private final String[] mSubDirectories;
     58     // where clause for restricting queries to files in mSubDirectories
     59     private String mSubDirectoriesWhere;
     60     // where arguments for restricting queries to files in mSubDirectories
     61     private String[] mSubDirectoriesWhereArgs;
     62 
     63     private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>();
     64 
     65     // cached property groups for single properties
     66     private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty
     67             = new HashMap<Integer, MtpPropertyGroup>();
     68 
     69     // cached property groups for all properties for a given format
     70     private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat
     71             = new HashMap<Integer, MtpPropertyGroup>();
     72 
     73     // true if the database has been modified in the current MTP session
     74     private boolean mDatabaseModified;
     75 
     76     // SharedPreferences for writable MTP device properties
     77     private SharedPreferences mDeviceProperties;
     78     private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1;
     79 
     80     private static final String[] ID_PROJECTION = new String[] {
     81             Files.FileColumns._ID, // 0
     82     };
     83     private static final String[] PATH_PROJECTION = new String[] {
     84             Files.FileColumns._ID, // 0
     85             Files.FileColumns.DATA, // 1
     86     };
     87     private static final String[] PATH_SIZE_FORMAT_PROJECTION = new String[] {
     88             Files.FileColumns._ID, // 0
     89             Files.FileColumns.DATA, // 1
     90             Files.FileColumns.SIZE, // 2
     91             Files.FileColumns.FORMAT, // 3
     92     };
     93     private static final String[] OBJECT_INFO_PROJECTION = new String[] {
     94             Files.FileColumns._ID, // 0
     95             Files.FileColumns.STORAGE_ID, // 1
     96             Files.FileColumns.FORMAT, // 2
     97             Files.FileColumns.PARENT, // 3
     98             Files.FileColumns.DATA, // 4
     99             Files.FileColumns.SIZE, // 5
    100             Files.FileColumns.DATE_MODIFIED, // 6
    101     };
    102     private static final String ID_WHERE = Files.FileColumns._ID + "=?";
    103     private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
    104 
    105     private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?";
    106     private static final String FORMAT_WHERE = Files.FileColumns.PARENT + "=?";
    107     private static final String PARENT_WHERE = Files.FileColumns.FORMAT + "=?";
    108     private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND "
    109                                             + Files.FileColumns.FORMAT + "=?";
    110     private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND "
    111                                             + Files.FileColumns.PARENT + "=?";
    112     private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND "
    113                                             + Files.FileColumns.PARENT + "=?";
    114     private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND "
    115                                             + Files.FileColumns.PARENT + "=?";
    116 
    117     private final MediaScanner mMediaScanner;
    118 
    119     static {
    120         System.loadLibrary("media_jni");
    121     }
    122 
    123     public MtpDatabase(Context context, String volumeName, String storagePath,
    124             String[] subDirectories) {
    125         native_setup();
    126 
    127         mContext = context;
    128         mMediaProvider = context.getContentResolver().acquireProvider("media");
    129         mVolumeName = volumeName;
    130         mMediaStoragePath = storagePath;
    131         mObjectsUri = Files.getMtpObjectsUri(volumeName);
    132         mMediaScanner = new MediaScanner(context);
    133 
    134         mSubDirectories = subDirectories;
    135         if (subDirectories != null) {
    136             // Compute "where" string for restricting queries to subdirectories
    137             StringBuilder builder = new StringBuilder();
    138             builder.append("(");
    139             int count = subDirectories.length;
    140             for (int i = 0; i < count; i++) {
    141                 builder.append(Files.FileColumns.DATA + "=? OR "
    142                         + Files.FileColumns.DATA + " LIKE ?");
    143                 if (i != count - 1) {
    144                     builder.append(" OR ");
    145                 }
    146             }
    147             builder.append(")");
    148             mSubDirectoriesWhere = builder.toString();
    149 
    150             // Compute "where" arguments for restricting queries to subdirectories
    151             mSubDirectoriesWhereArgs = new String[count * 2];
    152             for (int i = 0, j = 0; i < count; i++) {
    153                 String path = subDirectories[i];
    154                 mSubDirectoriesWhereArgs[j++] = path;
    155                 mSubDirectoriesWhereArgs[j++] = path + "/%";
    156             }
    157         }
    158 
    159         // Set locale to MediaScanner.
    160         Locale locale = context.getResources().getConfiguration().locale;
    161         if (locale != null) {
    162             String language = locale.getLanguage();
    163             String country = locale.getCountry();
    164             if (language != null) {
    165                 if (country != null) {
    166                     mMediaScanner.setLocale(language + "_" + country);
    167                 } else {
    168                     mMediaScanner.setLocale(language);
    169                 }
    170             }
    171         }
    172         initDeviceProperties(context);
    173     }
    174 
    175     @Override
    176     protected void finalize() throws Throwable {
    177         try {
    178             native_finalize();
    179         } finally {
    180             super.finalize();
    181         }
    182     }
    183 
    184     public void addStorage(MtpStorage storage) {
    185         mStorageMap.put(storage.getPath(), storage);
    186     }
    187 
    188     public void removeStorage(MtpStorage storage) {
    189         mStorageMap.remove(storage.getPath());
    190     }
    191 
    192     private void initDeviceProperties(Context context) {
    193         final String devicePropertiesName = "device-properties";
    194         mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE);
    195         File databaseFile = context.getDatabasePath(devicePropertiesName);
    196 
    197         if (databaseFile.exists()) {
    198             // for backward compatibility - read device properties from sqlite database
    199             // and migrate them to shared prefs
    200             SQLiteDatabase db = null;
    201             Cursor c = null;
    202             try {
    203                 db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
    204                 if (db != null) {
    205                     c = db.query("properties", new String[] { "_id", "code", "value" },
    206                             null, null, null, null, null);
    207                     if (c != null) {
    208                         SharedPreferences.Editor e = mDeviceProperties.edit();
    209                         while (c.moveToNext()) {
    210                             String name = c.getString(1);
    211                             String value = c.getString(2);
    212                             e.putString(name, value);
    213                         }
    214                         e.commit();
    215                     }
    216                 }
    217             } catch (Exception e) {
    218                 Log.e(TAG, "failed to migrate device properties", e);
    219             } finally {
    220                 if (c != null) c.close();
    221                 if (db != null) db.close();
    222             }
    223             databaseFile.delete();
    224         }
    225     }
    226 
    227     // check to see if the path is contained in one of our storage subdirectories
    228     // returns true if we have no special subdirectories
    229     private boolean inStorageSubDirectory(String path) {
    230         if (mSubDirectories == null) return true;
    231         if (path == null) return false;
    232 
    233         boolean allowed = false;
    234         int pathLength = path.length();
    235         for (int i = 0; i < mSubDirectories.length && !allowed; i++) {
    236             String subdir = mSubDirectories[i];
    237             int subdirLength = subdir.length();
    238             if (subdirLength < pathLength &&
    239                     path.charAt(subdirLength) == '/' &&
    240                     path.startsWith(subdir)) {
    241                 allowed = true;
    242             }
    243         }
    244         return allowed;
    245     }
    246 
    247     // check to see if the path matches one of our storage subdirectories
    248     // returns true if we have no special subdirectories
    249     private boolean isStorageSubDirectory(String path) {
    250     if (mSubDirectories == null) return false;
    251         for (int i = 0; i < mSubDirectories.length; i++) {
    252             if (path.equals(mSubDirectories[i])) {
    253                 return true;
    254             }
    255         }
    256         return false;
    257     }
    258 
    259     private int beginSendObject(String path, int format, int parent,
    260                          int storageId, long size, long modified) {
    261         // if mSubDirectories is not null, do not allow copying files to any other locations
    262         if (!inStorageSubDirectory(path)) return -1;
    263 
    264         // make sure the object does not exist
    265         if (path != null) {
    266             Cursor c = null;
    267             try {
    268                 c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
    269                         new String[] { path }, null);
    270                 if (c != null && c.getCount() > 0) {
    271                     Log.w(TAG, "file already exists in beginSendObject: " + path);
    272                     return -1;
    273                 }
    274             } catch (RemoteException e) {
    275                 Log.e(TAG, "RemoteException in beginSendObject", e);
    276             } finally {
    277                 if (c != null) {
    278                     c.close();
    279                 }
    280             }
    281         }
    282 
    283         mDatabaseModified = true;
    284         ContentValues values = new ContentValues();
    285         values.put(Files.FileColumns.DATA, path);
    286         values.put(Files.FileColumns.FORMAT, format);
    287         values.put(Files.FileColumns.PARENT, parent);
    288         values.put(Files.FileColumns.STORAGE_ID, storageId);
    289         values.put(Files.FileColumns.SIZE, size);
    290         values.put(Files.FileColumns.DATE_MODIFIED, modified);
    291 
    292         try {
    293             Uri uri = mMediaProvider.insert(mObjectsUri, values);
    294             if (uri != null) {
    295                 return Integer.parseInt(uri.getPathSegments().get(2));
    296             } else {
    297                 return -1;
    298             }
    299         } catch (RemoteException e) {
    300             Log.e(TAG, "RemoteException in beginSendObject", e);
    301             return -1;
    302         }
    303     }
    304 
    305     private void endSendObject(String path, int handle, int format, boolean succeeded) {
    306         if (succeeded) {
    307             // handle abstract playlists separately
    308             // they do not exist in the file system so don't use the media scanner here
    309             if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
    310                 // extract name from path
    311                 String name = path;
    312                 int lastSlash = name.lastIndexOf('/');
    313                 if (lastSlash >= 0) {
    314                     name = name.substring(lastSlash + 1);
    315                 }
    316                 // strip trailing ".pla" from the name
    317                 if (name.endsWith(".pla")) {
    318                     name = name.substring(0, name.length() - 4);
    319                 }
    320 
    321                 ContentValues values = new ContentValues(1);
    322                 values.put(Audio.Playlists.DATA, path);
    323                 values.put(Audio.Playlists.NAME, name);
    324                 values.put(Files.FileColumns.FORMAT, format);
    325                 values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
    326                 values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
    327                 try {
    328                     Uri uri = mMediaProvider.insert(Audio.Playlists.EXTERNAL_CONTENT_URI, values);
    329                 } catch (RemoteException e) {
    330                     Log.e(TAG, "RemoteException in endSendObject", e);
    331                 }
    332             } else {
    333                 mMediaScanner.scanMtpFile(path, mVolumeName, handle, format);
    334             }
    335         } else {
    336             deleteFile(handle);
    337         }
    338     }
    339 
    340     private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException {
    341         String where;
    342         String[] whereArgs;
    343 
    344         if (storageID == 0xFFFFFFFF) {
    345             // query all stores
    346             if (format == 0) {
    347                 // query all formats
    348                 if (parent == 0) {
    349                     // query all objects
    350                     where = null;
    351                     whereArgs = null;
    352                 } else {
    353                     if (parent == 0xFFFFFFFF) {
    354                         // all objects in root of store
    355                         parent = 0;
    356                     }
    357                     where = PARENT_WHERE;
    358                     whereArgs = new String[] { Integer.toString(parent) };
    359                 }
    360             } else {
    361                 // query specific format
    362                 if (parent == 0) {
    363                     // query all objects
    364                     where = FORMAT_WHERE;
    365                     whereArgs = new String[] { Integer.toString(format) };
    366                 } else {
    367                     if (parent == 0xFFFFFFFF) {
    368                         // all objects in root of store
    369                         parent = 0;
    370                     }
    371                     where = FORMAT_PARENT_WHERE;
    372                     whereArgs = new String[] { Integer.toString(format),
    373                                                Integer.toString(parent) };
    374                 }
    375             }
    376         } else {
    377             // query specific store
    378             if (format == 0) {
    379                 // query all formats
    380                 if (parent == 0) {
    381                     // query all objects
    382                     where = STORAGE_WHERE;
    383                     whereArgs = new String[] { Integer.toString(storageID) };
    384                 } else {
    385                     if (parent == 0xFFFFFFFF) {
    386                         // all objects in root of store
    387                         parent = 0;
    388                     }
    389                     where = STORAGE_PARENT_WHERE;
    390                     whereArgs = new String[] { Integer.toString(storageID),
    391                                                Integer.toString(parent) };
    392                 }
    393             } else {
    394                 // query specific format
    395                 if (parent == 0) {
    396                     // query all objects
    397                     where = STORAGE_FORMAT_WHERE;
    398                     whereArgs = new String[] {  Integer.toString(storageID),
    399                                                 Integer.toString(format) };
    400                 } else {
    401                     if (parent == 0xFFFFFFFF) {
    402                         // all objects in root of store
    403                         parent = 0;
    404                     }
    405                     where = STORAGE_FORMAT_PARENT_WHERE;
    406                     whereArgs = new String[] { Integer.toString(storageID),
    407                                                Integer.toString(format),
    408                                                Integer.toString(parent) };
    409                 }
    410             }
    411         }
    412 
    413         // if we are restricting queries to mSubDirectories, we need to add the restriction
    414         // onto our "where" arguments
    415         if (mSubDirectoriesWhere != null) {
    416             if (where == null) {
    417                 where = mSubDirectoriesWhere;
    418                 whereArgs = mSubDirectoriesWhereArgs;
    419             } else {
    420                 where = where + " AND " + mSubDirectoriesWhere;
    421 
    422                 // create new array to hold whereArgs and mSubDirectoriesWhereArgs
    423                 String[] newWhereArgs =
    424                         new String[whereArgs.length + mSubDirectoriesWhereArgs.length];
    425                 int i, j;
    426                 for (i = 0; i < whereArgs.length; i++) {
    427                     newWhereArgs[i] = whereArgs[i];
    428                 }
    429                 for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) {
    430                     newWhereArgs[i] = mSubDirectoriesWhereArgs[j];
    431                 }
    432                 whereArgs = newWhereArgs;
    433             }
    434         }
    435 
    436         return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where, whereArgs, null);
    437     }
    438 
    439     private int[] getObjectList(int storageID, int format, int parent) {
    440         Cursor c = null;
    441         try {
    442             c = createObjectQuery(storageID, format, parent);
    443             if (c == null) {
    444                 return null;
    445             }
    446             int count = c.getCount();
    447             if (count > 0) {
    448                 int[] result = new int[count];
    449                 for (int i = 0; i < count; i++) {
    450                     c.moveToNext();
    451                     result[i] = c.getInt(0);
    452                 }
    453                 return result;
    454             }
    455         } catch (RemoteException e) {
    456             Log.e(TAG, "RemoteException in getObjectList", e);
    457         } finally {
    458             if (c != null) {
    459                 c.close();
    460             }
    461         }
    462         return null;
    463     }
    464 
    465     private int getNumObjects(int storageID, int format, int parent) {
    466         Cursor c = null;
    467         try {
    468             c = createObjectQuery(storageID, format, parent);
    469             if (c != null) {
    470                 return c.getCount();
    471             }
    472         } catch (RemoteException e) {
    473             Log.e(TAG, "RemoteException in getNumObjects", e);
    474         } finally {
    475             if (c != null) {
    476                 c.close();
    477             }
    478         }
    479         return -1;
    480     }
    481 
    482     private int[] getSupportedPlaybackFormats() {
    483         return new int[] {
    484             // allow transfering arbitrary files
    485             MtpConstants.FORMAT_UNDEFINED,
    486 
    487             MtpConstants.FORMAT_ASSOCIATION,
    488             MtpConstants.FORMAT_TEXT,
    489             MtpConstants.FORMAT_HTML,
    490             MtpConstants.FORMAT_WAV,
    491             MtpConstants.FORMAT_MP3,
    492             MtpConstants.FORMAT_MPEG,
    493             MtpConstants.FORMAT_EXIF_JPEG,
    494             MtpConstants.FORMAT_TIFF_EP,
    495             MtpConstants.FORMAT_GIF,
    496             MtpConstants.FORMAT_JFIF,
    497             MtpConstants.FORMAT_PNG,
    498             MtpConstants.FORMAT_TIFF,
    499             MtpConstants.FORMAT_WMA,
    500             MtpConstants.FORMAT_OGG,
    501             MtpConstants.FORMAT_AAC,
    502             MtpConstants.FORMAT_MP4_CONTAINER,
    503             MtpConstants.FORMAT_MP2,
    504             MtpConstants.FORMAT_3GP_CONTAINER,
    505             MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
    506             MtpConstants.FORMAT_WPL_PLAYLIST,
    507             MtpConstants.FORMAT_M3U_PLAYLIST,
    508             MtpConstants.FORMAT_PLS_PLAYLIST,
    509             MtpConstants.FORMAT_XML_DOCUMENT,
    510             MtpConstants.FORMAT_FLAC,
    511         };
    512     }
    513 
    514     private int[] getSupportedCaptureFormats() {
    515         // no capture formats yet
    516         return null;
    517     }
    518 
    519     static final int[] FILE_PROPERTIES = {
    520             // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
    521             // and IMAGE_PROPERTIES below
    522             MtpConstants.PROPERTY_STORAGE_ID,
    523             MtpConstants.PROPERTY_OBJECT_FORMAT,
    524             MtpConstants.PROPERTY_PROTECTION_STATUS,
    525             MtpConstants.PROPERTY_OBJECT_SIZE,
    526             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
    527             MtpConstants.PROPERTY_DATE_MODIFIED,
    528             MtpConstants.PROPERTY_PARENT_OBJECT,
    529             MtpConstants.PROPERTY_PERSISTENT_UID,
    530             MtpConstants.PROPERTY_NAME,
    531             MtpConstants.PROPERTY_DATE_ADDED,
    532     };
    533 
    534     static final int[] AUDIO_PROPERTIES = {
    535             // NOTE must match FILE_PROPERTIES above
    536             MtpConstants.PROPERTY_STORAGE_ID,
    537             MtpConstants.PROPERTY_OBJECT_FORMAT,
    538             MtpConstants.PROPERTY_PROTECTION_STATUS,
    539             MtpConstants.PROPERTY_OBJECT_SIZE,
    540             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
    541             MtpConstants.PROPERTY_DATE_MODIFIED,
    542             MtpConstants.PROPERTY_PARENT_OBJECT,
    543             MtpConstants.PROPERTY_PERSISTENT_UID,
    544             MtpConstants.PROPERTY_NAME,
    545             MtpConstants.PROPERTY_DISPLAY_NAME,
    546             MtpConstants.PROPERTY_DATE_ADDED,
    547 
    548             // audio specific properties
    549             MtpConstants.PROPERTY_ARTIST,
    550             MtpConstants.PROPERTY_ALBUM_NAME,
    551             MtpConstants.PROPERTY_ALBUM_ARTIST,
    552             MtpConstants.PROPERTY_TRACK,
    553             MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
    554             MtpConstants.PROPERTY_DURATION,
    555             MtpConstants.PROPERTY_GENRE,
    556             MtpConstants.PROPERTY_COMPOSER,
    557     };
    558 
    559     static final int[] VIDEO_PROPERTIES = {
    560             // NOTE must match FILE_PROPERTIES above
    561             MtpConstants.PROPERTY_STORAGE_ID,
    562             MtpConstants.PROPERTY_OBJECT_FORMAT,
    563             MtpConstants.PROPERTY_PROTECTION_STATUS,
    564             MtpConstants.PROPERTY_OBJECT_SIZE,
    565             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
    566             MtpConstants.PROPERTY_DATE_MODIFIED,
    567             MtpConstants.PROPERTY_PARENT_OBJECT,
    568             MtpConstants.PROPERTY_PERSISTENT_UID,
    569             MtpConstants.PROPERTY_NAME,
    570             MtpConstants.PROPERTY_DISPLAY_NAME,
    571             MtpConstants.PROPERTY_DATE_ADDED,
    572 
    573             // video specific properties
    574             MtpConstants.PROPERTY_ARTIST,
    575             MtpConstants.PROPERTY_ALBUM_NAME,
    576             MtpConstants.PROPERTY_DURATION,
    577             MtpConstants.PROPERTY_DESCRIPTION,
    578     };
    579 
    580     static final int[] IMAGE_PROPERTIES = {
    581             // NOTE must match FILE_PROPERTIES above
    582             MtpConstants.PROPERTY_STORAGE_ID,
    583             MtpConstants.PROPERTY_OBJECT_FORMAT,
    584             MtpConstants.PROPERTY_PROTECTION_STATUS,
    585             MtpConstants.PROPERTY_OBJECT_SIZE,
    586             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
    587             MtpConstants.PROPERTY_DATE_MODIFIED,
    588             MtpConstants.PROPERTY_PARENT_OBJECT,
    589             MtpConstants.PROPERTY_PERSISTENT_UID,
    590             MtpConstants.PROPERTY_NAME,
    591             MtpConstants.PROPERTY_DISPLAY_NAME,
    592             MtpConstants.PROPERTY_DATE_ADDED,
    593 
    594             // image specific properties
    595             MtpConstants.PROPERTY_DESCRIPTION,
    596     };
    597 
    598     static final int[] ALL_PROPERTIES = {
    599             // NOTE must match FILE_PROPERTIES above
    600             MtpConstants.PROPERTY_STORAGE_ID,
    601             MtpConstants.PROPERTY_OBJECT_FORMAT,
    602             MtpConstants.PROPERTY_PROTECTION_STATUS,
    603             MtpConstants.PROPERTY_OBJECT_SIZE,
    604             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
    605             MtpConstants.PROPERTY_DATE_MODIFIED,
    606             MtpConstants.PROPERTY_PARENT_OBJECT,
    607             MtpConstants.PROPERTY_PERSISTENT_UID,
    608             MtpConstants.PROPERTY_NAME,
    609             MtpConstants.PROPERTY_DISPLAY_NAME,
    610             MtpConstants.PROPERTY_DATE_ADDED,
    611 
    612             // image specific properties
    613             MtpConstants.PROPERTY_DESCRIPTION,
    614 
    615             // audio specific properties
    616             MtpConstants.PROPERTY_ARTIST,
    617             MtpConstants.PROPERTY_ALBUM_NAME,
    618             MtpConstants.PROPERTY_ALBUM_ARTIST,
    619             MtpConstants.PROPERTY_TRACK,
    620             MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
    621             MtpConstants.PROPERTY_DURATION,
    622             MtpConstants.PROPERTY_GENRE,
    623             MtpConstants.PROPERTY_COMPOSER,
    624 
    625             // video specific properties
    626             MtpConstants.PROPERTY_ARTIST,
    627             MtpConstants.PROPERTY_ALBUM_NAME,
    628             MtpConstants.PROPERTY_DURATION,
    629             MtpConstants.PROPERTY_DESCRIPTION,
    630 
    631             // image specific properties
    632             MtpConstants.PROPERTY_DESCRIPTION,
    633     };
    634 
    635     private int[] getSupportedObjectProperties(int format) {
    636         switch (format) {
    637             case MtpConstants.FORMAT_MP3:
    638             case MtpConstants.FORMAT_WAV:
    639             case MtpConstants.FORMAT_WMA:
    640             case MtpConstants.FORMAT_OGG:
    641             case MtpConstants.FORMAT_AAC:
    642                 return AUDIO_PROPERTIES;
    643             case MtpConstants.FORMAT_MPEG:
    644             case MtpConstants.FORMAT_3GP_CONTAINER:
    645             case MtpConstants.FORMAT_WMV:
    646                 return VIDEO_PROPERTIES;
    647             case MtpConstants.FORMAT_EXIF_JPEG:
    648             case MtpConstants.FORMAT_GIF:
    649             case MtpConstants.FORMAT_PNG:
    650             case MtpConstants.FORMAT_BMP:
    651                 return IMAGE_PROPERTIES;
    652             case 0:
    653                 return ALL_PROPERTIES;
    654             default:
    655                 return FILE_PROPERTIES;
    656         }
    657     }
    658 
    659     private int[] getSupportedDeviceProperties() {
    660         return new int[] {
    661             MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
    662             MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
    663             MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
    664         };
    665     }
    666 
    667 
    668     private MtpPropertyList getObjectPropertyList(long handle, int format, long property,
    669                         int groupCode, int depth) {
    670         // FIXME - implement group support
    671         if (groupCode != 0) {
    672             return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
    673         }
    674 
    675         MtpPropertyGroup propertyGroup;
    676         if (property == 0xFFFFFFFFL) {
    677              propertyGroup = mPropertyGroupsByFormat.get(format);
    678              if (propertyGroup == null) {
    679                 int[] propertyList = getSupportedObjectProperties(format);
    680                 propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mVolumeName, propertyList);
    681                 mPropertyGroupsByFormat.put(new Integer(format), propertyGroup);
    682             }
    683         } else {
    684               propertyGroup = mPropertyGroupsByProperty.get(property);
    685              if (propertyGroup == null) {
    686                 int[] propertyList = new int[] { (int)property };
    687                 propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mVolumeName, propertyList);
    688                 mPropertyGroupsByProperty.put(new Integer((int)property), propertyGroup);
    689             }
    690         }
    691 
    692         return propertyGroup.getPropertyList((int)handle, format, depth);
    693     }
    694 
    695     private int renameFile(int handle, String newName) {
    696         Cursor c = null;
    697 
    698         // first compute current path
    699         String path = null;
    700         String[] whereArgs = new String[] {  Integer.toString(handle) };
    701         try {
    702             c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null);
    703             if (c != null && c.moveToNext()) {
    704                 path = c.getString(1);
    705             }
    706         } catch (RemoteException e) {
    707             Log.e(TAG, "RemoteException in getObjectFilePath", e);
    708             return MtpConstants.RESPONSE_GENERAL_ERROR;
    709         } finally {
    710             if (c != null) {
    711                 c.close();
    712             }
    713         }
    714         if (path == null) {
    715             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
    716         }
    717 
    718         // do not allow renaming any of the special subdirectories
    719         if (isStorageSubDirectory(path)) {
    720             return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
    721         }
    722 
    723         // now rename the file.  make sure this succeeds before updating database
    724         File oldFile = new File(path);
    725         int lastSlash = path.lastIndexOf('/');
    726         if (lastSlash <= 1) {
    727             return MtpConstants.RESPONSE_GENERAL_ERROR;
    728         }
    729         String newPath = path.substring(0, lastSlash + 1) + newName;
    730         File newFile = new File(newPath);
    731         boolean success = oldFile.renameTo(newFile);
    732         if (!success) {
    733             Log.w(TAG, "renaming "+ path + " to " + newPath + " failed");
    734             return MtpConstants.RESPONSE_GENERAL_ERROR;
    735         }
    736 
    737         // finally update database
    738         ContentValues values = new ContentValues();
    739         values.put(Files.FileColumns.DATA, newPath);
    740         int updated = 0;
    741         try {
    742             // note - we are relying on a special case in MediaProvider.update() to update
    743             // the paths for all children in the case where this is a directory.
    744             updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
    745         } catch (RemoteException e) {
    746             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
    747         }
    748         if (updated == 0) {
    749             Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
    750             // this shouldn't happen, but if it does we need to rename the file to its original name
    751             newFile.renameTo(oldFile);
    752             return MtpConstants.RESPONSE_GENERAL_ERROR;
    753         }
    754 
    755         return MtpConstants.RESPONSE_OK;
    756     }
    757 
    758     private int setObjectProperty(int handle, int property,
    759                             long intValue, String stringValue) {
    760         switch (property) {
    761             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
    762                 return renameFile(handle, stringValue);
    763 
    764             default:
    765                 return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
    766         }
    767     }
    768 
    769     private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
    770         switch (property) {
    771             case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
    772             case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
    773                 // writable string properties kept in shared preferences
    774                 String value = mDeviceProperties.getString(Integer.toString(property), "");
    775                 int length = value.length();
    776                 if (length > 255) {
    777                     length = 255;
    778                 }
    779                 value.getChars(0, length, outStringValue, 0);
    780                 outStringValue[length] = 0;
    781                 return MtpConstants.RESPONSE_OK;
    782 
    783             case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
    784                 // use screen size as max image size
    785                 Display display = ((WindowManager)mContext.getSystemService(
    786                         Context.WINDOW_SERVICE)).getDefaultDisplay();
    787                 int width = display.getMaximumSizeDimension();
    788                 int height = display.getMaximumSizeDimension();
    789                 String imageSize = Integer.toString(width) + "x" +  Integer.toString(height);
    790                 imageSize.getChars(0, imageSize.length(), outStringValue, 0);
    791                 outStringValue[imageSize.length()] = 0;
    792                 return MtpConstants.RESPONSE_OK;
    793 
    794             default:
    795                 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
    796         }
    797     }
    798 
    799     private int setDeviceProperty(int property, long intValue, String stringValue) {
    800         switch (property) {
    801             case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
    802             case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
    803                 // writable string properties kept in shared prefs
    804                 SharedPreferences.Editor e = mDeviceProperties.edit();
    805                 e.putString(Integer.toString(property), stringValue);
    806                 return (e.commit() ? MtpConstants.RESPONSE_OK
    807                         : MtpConstants.RESPONSE_GENERAL_ERROR);
    808         }
    809 
    810         return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
    811     }
    812 
    813     private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
    814                         char[] outName, long[] outSizeModified) {
    815         Cursor c = null;
    816         try {
    817             c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION,
    818                             ID_WHERE, new String[] {  Integer.toString(handle) }, null);
    819             if (c != null && c.moveToNext()) {
    820                 outStorageFormatParent[0] = c.getInt(1);
    821                 outStorageFormatParent[1] = c.getInt(2);
    822                 outStorageFormatParent[2] = c.getInt(3);
    823 
    824                 // extract name from path
    825                 String path = c.getString(4);
    826                 int lastSlash = path.lastIndexOf('/');
    827                 int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
    828                 int end = path.length();
    829                 if (end - start > 255) {
    830                     end = start + 255;
    831                 }
    832                 path.getChars(start, end, outName, 0);
    833                 outName[end - start] = 0;
    834 
    835                 outSizeModified[0] = c.getLong(5);
    836                 outSizeModified[1] = c.getLong(6);
    837                 return true;
    838             }
    839         } catch (RemoteException e) {
    840             Log.e(TAG, "RemoteException in getObjectInfo", e);
    841         } finally {
    842             if (c != null) {
    843                 c.close();
    844             }
    845         }
    846         return false;
    847     }
    848 
    849     private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
    850         if (handle == 0) {
    851             // special case root directory
    852             mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
    853             outFilePath[mMediaStoragePath.length()] = 0;
    854             outFileLengthFormat[0] = 0;
    855             outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
    856             return MtpConstants.RESPONSE_OK;
    857         }
    858         Cursor c = null;
    859         try {
    860             c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION,
    861                             ID_WHERE, new String[] {  Integer.toString(handle) }, null);
    862             if (c != null && c.moveToNext()) {
    863                 String path = c.getString(1);
    864                 path.getChars(0, path.length(), outFilePath, 0);
    865                 outFilePath[path.length()] = 0;
    866                 outFileLengthFormat[0] = c.getLong(2);
    867                 outFileLengthFormat[1] = c.getLong(3);
    868                 return MtpConstants.RESPONSE_OK;
    869             } else {
    870                 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
    871             }
    872         } catch (RemoteException e) {
    873             Log.e(TAG, "RemoteException in getObjectFilePath", e);
    874             return MtpConstants.RESPONSE_GENERAL_ERROR;
    875         } finally {
    876             if (c != null) {
    877                 c.close();
    878             }
    879         }
    880     }
    881 
    882     private int deleteFile(int handle) {
    883         mDatabaseModified = true;
    884         String path = null;
    885         int format = 0;
    886 
    887         Cursor c = null;
    888         try {
    889             c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION,
    890                             ID_WHERE, new String[] {  Integer.toString(handle) }, null);
    891             if (c != null && c.moveToNext()) {
    892                 // don't convert to media path here, since we will be matching
    893                 // against paths in the database matching /data/media
    894                 path = c.getString(1);
    895                 format = c.getInt(3);
    896             } else {
    897                 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
    898             }
    899 
    900             if (path == null || format == 0) {
    901                 return MtpConstants.RESPONSE_GENERAL_ERROR;
    902             }
    903 
    904             // do not allow deleting any of the special subdirectories
    905             if (isStorageSubDirectory(path)) {
    906                 return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
    907             }
    908 
    909             if (format == MtpConstants.FORMAT_ASSOCIATION) {
    910                 // recursive case - delete all children first
    911                 Uri uri = Files.getMtpObjectsUri(mVolumeName);
    912                 int count = mMediaProvider.delete(uri, "_data LIKE ?",
    913                         new String[] { path + "/%"});
    914             }
    915 
    916             Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
    917             if (mMediaProvider.delete(uri, null, null) > 0) {
    918                 return MtpConstants.RESPONSE_OK;
    919             } else {
    920                 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
    921             }
    922         } catch (RemoteException e) {
    923             Log.e(TAG, "RemoteException in deleteFile", e);
    924             return MtpConstants.RESPONSE_GENERAL_ERROR;
    925         } finally {
    926             if (c != null) {
    927                 c.close();
    928             }
    929         }
    930     }
    931 
    932     private int[] getObjectReferences(int handle) {
    933         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
    934         Cursor c = null;
    935         try {
    936             c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null);
    937             if (c == null) {
    938                 return null;
    939             }
    940             int count = c.getCount();
    941             if (count > 0) {
    942                 int[] result = new int[count];
    943                 for (int i = 0; i < count; i++) {
    944                     c.moveToNext();
    945                     result[i] = c.getInt(0);
    946                 }
    947                 return result;
    948             }
    949         } catch (RemoteException e) {
    950             Log.e(TAG, "RemoteException in getObjectList", e);
    951         } finally {
    952             if (c != null) {
    953                 c.close();
    954             }
    955         }
    956         return null;
    957     }
    958 
    959     private int setObjectReferences(int handle, int[] references) {
    960         mDatabaseModified = true;
    961         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
    962         int count = references.length;
    963         ContentValues[] valuesList = new ContentValues[count];
    964         for (int i = 0; i < count; i++) {
    965             ContentValues values = new ContentValues();
    966             values.put(Files.FileColumns._ID, references[i]);
    967             valuesList[i] = values;
    968         }
    969         try {
    970             if (mMediaProvider.bulkInsert(uri, valuesList) > 0) {
    971                 return MtpConstants.RESPONSE_OK;
    972             }
    973         } catch (RemoteException e) {
    974             Log.e(TAG, "RemoteException in setObjectReferences", e);
    975         }
    976         return MtpConstants.RESPONSE_GENERAL_ERROR;
    977     }
    978 
    979     private void sessionStarted() {
    980         mDatabaseModified = false;
    981     }
    982 
    983     private void sessionEnded() {
    984         if (mDatabaseModified) {
    985             mContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
    986             mDatabaseModified = false;
    987         }
    988     }
    989 
    990     // used by the JNI code
    991     private int mNativeContext;
    992 
    993     private native final void native_setup();
    994     private native final void native_finalize();
    995 }
    996