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);
+ }
+
+}