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