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
17package android.mtp;
18
19import android.content.ContentProviderClient;
20import android.database.Cursor;
21import android.net.Uri;
22import android.os.RemoteException;
23import android.provider.MediaStore.Audio;
24import android.provider.MediaStore.Files;
25import android.provider.MediaStore.Images;
26import android.provider.MediaStore.MediaColumns;
27import android.util.Log;
28
29import java.util.ArrayList;
30
31class 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