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