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