Offer to cache ringtones in system DE storage.

Ringtones often live on shared media, which is now encrypted with CE
keys and not available until after the user is unlocked.  To improve
the user experience while locked, cache the default ringtone,
notification sound, and alarm sound in a DE storage area.

Bug: 26730753
Change-Id: Ie6ad7790af4c87dd25759df3ed017e3b91a2fb87
diff --git a/api/current.txt b/api/current.txt
index abc9e9a..e51b47a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -21187,6 +21187,7 @@
     method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+    method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(java.io.FileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(java.io.FileDescriptor, long, long) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(android.media.MediaDataSource) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException;
diff --git a/api/system-current.txt b/api/system-current.txt
index b36d7e8..e18e6ec 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -22687,6 +22687,7 @@
     method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+    method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(java.io.FileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(java.io.FileDescriptor, long, long) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(android.media.MediaDataSource) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException;
diff --git a/api/test-current.txt b/api/test-current.txt
index 3878fbf..303d050 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -21196,6 +21196,7 @@
     method public void setDataSource(android.content.Context, android.net.Uri) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
     method public void setDataSource(java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.SecurityException;
+    method public void setDataSource(android.content.res.AssetFileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(java.io.FileDescriptor) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(java.io.FileDescriptor, long, long) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setDataSource(android.media.MediaDataSource) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 770cde7..eda181b 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2780,6 +2780,11 @@
          */
         public static final Uri DEFAULT_RINGTONE_URI = getUriFor(RINGTONE);
 
+        /** {@hide} */
+        public static final String RINGTONE_CACHE = "ringtone_cache";
+        /** {@hide} */
+        public static final Uri RINGTONE_CACHE_URI = getUriFor(RINGTONE_CACHE);
+
         /**
          * Persistent store for the system-wide default notification sound.
          *
@@ -2798,6 +2803,11 @@
          */
         public static final Uri DEFAULT_NOTIFICATION_URI = getUriFor(NOTIFICATION_SOUND);
 
+        /** {@hide} */
+        public static final String NOTIFICATION_SOUND_CACHE = "notification_sound_cache";
+        /** {@hide} */
+        public static final Uri NOTIFICATION_SOUND_CACHE_URI = getUriFor(NOTIFICATION_SOUND_CACHE);
+
         /**
          * Persistent store for the system-wide default alarm alert.
          *
@@ -2816,6 +2826,11 @@
          */
         public static final Uri DEFAULT_ALARM_ALERT_URI = getUriFor(ALARM_ALERT);
 
+        /** {@hide} */
+        public static final String ALARM_ALERT_CACHE = "alarm_alert_cache";
+        /** {@hide} */
+        public static final Uri ALARM_ALERT_CACHE_URI = getUriFor(ALARM_ALERT_CACHE);
+
         /**
          * Persistent store for the system default media button event receiver.
          *
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 26e466e..adf8551 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -57,6 +57,7 @@
 import android.media.SyncParams;
 
 import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.Preconditions;
 
 import libcore.io.IoBridge;
 import libcore.io.Libcore;
@@ -964,8 +965,8 @@
      * @param uri the Content URI of the data you want to play
      * @throws IllegalStateException if it is called in an invalid state
      */
-    public void setDataSource(Context context, Uri uri)
-        throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+    public void setDataSource(@NonNull Context context, @NonNull Uri uri)
+            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
         setDataSource(context, uri, null);
     }
 
@@ -981,47 +982,46 @@
      *                to disallow or allow cross domain redirection.
      * @throws IllegalStateException if it is called in an invalid state
      */
-    public void setDataSource(Context context, Uri uri, Map<String, String> headers)
-            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+    public void setDataSource(@NonNull Context context, @NonNull Uri uri,
+            @Nullable Map<String, String> headers) throws IOException, IllegalArgumentException,
+                    SecurityException, IllegalStateException {
+        final ContentResolver resolver = context.getContentResolver();
         final String scheme = uri.getScheme();
         if (ContentResolver.SCHEME_FILE.equals(scheme)) {
             setDataSource(uri.getPath());
             return;
         } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
                 && Settings.AUTHORITY.equals(uri.getAuthority())) {
-            // Redirect ringtones to go directly to underlying provider
-            uri = RingtoneManager.getActualDefaultRingtoneUri(context,
-                    RingtoneManager.getDefaultType(uri));
-            if (uri == null) {
-                throw new FileNotFoundException("Failed to resolve default ringtone");
-            }
-        }
-
-        AssetFileDescriptor fd = null;
-        try {
-            ContentResolver resolver = context.getContentResolver();
-            fd = resolver.openAssetFileDescriptor(uri, "r");
-            if (fd == null) {
+            // Try cached ringtone first since the actual provider may not be
+            // encryption aware, or it may be stored on CE media storage
+            final int type = RingtoneManager.getDefaultType(uri);
+            final Uri cacheUri = RingtoneManager.getCacheForType(type);
+            final Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+            if (attemptDataSource(resolver, cacheUri)) {
                 return;
-            }
-            // Note: using getDeclaredLength so that our behavior is the same
-            // as previous versions when the content provider is returning
-            // a full file.
-            if (fd.getDeclaredLength() < 0) {
-                setDataSource(fd.getFileDescriptor());
+            } else if (attemptDataSource(resolver, actualUri)) {
+                return;
             } else {
-                setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getDeclaredLength());
+                setDataSource(uri.toString(), headers);
             }
-            return;
-        } catch (SecurityException | IOException ex) {
-            Log.w(TAG, "Couldn't open file on client side; trying server side: " + ex);
-        } finally {
-            if (fd != null) {
-                fd.close();
+        } else {
+            // Try requested Uri locally first, or fallback to media server
+            if (attemptDataSource(resolver, uri)) {
+                return;
+            } else {
+                setDataSource(uri.toString(), headers);
             }
         }
+    }
 
-        setDataSource(uri.toString(), headers);
+    private boolean attemptDataSource(ContentResolver resolver, Uri uri) {
+        try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) {
+            setDataSource(afd);
+            return true;
+        } catch (NullPointerException | SecurityException | IOException ex) {
+            Log.w(TAG, "Couldn't open " + uri + ": " + ex);
+            return false;
+        }
     }
 
     /**
@@ -1102,6 +1102,26 @@
         throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;
 
     /**
+     * Sets the data source (AssetFileDescriptor) to use. It is the caller's
+     * responsibility to close the file descriptor. It is safe to do so as soon
+     * as this call returns.
+     *
+     * @param afd the AssetFileDescriptor for the file you want to play
+     */
+    public void setDataSource(@NonNull AssetFileDescriptor afd)
+            throws IOException, IllegalArgumentException, IllegalStateException {
+        Preconditions.checkNotNull(afd);
+        // Note: using getDeclaredLength so that our behavior is the same
+        // as previous versions when the content provider is returning
+        // a full file.
+        if (afd.getDeclaredLength() < 0) {
+            setDataSource(afd.getFileDescriptor());
+        } else {
+            setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getDeclaredLength());
+        }
+    }
+
+    /**
      * Sets the data source (FileDescriptor) to use. It is the caller's responsibility
      * to close the file descriptor. It is safe to do so as soon as this call returns.
      *
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index 025029e..06ac11b 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -18,9 +18,12 @@
 
 import com.android.internal.database.SortCursor;
 
+import libcore.io.Streams;
+
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.app.Activity;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -33,6 +36,9 @@
 import android.provider.Settings.System;
 import android.util.Log;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -654,8 +660,19 @@
         if (setting == null) return;
         Settings.System.putString(context.getContentResolver(), setting,
                 ringtoneUri != null ? ringtoneUri.toString() : null);
+
+        // Stream selected ringtone into cache so it's available for playback
+        // when CE storage is still locked
+        final ContentResolver cr = context.getContentResolver();
+        final Uri cacheUri = getCacheForType(type);
+        try (InputStream in = cr.openInputStream(ringtoneUri);
+                OutputStream out = cr.openOutputStream(cacheUri)) {
+            Streams.copy(in, out);
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to cache ringtone: " + e);
+        }
     }
-    
+
     private static String getSettingForType(int type) {
         if ((type & TYPE_RINGTONE) != 0) {
             return Settings.System.RINGTONE;
@@ -667,7 +684,20 @@
             return null;
         }
     }
-    
+
+    /** {@hide} */
+    public static Uri getCacheForType(int type) {
+        if ((type & TYPE_RINGTONE) != 0) {
+            return Settings.System.RINGTONE_CACHE_URI;
+        } else if ((type & TYPE_NOTIFICATION) != 0) {
+            return Settings.System.NOTIFICATION_SOUND_CACHE_URI;
+        } else if ((type & TYPE_ALARM) != 0) {
+            return Settings.System.ALARM_ALERT_CACHE_URI;
+        } else {
+            return null;
+        }
+    }
+
     /**
      * Returns whether the given {@link Uri} is one of the default ringtones.
      * 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 57d495f..9842e28 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -49,6 +49,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.SELinux;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -457,8 +458,28 @@
 
     @Override
     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        throw new FileNotFoundException("Direct file access no longer supported; "
-                + "ringtone playback is available through android.media.Ringtone");
+        final String cacheName;
+        if (Settings.System.RINGTONE_CACHE_URI.equals(uri)) {
+            cacheName = Settings.System.RINGTONE_CACHE;
+        } else if (Settings.System.NOTIFICATION_SOUND_CACHE_URI.equals(uri)) {
+            cacheName = Settings.System.NOTIFICATION_SOUND_CACHE;
+        } else if (Settings.System.ALARM_ALERT_CACHE_URI.equals(uri)) {
+            cacheName = Settings.System.ALARM_ALERT_CACHE;
+        } else {
+            throw new FileNotFoundException("Direct file access no longer supported; "
+                    + "ringtone playback is available through android.media.Ringtone");
+        }
+
+        final File cacheFile = new File(
+                getRingtoneCacheDir(UserHandle.getCallingUserId()), cacheName);
+        return ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.parseMode(mode));
+    }
+
+    private File getRingtoneCacheDir(int userId) {
+        final File cacheDir = new File(Environment.getDataSystemDeDirectory(userId), "ringtones");
+        cacheDir.mkdir();
+        SELinux.restorecon(cacheDir);
+        return cacheDir;
     }
 
     @Override
@@ -901,6 +922,21 @@
             return false;
         }
 
+        // Invalidate any relevant cache files
+        String cacheName = null;
+        if (Settings.System.RINGTONE.equals(name)) {
+            cacheName = Settings.System.RINGTONE_CACHE;
+        } else if (Settings.System.NOTIFICATION_SOUND.equals(name)) {
+            cacheName = Settings.System.NOTIFICATION_SOUND_CACHE;
+        } else if (Settings.System.ALARM_ALERT.equals(name)) {
+            cacheName = Settings.System.ALARM_ALERT_CACHE;
+        }
+        if (cacheName != null) {
+            final File cacheFile = new File(
+                    getRingtoneCacheDir(UserHandle.getCallingUserId()), cacheName);
+            cacheFile.delete();
+        }
+
         // Mutate the value.
         synchronized (mLock) {
             switch (operation) {