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 
Property(int code, int type, int column)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
MtpPropertyGroup(MtpDatabase database, IContentProvider provider, String packageName, String volume, int[] 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 
createProperty(int code, ArrayList<String> columns)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 
queryString(int id, String column)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 
queryAudio(int id, String column)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 
queryGenre(int id)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 
queryLong(int id, String column)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 
nameFromPath(String path)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 
getPropertyList(int handle, int format, int depth)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 
format_date_time(long seconds)470     private native String format_date_time(long seconds);
471 }
472