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