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 default: 176 type = MtpConstants.TYPE_UNDEFINED; 177 Log.e(TAG, "unsupported property " + code); 178 break; 179 } 180 181 if (column != null) { 182 columns.add(column); 183 return new Property(code, type, columns.size() - 1); 184 } else { 185 return new Property(code, type, -1); 186 } 187 } 188 queryString(int id, String column)189 private String queryString(int id, String column) { 190 Cursor c = null; 191 try { 192 // for now we are only reading properties from the "objects" table 193 c = mProvider.query(mPackageName, mUri, 194 new String [] { Files.FileColumns._ID, column }, 195 ID_WHERE, new String[] { Integer.toString(id) }, null, null); 196 if (c != null && c.moveToNext()) { 197 return c.getString(1); 198 } else { 199 return ""; 200 } 201 } catch (Exception e) { 202 return null; 203 } finally { 204 if (c != null) { 205 c.close(); 206 } 207 } 208 } 209 queryAudio(int id, String column)210 private String queryAudio(int id, String column) { 211 Cursor c = null; 212 try { 213 c = mProvider.query(mPackageName, Audio.Media.getContentUri(mVolumeName), 214 new String [] { Files.FileColumns._ID, column }, 215 ID_WHERE, new String[] { Integer.toString(id) }, null, null); 216 if (c != null && c.moveToNext()) { 217 return c.getString(1); 218 } else { 219 return ""; 220 } 221 } catch (Exception e) { 222 return null; 223 } finally { 224 if (c != null) { 225 c.close(); 226 } 227 } 228 } 229 queryGenre(int id)230 private String queryGenre(int id) { 231 Cursor c = null; 232 try { 233 Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id); 234 c = mProvider.query(mPackageName, uri, 235 new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME }, 236 null, null, null, null); 237 if (c != null && c.moveToNext()) { 238 return c.getString(1); 239 } else { 240 return ""; 241 } 242 } catch (Exception e) { 243 Log.e(TAG, "queryGenre exception", e); 244 return null; 245 } finally { 246 if (c != null) { 247 c.close(); 248 } 249 } 250 } 251 queryLong(int id, String column)252 private Long queryLong(int id, String column) { 253 Cursor c = null; 254 try { 255 // for now we are only reading properties from the "objects" table 256 c = mProvider.query(mPackageName, mUri, 257 new String [] { Files.FileColumns._ID, column }, 258 ID_WHERE, new String[] { Integer.toString(id) }, null, null); 259 if (c != null && c.moveToNext()) { 260 return new Long(c.getLong(1)); 261 } 262 } catch (Exception e) { 263 } finally { 264 if (c != null) { 265 c.close(); 266 } 267 } 268 return null; 269 } 270 nameFromPath(String path)271 private static String nameFromPath(String path) { 272 // extract name from full path 273 int start = 0; 274 int lastSlash = path.lastIndexOf('/'); 275 if (lastSlash >= 0) { 276 start = lastSlash + 1; 277 } 278 int end = path.length(); 279 if (end - start > 255) { 280 end = start + 255; 281 } 282 return path.substring(start, end); 283 } 284 getPropertyList(int handle, int format, int depth)285 MtpPropertyList getPropertyList(int handle, int format, int depth) { 286 //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth); 287 if (depth > 1) { 288 // we only support depth 0 and 1 289 // depth 0: single object, depth 1: immediate children 290 return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED); 291 } 292 293 String where; 294 String[] whereArgs; 295 if (format == 0) { 296 if (handle == 0xFFFFFFFF) { 297 // select all objects 298 where = null; 299 whereArgs = null; 300 } else { 301 whereArgs = new String[] { Integer.toString(handle) }; 302 if (depth == 1) { 303 where = PARENT_WHERE; 304 } else { 305 where = ID_WHERE; 306 } 307 } 308 } else { 309 if (handle == 0xFFFFFFFF) { 310 // select all objects with given format 311 where = FORMAT_WHERE; 312 whereArgs = new String[] { Integer.toString(format) }; 313 } else { 314 whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) }; 315 if (depth == 1) { 316 where = PARENT_FORMAT_WHERE; 317 } else { 318 where = ID_FORMAT_WHERE; 319 } 320 } 321 } 322 323 Cursor c = null; 324 try { 325 // don't query if not necessary 326 if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) { 327 c = mProvider.query(mPackageName, mUri, mColumns, where, whereArgs, null, null); 328 if (c == null) { 329 return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); 330 } 331 } 332 333 int count = (c == null ? 1 : c.getCount()); 334 MtpPropertyList result = new MtpPropertyList(count * mProperties.length, 335 MtpConstants.RESPONSE_OK); 336 337 // iterate over all objects in the query 338 for (int objectIndex = 0; objectIndex < count; objectIndex++) { 339 if (c != null) { 340 c.moveToNext(); 341 handle = (int)c.getLong(0); 342 } 343 344 // iterate over all properties in the query for the given object 345 for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) { 346 Property property = mProperties[propertyIndex]; 347 int propertyCode = property.code; 348 int column = property.column; 349 350 // handle some special cases 351 switch (propertyCode) { 352 case MtpConstants.PROPERTY_PROTECTION_STATUS: 353 // protection status is always 0 354 result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0); 355 break; 356 case MtpConstants.PROPERTY_OBJECT_FILE_NAME: 357 // special case - need to extract file name from full path 358 String value = c.getString(column); 359 if (value != null) { 360 result.append(handle, propertyCode, nameFromPath(value)); 361 } else { 362 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); 363 } 364 break; 365 case MtpConstants.PROPERTY_NAME: 366 // first try title 367 String name = c.getString(column); 368 // then try name 369 if (name == null) { 370 name = queryString(handle, Audio.PlaylistsColumns.NAME); 371 } 372 // if title and name fail, extract name from full path 373 if (name == null) { 374 name = queryString(handle, Files.FileColumns.DATA); 375 if (name != null) { 376 name = nameFromPath(name); 377 } 378 } 379 if (name != null) { 380 result.append(handle, propertyCode, name); 381 } else { 382 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); 383 } 384 break; 385 case MtpConstants.PROPERTY_DATE_MODIFIED: 386 case MtpConstants.PROPERTY_DATE_ADDED: 387 // convert from seconds to DateTime 388 result.append(handle, propertyCode, format_date_time(c.getInt(column))); 389 break; 390 case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: 391 // release date is stored internally as just the year 392 int year = c.getInt(column); 393 String dateTime = Integer.toString(year) + "0101T000000"; 394 result.append(handle, propertyCode, dateTime); 395 break; 396 case MtpConstants.PROPERTY_PERSISTENT_UID: 397 // PUID is concatenation of storageID and object handle 398 long puid = c.getLong(column); 399 puid <<= 32; 400 puid += handle; 401 result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid); 402 break; 403 case MtpConstants.PROPERTY_TRACK: 404 result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 405 c.getInt(column) % 1000); 406 break; 407 case MtpConstants.PROPERTY_ARTIST: 408 result.append(handle, propertyCode, 409 queryAudio(handle, Audio.AudioColumns.ARTIST)); 410 break; 411 case MtpConstants.PROPERTY_ALBUM_NAME: 412 result.append(handle, propertyCode, 413 queryAudio(handle, Audio.AudioColumns.ALBUM)); 414 break; 415 case MtpConstants.PROPERTY_GENRE: 416 String genre = queryGenre(handle); 417 if (genre != null) { 418 result.append(handle, propertyCode, genre); 419 } else { 420 result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); 421 } 422 break; 423 default: 424 if (property.type == MtpConstants.TYPE_STR) { 425 result.append(handle, propertyCode, c.getString(column)); 426 } else if (property.type == MtpConstants.TYPE_UNDEFINED) { 427 result.append(handle, propertyCode, property.type, 0); 428 } else { 429 result.append(handle, propertyCode, property.type, 430 c.getLong(column)); 431 } 432 break; 433 } 434 } 435 } 436 437 return result; 438 } catch (RemoteException e) { 439 return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR); 440 } finally { 441 if (c != null) { 442 c.close(); 443 } 444 } 445 // impossible to get here, so no return statement 446 } 447 format_date_time(long seconds)448 private native String format_date_time(long seconds); 449 } 450