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