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 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