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