diff options
3 files changed, 516 insertions, 11 deletions
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 0ad8c24b10c6..0ff1b1e19811 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -42,7 +42,9 @@ import android.os.ParcelFileDescriptor; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.provider.BaseColumns; import android.provider.MediaStore; +import android.provider.MediaStore.Audio.AudioColumns; import android.provider.MediaStore.MediaColumns; import android.provider.Settings; import android.provider.Settings.System; @@ -543,6 +545,95 @@ public class RingtoneManager { return getUriFromCursor(mContext, mCursor); } + /** + * Gets the valid ringtone uri by a given uri string and ringtone type for the restore purpose. + * + * @param contentResolver ContentResolver to execute media query. + * @param value a canonicalized uri which refers to the ringtone. + * @param ringtoneType an integer representation of the kind of uri that is being restored, can + * be RingtoneManager.TYPE_RINGTONE, RingtoneManager.TYPE_NOTIFICATION, or + * RingtoneManager.TYPE_ALARM. + * @hide + */ + public static @Nullable Uri getRingtoneUriForRestore( + @NonNull ContentResolver contentResolver, @Nullable String value, int ringtoneType) + throws FileNotFoundException, IllegalArgumentException { + if (value == null) { + // Return a valid null. It means the null value is intended instead of a failure. + return null; + } + + Uri ringtoneUri; + final Uri canonicalUri = Uri.parse(value); + + // Try to get the media uri via the regular uncanonicalize method first. + ringtoneUri = contentResolver.uncanonicalize(canonicalUri); + if (ringtoneUri != null) { + // Canonicalize it to make the result contain the right metadata of the media asset. + ringtoneUri = contentResolver.canonicalize(ringtoneUri); + return ringtoneUri; + } + + // Query the media by title and ringtone type. + final String title = canonicalUri.getQueryParameter(AudioColumns.TITLE); + Uri baseUri = ContentUris.removeId(canonicalUri).buildUpon().clearQuery().build(); + String ringtoneTypeSelection = ""; + switch (ringtoneType) { + case RingtoneManager.TYPE_RINGTONE: + ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_RINGTONE; + break; + case RingtoneManager.TYPE_NOTIFICATION: + ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_NOTIFICATION; + break; + case RingtoneManager.TYPE_ALARM: + ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_ALARM; + break; + default: + throw new IllegalArgumentException("Unknown ringtone type: " + ringtoneType); + } + + final String selection = ringtoneTypeSelection + "=1 AND " + AudioColumns.TITLE + "=?"; + Cursor cursor = null; + try { + cursor = + contentResolver.query( + baseUri, + /* projection */ new String[] {BaseColumns._ID}, + /* selection */ selection, + /* selectionArgs */ new String[] {title}, + /* sortOrder */ null, + /* cancellationSignal */ null); + + } catch (IllegalArgumentException e) { + throw new FileNotFoundException("Volume not found for " + baseUri); + } + if (cursor == null) { + throw new FileNotFoundException("Missing cursor for " + baseUri); + } else if (cursor.getCount() == 0) { + FileUtils.closeQuietly(cursor); + throw new FileNotFoundException("No item found for " + baseUri); + } else if (cursor.getCount() > 1) { + // Find more than 1 result. + // We are not sure which one is the right ringtone file so just abandon this case. + FileUtils.closeQuietly(cursor); + throw new FileNotFoundException( + "Find multiple ringtone candidates by title+ringtone_type query: count: " + + cursor.getCount()); + } + if (cursor.moveToFirst()) { + ringtoneUri = ContentUris.withAppendedId(baseUri, cursor.getLong(0)); + FileUtils.closeQuietly(cursor); + } else { + FileUtils.closeQuietly(cursor); + throw new FileNotFoundException("Failed to read row from the result."); + } + + // Canonicalize it to make the result contain the right metadata of the media asset. + ringtoneUri = contentResolver.canonicalize(ringtoneUri); + Log.v(TAG, "Find a valid result: " + ringtoneUri); + return ringtoneUri; + } + private static Uri getUriFromCursor(Context context, Cursor cursor) { final Uri uri = ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)), cursor.getLong(ID_COLUMN_INDEX)); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 9da1ab8ae69c..27a45dfc552e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -44,6 +44,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.LocalePicker; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; @@ -332,21 +333,30 @@ public class SettingsHelper { * @param value can be a canonicalized uri or "_silent" to indicate a silent (null) ringtone. */ private void setRingtone(String name, String value) { - // If it's null, don't change the default + Log.v(TAG, "Set ringtone for name: " + name + " value: " + value); + + // If it's null, don't change the default. if (value == null) return; - final Uri ringtoneUri; + final int ringtoneType = getRingtoneType(name); if (SILENT_RINGTONE.equals(value)) { - ringtoneUri = null; - } else { - Uri canonicalUri = Uri.parse(value); - ringtoneUri = mContext.getContentResolver().uncanonicalize(canonicalUri); - if (ringtoneUri == null) { - // Unrecognized or invalid Uri, don't restore - return; - } + // SILENT_RINGTONE is a special constant generated by onBackupValue in the source + // device. + RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, null); + return; + } + + Uri ringtoneUri = null; + try { + ringtoneUri = + RingtoneManager.getRingtoneUriForRestore( + mContext.getContentResolver(), value, ringtoneType); + } catch (FileNotFoundException | IllegalArgumentException e) { + Log.w(TAG, "Failed to resolve " + value + ": " + e); + // Unrecognized or invalid Uri, don't restore + return; } - final int ringtoneType = getRingtoneType(name); + Log.v(TAG, "setActualDefaultRingtoneUri type: " + ringtoneType + ", uri: " + ringtoneUri); RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, ringtoneUri); } diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java index bc81c4441af5..ef062dfd3ec3 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java @@ -26,15 +26,24 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.res.Resources; +import android.database.Cursor; +import android.database.MatrixCursor; import android.media.AudioManager; import android.net.Uri; +import android.os.Bundle; import android.os.LocaleList; +import android.provider.BaseColumns; +import android.provider.MediaStore; import android.provider.Settings; import android.telephony.TelephonyManager; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContentResolver; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -57,6 +66,13 @@ public class SettingsHelperTest { private static final String SETTING_VALUE = "setting_value"; private static final String SETTING_REAL_VALUE = "setting_real_value"; + private static final String DEFAULT_RINGTONE_VALUE = + "content://media/internal/audio/media/10?title=DefaultRingtone&canonical=1"; + private static final String DEFAULT_NOTIFICATION_VALUE = + "content://media/internal/audio/media/20?title=DefaultNotification&canonical=1"; + private static final String DEFAULT_ALARM_VALUE = + "content://media/internal/audio/media/30?title=DefaultAlarm&canonical=1"; + private SettingsHelper mSettingsHelper; @Mock private Context mContext; @@ -74,6 +90,7 @@ public class SettingsHelperTest { mTelephonyManager); when(mContext.getResources()).thenReturn(mResources); when(mContext.getApplicationContext()).thenReturn(mContext); + when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo()); when(mContext.getContentResolver()).thenReturn(getContentResolver()); mSettingsHelper = spy(new SettingsHelper(mContext)); @@ -338,6 +355,377 @@ public class SettingsHelperTest { } @Test + public void testRestoreValue_customRingtone_regularUncanonicalize_Success() { + final String sourceRingtoneValue = + "content://media/internal/audio/media/1?title=Song&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://media/internal/audio/media/100"; + final String newRingtoneValueCanonicalized = + "content://media/internal/audio/media/100?title=Song&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(sourceRingtoneValue)); + return Uri.parse(newRingtoneValueUncanonicalized); + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(DEFAULT_RINGTONE_VALUE); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_useCustomLookup_success() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/1?title=Song&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://0@media/external/audio/media/100"; + final String newRingtoneValueCanonicalized = + "content://0@media/external/audio/media/100?title=Song&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {100L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + assertThat(uri) + .isEqualTo(Uri.parse("content://0@media/external/audio/media")); + assertThat(projection).isEqualTo(new String[] {"_id"}); + assertThat(selection).isEqualTo("is_ringtone=1 AND title=?"); + assertThat(selectionArgs).isEqualTo(new String[] {"Song"}); + return cursor; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_notificationSound_useCustomLookup_success() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/2?title=notificationPing&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://0@media/external/audio/media/200"; + final String newRingtoneValueCanonicalized = + "content://0@media/external/audio/media/200?title=notificationPing&canonicalize=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {200L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + assertThat(uri) + .isEqualTo(Uri.parse("content://0@media/external/audio/media")); + assertThat(projection).isEqualTo(new String[] {"_id"}); + assertThat(selection).isEqualTo("is_notification=1 AND title=?"); + assertThat(selectionArgs).isEqualTo(new String[] {"notificationPing"}); + return cursor; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.NOTIFICATION_SOUND, + sourceRingtoneValue, + 0); + + assertThat( + Settings.System.getString( + mMockContentResolver, Settings.System.NOTIFICATION_SOUND)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_alarmSound_useCustomLookup_success() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/3?title=alarmSound&canonical=1"; + final String newRingtoneValueUncanonicalized = + "content://0@media/external/audio/media/300"; + final String newRingtoneValueCanonicalized = + "content://0@media/external/audio/media/300?title=alarmSound&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {300L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public Uri canonicalize(Uri url) { + assertThat(url).isEqualTo(Uri.parse(newRingtoneValueUncanonicalized)); + return Uri.parse(newRingtoneValueCanonicalized); + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + assertThat(uri) + .isEqualTo(Uri.parse("content://0@media/external/audio/media")); + assertThat(projection).isEqualTo(new String[] {"_id"}); + assertThat(selection).isEqualTo("is_alarm=1 AND title=?"); + assertThat(selectionArgs).isEqualTo(new String[] {"alarmSound"}); + return cursor; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.ALARM_ALERT, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.ALARM_ALERT)) + .isEqualTo(newRingtoneValueCanonicalized); + } + + @Test + public void testRestoreValue_customRingtone_useCustomLookup_multipleResults_notRestore() { + final String sourceRingtoneValue = + "content://0@media/external/audio/media/1?title=Song&canonical=1"; + + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + // This is to mock the case that there are multiple results by querying title + + // ringtone_type. + MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID}); + cursor.addRow(new Object[] {100L}); + cursor.addRow(new Object[] {110L}); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + sourceRingtoneValue, + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(DEFAULT_RINGTONE_VALUE); + } + + @Test + public void testRestoreValue_customRingtone_restoreSilentValue() { + MockContentResolver mMockContentResolver = new MockContentResolver(); + when(mContext.getContentResolver()).thenReturn(mMockContentResolver); + + ContentProvider mockMediaContentProvider = + new MockContentProvider(mContext) { + @Override + public Uri uncanonicalize(Uri url) { + // mock the lookup failure in regular MediaProvider.uncanonicalize. + return null; + } + + @Override + public String getType(Uri url) { + return "audio/ogg"; + } + }; + + ContentProvider mockSettingsContentProvider = + new MockSettingsProvider(mContext, getContentResolver()); + mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider); + mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider); + + resetRingtoneSettingsToDefault(mMockContentResolver); + + mSettingsHelper.restoreValue( + mContext, + mMockContentResolver, + new ContentValues(), + Uri.EMPTY, + Settings.System.RINGTONE, + "_silent", + 0); + + assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE)) + .isEqualTo(null); + } + + public static class MockSettingsProvider extends MockContentProvider { + ContentResolver mBaseContentResolver; + + public MockSettingsProvider(Context context, ContentResolver baseContentResolver) { + super(context); + this.mBaseContentResolver = baseContentResolver; + } + + @Override + public Bundle call(String method, String request, Bundle args) { + return mBaseContentResolver.call(Settings.AUTHORITY, method, request, args); + } + } + + @Test public void restoreValue_autoRotation_deviceStateAutoRotationDisabled_restoresValue() { when(mResources.getStringArray(R.array.config_perDeviceStateRotationLockDefaults)) .thenReturn(new String[]{}); @@ -400,4 +788,20 @@ public class SettingsHelperTest { Settings.Global.putString(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, null); Settings.Global.putString(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, null); } + + private void resetRingtoneSettingsToDefault(ContentResolver contentResolver) { + Settings.System.putString( + contentResolver, Settings.System.RINGTONE, DEFAULT_RINGTONE_VALUE); + Settings.System.putString( + contentResolver, Settings.System.NOTIFICATION_SOUND, DEFAULT_NOTIFICATION_VALUE); + Settings.System.putString( + contentResolver, Settings.System.ALARM_ALERT, DEFAULT_ALARM_VALUE); + + assertThat(Settings.System.getString(contentResolver, Settings.System.RINGTONE)) + .isEqualTo(DEFAULT_RINGTONE_VALUE); + assertThat(Settings.System.getString(contentResolver, Settings.System.NOTIFICATION_SOUND)) + .isEqualTo(DEFAULT_NOTIFICATION_VALUE); + assertThat(Settings.System.getString(contentResolver, Settings.System.ALARM_ALERT)) + .isEqualTo(DEFAULT_ALARM_VALUE); + } } |