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             default:
    176                 type = MtpConstants.TYPE_UNDEFINED;
    177                 Log.e(TAG, "unsupported property " + code);
    178                 break;
    179         }
    180 
    181         if (column != null) {
    182             columns.add(column);
    183             return new Property(code, type, columns.size() - 1);
    184         } else {
    185             return new Property(code, type, -1);
    186         }
    187     }
    188 
    189    private String queryString(int id, String column) {
    190         Cursor c = null;
    191         try {
    192             // for now we are only reading properties from the "objects" table
    193             c = mProvider.query(mPackageName, mUri,
    194                             new String [] { Files.FileColumns._ID, column },
    195                             ID_WHERE, new String[] { Integer.toString(id) }, null, null);
    196             if (c != null && c.moveToNext()) {
    197                 return c.getString(1);
    198             } else {
    199                 return "";
    200             }
    201         } catch (Exception e) {
    202             return null;
    203         } finally {
    204             if (c != null) {
    205                 c.close();
    206             }
    207         }
    208     }
    209 
    210     private String queryAudio(int id, String column) {
    211         Cursor c = null;
    212         try {
    213             c = mProvider.query(mPackageName, Audio.Media.getContentUri(mVolumeName),
    214                             new String [] { Files.FileColumns._ID, column },
    215                             ID_WHERE, new String[] { Integer.toString(id) }, null, null);
    216             if (c != null && c.moveToNext()) {
    217                 return c.getString(1);
    218             } else {
    219                 return "";
    220             }
    221         } catch (Exception e) {
    222             return null;
    223         } finally {
    224             if (c != null) {
    225                 c.close();
    226             }
    227         }
    228     }
    229 
    230     private String queryGenre(int id) {
    231         Cursor c = null;
    232         try {
    233             Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id);
    234             c = mProvider.query(mPackageName, uri,
    235                             new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME },
    236                             null, null, null, null);
    237             if (c != null && c.moveToNext()) {
    238                 return c.getString(1);
    239             } else {
    240                 return "";
    241             }
    242         } catch (Exception e) {
    243             Log.e(TAG, "queryGenre exception", e);
    244             return null;
    245         } finally {
    246             if (c != null) {
    247                 c.close();
    248             }
    249         }
    250     }
    251 
    252     private Long queryLong(int id, String column) {
    253         Cursor c = null;
    254         try {
    255             // for now we are only reading properties from the "objects" table
    256             c = mProvider.query(mPackageName, mUri,
    257                             new String [] { Files.FileColumns._ID, column },
    258                             ID_WHERE, new String[] { Integer.toString(id) }, null, null);
    259             if (c != null && c.moveToNext()) {
    260                 return new Long(c.getLong(1));
    261             }
    262         } catch (Exception e) {
    263         } finally {
    264             if (c != null) {
    265                 c.close();
    266             }
    267         }
    268         return null;
    269     }
    270 
    271     private static String nameFromPath(String path) {
    272         // extract name from full path
    273         int start = 0;
    274         int lastSlash = path.lastIndexOf('/');
    275         if (lastSlash >= 0) {
    276             start = lastSlash + 1;
    277         }
    278         int end = path.length();
    279         if (end - start > 255) {
    280             end = start + 255;
    281         }
    282         return path.substring(start, end);
    283     }
    284 
    285     MtpPropertyList getPropertyList(int handle, int format, int depth) {
    286         //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth);
    287         if (depth > 1) {
    288             // we only support depth 0 and 1
    289             // depth 0: single object, depth 1: immediate children
    290             return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
    291         }
    292 
    293         String where;
    294         String[] whereArgs;
    295         if (format == 0) {
    296             if (handle == 0xFFFFFFFF) {
    297                 // select all objects
    298                 where = null;
    299                 whereArgs = null;
    300             } else {
    301                 whereArgs = new String[] { Integer.toString(handle) };
    302                 if (depth == 1) {
    303                     where = PARENT_WHERE;
    304                 } else {
    305                     where = ID_WHERE;
    306                 }
    307             }
    308         } else {
    309             if (handle == 0xFFFFFFFF) {
    310                 // select all objects with given format
    311                 where = FORMAT_WHERE;
    312                 whereArgs = new String[] { Integer.toString(format) };
    313             } else {
    314                 whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) };
    315                 if (depth == 1) {
    316                     where = PARENT_FORMAT_WHERE;
    317                 } else {
    318                     where = ID_FORMAT_WHERE;
    319                 }
    320             }
    321         }
    322 
    323         Cursor c = null;
    324         try {
    325             // don't query if not necessary
    326             if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) {
    327                 c = mProvider.query(mPackageName, mUri, mColumns, where, whereArgs, null, null);
    328                 if (c == null) {
    329                     return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    330                 }
    331             }
    332 
    333             int count = (c == null ? 1 : c.getCount());
    334             MtpPropertyList result = new MtpPropertyList(count * mProperties.length,
    335                     MtpConstants.RESPONSE_OK);
    336 
    337             // iterate over all objects in the query
    338             for (int objectIndex = 0; objectIndex < count; objectIndex++) {
    339                 if (c != null) {
    340                     c.moveToNext();
    341                     handle = (int)c.getLong(0);
    342                 }
    343 
    344                 // iterate over all properties in the query for the given object
    345                 for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) {
    346                     Property property = mProperties[propertyIndex];
    347                     int propertyCode = property.code;
    348                     int column = property.column;
    349 
    350                     // handle some special cases
    351                     switch (propertyCode) {
    352                         case MtpConstants.PROPERTY_PROTECTION_STATUS:
    353                             // protection status is always 0
    354                             result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
    355                             break;
    356                         case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
    357                             // special case - need to extract file name from full path
    358                             String value = c.getString(column);
    359                             if (value != null) {
    360                                 result.append(handle, propertyCode, nameFromPath(value));
    361                             } else {
    362                                 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    363                             }
    364                             break;
    365                         case MtpConstants.PROPERTY_NAME:
    366                             // first try title
    367                             String name = c.getString(column);
    368                             // then try name
    369                             if (name == null) {
    370                                 name = queryString(handle, Audio.PlaylistsColumns.NAME);
    371                             }
    372                             // if title and name fail, extract name from full path
    373                             if (name == null) {
    374                                 name = queryString(handle, Files.FileColumns.DATA);
    375                                 if (name != null) {
    376                                     name = nameFromPath(name);
    377                                 }
    378                             }
    379                             if (name != null) {
    380                                 result.append(handle, propertyCode, name);
    381                             } else {
    382                                 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    383                             }
    384                             break;
    385                         case MtpConstants.PROPERTY_DATE_MODIFIED:
    386                         case MtpConstants.PROPERTY_DATE_ADDED:
    387                             // convert from seconds to DateTime
    388                             result.append(handle, propertyCode, format_date_time(c.getInt(column)));
    389                             break;
    390                         case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
    391                             // release date is stored internally as just the year
    392                             int year = c.getInt(column);
    393                             String dateTime = Integer.toString(year) + "0101T000000";
    394                             result.append(handle, propertyCode, dateTime);
    395                             break;
    396                         case MtpConstants.PROPERTY_PERSISTENT_UID:
    397                             // PUID is concatenation of storageID and object handle
    398                             long puid = c.getLong(column);
    399                             puid <<= 32;
    400                             puid += handle;
    401                             result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid);
    402                             break;
    403                         case MtpConstants.PROPERTY_TRACK:
    404                             result.append(handle, propertyCode, MtpConstants.TYPE_UINT16,
    405                                         c.getInt(column) % 1000);
    406                             break;
    407                         case MtpConstants.PROPERTY_ARTIST:
    408                             result.append(handle, propertyCode,
    409                                     queryAudio(handle, Audio.AudioColumns.ARTIST));
    410                             break;
    411                         case MtpConstants.PROPERTY_ALBUM_NAME:
    412                             result.append(handle, propertyCode,
    413                                     queryAudio(handle, Audio.AudioColumns.ALBUM));
    414                             break;
    415                         case MtpConstants.PROPERTY_GENRE:
    416                             String genre = queryGenre(handle);
    417                             if (genre != null) {
    418                                 result.append(handle, propertyCode, genre);
    419                             } else {
    420                                 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
    421                             }
    422                             break;
    423                         default:
    424                             if (property.type == MtpConstants.TYPE_STR) {
    425                                 result.append(handle, propertyCode, c.getString(column));
    426                             } else if (property.type == MtpConstants.TYPE_UNDEFINED) {
    427                                 result.append(handle, propertyCode, property.type, 0);
    428                             } else {
    429                                 result.append(handle, propertyCode, property.type,
    430                                         c.getLong(column));
    431                             }
    432                             break;
    433                     }
    434                 }
    435             }
    436 
    437             return result;
    438         } catch (RemoteException e) {
    439             return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR);
    440         } finally {
    441             if (c != null) {
    442                 c.close();
    443             }
    444         }
    445         // impossible to get here, so no return statement
    446     }
    447 
    448     private native String format_date_time(long seconds);
    449 }
    450