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