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
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index 5ae77b5..a9da832 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 0ff1b1e..9234479 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.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.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 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 @@
     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 @@
     }
 
     /**
+     * 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 @@
             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 @@
                 // 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 @@
         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 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 @@
         }
     }
 
-    @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);
 
-    @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);
+        Cursor cursor = query(contentUri, columns, whereClause, /* selectionArgs= */ null,
+                sortOrder, 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 @@
             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 @@
 
         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 0000000..55b98c4
--- /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 0000000..27eda07
--- /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 0000000..6f25c14
--- /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 0000000..18c99c7
--- /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 0000000..c1b2fdf
--- /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 0000000..a92b298
--- /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);
+    }
+
+}