2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.ContentValues;
22 import android.content.IContentProvider;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.SharedPreferences;
26 import android.database.Cursor;
27 import android.database.sqlite.SQLiteDatabase;
28 import android.media.MediaScanner;
29 import android.net.Uri;
30 import android.os.BatteryManager;
31 import android.os.BatteryStats;
32 import android.os.RemoteException;
33 import android.provider.MediaStore;
34 import android.provider.MediaStore.Audio;
35 import android.provider.MediaStore.Files;
36 import android.provider.MediaStore.MediaColumns;
37 import android.util.Log;
38 import android.view.Display;
39 import android.view.WindowManager;
42 import java.io.IOException;
43 import java.util.HashMap;
44 import java.util.Locale;
49 public class MtpDatabase {
51 private static final String TAG = "MtpDatabase";
53 private final Context mContext;
54 private final String mPackageName;
55 private final IContentProvider mMediaProvider;
56 private final String mVolumeName;
57 private final Uri mObjectsUri;
58 // path to primary storage
59 private final String mMediaStoragePath;
60 // if not null, restrict all queries to these subdirectories
61 private final String[] mSubDirectories;
62 // where clause for restricting queries to files in mSubDirectories
63 private String mSubDirectoriesWhere;
64 // where arguments for restricting queries to files in mSubDirectories
65 private String[] mSubDirectoriesWhereArgs;
67 private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>();
69 // cached property groups for single properties
70 private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty
71 = new HashMap<Integer, MtpPropertyGroup>();
73 // cached property groups for all properties for a given format
74 private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat
75 = new HashMap<Integer, MtpPropertyGroup>();
77 // true if the database has been modified in the current MTP session
78 private boolean mDatabaseModified;
80 // SharedPreferences for writable MTP device properties
81 private SharedPreferences mDeviceProperties;
82 private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1;
84 private static final String[] ID_PROJECTION = new String[] {
85 Files.FileColumns._ID, // 0
87 private static final String[] PATH_PROJECTION = new String[] {
88 Files.FileColumns._ID, // 0
89 Files.FileColumns.DATA, // 1
91 private static final String[] PATH_FORMAT_PROJECTION = new String[] {
92 Files.FileColumns._ID, // 0
93 Files.FileColumns.DATA, // 1
94 Files.FileColumns.FORMAT, // 2
96 private static final String[] OBJECT_INFO_PROJECTION = new String[] {
97 Files.FileColumns._ID, // 0
98 Files.FileColumns.STORAGE_ID, // 1
99 Files.FileColumns.FORMAT, // 2
100 Files.FileColumns.PARENT, // 3
101 Files.FileColumns.DATA, // 4
102 Files.FileColumns.DATE_ADDED, // 5
103 Files.FileColumns.DATE_MODIFIED, // 6
105 private static final String ID_WHERE = Files.FileColumns._ID + "=?";
106 private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
108 private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?";
109 private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
110 private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
111 private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND "
112 + Files.FileColumns.FORMAT + "=?";
113 private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND "
114 + Files.FileColumns.PARENT + "=?";
115 private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND "
116 + Files.FileColumns.PARENT + "=?";
117 private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND "
118 + Files.FileColumns.PARENT + "=?";
120 private final MediaScanner mMediaScanner;
121 private MtpServer mServer;
123 // read from native code
124 private int mBatteryLevel;
125 private int mBatteryScale;
128 System.loadLibrary("media_jni");
131 private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
133 public void onReceive(Context context, Intent intent) {
134 String action = intent.getAction();
135 if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
136 mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
137 int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
138 if (newLevel != mBatteryLevel) {
139 mBatteryLevel = newLevel;
140 if (mServer != null) {
141 // send device property changed event
142 mServer.sendDevicePropertyChanged(
143 MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
150 public MtpDatabase(Context context, String volumeName, String storagePath,
151 String[] subDirectories) {
155 mPackageName = context.getPackageName();
156 mMediaProvider = context.getContentResolver().acquireProvider("media");
157 mVolumeName = volumeName;
158 mMediaStoragePath = storagePath;
159 mObjectsUri = Files.getMtpObjectsUri(volumeName);
160 mMediaScanner = new MediaScanner(context);
162 mSubDirectories = subDirectories;
163 if (subDirectories != null) {
164 // Compute "where" string for restricting queries to subdirectories
165 StringBuilder builder = new StringBuilder();
167 int count = subDirectories.length;
168 for (int i = 0; i < count; i++) {
169 builder.append(Files.FileColumns.DATA + "=? OR "
170 + Files.FileColumns.DATA + " LIKE ?");
171 if (i != count - 1) {
172 builder.append(" OR ");
176 mSubDirectoriesWhere = builder.toString();
178 // Compute "where" arguments for restricting queries to subdirectories
179 mSubDirectoriesWhereArgs = new String[count * 2];
180 for (int i = 0, j = 0; i < count; i++) {
181 String path = subDirectories[i];
182 mSubDirectoriesWhereArgs[j++] = path;
183 mSubDirectoriesWhereArgs[j++] = path + "/%";
187 // Set locale to MediaScanner.
188 Locale locale = context.getResources().getConfiguration().locale;
189 if (locale != null) {
190 String language = locale.getLanguage();
191 String country = locale.getCountry();
192 if (language != null) {
193 if (country != null) {
194 mMediaScanner.setLocale(language + "_" + country);
196 mMediaScanner.setLocale(language);
200 initDeviceProperties(context);
203 public void setServer(MtpServer server) {
206 // always unregister before registering
208 mContext.unregisterReceiver(mBatteryReceiver);
209 } catch (IllegalArgumentException e) {
210 // wasn't previously registered, ignore
213 // register for battery notifications when we are connected
214 if (server != null) {
215 mContext.registerReceiver(mBatteryReceiver,
216 new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
221 protected void finalize() throws Throwable {
229 public void addStorage(MtpStorage storage) {
230 mStorageMap.put(storage.getPath(), storage);
233 public void removeStorage(MtpStorage storage) {
234 mStorageMap.remove(storage.getPath());
237 private void initDeviceProperties(Context context) {
238 final String devicePropertiesName = "device-properties";
239 mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE);
240 File databaseFile = context.getDatabasePath(devicePropertiesName);
242 if (databaseFile.exists()) {
243 // for backward compatibility - read device properties from sqlite database
244 // and migrate them to shared prefs
245 SQLiteDatabase db = null;
248 db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
250 c = db.query("properties", new String[] { "_id", "code", "value" },
251 null, null, null, null, null);
253 SharedPreferences.Editor e = mDeviceProperties.edit();
254 while (c.moveToNext()) {
255 String name = c.getString(1);
256 String value = c.getString(2);
257 e.putString(name, value);
262 } catch (Exception e) {
263 Log.e(TAG, "failed to migrate device properties", e);
265 if (c != null) c.close();
266 if (db != null) db.close();
268 context.deleteDatabase(devicePropertiesName);
272 // check to see if the path is contained in one of our storage subdirectories
273 // returns true if we have no special subdirectories
274 private boolean inStorageSubDirectory(String path) {
275 if (mSubDirectories == null) return true;
276 if (path == null) return false;
278 boolean allowed = false;
279 int pathLength = path.length();
280 for (int i = 0; i < mSubDirectories.length && !allowed; i++) {
281 String subdir = mSubDirectories[i];
282 int subdirLength = subdir.length();
283 if (subdirLength < pathLength &&
284 path.charAt(subdirLength) == '/' &&
285 path.startsWith(subdir)) {
292 // check to see if the path matches one of our storage subdirectories
293 // returns true if we have no special subdirectories
294 private boolean isStorageSubDirectory(String path) {
295 if (mSubDirectories == null) return false;
296 for (int i = 0; i < mSubDirectories.length; i++) {
297 if (path.equals(mSubDirectories[i])) {
304 // returns true if the path is in the storage root
305 private boolean inStorageRoot(String path) {
307 File f = new File(path);
308 String canonical = f.getCanonicalPath();
309 if (canonical.startsWith(mMediaStoragePath)) {
312 } catch (IOException e) {
318 private int beginSendObject(String path, int format, int parent,
319 int storageId, long size, long modified) {
320 // if the path is outside of the storage root, do not allow access
321 if (!inStorageRoot(path)) {
322 Log.e(TAG, "attempt to put file outside of storage area: " + path);
325 // if mSubDirectories is not null, do not allow copying files to any other locations
326 if (!inStorageSubDirectory(path)) return -1;
328 // make sure the object does not exist
332 c = mMediaProvider.query(mPackageName, mObjectsUri, ID_PROJECTION, PATH_WHERE,
333 new String[] { path }, null, null);
334 if (c != null && c.getCount() > 0) {
335 Log.w(TAG, "file already exists in beginSendObject: " + path);
338 } catch (RemoteException e) {
339 Log.e(TAG, "RemoteException in beginSendObject", e);
347 mDatabaseModified = true;
348 ContentValues values = new ContentValues();
349 values.put(Files.FileColumns.DATA, path);
350 values.put(Files.FileColumns.FORMAT, format);
351 values.put(Files.FileColumns.PARENT, parent);
352 values.put(Files.FileColumns.STORAGE_ID, storageId);
353 values.put(Files.FileColumns.SIZE, size);
354 values.put(Files.FileColumns.DATE_MODIFIED, modified);
357 Uri uri = mMediaProvider.insert(mPackageName, mObjectsUri, values);
359 return Integer.parseInt(uri.getPathSegments().get(2));
363 } catch (RemoteException e) {
364 Log.e(TAG, "RemoteException in beginSendObject", e);
369 private void endSendObject(String path, int handle, int format, boolean succeeded) {
371 // handle abstract playlists separately
372 // they do not exist in the file system so don't use the media scanner here
373 if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
374 // extract name from path
376 int lastSlash = name.lastIndexOf('/');
377 if (lastSlash >= 0) {
378 name = name.substring(lastSlash + 1);
380 // strip trailing ".pla" from the name
381 if (name.endsWith(".pla")) {
382 name = name.substring(0, name.length() - 4);
385 ContentValues values = new ContentValues(1);
386 values.put(Audio.Playlists.DATA, path);
387 values.put(Audio.Playlists.NAME, name);
388 values.put(Files.FileColumns.FORMAT, format);
389 values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
390 values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
392 Uri uri = mMediaProvider.insert(mPackageName,
393 Audio.Playlists.EXTERNAL_CONTENT_URI, values);
394 } catch (RemoteException e) {
395 Log.e(TAG, "RemoteException in endSendObject", e);
398 mMediaScanner.scanMtpFile(path, mVolumeName, handle, format);
405 private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException {
409 if (storageID == 0xFFFFFFFF) {
418 if (parent == 0xFFFFFFFF) {
419 // all objects in root of store
422 where = PARENT_WHERE;
423 whereArgs = new String[] { Integer.toString(parent) };
426 // query specific format
429 where = FORMAT_WHERE;
430 whereArgs = new String[] { Integer.toString(format) };
432 if (parent == 0xFFFFFFFF) {
433 // all objects in root of store
436 where = FORMAT_PARENT_WHERE;
437 whereArgs = new String[] { Integer.toString(format),
438 Integer.toString(parent) };
442 // query specific store
447 where = STORAGE_WHERE;
448 whereArgs = new String[] { Integer.toString(storageID) };
450 if (parent == 0xFFFFFFFF) {
451 // all objects in root of store
454 where = STORAGE_PARENT_WHERE;
455 whereArgs = new String[] { Integer.toString(storageID),
456 Integer.toString(parent) };
459 // query specific format
462 where = STORAGE_FORMAT_WHERE;
463 whereArgs = new String[] { Integer.toString(storageID),
464 Integer.toString(format) };
466 if (parent == 0xFFFFFFFF) {
467 // all objects in root of store
470 where = STORAGE_FORMAT_PARENT_WHERE;
471 whereArgs = new String[] { Integer.toString(storageID),
472 Integer.toString(format),
473 Integer.toString(parent) };
478 // if we are restricting queries to mSubDirectories, we need to add the restriction
479 // onto our "where" arguments
480 if (mSubDirectoriesWhere != null) {
482 where = mSubDirectoriesWhere;
483 whereArgs = mSubDirectoriesWhereArgs;
485 where = where + " AND " + mSubDirectoriesWhere;
487 // create new array to hold whereArgs and mSubDirectoriesWhereArgs
488 String[] newWhereArgs =
489 new String[whereArgs.length + mSubDirectoriesWhereArgs.length];
491 for (i = 0; i < whereArgs.length; i++) {
492 newWhereArgs[i] = whereArgs[i];
494 for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) {
495 newWhereArgs[i] = mSubDirectoriesWhereArgs[j];
497 whereArgs = newWhereArgs;
501 return mMediaProvider.query(mPackageName, mObjectsUri, ID_PROJECTION, where,
502 whereArgs, null, null);
505 private int[] getObjectList(int storageID, int format, int parent) {
508 c = createObjectQuery(storageID, format, parent);
512 int count = c.getCount();
514 int[] result = new int[count];
515 for (int i = 0; i < count; i++) {
517 result[i] = c.getInt(0);
521 } catch (RemoteException e) {
522 Log.e(TAG, "RemoteException in getObjectList", e);
531 private int getNumObjects(int storageID, int format, int parent) {
534 c = createObjectQuery(storageID, format, parent);
538 } catch (RemoteException e) {
539 Log.e(TAG, "RemoteException in getNumObjects", e);
548 private int[] getSupportedPlaybackFormats() {
550 // allow transfering arbitrary files
551 MtpConstants.FORMAT_UNDEFINED,
553 MtpConstants.FORMAT_ASSOCIATION,
554 MtpConstants.FORMAT_TEXT,
555 MtpConstants.FORMAT_HTML,
556 MtpConstants.FORMAT_WAV,
557 MtpConstants.FORMAT_MP3,
558 MtpConstants.FORMAT_MPEG,
559 MtpConstants.FORMAT_EXIF_JPEG,
560 MtpConstants.FORMAT_TIFF_EP,
561 MtpConstants.FORMAT_BMP,
562 MtpConstants.FORMAT_GIF,
563 MtpConstants.FORMAT_JFIF,
564 MtpConstants.FORMAT_PNG,
565 MtpConstants.FORMAT_TIFF,
566 MtpConstants.FORMAT_WMA,
567 MtpConstants.FORMAT_OGG,
568 MtpConstants.FORMAT_AAC,
569 MtpConstants.FORMAT_MP4_CONTAINER,
570 MtpConstants.FORMAT_MP2,
571 MtpConstants.FORMAT_3GP_CONTAINER,
572 MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
573 MtpConstants.FORMAT_WPL_PLAYLIST,
574 MtpConstants.FORMAT_M3U_PLAYLIST,
575 MtpConstants.FORMAT_PLS_PLAYLIST,
576 MtpConstants.FORMAT_XML_DOCUMENT,
577 MtpConstants.FORMAT_FLAC,
581 private int[] getSupportedCaptureFormats() {
582 // no capture formats yet
586 static final int[] FILE_PROPERTIES = {
587 // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
588 // and IMAGE_PROPERTIES below
589 MtpConstants.PROPERTY_STORAGE_ID,
590 MtpConstants.PROPERTY_OBJECT_FORMAT,
591 MtpConstants.PROPERTY_PROTECTION_STATUS,
592 MtpConstants.PROPERTY_OBJECT_SIZE,
593 MtpConstants.PROPERTY_OBJECT_FILE_NAME,
594 MtpConstants.PROPERTY_DATE_MODIFIED,
595 MtpConstants.PROPERTY_PARENT_OBJECT,
596 MtpConstants.PROPERTY_PERSISTENT_UID,
597 MtpConstants.PROPERTY_NAME,
598 MtpConstants.PROPERTY_DATE_ADDED,
601 static final int[] AUDIO_PROPERTIES = {
602 // NOTE must match FILE_PROPERTIES above
603 MtpConstants.PROPERTY_STORAGE_ID,
604 MtpConstants.PROPERTY_OBJECT_FORMAT,
605 MtpConstants.PROPERTY_PROTECTION_STATUS,
606 MtpConstants.PROPERTY_OBJECT_SIZE,
607 MtpConstants.PROPERTY_OBJECT_FILE_NAME,
608 MtpConstants.PROPERTY_DATE_MODIFIED,
609 MtpConstants.PROPERTY_PARENT_OBJECT,
610 MtpConstants.PROPERTY_PERSISTENT_UID,
611 MtpConstants.PROPERTY_NAME,
612 MtpConstants.PROPERTY_DISPLAY_NAME,
613 MtpConstants.PROPERTY_DATE_ADDED,
615 // audio specific properties
616 MtpConstants.PROPERTY_ARTIST,
617 MtpConstants.PROPERTY_ALBUM_NAME,
618 MtpConstants.PROPERTY_ALBUM_ARTIST,
619 MtpConstants.PROPERTY_TRACK,
620 MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
621 MtpConstants.PROPERTY_DURATION,
622 MtpConstants.PROPERTY_GENRE,
623 MtpConstants.PROPERTY_COMPOSER,
624 MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
625 MtpConstants.PROPERTY_BITRATE_TYPE,
626 MtpConstants.PROPERTY_AUDIO_BITRATE,
627 MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
628 MtpConstants.PROPERTY_SAMPLE_RATE,
631 static final int[] VIDEO_PROPERTIES = {
632 // NOTE must match FILE_PROPERTIES above
633 MtpConstants.PROPERTY_STORAGE_ID,
634 MtpConstants.PROPERTY_OBJECT_FORMAT,
635 MtpConstants.PROPERTY_PROTECTION_STATUS,
636 MtpConstants.PROPERTY_OBJECT_SIZE,
637 MtpConstants.PROPERTY_OBJECT_FILE_NAME,
638 MtpConstants.PROPERTY_DATE_MODIFIED,
639 MtpConstants.PROPERTY_PARENT_OBJECT,
640 MtpConstants.PROPERTY_PERSISTENT_UID,
641 MtpConstants.PROPERTY_NAME,
642 MtpConstants.PROPERTY_DISPLAY_NAME,
643 MtpConstants.PROPERTY_DATE_ADDED,
645 // video specific properties
646 MtpConstants.PROPERTY_ARTIST,
647 MtpConstants.PROPERTY_ALBUM_NAME,
648 MtpConstants.PROPERTY_DURATION,
649 MtpConstants.PROPERTY_DESCRIPTION,
652 static final int[] IMAGE_PROPERTIES = {
653 // NOTE must match FILE_PROPERTIES above
654 MtpConstants.PROPERTY_STORAGE_ID,
655 MtpConstants.PROPERTY_OBJECT_FORMAT,
656 MtpConstants.PROPERTY_PROTECTION_STATUS,
657 MtpConstants.PROPERTY_OBJECT_SIZE,
658 MtpConstants.PROPERTY_OBJECT_FILE_NAME,
659 MtpConstants.PROPERTY_DATE_MODIFIED,
660 MtpConstants.PROPERTY_PARENT_OBJECT,
661 MtpConstants.PROPERTY_PERSISTENT_UID,
662 MtpConstants.PROPERTY_NAME,
663 MtpConstants.PROPERTY_DISPLAY_NAME,
664 MtpConstants.PROPERTY_DATE_ADDED,
666 // image specific properties
667 MtpConstants.PROPERTY_DESCRIPTION,
670 static final int[] ALL_PROPERTIES = {
671 // NOTE must match FILE_PROPERTIES above
672 MtpConstants.PROPERTY_STORAGE_ID,
673 MtpConstants.PROPERTY_OBJECT_FORMAT,
674 MtpConstants.PROPERTY_PROTECTION_STATUS,
675 MtpConstants.PROPERTY_OBJECT_SIZE,
676 MtpConstants.PROPERTY_OBJECT_FILE_NAME,
677 MtpConstants.PROPERTY_DATE_MODIFIED,
678 MtpConstants.PROPERTY_PARENT_OBJECT,
679 MtpConstants.PROPERTY_PERSISTENT_UID,
680 MtpConstants.PROPERTY_NAME,
681 MtpConstants.PROPERTY_DISPLAY_NAME,
682 MtpConstants.PROPERTY_DATE_ADDED,
684 // image specific properties
685 MtpConstants.PROPERTY_DESCRIPTION,
687 // audio specific properties
688 MtpConstants.PROPERTY_ARTIST,
689 MtpConstants.PROPERTY_ALBUM_NAME,
690 MtpConstants.PROPERTY_ALBUM_ARTIST,
691 MtpConstants.PROPERTY_TRACK,
692 MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
693 MtpConstants.PROPERTY_DURATION,
694 MtpConstants.PROPERTY_GENRE,
695 MtpConstants.PROPERTY_COMPOSER,
697 // video specific properties
698 MtpConstants.PROPERTY_ARTIST,
699 MtpConstants.PROPERTY_ALBUM_NAME,
700 MtpConstants.PROPERTY_DURATION,
701 MtpConstants.PROPERTY_DESCRIPTION,
703 // image specific properties
704 MtpConstants.PROPERTY_DESCRIPTION,
707 private int[] getSupportedObjectProperties(int format) {
709 case MtpConstants.FORMAT_MP3:
710 case MtpConstants.FORMAT_WAV:
711 case MtpConstants.FORMAT_WMA:
712 case MtpConstants.FORMAT_OGG:
713 case MtpConstants.FORMAT_AAC:
714 return AUDIO_PROPERTIES;
715 case MtpConstants.FORMAT_MPEG:
716 case MtpConstants.FORMAT_3GP_CONTAINER:
717 case MtpConstants.FORMAT_WMV:
718 return VIDEO_PROPERTIES;
719 case MtpConstants.FORMAT_EXIF_JPEG:
720 case MtpConstants.FORMAT_GIF:
721 case MtpConstants.FORMAT_PNG:
722 case MtpConstants.FORMAT_BMP:
723 return IMAGE_PROPERTIES;
725 return ALL_PROPERTIES;
727 return FILE_PROPERTIES;
731 private int[] getSupportedDeviceProperties() {
733 MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
734 MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
735 MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
736 MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
741 private MtpPropertyList getObjectPropertyList(long handle, int format, long property,
742 int groupCode, int depth) {
743 // FIXME - implement group support
744 if (groupCode != 0) {
745 return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
748 MtpPropertyGroup propertyGroup;
749 if (property == 0xFFFFFFFFL) {
750 propertyGroup = mPropertyGroupsByFormat.get(format);
751 if (propertyGroup == null) {
752 int[] propertyList = getSupportedObjectProperties(format);
753 propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mPackageName,
754 mVolumeName, propertyList);
755 mPropertyGroupsByFormat.put(new Integer(format), propertyGroup);
758 propertyGroup = mPropertyGroupsByProperty.get(property);
759 if (propertyGroup == null) {
760 int[] propertyList = new int[] { (int)property };
761 propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mPackageName,
762 mVolumeName, propertyList);
763 mPropertyGroupsByProperty.put(new Integer((int)property), propertyGroup);
767 return propertyGroup.getPropertyList((int)handle, format, depth);
770 private int renameFile(int handle, String newName) {
773 // first compute current path
775 String[] whereArgs = new String[] { Integer.toString(handle) };
777 c = mMediaProvider.query(mPackageName, mObjectsUri, PATH_PROJECTION, ID_WHERE,
778 whereArgs, null, null);
779 if (c != null && c.moveToNext()) {
780 path = c.getString(1);
782 } catch (RemoteException e) {
783 Log.e(TAG, "RemoteException in getObjectFilePath", e);
784 return MtpConstants.RESPONSE_GENERAL_ERROR;
791 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
794 // do not allow renaming any of the special subdirectories
795 if (isStorageSubDirectory(path)) {
796 return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
799 // now rename the file. make sure this succeeds before updating database
800 File oldFile = new File(path);
801 int lastSlash = path.lastIndexOf('/');
802 if (lastSlash <= 1) {
803 return MtpConstants.RESPONSE_GENERAL_ERROR;
805 String newPath = path.substring(0, lastSlash + 1) + newName;
806 File newFile = new File(newPath);
807 boolean success = oldFile.renameTo(newFile);
809 Log.w(TAG, "renaming "+ path + " to " + newPath + " failed");
810 return MtpConstants.RESPONSE_GENERAL_ERROR;
813 // finally update database
814 ContentValues values = new ContentValues();
815 values.put(Files.FileColumns.DATA, newPath);
818 // note - we are relying on a special case in MediaProvider.update() to update
819 // the paths for all children in the case where this is a directory.
820 updated = mMediaProvider.update(mPackageName, mObjectsUri, values, ID_WHERE, whereArgs);
821 } catch (RemoteException e) {
822 Log.e(TAG, "RemoteException in mMediaProvider.update", e);
825 Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
826 // this shouldn't happen, but if it does we need to rename the file to its original name
827 newFile.renameTo(oldFile);
828 return MtpConstants.RESPONSE_GENERAL_ERROR;
831 // check if nomedia status changed
832 if (newFile.isDirectory()) {
833 // for directories, check if renamed from something hidden to something non-hidden
834 if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) {
835 // directory was unhidden
837 mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, newPath, null);
838 } catch (RemoteException e) {
839 Log.e(TAG, "failed to unhide/rescan for " + newPath);
843 // for files, check if renamed from .nomedia to something else
844 if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia")
845 && !newPath.toLowerCase(Locale.US).equals(".nomedia")) {
847 mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, oldFile.getParent(), null);
848 } catch (RemoteException e) {
849 Log.e(TAG, "failed to unhide/rescan for " + newPath);
854 return MtpConstants.RESPONSE_OK;
857 private int setObjectProperty(int handle, int property,
858 long intValue, String stringValue) {
860 case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
861 return renameFile(handle, stringValue);
864 return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
868 private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
870 case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
871 case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
872 // writable string properties kept in shared preferences
873 String value = mDeviceProperties.getString(Integer.toString(property), "");
874 int length = value.length();
878 value.getChars(0, length, outStringValue, 0);
879 outStringValue[length] = 0;
880 return MtpConstants.RESPONSE_OK;
882 case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
883 // use screen size as max image size
884 Display display = ((WindowManager)mContext.getSystemService(
885 Context.WINDOW_SERVICE)).getDefaultDisplay();
886 int width = display.getMaximumSizeDimension();
887 int height = display.getMaximumSizeDimension();
888 String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
889 imageSize.getChars(0, imageSize.length(), outStringValue, 0);
890 outStringValue[imageSize.length()] = 0;
891 return MtpConstants.RESPONSE_OK;
893 // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code
896 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
900 private int setDeviceProperty(int property, long intValue, String stringValue) {
902 case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
903 case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
904 // writable string properties kept in shared prefs
905 SharedPreferences.Editor e = mDeviceProperties.edit();
906 e.putString(Integer.toString(property), stringValue);
907 return (e.commit() ? MtpConstants.RESPONSE_OK
908 : MtpConstants.RESPONSE_GENERAL_ERROR);
911 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
914 private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
915 char[] outName, long[] outCreatedModified) {
918 c = mMediaProvider.query(mPackageName, mObjectsUri, OBJECT_INFO_PROJECTION,
919 ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
920 if (c != null && c.moveToNext()) {
921 outStorageFormatParent[0] = c.getInt(1);
922 outStorageFormatParent[1] = c.getInt(2);
923 outStorageFormatParent[2] = c.getInt(3);
925 // extract name from path
926 String path = c.getString(4);
927 int lastSlash = path.lastIndexOf('/');
928 int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
929 int end = path.length();
930 if (end - start > 255) {
933 path.getChars(start, end, outName, 0);
934 outName[end - start] = 0;
936 outCreatedModified[0] = c.getLong(5);
937 outCreatedModified[1] = c.getLong(6);
938 // use modification date as creation date if date added is not set
939 if (outCreatedModified[0] == 0) {
940 outCreatedModified[0] = outCreatedModified[1];
944 } catch (RemoteException e) {
945 Log.e(TAG, "RemoteException in getObjectInfo", e);
954 private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
956 // special case root directory
957 mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
958 outFilePath[mMediaStoragePath.length()] = 0;
959 outFileLengthFormat[0] = 0;
960 outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
961 return MtpConstants.RESPONSE_OK;
965 c = mMediaProvider.query(mPackageName, mObjectsUri, PATH_FORMAT_PROJECTION,
966 ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
967 if (c != null && c.moveToNext()) {
968 String path = c.getString(1);
969 path.getChars(0, path.length(), outFilePath, 0);
970 outFilePath[path.length()] = 0;
971 // File transfers from device to host will likely fail if the size is incorrect.
972 // So to be safe, use the actual file size here.
973 outFileLengthFormat[0] = new File(path).length();
974 outFileLengthFormat[1] = c.getLong(2);
975 return MtpConstants.RESPONSE_OK;
977 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
979 } catch (RemoteException e) {
980 Log.e(TAG, "RemoteException in getObjectFilePath", e);
981 return MtpConstants.RESPONSE_GENERAL_ERROR;
989 private int deleteFile(int handle) {
990 mDatabaseModified = true;
996 c = mMediaProvider.query(mPackageName, mObjectsUri, PATH_FORMAT_PROJECTION,
997 ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
998 if (c != null && c.moveToNext()) {
999 // don't convert to media path here, since we will be matching
1000 // against paths in the database matching /data/media
1001 path = c.getString(1);
1002 format = c.getInt(2);
1004 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
1007 if (path == null || format == 0) {
1008 return MtpConstants.RESPONSE_GENERAL_ERROR;
1011 // do not allow deleting any of the special subdirectories
1012 if (isStorageSubDirectory(path)) {
1013 return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
1016 if (format == MtpConstants.FORMAT_ASSOCIATION) {
1017 // recursive case - delete all children first
1018 Uri uri = Files.getMtpObjectsUri(mVolumeName);
1019 int count = mMediaProvider.delete(mPackageName, uri,
1020 // the 'like' makes it use the index, the 'lower()' makes it correct
1021 // when the path contains sqlite wildcard characters
1022 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
1023 new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"});
1026 Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
1027 if (mMediaProvider.delete(mPackageName, uri, null, null) > 0) {
1028 if (format != MtpConstants.FORMAT_ASSOCIATION
1029 && path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
1031 String parentPath = path.substring(0, path.lastIndexOf("/"));
1032 mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, parentPath, null);
1033 } catch (RemoteException e) {
1034 Log.e(TAG, "failed to unhide/rescan for " + path);
1037 return MtpConstants.RESPONSE_OK;
1039 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
1041 } catch (RemoteException e) {
1042 Log.e(TAG, "RemoteException in deleteFile", e);
1043 return MtpConstants.RESPONSE_GENERAL_ERROR;
1051 private int[] getObjectReferences(int handle) {
1052 Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
1055 c = mMediaProvider.query(mPackageName, uri, ID_PROJECTION, null, null, null, null);
1059 int count = c.getCount();
1061 int[] result = new int[count];
1062 for (int i = 0; i < count; i++) {
1064 result[i] = c.getInt(0);
1068 } catch (RemoteException e) {
1069 Log.e(TAG, "RemoteException in getObjectList", e);
1078 private int setObjectReferences(int handle, int[] references) {
1079 mDatabaseModified = true;
1080 Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
1081 int count = references.length;
1082 ContentValues[] valuesList = new ContentValues[count];
1083 for (int i = 0; i < count; i++) {
1084 ContentValues values = new ContentValues();
1085 values.put(Files.FileColumns._ID, references[i]);
1086 valuesList[i] = values;
1089 if (mMediaProvider.bulkInsert(mPackageName, uri, valuesList) > 0) {
1090 return MtpConstants.RESPONSE_OK;
1092 } catch (RemoteException e) {
1093 Log.e(TAG, "RemoteException in setObjectReferences", e);
1095 return MtpConstants.RESPONSE_GENERAL_ERROR;
1098 private void sessionStarted() {
1099 mDatabaseModified = false;
1102 private void sessionEnded() {
1103 if (mDatabaseModified) {
1104 mContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
1105 mDatabaseModified = false;
1109 // used by the JNI code
1110 private long mNativeContext;
1112 private native final void native_setup();
1113 private native final void native_finalize();