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; 24 import android.provider.MediaStore.Audio; 25 import android.provider.MediaStore.Files; 26 import android.provider.MediaStore.Images; 27 import android.provider.MediaStore.MediaColumns; 28 import android.util.Log; 29 30 import java.util.ArrayList; 31 32 class 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 Property(int code, int type, int column)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 MtpPropertyGroup(MtpDatabase database, IContentProvider provider, String packageName, String volume, int[] 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 createProperty(int code, ArrayList<String> columns)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 queryString(int id, String column)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 queryAudio(int id, String column)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 queryGenre(int id)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 queryLong(int id, String column)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 nameFromPath(String path)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 getPropertyList(int handle, int format, int depth)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 format_date_time(long seconds)449 private native String format_date_time(long seconds); 450 } 451