[automerger skipped] Import translations. DO NOT MERGE skipped: cd6db9ecb1 skipped: 60f9184f36 am: 6501a185b9 -s ours am: 7dafb577cc -s ours
am: 3b45644f85 -s ours
Change-Id: Ia6630bb3da5bc1e5687bb05b4091681673502357
diff --git a/Android.mk b/Android.mk
index eb20588..033ea9d 100644
--- a/Android.mk
+++ b/Android.mk
@@ -9,6 +9,7 @@
LOCAL_JAVA_LIBRARIES :=
LOCAL_PACKAGE_NAME := MediaProvider
+LOCAL_PRIVATE_PLATFORM_APIS := true
LOCAL_CERTIFICATE := media
LOCAL_PRIVILEGED_MODULE := true
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b4c9e64..a957ca0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,9 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
package="com.android.providers.media"
android:sharedUserId="android.media"
android:sharedUserLabel="@string/uid_label"
- android:versionCode="800">
+ android:versionCode="900">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
@@ -13,6 +12,8 @@
<uses-permission android:name="android.permission.ACCESS_MTP" />
<uses-permission android:name="android.permission.MANAGE_USERS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.USE_RESERVED_DISK" />
<application android:process="android.process.media"
android:label="@string/app_label"
@@ -52,6 +53,7 @@
<receiver android:name="MediaScannerReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
@@ -73,8 +75,7 @@
</intent-filter>
</service>
- <receiver android:name=".MtpReceiver"
- androidprv:systemUserOnly="true">
+ <receiver android:name=".MtpReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
@@ -83,8 +84,7 @@
</intent-filter>
</receiver>
- <service android:name="MtpService"
- androidprv:systemUserOnly="true"/>
+ <service android:name="MtpService" />
<activity android:name="RingtonePickerActivity"
android:theme="@style/PickerDialogTheme"
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..7f4b137
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,3 @@
+deckard@android.com
+marcone@google.com
+zhangjerry@google.com
diff --git a/res/drawable/ic_add_padded.xml b/res/drawable/ic_add_padded.xml
new file mode 100644
index 0000000..c376867
--- /dev/null
+++ b/res/drawable/ic_add_padded.xml
@@ -0,0 +1,22 @@
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/ic_add"
+ android:insetTop="4dp"
+ android:insetRight="4dp"
+ android:insetBottom="4dp"
+ android:insetLeft="4dp"/>
diff --git a/res/layout-watch/add_ringtone_item.xml b/res/layout-watch/add_ringtone_item.xml
new file mode 100644
index 0000000..aef30e3
--- /dev/null
+++ b/res/layout-watch/add_ringtone_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+ Currently, no file manager app on watch could handle ACTION_GET_CONTENT intent.
+ Make the visibility to "gone" to prevent failures.
+ -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/add_ringtone_text"
+ android:textColor="?android:attr/colorAccent"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:drawableStart="@drawable/ic_add_padded"
+ android:drawablePadding="8dp"
+ android:ellipsize="marquee"
+ android:visibility="gone" />
diff --git a/res/layout-watch/radio_with_work_badge.xml b/res/layout-watch/radio_with_work_badge.xml
new file mode 100644
index 0000000..0e11621
--- /dev/null
+++ b/res/layout-watch/radio_with_work_badge.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.providers.media.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackground"
+ >
+
+ <CheckedTextView
+ android:id="@+id/checked_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorAlertDialogListItem"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:drawableStart="?android:attr/listChoiceIndicatorSingle"
+ android:drawablePadding="8dp"
+ android:ellipsize="marquee"
+ android:layout_toLeftOf="@+id/work_icon"
+ android:maxLines="3" />
+
+ <ImageView
+ android:id="@id/work_icon"
+ android:layout_width="18dp"
+ android:layout_height="18dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:scaleType="centerCrop"
+ android:layout_marginRight="20dp" />
+</com.android.providers.media.CheckedListItem>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 3685ab5..ca1e8a4 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -20,7 +20,7 @@
<string name="storage_description" msgid="4081716890357580107">"التخزين المحلي"</string>
<string name="app_label" msgid="9035307001052716210">"تخزين الوسائط"</string>
<string name="artist_label" msgid="8105600993099120273">"الفنان"</string>
- <string name="ringtone_default" msgid="1744846699922263043">"نغمة الرنين الافتراضية"</string>
+ <string name="ringtone_default" msgid="1744846699922263043">"نغمة الرنين التلقائية"</string>
<string name="notification_sound_default" msgid="6541609166469990135">"الصوت التلقائي للإشعارات"</string>
<string name="alarm_sound_default" msgid="5488847775252870314">"الصوت التلقائي للتنبيه"</string>
<string name="root_images" msgid="5861633549189045666">"الصور"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
new file mode 100644
index 0000000..bab716b
--- /dev/null
+++ b/res/values-as/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="uid_label" msgid="8421971615411294156">"মিডিয়া"</string>
+ <string name="storage_description" msgid="4081716890357580107">"স্থানীয় সঞ্চয়াগাৰ"</string>
+ <string name="app_label" msgid="9035307001052716210">"মিডিয়া সঞ্চয়াগাৰ"</string>
+ <string name="artist_label" msgid="8105600993099120273">"শিল্পী"</string>
+ <string name="ringtone_default" msgid="1744846699922263043">"ডিফ\'ল্ট ৰিংট\'ন"</string>
+ <string name="notification_sound_default" msgid="6541609166469990135">"জাননীৰ ডিফ\'ল্ট ধ্বনি"</string>
+ <string name="alarm_sound_default" msgid="5488847775252870314">"এলাৰ্মৰ ডিফ\'ল্ট ধ্বনি"</string>
+ <string name="root_images" msgid="5861633549189045666">"প্ৰতিচ্ছবিসমূহ"</string>
+ <string name="root_videos" msgid="8792703517064649453">"ভিডিঅ\'সমূহ"</string>
+ <string name="root_audio" msgid="3505830755201326018">"অডিঅ’"</string>
+ <string name="sound_name_awaken" msgid="5266892392848526147">"জাগি উঠাৰ ধ্বনি"</string>
+ <string name="sound_name_bounce" msgid="8771447635446665231">"বাউঞ্চ ধ্বনি"</string>
+ <string name="sound_name_drip" msgid="1744684469020662152">"ড্ৰিপ ধ্বনি"</string>
+ <string name="sound_name_gallop" msgid="2664454314532060876">"গেল\'প ধ্বনি"</string>
+ <string name="sound_name_nudge" msgid="5445751598250698244">"নাজ ধ্বনি"</string>
+ <string name="sound_name_orbit" msgid="4623457897813255481">"অৰবিট ধ্বনি"</string>
+ <string name="sound_name_rise" msgid="2200258555031675806">"ৰাইজ ধ্বনি"</string>
+ <string name="sound_name_sway" msgid="348448316663085643">"শ্ব\'ৱে ধ্বনি"</string>
+ <string name="sound_name_wag" msgid="2730733083078126563">"ৱাগ ধ্বনি"</string>
+ <string name="add_ringtone_text" msgid="8077717303037760418">"ৰিংট\'ন যোগ কৰক"</string>
+ <string name="delete_ringtone_text" msgid="2963662097583300181">"মচক"</string>
+ <string name="unable_to_add_ringtone" msgid="6256097521165711269">"নিজৰ উপযোগিতা অনুযায়ী তৈয়াৰ কৰা ৰিংট\'ন যোগ কৰিব পৰা নগ\'ল"</string>
+ <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"নিজৰ উপযোগিতা অনুযায়ী তৈয়াৰ কৰা ৰিংট\'ন মচিব পৰা নগ\'ল"</string>
+</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 6fa3d45..1588eb8 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -22,7 +22,7 @@
<string name="artist_label" msgid="8105600993099120273">"Artista"</string>
<string name="ringtone_default" msgid="1744846699922263043">"Tono de llamada predeterminado"</string>
<string name="notification_sound_default" msgid="6541609166469990135">"Sonido de notificación predeterminado"</string>
- <string name="alarm_sound_default" msgid="5488847775252870314">"Sonido de alarma predeterminado"</string>
+ <string name="alarm_sound_default" msgid="5488847775252870314">"Sonido de alarma pred."</string>
<string name="root_images" msgid="5861633549189045666">"Imágenes"</string>
<string name="root_videos" msgid="8792703517064649453">"Vídeos"</string>
<string name="root_audio" msgid="3505830755201326018">"Audio"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index f018e2d..f1785bb 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -22,7 +22,7 @@
<string name="artist_label" msgid="8105600993099120273">"Artiste"</string>
<string name="ringtone_default" msgid="1744846699922263043">"Sonnerie par défaut"</string>
<string name="notification_sound_default" msgid="6541609166469990135">"Son de notification par défaut"</string>
- <string name="alarm_sound_default" msgid="5488847775252870314">"Son par défaut pour l\'alarme"</string>
+ <string name="alarm_sound_default" msgid="5488847775252870314">"Son de l\'alarme par défaut"</string>
<string name="root_images" msgid="5861633549189045666">"Images"</string>
<string name="root_videos" msgid="8792703517064649453">"Vidéos"</string>
<string name="root_audio" msgid="3505830755201326018">"Audio"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 339ee5b..0ef4bc5 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -21,7 +21,7 @@
<string name="app_label" msgid="9035307001052716210">"Stockage multimédia"</string>
<string name="artist_label" msgid="8105600993099120273">"Artiste"</string>
<string name="ringtone_default" msgid="1744846699922263043">"Sonnerie par défaut"</string>
- <string name="notification_sound_default" msgid="6541609166469990135">"Son de notification par défaut"</string>
+ <string name="notification_sound_default" msgid="6541609166469990135">"Son notif. par défaut"</string>
<string name="alarm_sound_default" msgid="5488847775252870314">"Son de l\'alarme par défaut"</string>
<string name="root_images" msgid="5861633549189045666">"Images"</string>
<string name="root_videos" msgid="8792703517064649453">"Vidéos"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 3ff4341..64ab7e0 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -21,7 +21,7 @@
<string name="app_label" msgid="9035307001052716210">"Медиа санах ой"</string>
<string name="artist_label" msgid="8105600993099120273">"Уран бүтээлч"</string>
<string name="ringtone_default" msgid="1744846699922263043">"Үндсэн хонхны ая"</string>
- <string name="notification_sound_default" msgid="6541609166469990135">"Мэдэгдлийн үндсэн ая"</string>
+ <string name="notification_sound_default" msgid="6541609166469990135">"Мэдэгдлийн өгөгдмөл ая"</string>
<string name="alarm_sound_default" msgid="5488847775252870314">"Сэрүүлгийн өгөгдмөл дуу"</string>
<string name="root_images" msgid="5861633549189045666">"Зураг"</string>
<string name="root_videos" msgid="8792703517064649453">"Бичлэг"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index bf6888b..fab60ec 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -37,6 +37,6 @@
<string name="sound_name_wag" msgid="2730733083078126563">"वॉग"</string>
<string name="add_ringtone_text" msgid="8077717303037760418">"रिंगटोन जोडा"</string>
<string name="delete_ringtone_text" msgid="2963662097583300181">"हटवा"</string>
- <string name="unable_to_add_ringtone" msgid="6256097521165711269">"सानुकूल रिंगटोन जोडण्यात अक्षम"</string>
- <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"सानुकूल रिंगटोन हटविण्यात अक्षम"</string>
+ <string name="unable_to_add_ringtone" msgid="6256097521165711269">"कस्टम रिंगटोन जोडण्यात अक्षम"</string>
+ <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"कस्टम रिंगटोन हटविण्यात अक्षम"</string>
</resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
new file mode 100644
index 0000000..3ba7dfb
--- /dev/null
+++ b/res/values-or/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="uid_label" msgid="8421971615411294156">"ମିଡିଆ"</string>
+ <string name="storage_description" msgid="4081716890357580107">"ଲୋକାଲ୍ ଷ୍ଟୋରେଜ୍"</string>
+ <string name="app_label" msgid="9035307001052716210">"ମିଡିଆ ଷ୍ଟୋରେଜ୍"</string>
+ <string name="artist_label" msgid="8105600993099120273">"କଳାକାର"</string>
+ <string name="ringtone_default" msgid="1744846699922263043">"ଡିଫଲ୍ଟ ରିଙ୍ଗଟୋନ୍"</string>
+ <string name="notification_sound_default" msgid="6541609166469990135">"ଡିଫଲ୍ଟ ବିଜ୍ଞପ୍ତି ଶବ୍ଦ"</string>
+ <string name="alarm_sound_default" msgid="5488847775252870314">"ଡିଫଲ୍ଟ ଆଲାର୍ମ ଶବ୍ଦ"</string>
+ <string name="root_images" msgid="5861633549189045666">"ଇମେଜ୍"</string>
+ <string name="root_videos" msgid="8792703517064649453">"ଭିଡିଓ"</string>
+ <string name="root_audio" msgid="3505830755201326018">"ଅଡିଓ"</string>
+ <string name="sound_name_awaken" msgid="5266892392848526147">"ଆୱେକନ୍"</string>
+ <string name="sound_name_bounce" msgid="8771447635446665231">"ବାଉନ୍ସ"</string>
+ <string name="sound_name_drip" msgid="1744684469020662152">"ଡ୍ରିପ୍"</string>
+ <string name="sound_name_gallop" msgid="2664454314532060876">"ଗେଲପ୍"</string>
+ <string name="sound_name_nudge" msgid="5445751598250698244">"ନଜ୍"</string>
+ <string name="sound_name_orbit" msgid="4623457897813255481">"ଅରବିଟ୍"</string>
+ <string name="sound_name_rise" msgid="2200258555031675806">"ରାଇଜ୍"</string>
+ <string name="sound_name_sway" msgid="348448316663085643">"ସ୍ୱେ"</string>
+ <string name="sound_name_wag" msgid="2730733083078126563">"ୱେଗ୍"</string>
+ <string name="add_ringtone_text" msgid="8077717303037760418">"ରିଙ୍ଗଟୋନ୍ ଯୋଡ଼ନ୍ତୁ"</string>
+ <string name="delete_ringtone_text" msgid="2963662097583300181">"ଡିଲିଟ୍ କରନ୍ତୁ"</string>
+ <string name="unable_to_add_ringtone" msgid="6256097521165711269">"କଷ୍ଟମ୍ ରିଙ୍ଗଟୋନ୍ ଯୋଡ଼ିପାରିବ ନାହିଁ"</string>
+ <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"କଷ୍ଟମ୍ ରିଙ୍ଗଟୋନ୍ ଡିଲିଟ୍ କରିପାରିବ ନାହିଁ"</string>
+</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index e53c849..9835eda 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -21,7 +21,7 @@
<string name="app_label" msgid="9035307001052716210">"Armazenamento de multimédia"</string>
<string name="artist_label" msgid="8105600993099120273">"Artista"</string>
<string name="ringtone_default" msgid="1744846699922263043">"Toque predefinido"</string>
- <string name="notification_sound_default" msgid="6541609166469990135">"Som de notificação predefinido"</string>
+ <string name="notification_sound_default" msgid="6541609166469990135">"Som notif. predefinido"</string>
<string name="alarm_sound_default" msgid="5488847775252870314">"Som de alarme predefinido"</string>
<string name="root_images" msgid="5861633549189045666">"Imagens"</string>
<string name="root_videos" msgid="8792703517064649453">"Vídeos"</string>
diff --git a/src/com/android/providers/media/IMtpService.aidl b/src/com/android/providers/media/IMtpService.aidl
index e599f7b..b2a32cb 100644
--- a/src/com/android/providers/media/IMtpService.aidl
+++ b/src/com/android/providers/media/IMtpService.aidl
@@ -18,6 +18,4 @@
interface IMtpService
{
- void sendObjectAdded(int objectHandle);
- void sendObjectRemoved(int objectHandle);
}
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 65a6769..c5af9db 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -16,6 +16,7 @@
package com.android.providers.media;
+import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
@@ -25,11 +26,16 @@
import android.database.MatrixCursor.RowBuilder;
import android.graphics.BitmapFactory;
import android.graphics.Point;
+import android.media.ExifInterface;
+import android.media.MediaMetadata;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.IBinder;
import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.BaseColumns;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
@@ -47,14 +53,23 @@
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.Video;
import android.provider.MediaStore.Video.VideoColumns;
+import android.provider.MetadataReader;
import android.text.TextUtils;
+import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Log;
import libcore.io.IoUtils;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
/**
* Presents a {@link DocumentsContract} view of {@link MediaProvider} external
@@ -103,6 +118,47 @@
return TextUtils.join("\n", args);
}
+ public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio";
+ public static final String METADATA_KEY_VIDEO = "android.media.metadata.video";
+ // Video lat/long are just that. Lat/long. Unlike EXIF where the values are
+ // in fact some funky string encoding. So we add our own contstant to convey coords.
+ public static final String METADATA_VIDEO_LATITUDE = "android.media.metadata.video:latitude";
+ public static final String METADATA_VIDEO_LONGITUTE = "android.media.metadata.video:longitude";
+
+ /*
+ * A mapping between media colums and metadata tag names. These keys of the
+ * map form the projection for queries against the media store database.
+ */
+ private static final Map<String, String> IMAGE_COLUMN_MAP = new HashMap<>();
+ private static final Map<String, String> VIDEO_COLUMN_MAP = new HashMap<>();
+ private static final Map<String, String> AUDIO_COLUMN_MAP = new HashMap<>();
+
+ static {
+ /**
+ * Note that for images (jpegs at least) we'll first try an alternate
+ * means of extracting metadata, one that provides more data. But if
+ * that fails, or if the image type is not JPEG, we fall back to these columns.
+ */
+ IMAGE_COLUMN_MAP.put(ImageColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH);
+ IMAGE_COLUMN_MAP.put(ImageColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH);
+ IMAGE_COLUMN_MAP.put(ImageColumns.DATE_TAKEN, ExifInterface.TAG_DATETIME);
+ IMAGE_COLUMN_MAP.put(ImageColumns.LATITUDE, ExifInterface.TAG_GPS_LATITUDE);
+ IMAGE_COLUMN_MAP.put(ImageColumns.LONGITUDE, ExifInterface.TAG_GPS_LONGITUDE);
+
+ VIDEO_COLUMN_MAP.put(VideoColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION);
+ VIDEO_COLUMN_MAP.put(VideoColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH);
+ VIDEO_COLUMN_MAP.put(VideoColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH);
+ VIDEO_COLUMN_MAP.put(VideoColumns.LATITUDE, METADATA_VIDEO_LATITUDE);
+ VIDEO_COLUMN_MAP.put(VideoColumns.LONGITUDE, METADATA_VIDEO_LONGITUTE);
+ VIDEO_COLUMN_MAP.put(VideoColumns.DATE_TAKEN, MediaMetadata.METADATA_KEY_DATE);
+
+ AUDIO_COLUMN_MAP.put(AudioColumns.ARTIST, MediaMetadata.METADATA_KEY_ARTIST);
+ AUDIO_COLUMN_MAP.put(AudioColumns.COMPOSER, MediaMetadata.METADATA_KEY_COMPOSER);
+ AUDIO_COLUMN_MAP.put(AudioColumns.ALBUM, MediaMetadata.METADATA_KEY_ALBUM);
+ AUDIO_COLUMN_MAP.put(AudioColumns.YEAR, MediaMetadata.METADATA_KEY_YEAR);
+ AUDIO_COLUMN_MAP.put(AudioColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION);
+ }
+
private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
}
@@ -112,6 +168,29 @@
return true;
}
+ private void enforceShellRestrictions() {
+ if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
+ && getContext().getSystemService(UserManager.class)
+ .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
+ throw new SecurityException(
+ "Shell user cannot access files for user " + UserHandle.myUserId());
+ }
+ }
+
+ @Override
+ protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
+ throws SecurityException {
+ enforceShellRestrictions();
+ return super.enforceReadPermissionInner(uri, callingPkg, callerToken);
+ }
+
+ @Override
+ protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)
+ throws SecurityException {
+ enforceShellRestrictions();
+ return super.enforceWritePermissionInner(uri, callingPkg, callerToken);
+ }
+
private static void notifyRootsChanged(Context context) {
context.getContentResolver()
.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false);
@@ -217,6 +296,143 @@
}
@Override
+ public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException {
+
+ String mimeType = getDocumentType(docId);
+
+ if (MetadataReader.isSupportedMimeType(mimeType)) {
+ return getDocumentMetadataFromStream(docId, mimeType);
+ } else {
+ return getDocumentMetadataFromIndex(docId);
+ }
+ }
+
+ private @Nullable Bundle getDocumentMetadataFromStream(String docId, String mimeType) {
+ assert MetadataReader.isSupportedMimeType(mimeType);
+ InputStream stream = null;
+ try {
+ stream = new ParcelFileDescriptor.AutoCloseInputStream(
+ openDocument(docId, "r", null));
+ Bundle metadata = new Bundle();
+ MetadataReader.getMetadata(metadata, stream, mimeType, null);
+ return metadata;
+ } catch (IOException io) {
+ return null;
+ } finally {
+ IoUtils.closeQuietly(stream);
+ }
+ }
+
+ public @Nullable Bundle getDocumentMetadataFromIndex(String docId)
+ throws FileNotFoundException {
+
+ final Ident ident = getIdentForDocId(docId);
+
+ Map<String, String> columnMap = null;
+ String tagType;
+ Uri query;
+
+ switch (ident.type) {
+ case TYPE_IMAGE:
+ columnMap = IMAGE_COLUMN_MAP;
+ tagType = DocumentsContract.METADATA_EXIF;
+ query = Images.Media.EXTERNAL_CONTENT_URI;
+ break;
+ case TYPE_VIDEO:
+ columnMap = VIDEO_COLUMN_MAP;
+ tagType = METADATA_KEY_VIDEO;
+ query = Video.Media.EXTERNAL_CONTENT_URI;
+ break;
+ case TYPE_AUDIO:
+ columnMap = AUDIO_COLUMN_MAP;
+ tagType = METADATA_KEY_AUDIO;
+ query = Audio.Media.EXTERNAL_CONTENT_URI;
+ break;
+ default:
+ // Unsupported file type.
+ throw new FileNotFoundException(
+ "Metadata request for unsupported file type: " + ident.type);
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ Cursor cursor = null;
+ Bundle result = null;
+
+ final ContentResolver resolver = getContext().getContentResolver();
+ Collection<String> columns = columnMap.keySet();
+ String[] projection = columns.toArray(new String[columns.size()]);
+ try {
+ cursor = resolver.query(
+ query,
+ projection,
+ BaseColumns._ID + "=?",
+ new String[]{Long.toString(ident.id)},
+ null);
+
+ if (!cursor.moveToFirst()) {
+ throw new FileNotFoundException("Can't find document id: " + docId);
+ }
+
+ final Bundle metadata = extractMetadataFromCursor(cursor, columnMap);
+ result = new Bundle();
+ result.putBundle(tagType, metadata);
+ result.putStringArray(
+ DocumentsContract.METADATA_TYPES,
+ new String[]{tagType});
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ Binder.restoreCallingIdentity(token);
+ }
+ return result;
+ }
+
+ private static Bundle extractMetadataFromCursor(Cursor cursor, Map<String, String> columns) {
+
+ assert (cursor.getCount() == 1);
+
+ final Bundle metadata = new Bundle();
+ for (String col : columns.keySet()) {
+
+ int index = cursor.getColumnIndex(col);
+ String bundleTag = columns.get(col);
+
+ // Special case to be able to pull longs out of a cursor, as long is not a supported
+ // field of getType.
+ if (ExifInterface.TAG_DATETIME.equals(bundleTag)) {
+ // formate string to be consistent with how EXIF interface formats the date.
+ long date = cursor.getLong(index);
+ String format = DateFormat.getBestDateTimePattern(Locale.getDefault(),
+ "MMM dd, yyyy, hh:mm");
+ metadata.putString(bundleTag, DateFormat.format(format, date).toString());
+ continue;
+ }
+
+ switch (cursor.getType(index)) {
+ case Cursor.FIELD_TYPE_INTEGER:
+ metadata.putInt(bundleTag, cursor.getInt(index));
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ //Errors on the side of greater precision since interface doesnt support doubles
+ metadata.putFloat(bundleTag, cursor.getFloat(index));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ metadata.putString(bundleTag, cursor.getString(index));
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ Log.d(TAG, "Unsupported type, blob, for col: " + bundleTag);
+ break;
+ case Cursor.FIELD_TYPE_NULL:
+ Log.d(TAG, "Unsupported type, null, for col: " + bundleTag);
+ break;
+ default:
+ throw new RuntimeException("Data type not supported");
+ }
+ }
+
+ return metadata;
+ }
+
+ @Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
includeImagesRoot(result);
@@ -669,7 +885,9 @@
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(ImageQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
row.add(Document.COLUMN_FLAGS,
- Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_SUPPORTS_DELETE);
+ Document.FLAG_SUPPORTS_THUMBNAIL
+ | Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_SUPPORTS_METADATA);
}
private interface VideosBucketQuery {
@@ -727,7 +945,9 @@
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(VideoQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
row.add(Document.COLUMN_FLAGS,
- Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_SUPPORTS_DELETE);
+ Document.FLAG_SUPPORTS_THUMBNAIL
+ | Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_SUPPORTS_METADATA);
}
private interface ArtistQuery {
@@ -796,7 +1016,8 @@
row.add(Document.COLUMN_MIME_TYPE, cursor.getString(SongQuery.MIME_TYPE));
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(SongQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
- row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
+ row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_SUPPORTS_METADATA);
}
private interface ImagesBucketThumbnailQuery {
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index b058008..8f3b54b 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -43,6 +43,7 @@
import android.content.UriMatcher;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseUtils;
@@ -66,11 +67,13 @@
import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.IBinder;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
-import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.os.storage.VolumeInfo;
@@ -91,10 +94,6 @@
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
-
-import libcore.io.IoUtils;
-import libcore.util.EmptyArray;
-
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
@@ -112,6 +111,8 @@
import java.util.Locale;
import java.util.PriorityQueue;
import java.util.Stack;
+import libcore.io.IoUtils;
+import libcore.util.EmptyArray;
/**
* Media content provider. See {@link android.provider.MediaStore} for details.
@@ -152,6 +153,7 @@
private StorageManager mStorageManager;
private AppOpsManager mAppOpsManager;
+ private PackageManager mPackageManager;
// In memory cache of path<->id mappings, to speed up inserts during media scan
HashMap<String, Long> mDirectoryCache = new HashMap<String, Long>();
@@ -288,30 +290,17 @@
context.sendBroadcast(
new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
- // don't send objectRemoved events - MTP be sending StorageRemoved anyway
- mDisableMtpObjectCallbacks = true;
Log.d(TAG, "deleting all entries for storage " + storage);
- SQLiteDatabase db = database.getWritableDatabase();
- // First clear the file path to disable the _DELETE_FILE database hook.
- // We do this to avoid deleting files if the volume is remounted while
- // we are still processing the unmount event.
- ContentValues values = new ContentValues();
- values.putNull(Files.FileColumns.DATA);
- String where = FileColumns.STORAGE_ID + "=?";
- String[] whereArgs = new String[] { Integer.toString(storage.getStorageId()) };
- database.mNumUpdates++;
- db.beginTransaction();
- try {
- db.update("files", values, where, whereArgs);
- // now delete the records
- database.mNumDeletes++;
- int numpurged = db.delete("files", where, whereArgs);
- logToDb(db, "removed " + numpurged +
- " rows for ejected filesystem " + storage.getPath());
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
+ Uri.Builder builder =
+ Files.getMtpObjectsUri(EXTERNAL_VOLUME).buildUpon();
+ builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
+ delete(builder.build(),
+ // the 'like' makes it use the index, the 'lower()' makes it
+ // correct when the path contains sqlite wildcard characters
+ "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
+ new String[]{storage.getPath() + "/%",
+ Integer.toString(storage.getPath().length() + 1),
+ storage.getPath() + "/"});
// notify on media Uris as well as the files Uri
context.getContentResolver().notifyChange(
Audio.Media.getContentUri(EXTERNAL_VOLUME), null);
@@ -326,7 +315,6 @@
} finally {
context.sendBroadcast(
new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
- mDisableMtpObjectCallbacks = false;
}
}
}
@@ -334,9 +322,6 @@
}
};
- // set to disable sending events when the operation originates from MTP
- private boolean mDisableMtpObjectCallbacks;
-
private final SQLiteDatabase.CustomFunction mObjectRemovedCallback =
new SQLiteDatabase.CustomFunction() {
@Override
@@ -347,18 +332,6 @@
// TODO: include the path in the callback and only remove the affected
// entry from the cache
mDirectoryCache.clear();
- // do nothing if the operation originated from MTP
- if (mDisableMtpObjectCallbacks) return;
-
- Log.d(TAG, "object removed " + args[0]);
- IMtpService mtpService = mMtpService;
- if (mtpService != null) {
- try {
- sendObjectRemoved(Integer.parseInt(args[0]));
- } catch (NumberFormatException e) {
- Log.e(TAG, "NumberFormatException in mObjectRemovedCallback", e);
- }
- }
}
};
@@ -605,6 +578,7 @@
mStorageManager = context.getSystemService(StorageManager.class);
mAppOpsManager = context.getSystemService(AppOpsManager.class);
+ mPackageManager = context.getPackageManager();
sArtistAlbumsMap.put(MediaStore.Audio.Albums._ID, "audio.album_id AS " +
MediaStore.Audio.Albums._ID);
@@ -715,6 +689,29 @@
return true;
}
+ private void enforceShellRestrictions() {
+ if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
+ && getContext().getSystemService(UserManager.class)
+ .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
+ throw new SecurityException(
+ "Shell user cannot access files for user " + UserHandle.myUserId());
+ }
+ }
+
+ @Override
+ protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
+ throws SecurityException {
+ enforceShellRestrictions();
+ return super.enforceReadPermissionInner(uri, callingPkg, callerToken);
+ }
+
+ @Override
+ protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)
+ throws SecurityException {
+ enforceShellRestrictions();
+ return super.enforceWritePermissionInner(uri, callingPkg, callerToken);
+ }
+
private static final String TABLE_FILES = "files";
private static final String TABLE_ALBUM_ART = "album_art";
private static final String TABLE_THUMBNAILS = "thumbnails";
@@ -823,8 +820,8 @@
+ "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT,"
+ "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT,"
+ "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT,"
- + "media_type INTEGER,old_id INTEGER,storage_id INTEGER,is_drm INTEGER,"
- + "width INTEGER, height INTEGER)");
+ + "media_type INTEGER,old_id INTEGER,is_drm INTEGER,"
+ + "width INTEGER, height INTEGER, title_resource_uri TEXT)");
db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
if (!internal) {
db.execSQL("CREATE TABLE audio_genres (_id INTEGER PRIMARY KEY,name TEXT NOT NULL)");
@@ -918,7 +915,7 @@
+ " BEGIN SELECT _DELETE_FILE(old._data);END");
}
- private static void updateFromKKSchema(SQLiteDatabase db, boolean internal, int fromVersion) {
+ private static void updateFromKKSchema(SQLiteDatabase db) {
// Delete albums and artists, then clear the modification time on songs, which
// will cause the media scanner to rescan everything, rebuilding the artist and
// album tables along the way, while preserving playlists.
@@ -926,7 +923,16 @@
// collation keys
db.execSQL("DELETE from albums");
db.execSQL("DELETE from artists");
- db.execSQL("UPDATE files SET date_modified=0;");
+ db.execSQL("ALTER TABLE files ADD COLUMN title_resource_uri TEXT DEFAULT NULL");
+ db.execSQL("UPDATE files SET date_modified=0");
+ }
+
+ private static void updateFromOCSchema(SQLiteDatabase db) {
+ // Add the column used for title localization, and force a rescan of any
+ // ringtones, alarms and notifications that may be using it.
+ db.execSQL("ALTER TABLE files ADD COLUMN title_resource_uri TEXT DEFAULT NULL");
+ db.execSQL("UPDATE files SET date_modified=0"
+ + " WHERE (is_alarm IS 1) OR (is_ringtone IS 1) OR (is_notification IS 1)");
}
/**
@@ -956,7 +962,9 @@
// Anything older than KK is recreated from scratch
createLatestSchema(db, internal);
} else if (fromVersion < 800) {
- updateFromKKSchema(db, internal, fromVersion);
+ updateFromKKSchema(db);
+ } else if (fromVersion < 900) {
+ updateFromOCSchema(db);
}
sanityCheck(db, fromVersion);
@@ -1197,7 +1205,7 @@
// Construct a canonical Uri by tacking on some query parameters
builder = uri.buildUpon();
builder.appendQueryParameter(CANONICAL, "1");
- title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE));
+ title = getDefaultTitleFromCursor(c);
} finally {
IoUtils.closeQuietly(c);
}
@@ -1229,7 +1237,7 @@
try {
int titleIdx = c.getColumnIndex(MediaStore.Audio.Media.TITLE);
if (c != null && c.getCount() == 1 && c.moveToNext() &&
- titleFromUri.equals(c.getString(titleIdx))) {
+ titleFromUri.equals(getDefaultTitleFromCursor(c))) {
// the result matched perfectly
return uri;
}
@@ -1809,32 +1817,6 @@
return values;
}
- private void sendObjectAdded(long objectHandle) {
- synchronized (mMtpServiceConnection) {
- if (mMtpService != null) {
- try {
- mMtpService.sendObjectAdded((int)objectHandle);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in sendObjectAdded", e);
- mMtpService = null;
- }
- }
- }
- }
-
- private void sendObjectRemoved(long objectHandle) {
- synchronized (mMtpServiceConnection) {
- if (mMtpService != null) {
- try {
- mMtpService.sendObjectRemoved((int)objectHandle);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in sendObjectRemoved", e);
- mMtpService = null;
- }
- }
- }
- }
-
@Override
public int bulkInsert(Uri uri, ContentValues values[]) {
int match = URI_MATCHER.match(uri);
@@ -1878,13 +1860,6 @@
}
}
- // Notify MTP (outside of successful transaction)
- if (uri != null) {
- if (uri.toString().startsWith("content://media/external/")) {
- notifyMtp(notifyRowIds);
- }
- }
-
getContext().getContentResolver().notifyChange(uri, null);
return numInserted;
}
@@ -1895,11 +1870,6 @@
ArrayList<Long> notifyRowIds = new ArrayList<Long>();
Uri newUri = insertInternal(uri, match, initialValues, notifyRowIds);
- if (uri != null) {
- if (uri.toString().startsWith("content://media/external/")) {
- notifyMtp(notifyRowIds);
- }
- }
// do not signal notification for MTP objects.
// we will signal instead after file transfer is successful.
@@ -1921,13 +1891,6 @@
return newUri;
}
- private void notifyMtp(ArrayList<Long> rowIds) {
- int size = rowIds.size();
- for (int i = 0; i < size; i++) {
- sendObjectAdded(rowIds.get(i).longValue());
- }
- }
-
private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) {
DatabaseUtils.InsertHelper helper =
new DatabaseUtils.InsertHelper(db, "audio_playlists_map");
@@ -1971,14 +1934,12 @@
values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
values.put(FileColumns.DATA, path);
values.put(FileColumns.PARENT, getParent(helper, db, path));
- values.put(FileColumns.STORAGE_ID, getStorageId(path));
File file = new File(path);
if (file.exists()) {
values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
}
helper.mNumInserts++;
long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
- sendObjectAdded(rowId);
return rowId;
}
@@ -2027,14 +1988,136 @@
}
}
- private int getStorageId(String path) {
- final StorageManager storage = getContext().getSystemService(StorageManager.class);
- final StorageVolume vol = storage.getStorageVolume(new File(path));
- if (vol != null) {
- return vol.getStorageId();
+ /**
+ * @param c the Cursor whose title to retrieve
+ * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise
+ * the value of the {@code MediaStore.Audio.Media.TITLE} column
+ */
+ private String getDefaultTitleFromCursor(Cursor c) {
+ String title = null;
+ final int columnIndex = c.getColumnIndex("title_resource_uri");
+ // Necessary to check for existence because we may be reading from an old DB version
+ if (columnIndex > -1) {
+ final String titleResourceUri = c.getString(columnIndex);
+ if (titleResourceUri != null) {
+ try {
+ title = getDefaultTitle(titleResourceUri);
+ } catch (Exception e) {
+ // Best attempt only
+ }
+ }
+ }
+ if (title == null) {
+ title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE));
+ }
+ return title;
+ }
+
+ /**
+ * @param title_resource_uri The title resource for which to retrieve the default localization
+ * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable
+ * @throws Exception Thrown if the title appears to be localizable, but the localization failed
+ * for any reason. For example, the application from which the localized title is fetched is not
+ * installed, or it does not have the resource which needs to be localized
+ */
+ private String getDefaultTitle(String title_resource_uri) throws Exception{
+ try {
+ return getTitleFromResourceUri(title_resource_uri, false);
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting default title for " + title_resource_uri, e);
+ throw e;
+ }
+ }
+
+ /**
+ * @param title_resource_uri The title resource to localize
+ * @return The localized title, or {@code null} if unlocalizable
+ * @throws Exception Thrown if the title appears to be localizable, but the localization failed
+ * for any reason. For example, the application from which the localized title is fetched is not
+ * installed, or it does not have the resource which needs to be localized
+ */
+ private String getLocalizedTitle(String title_resource_uri) throws Exception {
+ try {
+ return getTitleFromResourceUri(title_resource_uri, true);
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting localized title for " + title_resource_uri, e);
+ throw e;
+ }
+ }
+
+ /**
+ * Localizable titles conform to this URI pattern:
+ * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE}
+ * Authority: Package Name of ringtone title provider
+ * First Path Segment: Type of resource (must be "string")
+ * Second Path Segment: Resource name of title
+ *
+ * @param title_resource_uri The title resource to retrieve
+ * @param localize Whether or not to localize the title
+ * @return The title, or {@code null} if unlocalizable
+ * @throws Exception Thrown if the title appears to be localizable, but the localization failed
+ * for any reason. For example, the application from which the localized title is fetched is not
+ * installed, or it does not have the resource which needs to be localized
+ */
+ private String getTitleFromResourceUri(String title_resource_uri, boolean localize)
+ throws Exception {
+ if (TextUtils.isEmpty(title_resource_uri)) {
+ return null;
+ }
+ final Uri titleUri = Uri.parse(title_resource_uri);
+ final String scheme = titleUri.getScheme();
+ if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+ return null;
+ }
+ final List<String> pathSegments = titleUri.getPathSegments();
+ if (pathSegments.size() != 2) {
+ Log.e(TAG, "Error getting localized title for " + title_resource_uri
+ + ", must have 2 path segments");
+ return null;
+ }
+ final String type = pathSegments.get(0);
+ if (!"string".equals(type)) {
+ Log.e(TAG, "Error getting localized title for " + title_resource_uri
+ + ", first path segment must be \"string\"");
+ return null;
+ }
+ final String packageName = titleUri.getAuthority();
+ final Resources resources;
+ if (localize) {
+ resources = mPackageManager.getResourcesForApplication(packageName);
} else {
- Log.w(TAG, "Missing volume for " + path + "; assuming invalid");
- return StorageVolume.STORAGE_ID_INVALID;
+ final Context packageContext = getContext().createPackageContext(packageName, 0);
+ final Configuration configuration = packageContext.getResources().getConfiguration();
+ configuration.setLocale(Locale.US);
+ resources = packageContext.createConfigurationContext(configuration).getResources();
+ }
+ final String resourceIdentifier = pathSegments.get(1);
+ final int id = resources.getIdentifier(resourceIdentifier, type, packageName);
+ return resources.getString(id);
+ }
+
+ private void localizeTitles() {
+ for (DatabaseHelper helper : mDatabases.values()) {
+ final SQLiteDatabase db = helper.getWritableDatabase();
+ try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"},
+ "title_resource_uri IS NOT NULL", null, null, null, null)) {
+ while (c.moveToNext()) {
+ final String id = c.getString(0);
+ final String titleResourceUri = c.getString(1);
+ final ContentValues values = new ContentValues();
+ try {
+ final String localizedTitle = getLocalizedTitle(titleResourceUri);
+ values.put("title_key", MediaStore.Audio.keyFor(localizedTitle));
+ // do a final trim of the title, in case it started with the special
+ // "sort first" character (ascii \001)
+ values.put("title", localizedTitle.trim());
+ db.update("files", values, "_id=?", new String[]{id});
+ } catch (Exception e) {
+ Log.e(TAG, "Error updating localized title for " + titleResourceUri
+ + ", keeping old localization");
+ }
+ }
+ }
}
}
@@ -2119,10 +2202,21 @@
values.put("album_id", Integer.toString((int)albumRowId));
so = values.getAsString("title");
s = (so == null ? "" : so.toString());
+
+ try {
+ final String localizedTitle = getLocalizedTitle(s);
+ if (localizedTitle != null) {
+ values.put("title_resource_uri", s);
+ s = localizedTitle;
+ } else {
+ values.putNull("title_resource_uri");
+ }
+ } catch (Exception e) {
+ values.put("title_resource_uri", s);
+ }
values.put("title_key", MediaStore.Audio.keyFor(s));
// do a final trim of the title, in case it started with the special
// "sort first" character (ascii \001)
- values.remove("title");
values.put("title", s.trim());
computeDisplayName(values.getAsString(MediaStore.MediaColumns.DATA), values);
@@ -2197,10 +2291,15 @@
if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) {
mimeType = MediaFile.getMimeTypeForFile(path);
}
+
if (mimeType != null) {
values.put(FileColumns.MIME_TYPE, mimeType);
- if (mediaType == FileColumns.MEDIA_TYPE_NONE && !MediaScanner.isNoMediaPath(path)) {
+ // If 'values' contained the media type, then the caller wants us
+ // to use that exact type, so don't override it based on mimetype
+ if (!values.containsKey(FileColumns.MEDIA_TYPE) &&
+ mediaType == FileColumns.MEDIA_TYPE_NONE &&
+ !MediaScanner.isNoMediaPath(path)) {
int fileType = MediaFile.getFileTypeForMimeType(mimeType);
if (MediaFile.isAudioFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_AUDIO;
@@ -2255,11 +2354,6 @@
values.put(FileColumns.PARENT, parentId);
}
}
- Integer storage = values.getAsInteger(FileColumns.STORAGE_ID);
- if (storage == null) {
- int storageId = getStorageId(path);
- values.put(FileColumns.STORAGE_ID, storageId);
- }
helper.mNumInserts++;
rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
@@ -3313,14 +3407,8 @@
switch (match) {
case MTP_OBJECTS:
case MTP_OBJECTS_ID:
- try {
- // don't send objectRemoved event since this originated from MTP
- mDisableMtpObjectCallbacks = true;
- database.mNumDeletes++;
- count = db.delete("files", tableAndWhere.where, whereArgs);
- } finally {
- mDisableMtpObjectCallbacks = false;
- }
+ database.mNumDeletes++;
+ count = db.delete("files", tableAndWhere.where, whereArgs);
break;
case AUDIO_GENRES_ID_MEMBERS:
database.mNumDeletes++;
@@ -3374,6 +3462,10 @@
processRemovedNoMediaPath(arg);
return null;
}
+ if (MediaStore.RETRANSLATE_CALL.equals(method)) {
+ localizeTitles();
+ return null;
+ }
throw new UnsupportedOperationException("Unsupported call: " + method);
}
@@ -3512,7 +3604,12 @@
// in this case we must update all paths in the database with
// the directory name as a prefix
if ((match == MTP_OBJECTS || match == MTP_OBJECTS_ID || match == FILES_DIRECTORY)
- && initialValues != null && initialValues.size() == 1) {
+ && initialValues != null
+ // Is a rename operation
+ && ((initialValues.size() == 1 && initialValues.containsKey(FileColumns.DATA))
+ // Is a move operation
+ || (initialValues.size() == 2 && initialValues.containsKey(FileColumns.DATA)
+ && initialValues.containsKey(FileColumns.PARENT)))) {
String oldPath = null;
String newPath = initialValues.getAsString(MediaStore.MediaColumns.DATA);
mDirectoryCache.remove(newPath);
@@ -3660,12 +3757,21 @@
// If the title field is modified, update the title_key
so = values.getAsString("title");
if (so != null) {
- String s = so.toString();
- values.put("title_key", MediaStore.Audio.keyFor(s));
+ try {
+ final String localizedTitle = getLocalizedTitle(so);
+ if (localizedTitle != null) {
+ values.put("title_resource_uri", so);
+ so = localizedTitle;
+ } else {
+ values.putNull("title_resource_uri");
+ }
+ } catch (Exception e) {
+ values.put("title_resource_uri", so);
+ }
+ values.put("title_key", MediaStore.Audio.keyFor(so));
// do a final trim of the title, in case it started with the special
// "sort first" character (ascii \001)
- values.remove("title");
- values.put("title", s.trim());
+ values.put("title", so.trim());
}
helper.mNumUpdates++;
diff --git a/src/com/android/providers/media/MediaScannerReceiver.java b/src/com/android/providers/media/MediaScannerReceiver.java
index 8a098af..8107dfe 100644
--- a/src/com/android/providers/media/MediaScannerReceiver.java
+++ b/src/com/android/providers/media/MediaScannerReceiver.java
@@ -23,6 +23,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
+import android.provider.MediaStore;
import android.util.Log;
import java.io.File;
@@ -38,6 +39,8 @@
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
// Scan internal only.
scan(context, MediaProvider.INTERNAL_VOLUME);
+ } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
+ scanTranslatable(context);
} else {
if (uri.getScheme().equals("file")) {
// handle intents related to external storage
@@ -72,12 +75,18 @@
args.putString("volume", volume);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
- }
+ }
private void scanFile(Context context, String path) {
Bundle args = new Bundle();
args.putString("filepath", path);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
- }
+ }
+
+ private void scanTranslatable(Context context) {
+ final Bundle args = new Bundle();
+ args.putBoolean(MediaStore.RETRANSLATE_CALL, true);
+ context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
+ }
}
diff --git a/src/com/android/providers/media/MediaScannerService.java b/src/com/android/providers/media/MediaScannerService.java
index 1120676..83aeb81 100644
--- a/src/com/android/providers/media/MediaScannerService.java
+++ b/src/com/android/providers/media/MediaScannerService.java
@@ -18,6 +18,7 @@
package com.android.providers.media;
import android.app.Service;
+import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -224,6 +225,10 @@
if (listener != null) {
listener.scanCompleted(filePath, uri);
}
+ } else if (arguments.getBoolean(MediaStore.RETRANSLATE_CALL)) {
+ ContentProviderClient mediaProvider = getBaseContext().getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY);
+ mediaProvider.call(MediaStore.RETRANSLATE_CALL, null, null);
} else {
String volume = arguments.getString("volume");
String[] directories = null;
@@ -233,6 +238,7 @@
directories = new String[] {
Environment.getRootDirectory() + "/media",
Environment.getOemDirectory() + "/media",
+ Environment.getProductDirectory() + "/media",
};
}
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
@@ -259,5 +265,5 @@
stopSelf(msg.arg1);
}
- };
+ }
}
diff --git a/src/com/android/providers/media/MtpReceiver.java b/src/com/android/providers/media/MtpReceiver.java
index 9841528..d219310 100644
--- a/src/com/android/providers/media/MtpReceiver.java
+++ b/src/com/android/providers/media/MtpReceiver.java
@@ -26,7 +26,6 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.util.Log;
-import android.mtp.MtpServer;
public class MtpReceiver extends BroadcastReceiver {
private static final String TAG = MtpReceiver.class.getSimpleName();
@@ -36,10 +35,6 @@
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
- // If we somehow fail to configure after boot, it becomes difficult to
- // recover usb state. Thus we always configure once on boot, but it
- // has no effect if Mtp is disabled or already configured.
- MtpServer.configure(false);
final Intent usbState = context.registerReceiver(
null, new IntentFilter(UsbManager.ACTION_USB_STATE));
if (usbState != null) {
@@ -57,14 +52,13 @@
boolean mtpEnabled = extras.getBoolean(UsbManager.USB_FUNCTION_MTP);
boolean ptpEnabled = extras.getBoolean(UsbManager.USB_FUNCTION_PTP);
boolean unlocked = extras.getBoolean(UsbManager.USB_DATA_UNLOCKED);
- boolean configChanged = extras.getBoolean(UsbManager.USB_CONFIG_CHANGED);
+ boolean isCurrentUser = UserHandle.myUserId() == ActivityManager.getCurrentUser();
- if ((configChanged || (connected && !configured)) && (mtpEnabled || ptpEnabled)) {
- MtpServer.configure(ptpEnabled);
- // tell MediaProvider MTP is configured so it can bind to the service
+ if (configured && (mtpEnabled || ptpEnabled)) {
+ if (!isCurrentUser)
+ return;
context.getContentResolver().insert(Uri.parse(
"content://media/none/mtp_connected"), null);
- } else if (configured && (mtpEnabled || ptpEnabled)) {
intent = new Intent(context, MtpService.class);
intent.putExtra(UsbManager.USB_DATA_UNLOCKED, unlocked);
if (ptpEnabled) {
diff --git a/src/com/android/providers/media/MtpService.java b/src/com/android/providers/media/MtpService.java
index b0f2db4..d559b2b 100644
--- a/src/com/android/providers/media/MtpService.java
+++ b/src/com/android/providers/media/MtpService.java
@@ -19,15 +19,19 @@
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbManager;
+import android.Manifest;
import android.mtp.MtpDatabase;
import android.mtp.MtpServer;
-import android.mtp.MtpStorage;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.storage.StorageEventListener;
import android.os.storage.StorageManager;
@@ -36,10 +40,17 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import android.hardware.usb.IUsbManager;
import java.io.File;
+import java.io.FileDescriptor;
import java.util.HashMap;
+/**
+ * The singleton service backing instances of MtpServer that are started for the foreground user.
+ * The service has the responsibility of retrieving user storage information and managing server
+ * lifetime.
+ */
public class MtpService extends Service {
private static final String TAG = "MtpService";
private static final boolean LOGD = false;
@@ -50,132 +61,108 @@
Environment.DIRECTORY_PICTURES,
};
- private void addStorageDevicesLocked() {
- if (mPtpMode) {
- // In PTP mode we support only primary storage
- final StorageVolume primary = StorageManager.getPrimaryVolume(mVolumes);
- final String path = primary.getPath();
- if (path != null) {
- String state = primary.getState();
- if (Environment.MEDIA_MOUNTED.equals(state)) {
- addStorageLocked(mVolumeMap.get(path));
- } else {
- Log.e(TAG, "Couldn't add primary storage " + path + " in state " + state);
- }
- }
- } else {
- for (StorageVolume volume : mVolumeMap.values()) {
- addStorageLocked(volume);
- }
- }
- }
-
private final StorageEventListener mStorageEventListener = new StorageEventListener() {
@Override
public void onStorageStateChanged(String path, String oldState, String newState) {
- synchronized (this) {
+ synchronized (MtpService.this) {
Log.d(TAG, "onStorageStateChanged " + path + " " + oldState + " -> " + newState);
if (Environment.MEDIA_MOUNTED.equals(newState)) {
- volumeMountedLocked(path);
+ for (int i = 0; i < mVolumes.length; i++) {
+ StorageVolume volume = mVolumes[i];
+ if (volume.getPath().equals(path)) {
+ mVolumeMap.put(path, volume);
+ if (mUnlocked && (volume.isPrimary() || !mPtpMode)) {
+ addStorage(volume);
+ }
+ break;
+ }
+ }
} else if (Environment.MEDIA_MOUNTED.equals(oldState)) {
- StorageVolume volume = mVolumeMap.remove(path);
- if (volume != null) {
- removeStorageLocked(volume);
+ if (mVolumeMap.containsKey(path)) {
+ removeStorage(mVolumeMap.remove(path));
}
}
}
}
};
- @GuardedBy("this")
- private ServerHolder sServerHolder;
+ /**
+ * Static state of MtpServer. MtpServer opens FD for MTP driver internally and we cannot open
+ * multiple MtpServer at the same time. The static field used to handle the case where MtpServer
+ * lives beyond the lifetime of MtpService.
+ *
+ * Lock MtpService.this before locking MtpService.class if needed. Otherwise it goes to
+ * deadlock.
+ */
+ @GuardedBy("MtpService.class")
+ private static ServerHolder sServerHolder;
private StorageManager mStorageManager;
- /** Flag indicating if MTP is disabled due to keyguard */
- @GuardedBy("this")
- private boolean mMtpDisabled;
@GuardedBy("this")
private boolean mUnlocked;
@GuardedBy("this")
private boolean mPtpMode;
+ // A map of user volumes that are currently mounted.
@GuardedBy("this")
private HashMap<String, StorageVolume> mVolumeMap;
- @GuardedBy("this")
- private HashMap<String, MtpStorage> mStorageMap;
+
+ // All user volumes in existence, in any state.
@GuardedBy("this")
private StorageVolume[] mVolumes;
@Override
public void onCreate() {
+ mVolumes = StorageManager.getVolumeList(getUserId(), 0);
+ mVolumeMap = new HashMap<>();
+
mStorageManager = this.getSystemService(StorageManager.class);
+ mStorageManager.registerListener(mStorageEventListener);
}
@Override
public void onDestroy() {
mStorageManager.unregisterListener(mStorageEventListener);
+ synchronized (MtpService.class) {
+ if (sServerHolder != null) {
+ sServerHolder.database.setServer(null);
+ }
+ }
}
@Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- UserHandle user = new UserHandle(ActivityManager.getCurrentUser());
- synchronized (this) {
- mVolumeMap = new HashMap<>();
- mStorageMap = new HashMap<>();
- mStorageManager.registerListener(mStorageEventListener);
- mVolumes = StorageManager.getVolumeList(user.getIdentifier(), 0);
- for (StorageVolume volume : mVolumes) {
- if (Environment.MEDIA_MOUNTED.equals(volume.getState())) {
- volumeMountedLocked(volume.getPath());
- } else {
- Log.e(TAG, "StorageVolume not mounted " + volume.getPath());
- }
+ public synchronized int onStartCommand(Intent intent, int flags, int startId) {
+ mUnlocked = intent.getBooleanExtra(UsbManager.USB_DATA_UNLOCKED, false);
+ mPtpMode = intent.getBooleanExtra(UsbManager.USB_FUNCTION_PTP, false);
+
+ for (StorageVolume v : mVolumes) {
+ if (v.getState().equals(Environment.MEDIA_MOUNTED)) {
+ mVolumeMap.put(v.getPath(), v);
}
}
-
- synchronized (this) {
- mUnlocked = intent.getBooleanExtra(UsbManager.USB_DATA_UNLOCKED, false);
- updateDisabledStateLocked();
- mPtpMode = (intent == null ? false
- : intent.getBooleanExtra(UsbManager.USB_FUNCTION_PTP, false));
- String[] subdirs = null;
- if (mPtpMode) {
- Environment.UserEnvironment env = new Environment.UserEnvironment(
- user.getIdentifier());
- int count = PTP_DIRECTORIES.length;
- subdirs = new String[count];
- for (int i = 0; i < count; i++) {
- File file = env.buildExternalStoragePublicDirs(PTP_DIRECTORIES[i])[0];
- // make sure this directory exists
- file.mkdirs();
- subdirs[i] = file.getPath();
- }
- }
- final StorageVolume primary = StorageManager.getPrimaryVolume(mVolumes);
- try {
- manageServiceLocked(primary, subdirs, user);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Couldn't find the current user!: " + e.getMessage());
+ String[] subdirs = null;
+ if (mPtpMode) {
+ Environment.UserEnvironment env = new Environment.UserEnvironment(getUserId());
+ int count = PTP_DIRECTORIES.length;
+ subdirs = new String[count];
+ for (int i = 0; i < count; i++) {
+ File file = env.buildExternalStoragePublicDirs(PTP_DIRECTORIES[i])[0];
+ // make sure this directory exists
+ file.mkdirs();
+ subdirs[i] = file.getName();
}
}
-
+ final StorageVolume primary = StorageManager.getPrimaryVolume(mVolumes);
+ startServer(primary, subdirs);
return START_REDELIVER_INTENT;
}
- private void updateDisabledStateLocked() {
- mMtpDisabled = !mUnlocked;
- if (LOGD) {
- Log.d(TAG, "updating state; mMtpLocked=" + mMtpDisabled);
+ private synchronized void startServer(StorageVolume primary, String[] subdirs) {
+ if (!(UserHandle.myUserId() == ActivityManager.getCurrentUser())) {
+ return;
}
- }
-
- /**
- * Manage {@link #mServer}, creating only when running as the current user.
- */
- private void manageServiceLocked(StorageVolume primary, String[] subdirs, UserHandle user)
- throws PackageManager.NameNotFoundException {
- synchronized (this) {
+ synchronized (MtpService.class) {
if (sServerHolder != null) {
if (LOGD) {
Log.d(TAG, "Cannot launch second MTP server.");
@@ -187,32 +174,45 @@
}
Log.d(TAG, "starting MTP server in " + (mPtpMode ? "PTP mode" : "MTP mode") +
- " with storage " + primary.getPath() + (mMtpDisabled ? " disabled" : ""));
- final MtpDatabase database = new MtpDatabase(this,
- createPackageContextAsUser(this.getPackageName(), 0, user),
- MediaProvider.EXTERNAL_VOLUME,
- primary.getPath(), subdirs);
- String deviceSerialNumber = Build.SERIAL;
+ " with storage " + primary.getPath() + (mUnlocked ? " unlocked" : "") + " as user " + UserHandle.myUserId());
+
+ final MtpDatabase database = new MtpDatabase(this, MediaProvider.EXTERNAL_VOLUME, subdirs);
+ String deviceSerialNumber = Build.getSerial();
if (Build.UNKNOWN.equals(deviceSerialNumber)) {
deviceSerialNumber = "????????";
}
+ IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService(
+ Context.USB_SERVICE));
+ ParcelFileDescriptor controlFd = null;
+ try {
+ controlFd = usbMgr.getControlFd(
+ mPtpMode ? UsbManager.FUNCTION_PTP : UsbManager.FUNCTION_MTP);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error communicating with UsbManager: " + e);
+ }
+ FileDescriptor fd = null;
+ if (controlFd == null) {
+ Log.i(TAG, "Couldn't get control FD!");
+ } else {
+ fd = controlFd.getFileDescriptor();
+ }
+
final MtpServer server =
- new MtpServer(
- database,
- mPtpMode,
- new OnServerTerminated(),
- Build.MANUFACTURER, // MTP DeviceInfo: Manufacturer
- Build.MODEL, // MTP DeviceInfo: Model
- "1.0", // MTP DeviceInfo: Device Version
- deviceSerialNumber // MTP DeviceInfo: Serial Number
- );
+ new MtpServer(database, fd, mPtpMode,
+ new OnServerTerminated(), Build.MANUFACTURER,
+ Build.MODEL, "1.0", deviceSerialNumber);
database.setServer(server);
sServerHolder = new ServerHolder(server, database);
- // Need to run addStorageDevicesLocked after sServerHolder is set since it accesses
- // sServerHolder.
- if (!mMtpDisabled) {
- addStorageDevicesLocked();
+ // Add currently mounted and enabled storages to the server
+ if (mUnlocked) {
+ if (mPtpMode) {
+ addStorage(primary);
+ } else {
+ for (StorageVolume v : mVolumeMap.values()) {
+ addStorage(v);
+ }
+ }
}
server.start();
}
@@ -220,80 +220,26 @@
private final IMtpService.Stub mBinder =
new IMtpService.Stub() {
- @Override
- public void sendObjectAdded(int objectHandle) {
- synchronized (MtpService.class) {
- if (sServerHolder != null) {
- sServerHolder.server.sendObjectAdded(objectHandle);
- }
- }
- }
-
- @Override
- public void sendObjectRemoved(int objectHandle) {
- synchronized (MtpService.class) {
- if (sServerHolder != null) {
- sServerHolder.server.sendObjectRemoved(objectHandle);
- }
- }
- }
- };
+ };
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
- private void volumeMountedLocked(String path) {
- for (int i = 0; i < mVolumes.length; i++) {
- StorageVolume volume = mVolumes[i];
- if (volume.getPath().equals(path)) {
- mVolumeMap.put(path, volume);
- if (!mMtpDisabled) {
- // In PTP mode we support only primary storage
- if (volume.isPrimary() || !mPtpMode) {
- addStorageLocked(volume);
- }
- }
- break;
+ private void addStorage(StorageVolume volume) {
+ Log.v(TAG, "Adding MTP storage:" + volume.getPath());
+ synchronized (this) {
+ if (sServerHolder != null) {
+ sServerHolder.database.addStorage(volume);
}
}
}
- private void addStorageLocked(StorageVolume volume) {
- MtpStorage storage = new MtpStorage(volume, getApplicationContext());
- mStorageMap.put(storage.getPath(), storage);
-
- if (storage.getStorageId() == StorageVolume.STORAGE_ID_INVALID) {
- Log.w(TAG, "Ignoring volume with invalid MTP storage ID: " + storage);
- return;
- } else {
- Log.d(TAG, "Adding MTP storage 0x" + Integer.toHexString(storage.getStorageId())
- + " at " + storage.getPath());
- }
-
+ private void removeStorage(StorageVolume volume) {
synchronized (MtpService.class) {
if (sServerHolder != null) {
- sServerHolder.database.addStorage(storage);
- sServerHolder.server.addStorage(storage);
- }
- }
- }
-
- private void removeStorageLocked(StorageVolume volume) {
- MtpStorage storage = mStorageMap.remove(volume.getPath());
- if (storage == null) {
- Log.e(TAG, "Missing MtpStorage for " + volume.getPath());
- return;
- }
-
- Log.d(TAG, "Removing MTP storage " + Integer.toHexString(storage.getStorageId()) + " at "
- + storage.getPath());
-
- synchronized (MtpService.class) {
- if (sServerHolder != null) {
- sServerHolder.database.removeStorage(storage);
- sServerHolder.server.removeStorage(storage);
+ sServerHolder.database.removeStorage(volume);
}
}
}
diff --git a/src/com/android/providers/media/RingtonePickerActivity.java b/src/com/android/providers/media/RingtonePickerActivity.java
index 7d91b6b..fffd830 100644
--- a/src/com/android/providers/media/RingtonePickerActivity.java
+++ b/src/com/android/providers/media/RingtonePickerActivity.java
@@ -168,7 +168,7 @@
// In the buttonless (watch-only) version, preemptively set our result since we won't
// have another chance to do so before the activity closes.
if (!mShowOkCancelButtons) {
- setResultFromSelection();
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
}
// Play clip
@@ -372,7 +372,7 @@
// In the buttonless (watch-only) version, preemptively set our result since we won't
// have another chance to do so before the activity closes.
if (!mShowOkCancelButtons) {
- setResultFromSelection();
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
}
// If external storage is available, add a button to install sounds from storage.
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
@@ -481,7 +481,7 @@
mRingtoneManager.stopPreviousRingtone();
if (positiveResult) {
- setResultFromSelection();
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
} else {
setResult(RESULT_CANCELED);
}
@@ -498,7 +498,7 @@
// In the buttonless (watch-only) version, preemptively set our result since we won't
// have another chance to do so before the activity closes.
if (!mShowOkCancelButtons) {
- setResultFromSelection();
+ setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
}
}
@@ -566,27 +566,21 @@
}
}
- private void setResultFromSelection() {
- // Obtain the currently selected ringtone
- Uri uri = null;
- if (getCheckedItem() == mDefaultRingtonePos) {
- // Set it to the default Uri that they originally gave us
- uri = mUriForDefaultItem;
- } else if (getCheckedItem() == mSilentPos) {
- // A null Uri is for the 'Silent' item
- uri = null;
- } else {
- uri = mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(getCheckedItem()));
- }
+ private void setSuccessResultWithRingtone(Uri ringtoneUri) {
+ setResult(RESULT_OK,
+ new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
+ }
- // Return new URI if another ringtone was selected, as there's no ok/cancel button
- if (Objects.equals(uri, mExistingUri)) {
- setResult(RESULT_CANCELED);
- } else {
- Intent resultIntent = new Intent();
- resultIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, uri);
- setResult(RESULT_OK, resultIntent);
- }
+ private Uri getCurrentlySelectedRingtoneUri() {
+ if (getCheckedItem() == mDefaultRingtonePos) {
+ // Use the default Uri that they originally gave us.
+ return mUriForDefaultItem;
+ } else if (getCheckedItem() == mSilentPos) {
+ // Use a null Uri for the 'Silent' item.
+ return null;
+ } else {
+ return mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(getCheckedItem()));
+ }
}
private void saveAnyPlayingRingtone() {