summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ahmad Khalil <khalilahmad@google.com> 2023-08-04 14:53:55 +0000
committer Ahmad Khalil <khalilahmad@google.com> 2023-08-22 20:36:06 +0000
commitd073d325be8c36f4f7c2dc8792170e24618cce5d (patch)
treeb31e93216a4f53a0435d9cbcd755601d4189f660
parent8c5aa74b217171d25e8b6bd93d38c1a6bbfd52c5 (diff)
Create haptic vibration library
Add an api to set RingtoneManager media type, and then use this type to determine whether the cursor will return Sound or Vibration items. Bug: 273903859 Test: atest RingtoneManagerTest Change-Id: I5a1cc0355fc52d738b6ae266846410556f1f2f1e
-rw-r--r--media/TEST_MAPPING11
-rw-r--r--media/java/android/media/RingtoneManager.java190
-rw-r--r--media/tests/ringtone/Android.bp30
-rw-r--r--media/tests/ringtone/AndroidManifest.xml41
-rw-r--r--media/tests/ringtone/TEST_MAPPING20
-rw-r--r--media/tests/ringtone/res/raw/test_haptic_file.ahv17
-rw-r--r--media/tests/ringtone/res/raw/test_sound_file.mp3bin0 -> 4491 bytes
-rw-r--r--media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java233
8 files changed, 492 insertions, 50 deletions
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index 5ae77b5a8e2f..a9da832b2a5a 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -37,6 +37,17 @@
}
],
"file_patterns": ["(?i)drm|crypto"]
+ },
+ {
+ "file_patterns": [
+ "[^/]*(Ringtone)[^/]*\\.java"
+ ],
+ "name": "MediaRingtoneTests",
+ "options": [
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
}
]
}
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index 0ff1b1e19811..9234479ea8fd 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -16,6 +16,7 @@
package android.media;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -35,19 +36,20 @@ import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.StaleDataException;
import android.net.Uri;
-import android.os.Build;
import android.os.Environment;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
+import android.os.vibrator.persistence.VibrationXmlParser;
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;
+import android.text.TextUtils;
import android.util.Log;
import com.android.internal.database.SortCursor;
@@ -58,6 +60,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
@@ -209,21 +213,30 @@ public class RingtoneManager {
*/
public static final String EXTRA_RINGTONE_PICKED_URI =
"android.intent.extra.ringtone.PICKED_URI";
-
+
+ /**
+ * Declares the allowed types of media for this RingtoneManager.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "MEDIA_", value = {
+ Ringtone.MEDIA_SOUND,
+ Ringtone.MEDIA_VIBRATION,
+ })
+ public @interface MediaType {}
+
// Make sure the column ordering and then ..._COLUMN_INDEX are in sync
- private static final String[] INTERNAL_COLUMNS = new String[] {
+ private static final String[] MEDIA_AUDIO_COLUMNS = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.TITLE_KEY,
};
- private static final String[] MEDIA_COLUMNS = new String[] {
- MediaStore.Audio.Media._ID,
- MediaStore.Audio.Media.TITLE,
- MediaStore.Audio.Media.TITLE,
- MediaStore.Audio.Media.TITLE_KEY,
+ private static final String[] MEDIA_VIBRATION_COLUMNS = new String[]{
+ MediaStore.Files.FileColumns._ID,
+ MediaStore.Files.FileColumns.TITLE,
};
/**
@@ -251,7 +264,9 @@ public class RingtoneManager {
private Cursor mCursor;
private int mType = TYPE_RINGTONE;
-
+ @MediaType
+ private int mMediaType = Ringtone.MEDIA_SOUND;
+
/**
* If a column (item from this list) exists in the Cursor, its value must
* be true (value of 1) for the row to be returned.
@@ -318,6 +333,41 @@ public class RingtoneManager {
}
/**
+ * Sets the media type that will be listed by the RingtoneManager.
+ *
+ * <p>This method should be called before calling {@link RingtoneManager#getCursor()}.
+ *
+ * @hide
+ */
+ public void setMediaType(@MediaType int mediaType) {
+ if (mCursor != null) {
+ throw new IllegalStateException(
+ "Setting media should be done before calling getCursor().");
+ }
+
+ switch (mediaType) {
+ case Ringtone.MEDIA_SOUND:
+ case Ringtone.MEDIA_VIBRATION:
+ mMediaType = mediaType;
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported media type " + mediaType);
+ }
+ }
+
+ /**
+ * Returns the RingtoneManagers media type.
+ *
+ * @return the media type.
+ * @see #setMediaType
+ * @hide
+ */
+ @MediaType
+ public int getMediaType() {
+ return mMediaType;
+ }
+
+ /**
* Sets which type(s) of ringtones will be listed by this.
*
* @param type The type(s), one or more of {@link #TYPE_RINGTONE},
@@ -454,19 +504,19 @@ public class RingtoneManager {
return mCursor;
}
- ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
- ringtoneCursors.add(getInternalRingtones());
- ringtoneCursors.add(getMediaRingtones());
+ ArrayList<Cursor> cursors = new ArrayList<>();
+
+ cursors.add(queryMediaStore(/* internal= */ true));
+ cursors.add(queryMediaStore(/* internal= */ false));
if (mIncludeParentRingtones) {
Cursor parentRingtonesCursor = getParentProfileRingtones();
if (parentRingtonesCursor != null) {
- ringtoneCursors.add(parentRingtonesCursor);
+ cursors.add(parentRingtonesCursor);
}
}
-
- return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
- MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ return mCursor = new SortCursor(cursors.toArray(new Cursor[cursors.size()]),
+ getSortOrderForMedia(mMediaType));
}
private Cursor getParentProfileRingtones() {
@@ -478,9 +528,7 @@ public class RingtoneManager {
// We don't need to re-add the internal ringtones for the work profile since
// they are the same as the personal profile. We just need the external
// ringtones.
- final Cursor res = getMediaRingtones(parentContext);
- return new ExternalRingtonesCursorWrapper(res, ContentProvider.maybeAddUserId(
- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, parentInfo.id));
+ return queryMediaStore(parentContext, /* internal= */ false);
}
}
return null;
@@ -502,7 +550,7 @@ public class RingtoneManager {
Uri positionUri = getRingtoneUri(position);
if (Ringtone.useRingtoneV2()) {
mPreviousRingtone = new Ringtone.Builder(
- mContext, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(mType))
+ mContext, mMediaType, getDefaultAudioAttributes(mType))
.setUri(positionUri)
.build();
} else {
@@ -675,11 +723,13 @@ public class RingtoneManager {
*/
public static Uri getValidRingtoneUri(Context context) {
final RingtoneManager rm = new RingtoneManager(context);
-
- Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
+
+ Uri uri = getValidRingtoneUriFromCursorAndClose(context,
+ rm.queryMediaStore(/* internal= */ true));
if (uri == null) {
- uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
+ uri = getValidRingtoneUriFromCursorAndClose(context,
+ rm.queryMediaStore(/* internal= */ false));
}
return uri;
@@ -700,28 +750,26 @@ public class RingtoneManager {
}
}
- @UnsupportedAppUsage
- private Cursor getInternalRingtones() {
- final Cursor res = query(
- MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
- constructBooleanTrueWhereClause(mFilterColumns),
- null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
- return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
+ private Cursor queryMediaStore(boolean internal) {
+ return queryMediaStore(mContext, internal);
}
- private Cursor getMediaRingtones() {
- final Cursor res = getMediaRingtones(mContext);
- return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
- }
+ private Cursor queryMediaStore(Context context, boolean internal) {
+ Uri contentUri = getContentUriForMedia(mMediaType, internal);
+ String[] columns =
+ mMediaType == Ringtone.MEDIA_VIBRATION ? MEDIA_VIBRATION_COLUMNS
+ : MEDIA_AUDIO_COLUMNS;
+ String whereClause = getWhereClauseForMedia(mMediaType, mFilterColumns);
+ String sortOrder = getSortOrderForMedia(mMediaType);
+
+ Cursor cursor = query(contentUri, columns, whereClause, /* selectionArgs= */ null,
+ sortOrder, context);
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- private Cursor getMediaRingtones(Context context) {
- // MediaStore now returns ringtones on other storage devices, even when
- // we don't have storage or audio permissions
- return query(
- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
- constructBooleanTrueWhereClause(mFilterColumns), null,
- MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context);
+ if (context.getUserId() != mContext.getUserId()) {
+ contentUri = ContentProvider.maybeAddUserId(contentUri, context.getUserId());
+ }
+
+ return new ExternalRingtonesCursorWrapper(cursor, contentUri);
}
private void setFilterColumnsList(int type) {
@@ -740,6 +788,56 @@ public class RingtoneManager {
columns.add(MediaStore.Audio.AudioColumns.IS_ALARM);
}
}
+
+ /**
+ * Returns the sort order for the specified media.
+ *
+ * @param media The RingtoneManager media type.
+ * @return The sort order column.
+ */
+ private static String getSortOrderForMedia(@MediaType int media) {
+ return media == Ringtone.MEDIA_VIBRATION ? MediaStore.Files.FileColumns.TITLE
+ : MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+ }
+
+ /**
+ * Returns the content URI based on the specified media and whether it's internal or external
+ * storage.
+ *
+ * @param media The RingtoneManager media type.
+ * @param internal Whether it's for internal or external storage.
+ * @return The media content URI.
+ */
+ private static Uri getContentUriForMedia(@MediaType int media, boolean internal) {
+ switch (media) {
+ case Ringtone.MEDIA_VIBRATION:
+ return MediaStore.Files.getContentUri(
+ internal ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL);
+ case Ringtone.MEDIA_SOUND:
+ return internal ? MediaStore.Audio.Media.INTERNAL_CONTENT_URI
+ : MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ default:
+ throw new IllegalArgumentException("Unsupported media type " + media);
+ }
+ }
+
+ /**
+ * Constructs a where clause based on the media type. This will be used to find all matching
+ * sound or vibration files.
+ *
+ * @param media The RingtoneManager media type.
+ * @param columns The columns that must be true, when media type is {@link Ringtone#MEDIA_SOUND}
+ * @return The where clause.
+ */
+ private static String getWhereClauseForMedia(@MediaType int media, List<String> columns) {
+ // TODO(b/296213309): Filtering by ringtone-type isn't supported yet for vibrations.
+ if (media == Ringtone.MEDIA_VIBRATION) {
+ return TextUtils.formatSimple("(%s='%s')", MediaStore.Files.FileColumns.MIME_TYPE,
+ VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE);
+ }
+
+ return constructBooleanTrueWhereClause(columns);
+ }
/**
* Constructs a where clause that consists of at least one column being 1
@@ -769,14 +867,6 @@ public class RingtoneManager {
return sb.toString();
}
-
- private Cursor query(Uri uri,
- String[] projection,
- String selection,
- String[] selectionArgs,
- String sortOrder) {
- return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
- }
private Cursor query(Uri uri,
String[] projection,
diff --git a/media/tests/ringtone/Android.bp b/media/tests/ringtone/Android.bp
new file mode 100644
index 000000000000..55b98c4704b1
--- /dev/null
+++ b/media/tests/ringtone/Android.bp
@@ -0,0 +1,30 @@
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "MediaRingtoneTests",
+
+ srcs: ["src/**/*.java"],
+
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+
+ static_libs: [
+ "androidx.test.rules",
+ "testng",
+ "androidx.test.ext.truth",
+ "frameworks-base-testutils",
+ ],
+
+ test_suites: [
+ "device-tests",
+ "automotive-tests",
+ ],
+
+ platform_apis: true,
+ certificate: "platform",
+}
diff --git a/media/tests/ringtone/AndroidManifest.xml b/media/tests/ringtone/AndroidManifest.xml
new file mode 100644
index 000000000000..27eda07cd0d3
--- /dev/null
+++ b/media/tests/ringtone/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.framework.base.media.ringtone.tests">
+
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.MANAGE_USERS" />
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+
+ <activity android:name="MediaRingtoneTests"
+ android:label="Media Ringtone Tests"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.framework.base.media.ringtone.tests"
+ android:label="Media Ringtone Tests"/>
+</manifest>
diff --git a/media/tests/ringtone/TEST_MAPPING b/media/tests/ringtone/TEST_MAPPING
new file mode 100644
index 000000000000..6f25c147076c
--- /dev/null
+++ b/media/tests/ringtone/TEST_MAPPING
@@ -0,0 +1,20 @@
+{
+ "presubmit": [
+ {
+ "name": "MediaRingtoneTests",
+ "options": [
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "MediaRingtoneTests",
+ "options": [
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/media/tests/ringtone/res/raw/test_haptic_file.ahv b/media/tests/ringtone/res/raw/test_haptic_file.ahv
new file mode 100644
index 000000000000..18c99c79814f
--- /dev/null
+++ b/media/tests/ringtone/res/raw/test_haptic_file.ahv
@@ -0,0 +1,17 @@
+<vibration>
+ <waveform-effect>
+ <waveform-entry durationMs="63" amplitude="255"/>
+ <waveform-entry durationMs="63" amplitude="231"/>
+ <waveform-entry durationMs="63" amplitude="208"/>
+ <waveform-entry durationMs="63" amplitude="185"/>
+ <waveform-entry durationMs="63" amplitude="162"/>
+ <waveform-entry durationMs="63" amplitude="139"/>
+ <waveform-entry durationMs="63" amplitude="115"/>
+ <waveform-entry durationMs="63" amplitude="92"/>
+ <waveform-entry durationMs="63" amplitude="69"/>
+ <waveform-entry durationMs="63" amplitude="46"/>
+ <waveform-entry durationMs="63" amplitude="23"/>
+ <waveform-entry durationMs="63" amplitude="0"/>
+ <waveform-entry durationMs="1250" amplitude="0"/>
+ </waveform-effect>
+</vibration>
diff --git a/media/tests/ringtone/res/raw/test_sound_file.mp3 b/media/tests/ringtone/res/raw/test_sound_file.mp3
new file mode 100644
index 000000000000..c1b2fdf93991
--- /dev/null
+++ b/media/tests/ringtone/res/raw/test_sound_file.mp3
Binary files differ
diff --git a/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java b/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java
new file mode 100644
index 000000000000..a92b29883ce7
--- /dev/null
+++ b/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2023 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.
+ */
+
+package com.android.media;
+
+import static com.google.android.mms.ContentType.AUDIO_MP3;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.os.vibrator.persistence.VibrationXmlParser;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.framework.base.media.ringtone.tests.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public class RingtoneManagerTest {
+ @RingtoneManager.MediaType
+ private final int mMediaType;
+ private final List<Uri> mAddedFilesUri;
+ private Context mContext;
+ private RingtoneManager mRingtoneManager;
+ private long mTimestamp;
+
+ @Parameterized.Parameters(name = "media = {0}")
+ public static Iterable<?> data() {
+ return Arrays.asList(Ringtone.MEDIA_SOUND, Ringtone.MEDIA_VIBRATION);
+ }
+
+ public RingtoneManagerTest(@RingtoneManager.MediaType int mediaType) {
+ mMediaType = mediaType;
+ mAddedFilesUri = new ArrayList<>();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mTimestamp = SystemClock.uptimeMillis();
+ mRingtoneManager = new RingtoneManager(mContext);
+ mRingtoneManager.setMediaType(mMediaType);
+ }
+
+ @After
+ public void tearDown() {
+ // Clean up media store
+ for (Uri fileUri : mAddedFilesUri) {
+ mContext.getContentResolver().delete(fileUri, null);
+ }
+ }
+
+ @Test
+ public void testSetMediaType_withValidValue_setsMediaCorrectly() {
+ mRingtoneManager.setMediaType(mMediaType);
+ assertThat(mRingtoneManager.getMediaType()).isEqualTo(mMediaType);
+ }
+
+ @Test
+ public void testSetMediaType_withInvalidValue_throwsException() {
+ assertThrows(IllegalArgumentException.class, () -> mRingtoneManager.setMediaType(999));
+ }
+
+ @Test
+ public void testSetMediaType_afterCallingGetCursor_throwsException() {
+ mRingtoneManager.getCursor();
+ assertThrows(IllegalStateException.class, () -> mRingtoneManager.setMediaType(mMediaType));
+ }
+
+ @Test
+ public void testGetRingtone_ringtoneHasCorrectTitle() throws Exception {
+ String fileName = generateUniqueFileName("new_file");
+ Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName);
+
+ assertThat(ringtone.getTitle(mContext)).isEqualTo(fileName);
+ }
+
+ @Test
+ public void testGetRingtone_ringtoneCanBePlayedAndStopped() throws Exception {
+ //TODO(b/261571543) Remove this assumption once we support playing vibrations.
+ assumeTrue(mMediaType == Ringtone.MEDIA_SOUND);
+ String fileName = generateUniqueFileName("new_file");
+ Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName);
+
+ ringtone.play();
+ assertThat(ringtone.isPlaying()).isTrue();
+
+ ringtone.stop();
+ assertThat(ringtone.isPlaying()).isFalse();
+ }
+
+ @Test
+ public void testGetCursor_withDifferentMedia_returnsCorrectCursor() throws Exception {
+ RingtoneManager audioRingtoneManager = new RingtoneManager(mContext);
+ String audioFileName = generateUniqueFileName("ringtone");
+ addNewRingtoneToMediaStore(audioRingtoneManager, audioFileName);
+
+ RingtoneManager vibrationRingtoneManager = new RingtoneManager(mContext);
+ vibrationRingtoneManager.setMediaType(Ringtone.MEDIA_VIBRATION);
+ String vibrationFileName = generateUniqueFileName("vibration");
+ addNewRingtoneToMediaStore(vibrationRingtoneManager, vibrationFileName);
+
+ Cursor audioCursor = audioRingtoneManager.getCursor();
+ Cursor vibrationCursor = vibrationRingtoneManager.getCursor();
+
+ List<String> audioTitles = extractRecordTitles(audioCursor);
+ List<String> vibrationTitles = extractRecordTitles(vibrationCursor);
+
+ assertThat(audioTitles).contains(audioFileName);
+ assertThat(audioTitles).doesNotContain(vibrationFileName);
+
+ assertThat(vibrationTitles).contains(vibrationFileName);
+ assertThat(vibrationTitles).doesNotContain(audioFileName);
+ }
+
+ private List<String> extractRecordTitles(Cursor cursor) {
+ List<String> titles = new ArrayList<>();
+
+ if (cursor.moveToFirst()) {
+ do {
+ String title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
+ titles.add(title);
+ } while (cursor.moveToNext());
+ }
+
+ return titles;
+ }
+
+ private Ringtone addNewRingtoneToMediaStore(RingtoneManager ringtoneManager, String fileName)
+ throws Exception {
+ Uri fileUri = ringtoneManager.getMediaType() == Ringtone.MEDIA_SOUND ? addAudioFile(
+ fileName) : addVibrationFile(fileName);
+ mAddedFilesUri.add(fileUri);
+
+ int ringtonePosition = ringtoneManager.getRingtonePosition(fileUri);
+ Ringtone ringtone = ringtoneManager.getRingtone(ringtonePosition);
+ // Validate this is the expected ringtone.
+ assertThat(ringtone.getUri()).isEqualTo(fileUri);
+ return ringtone;
+ }
+
+ private Uri addAudioFile(String fileName) throws Exception {
+ ContentResolver resolver = mContext.getContentResolver();
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName + ".mp3");
+ contentValues.put(MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_RINGTONES);
+ contentValues.put(MediaStore.Audio.Media.MIME_TYPE, AUDIO_MP3);
+ contentValues.put(MediaStore.Audio.Media.TITLE, fileName);
+ contentValues.put(MediaStore.Audio.Media.IS_RINGTONE, 1);
+
+ Uri contentUri = resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ contentValues);
+ writeRawDataToFile(resolver, contentUri, R.raw.test_sound_file);
+
+ return resolver.canonicalizeOrElse(contentUri);
+ }
+
+ private Uri addVibrationFile(String fileName) throws Exception {
+ ContentResolver resolver = mContext.getContentResolver();
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName + ".ahv");
+ contentValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH,
+ Environment.DIRECTORY_DOWNLOADS);
+ contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE,
+ VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE);
+ contentValues.put(MediaStore.Files.FileColumns.TITLE, fileName);
+
+ Uri contentUri = resolver.insert(MediaStore.Files.getContentUri(MediaStore
+ .VOLUME_EXTERNAL), contentValues);
+ writeRawDataToFile(resolver, contentUri, R.raw.test_haptic_file);
+
+ return resolver.canonicalizeOrElse(contentUri);
+ }
+
+ private void writeRawDataToFile(ContentResolver resolver, Uri contentUri, int rawResource)
+ throws Exception {
+ try (ParcelFileDescriptor pfd =
+ resolver.openFileDescriptor(contentUri, "w", null)) {
+ InputStream inputStream = mContext.getResources().openRawResource(rawResource);
+ FileOutputStream outputStream = new FileOutputStream(pfd.getFileDescriptor());
+ outputStream.write(inputStream.readAllBytes());
+
+ inputStream.close();
+ outputStream.flush();
+ outputStream.close();
+
+ } catch (Exception e) {
+ throw new Exception("Failed to write data to file", e);
+ }
+ }
+
+ private String generateUniqueFileName(String prefix) {
+ return TextUtils.formatSimple("%s_%d", prefix, mTimestamp);
+ }
+
+}