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 
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 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
MtpPropertyGroup(MtpDatabase database, ContentProviderClient provider, String volumeName, int[] 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 
createProperty(int code, ArrayList<String> columns)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 
queryString(int id, String column)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 
queryAudio(int id, String column)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 
queryGenre(int id)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 
queryLong(int id, String column)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 
nameFromPath(String path)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 
getPropertyList(int handle, int format, int depth)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 
format_date_time(long seconds)468     private native String format_date_time(long seconds);
469 }
470