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