From 783ee0ca8707a4e2977d1afd42cd01a3885a06e6 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Sat, 5 Mar 2016 17:23:28 -0700 Subject: [PATCH] Ask RingtonePlayer to open data for caching. When setting default ringtones, RingtoneManager now caches the selected media for playback before the device is unlocked. However, this API hasn't historically required the caller to hold storage permissions. To keep this working, we attempt to delegate ringtone access over through RingtonePlayer, which is what we do for playback. However, because we're caching the real ringtone bits now, we need to be much more careful about the PFDs we're willing to return. This change requires that they be in external storage, and that they have the ringtone/alarm/notification bit set. Bug: 27366059 Change-Id: I59c2adc1d1250a3eac281f190f35a7cb3119967b --- media/java/android/media/IRingtonePlayer.aidl | 3 ++ media/java/android/media/RingtoneManager.java | 44 +++++++++++++++---- .../systemui/media/RingtonePlayer.java | 35 +++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/media/java/android/media/IRingtonePlayer.aidl b/media/java/android/media/IRingtonePlayer.aidl index 809142125031d..4b1e39f2fc9d1 100644 --- a/media/java/android/media/IRingtonePlayer.aidl +++ b/media/java/android/media/IRingtonePlayer.aidl @@ -18,6 +18,7 @@ package android.media; import android.media.AudioAttributes; import android.net.Uri; +import android.os.ParcelFileDescriptor; import android.os.UserHandle; /** @@ -36,4 +37,6 @@ interface IRingtonePlayer { /** Return the title of the media. */ String getTitle(in Uri uri); + + ParcelFileDescriptor openRingtone(in Uri uri); } diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index feb490de2c3e3..4977391ed5c02 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -30,7 +30,9 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Environment; +import android.os.ParcelFileDescriptor; import android.os.Process; +import android.os.RemoteException; import android.provider.MediaStore; import android.provider.Settings; import android.provider.Settings.System; @@ -223,9 +225,9 @@ public class RingtoneManager { */ public static final int URI_COLUMN_INDEX = 2; - private Activity mActivity; - private Context mContext; - + private final Activity mActivity; + private final Context mContext; + private Cursor mCursor; private int mType = TYPE_RINGTONE; @@ -246,7 +248,8 @@ public class RingtoneManager { * @param activity The activity used to get a managed cursor. */ public RingtoneManager(Activity activity) { - mContext = mActivity = activity; + mActivity = activity; + mContext = activity; setType(mType); } @@ -258,6 +261,7 @@ public class RingtoneManager { * @param context The context to used to get a cursor. */ public RingtoneManager(Context context) { + mActivity = null; mContext = context; setType(mType); } @@ -271,7 +275,6 @@ public class RingtoneManager { * @see #EXTRA_RINGTONE_TYPE */ public void setType(int type) { - if (mCursor != null) { throw new IllegalStateException( "Setting filter columns should be done before querying for ringtones."); @@ -656,18 +659,19 @@ public class RingtoneManager { * @see #getActualDefaultRingtoneUri(Context, int) */ public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) { + final ContentResolver resolver = context.getContentResolver(); + String setting = getSettingForType(type); if (setting == null) return; - Settings.System.putString(context.getContentResolver(), setting, + Settings.System.putString(resolver, setting, ringtoneUri != null ? ringtoneUri.toString() : null); // Stream selected ringtone into cache so it's available for playback // when CE storage is still locked if (ringtoneUri != null) { - final ContentResolver cr = context.getContentResolver(); final Uri cacheUri = getCacheForType(type); - try (InputStream in = cr.openInputStream(ringtoneUri); - OutputStream out = cr.openOutputStream(cacheUri)) { + try (InputStream in = openRingtone(context, ringtoneUri); + OutputStream out = resolver.openOutputStream(cacheUri)) { Streams.copy(in, out); } catch (IOException e) { Log.w(TAG, "Failed to cache ringtone: " + e); @@ -675,6 +679,28 @@ public class RingtoneManager { } } + /** + * Try opening the given ringtone locally first, but failover to + * {@link IRingtonePlayer} if we can't access it directly. Typically happens + * when process doesn't hold + * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}. + */ + private static InputStream openRingtone(Context context, Uri uri) throws IOException { + final ContentResolver resolver = context.getContentResolver(); + try { + return resolver.openInputStream(uri); + } catch (SecurityException | IOException e) { + Log.w(TAG, "Failed to open directly; attempting failover: " + e); + final IRingtonePlayer player = context.getSystemService(AudioManager.class) + .getRingtonePlayer(); + try { + return new ParcelFileDescriptor.AutoCloseInputStream(player.openRingtone(uri)); + } catch (Exception e2) { + throw new IOException(e2); + } + } + } + private static String getSettingForType(int type) { if ((type & TYPE_RINGTONE) != 0) { return Settings.System.RINGTONE; diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java index f39f3026f26b0..ab612dd93dd13 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java +++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java @@ -16,8 +16,10 @@ package com.android.systemui.media; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; +import android.database.Cursor; import android.media.AudioAttributes; import android.media.IAudioService; import android.media.IRingtonePlayer; @@ -25,15 +27,20 @@ import android.media.Ringtone; import android.net.Uri; import android.os.Binder; import android.os.IBinder; +import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.provider.MediaStore; +import android.provider.MediaStore.Audio.AudioColumns; import android.util.Log; +import com.android.internal.util.Preconditions; import com.android.systemui.SystemUI; import java.io.FileDescriptor; +import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; @@ -180,6 +187,34 @@ public class RingtonePlayer extends SystemUI { return Ringtone.getTitle(getContextForUser(user), uri, false /*followSettingsUri*/, false /*allowRemote*/); } + + @Override + public ParcelFileDescriptor openRingtone(Uri uri) { + final UserHandle user = Binder.getCallingUserHandle(); + final ContentResolver resolver = getContextForUser(user).getContentResolver(); + + // Only open the requested Uri if it's a well-known ringtone or + // other sound from the platform media store, otherwise this opens + // up arbitrary access to any file on external storage. + if (uri.toString().startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) { + try (Cursor c = resolver.query(uri, new String[] { + MediaStore.Audio.AudioColumns.IS_RINGTONE, + MediaStore.Audio.AudioColumns.IS_ALARM, + MediaStore.Audio.AudioColumns.IS_NOTIFICATION + }, null, null, null)) { + if (c.moveToFirst()) { + if (c.getInt(0) != 0 || c.getInt(1) != 0 || c.getInt(2) != 0) { + try { + return resolver.openFileDescriptor(uri, "r"); + } catch (IOException e) { + throw new SecurityException(e); + } + } + } + } + } + throw new SecurityException("Uri is not ringtone, alarm, or notification: " + uri); + } }; private Context getContextForUser(UserHandle user) {