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