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.IContentProvider;
     20 import android.database.Cursor;
     21 import android.net.Uri;
     22 import android.os.RemoteException;
     23 import android.provider.MediaStore;
     24 import android.provider.MediaStore.Audio;
     25 import android.provider.MediaStore.Files;
     26 import android.provider.MediaStore.Images;
     27 import android.provider.MediaStore.MediaColumns;
     28 import android.util.Log;
     29 
     30 import java.util.ArrayList;
     31 
     32 class MtpPropertyGroup {
     33 
     34     private static final String TAG = "MtpPropertyGroup";
     35 
     36     private class Property {
     37         // MTP property code
     38         int     code;
     39         // MTP data type
     40         int     type;
     41         // column index for our query
     42         int     column;
     43 
     44         Property(int code, int type, int column) {
     45             this.code = code;
     46             this.type = type;
     47             this.column = column;
     48         }
     49     }
     50 
     51     private final MtpDatabase mDatabase;
     52     private final IContentProvider mProvider;
     53     private final String mPackageName;
     54     private final String mVolumeName;
     55     private final Uri mUri;
     56 
     57     // list of all properties in this group
     58     private final Property[]    mProperties;
     59 
     60     // list of columns for database query
     61     private String[]             mColumns;
     62 
     63     private static final String ID_WHERE = Files.FileColumns._ID + "=?";
     64     private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
     65     private static final String ID_FORMAT_WHERE = ID_WHERE + " AND " + FORMAT_WHERE;
     66     private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
     67     private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + FORMAT_WHERE;
     68     // constructs a property group for a list of properties
     69     public MtpPropertyGroup(MtpDatabase database, IContentProvider provider, String packageName,
     70             String volume, int[] properties) {
     71         mDatabase = database;
     72         mProvider = provider;
     73         mPackageName = packageName;
     74         mVolumeName = volume;
     75         mUri = Files.getMtpObjectsUri(volume);
     76 
     77         int count = properties.length;
     78         ArrayList<String> columns = new ArrayList<String>(count);
     79         columns.add(Files.FileColumns._ID);
     80 
     81         mProperties = new Property[count];
     82         for (int i = 0; i < count; i++) {
     83             mProperties[i] = createProperty(properties[i], columns);
     84         }
     85         count = columns.size();
     86         mColumns = new String[count];
     87         for (int i = 0; i < count; i++) {
     88             mColumns[i] = columns.get(i);
     89         }
     90     }
     91 
     92     private Property createProperty(int code, ArrayList<String> columns) {
     93         String column = null;
     94         int type;
     95 
     96          switch (code) {
     97             case MtpConstants.PROPERTY_STORAGE_ID:
     98                 column = Files.FileColumns.STORAGE_ID;
     99                 type = MtpConstants.TYPE_UINT32;
    100                 break;
    101              case MtpConstants.PROPERTY_OBJECT_FORMAT:
    102                 column = Files.FileColumns.FORMAT;
    103                 type = MtpConstants.TYPE_UINT16;
    104                 break;
    105             case MtpConstants.PROPERTY_PROTECTION_STATUS:
    106                 // protection status is always 0
    107                 type = MtpConstants.TYPE_UINT16;
    108                 break;
    109             case MtpConstants.PROPERTY_OBJECT_SIZE:
    110                 column = Files.FileColumns.SIZE;
    111                 type = MtpConstants.TYPE_UINT64;
    112                 break;
    113             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
    114                 column = Files.FileColumns.DATA;
    115                 type = MtpConstants.TYPE_STR;
    116                 break;
    117             case MtpConstants.PROPERTY_NAME:
    118                 column = MediaColumns.TITLE;
    119                 type = MtpConstants.TYPE_STR;
    120                 break;
    121             case MtpConstants.PROPERTY_DATE_MODIFIED:
    122                 column = Files.FileColumns.DATE_MODIFIED;
    123                 type = MtpConstants.TYPE_STR;
    124                 break;
    125             case MtpConstants.PROPERTY_DATE_ADDED:
    126                 column = Files.FileColumns.DATE_ADDED;
    127                 type = MtpConstants.TYPE_STR;
    128                 break;
    129             case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
    130                 column = Audio.AudioColumns.YEAR;
    131                 type = MtpConstants.TYPE_STR;
    132                 break;
    133             case MtpConstants.PROPERTY_PARENT_OBJECT:
    134                 column = Files.FileColumns.PARENT;
    135                 type = MtpConstants.TYPE_UINT32;
    136                 break;
    137             case MtpConstants.PROPERTY_PERSISTENT_UID:
    138                 // PUID is concatenation of storageID and object handle
    139                 column = Files.FileColumns.STORAGE_ID;
    140                 type = MtpConstants.TYPE_UINT128;
    141                 break;
    142             case MtpConstants.PROPERTY_DURATION:
    143                 column = Audio.AudioColumns.DURATION;
    144                 type = MtpConstants.TYPE_UINT32;
    145                 break;
    146             case MtpConstants.PROPERTY_TRACK:
    147                 column = Audio.AudioColumns.TRACK;
    148                 type = MtpConstants.TYPE_UINT16;
    149                 break;
    150             case MtpConstants.PROPERTY_DISPLAY_NAME:
    151                 column = MediaColumns.DISPLAY_NAME;
    152                 type = MtpConstants.TYPE_STR;
    153                 break;
    154             case MtpConstants.PROPERTY_ARTIST:
    155                 type = MtpConstants.TYPE_STR;
    156                 break;
    157             case MtpConstants.PROPERTY_ALBUM_NAME:
    158                 type = MtpConstants.TYPE_STR;
    159                 break;
    160             case MtpConstants.PROPERTY_ALBUM_ARTIST:
    161                 column = Audio.AudioColumns.ALBUM_ARTIST;
    162                 type = MtpConstants.TYPE_STR;
    163                 break;
    164             case MtpConstants.PROPERTY_GENRE:
    165                 // genre requires a special query
    166                 type = MtpConstants.TYPE_STR;
    167                 break;
    168             case MtpConstants.PROPERTY_COMPOSER:
    169                 column = Audio.AudioColumns.COMPOSER;
    170                 type = MtpConstants.TYPE_STR;
    171                 break;
    172             case MtpConstants.PROPERTY_DESCRIPTION:
    173                 column = Images.ImageColumns.DESCRIPTION;
    174                 type = MtpConstants.TYPE_STR;
    175                 break;
    176             default:
    177                 type = MtpConstants.TYPE_UNDEFINED;
    178                 Log.e(TAG, "unsupported property " + code);
    179                 break;
    180         }
    181 
    182         if (column != null) {
    183             columns.add(column);
    184             return new Property(code, type, columns.size() - 1);
    185         } else {
    186             return new Property(code, type, -1);
    187         }
    188     }
    189 
    190    private String queryString(int id, String column) {
    191         Cursor c = null;
    192         try {
    193             // for now we are only reading properties from the "objects" table
    194             c = mProvider.query(mPackageName, mUri,
    195                             new String [] { Files.FileColumns._ID, column },
    196                             ID_WHERE, new String[] { Integer.toString(id) }, null, null);
    197             if (c != null && c.moveToNext()) {
    198                 return c.getString(1);
    199             } else {
    200                 return "";
    201             }
    202         } catch (Exception e) {
    203             return null;
    204         } finally {
    205             if (c != null) {
    206                 c.close();
    207             }
    208         }
    209     }
    210 
    211     private String queryAudio(int id, String column) {
    212         Cursor c = null;
    213         try {
    214             c = mProvider.query(mPackageName, Audio.Media.getContentUri(mVolumeName),
    215                             new String [] { Files.FileColumns._ID, column },
    216                             ID_WHERE, new String[] { Integer.toString(id) }, null, null);
    217             if (c != null && c.moveToNext()) {
    218                 return c.getString(1);
    219             } else {
    220                 return "";
    221             }
    222         } catch (Exception e) {
    223             return null;
    224         } finally {
    225             if (c != null) {
    226                 c.close();
    227             }
    228         }
    229     }
    230 
    231     private String queryGenre(int id) {
    232         Cursor c = null;
    233         try {
    234             Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id);
    235             c = mProvider.query(mPackageName, uri,
    236                             new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME },
    237                             null, null, null, null);
    238             if (c != null && c.moveToNext()) {
    239                 return c.getString(1);
    240             } else {
    241                 return "";
    242             }
    243         } catch (Exception e) {
    244             Log.e(TAG, "queryGenre exception", e);
    245             return null;
    246         } finally {
    247             if (c != null) {
    248                 c.close();
    249             }
    250         }
    251     }
    252 
    253     private Long queryLong(int id, String column) {
    254         Cursor c = null;
    255         try {
    256             // for now we are only reading properties from the "objects" table
    257             c = mProvider.query(mPackageName, mUri,
    258                             new String [] { Files.FileColumns._ID, column },
    259                             ID_WHERE, new String[] { Integer.toString(id) }, null, null);
    260             if (c != null && c.moveToNext()) {
    261                 return new Long(c.getLong(1));
    262             }
    263         } catch (Exception e) {
    264         } finally {
    265             if (c != null) {
    266                 c.close();
    267             }
    268         }
    269         return null;
    270     }
    271 
    272     private static String nameFromPath(String path) {
    273         // extract name from full path
    274         int start = 0;
    275         int lastSlash = path.lastIndexOf('/');
    276         if (lastSlash >= 0) {
    277             start = lastSlash + 1;
    278         }
    279         int end = path.length();
    280         if (end - start > 255) {
    281             end = start + 255;
    282         }
    283         return path.substring(start, end);
    284     }
    285 
    286     MtpPropertyList getPropertyList(int handle, int format, int depth) {
    287         //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth);
    288         if (depth > 1) {
    289             // we only support depth 0 and 1
    290             // depth 0: single object, depth 1: immediate children
    291             return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
    292         }
    293 
    294         String where;
    295         String[] whereArgs;
    296         if (format == 0) {
    297             if (handle == 0xFFFFFFFF) {
    298                 // select all objects
    299                 where = null;
    300                 whereArgs = null;
    301             } else {
    302                 whereArgs = new String[] { Integer.toString(handle) };
    303                 if (depth == 1) {
    304                     where = PARENT_WHERE;
    305                 } else {
    306                     where = ID_WHERE;
    307                 }
    308             }
    309         } else {
    310             if (handle == 0xFFFFFFFF) {
    311                 // select all objects with given format
    312                 where = FORMAT_WHERE;
    313                 whereArgs = new String[] { Integer.toString(format) };
    314             } else {
    315                 whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) };
    316                 if (depth == 1) {
    317                     where = PARENT_FORMAT_WHERE;
    318                 } else {
    319                     where = ID_FORMAT_WHERE;
    320                 }
    321             }
    322         }
    323 
    324         Cursor c = null;
    325         try {
    326             // don't query if not necessary
    327             if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) {
    328                 c = mProvider.query(mPackageName, mUri, mColumns, where, whereArgs, null, null);
    329                 if (c == null) {
    330                     return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    331                 }
    332             }
    333 
    334             int count = (c == null ? 1 : c.getCount());
    335             MtpPropertyList result = new MtpPropertyList(count * mProperties.length,
    336                     MtpConstants.RESPONSE_OK);
    337 
    338             // iterate over all objects in the query
    339             for (int objectIndex = 0; objectIndex < count; objectIndex++) {
    340                 if (c != null) {
    341                     c.moveToNext();
    342                     handle = (int)c.getLong(0);
    343                 }
    344 
    345                 // iterate over all properties in the query for the given object
    346                 for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) {
    347                     Property property = mProperties[propertyIndex];
    348                     int propertyCode = property.code;
    349                     int column = property.column;
    350 
    351                     // handle some special cases
    352                     switch (propertyCode) {
    353                         case MtpConstants.PROPERTY_PROTECTION_STATUS:
    354                             // protection status is always 0
    355                             result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
    356                             break;
    357                         case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
    358                             // special case - need to extract file name from full path
    359                             String value = c.getString(column);
    360                             if (value != null) {
    361                                 result.append(handle, propertyCode, nameFromPath(value));
    362                             } else {
    363                                 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    364                             }
    365                             break;
    366                         case MtpConstants.PROPERTY_NAME:
    367                             // first try title
    368                             String name = c.getString(column);
    369                             // then try name
    370                             if (name == null) {
    371                                 name = queryString(handle, Audio.PlaylistsColumns.NAME);
    372                             }
    373                             // if title and name fail, extract name from full path
    374                             if (name == null) {
    375                                 name = queryString(handle, Files.FileColumns.DATA);
    376                                 if (name != null) {
    377                                     name = nameFromPath(name);
    378                                 }
    379                             }
    380                             if (name != null) {
    381                                 result.append(handle, propertyCode, name);
    382                             } else {
    383                                 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    384                             }
    385                             break;
    386                         case MtpConstants.PROPERTY_DATE_MODIFIED:
    387                         case MtpConstants.PROPERTY_DATE_ADDED:
    388                             // convert from seconds to DateTime
    389                             result.append(handle, propertyCode, format_date_time(c.getInt(column)));
    390                             break;
    391                         case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
    392                             // release date is stored internally as just the year
    393                             int year = c.getInt(column);
    394                             String dateTime = Integer.toString(year) + "0101T000000";
    395                             result.append(handle, propertyCode, dateTime);
    396                             break;
    397                         case MtpConstants.PROPERTY_PERSISTENT_UID:
    398                             // PUID is concatenation of storageID and object handle
    399                             long puid = c.getLong(column);
    400                             puid <<= 32;
    401                             puid += handle;
    402                             result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid);
    403                             break;
    404                         case MtpConstants.PROPERTY_TRACK:
    405                             result.append(handle, propertyCode, MtpConstants.TYPE_UINT16,
    406                                         c.getInt(column) % 1000);
    407                             break;
    408                         case MtpConstants.PROPERTY_ARTIST:
    409                             result.append(handle, propertyCode,
    410                                     queryAudio(handle, Audio.AudioColumns.ARTIST));
    411                             break;
    412                         case MtpConstants.PROPERTY_ALBUM_NAME:
    413                             result.append(handle, propertyCode,
    414                                     queryAudio(handle, Audio.AudioColumns.ALBUM));
    415                             break;
    416                         case MtpConstants.PROPERTY_GENRE:
    417                             String genre = queryGenre(handle);
    418                             if (genre != null) {
    419                                 result.append(handle, propertyCode, genre);
    420                             } else {
    421                                 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    422                             }
    423                             break;
    424                         default:
    425                             if (property.type == MtpConstants.TYPE_STR) {
    426                                 result.append(handle, propertyCode, c.getString(column));
    427                             } else if (property.type == MtpConstants.TYPE_UNDEFINED) {
    428                                 result.append(handle, propertyCode, property.type, 0);
    429                             } else {
    430                                 result.append(handle, propertyCode, property.type,
    431                                         c.getLong(column));
    432                             }
    433                             break;
    434                     }
    435                 }
    436             }
    437 
    438             return result;
    439         } catch (RemoteException e) {
    440             return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR);
    441         } finally {
    442             if (c != null) {
    443                 c.close();
    444             }
    445         }
    446         // impossible to get here, so no return statement
    447     }
    448 
    449     private native String format_date_time(long seconds);
    450 }
    451