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