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