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