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