[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() {