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