How can I add my app's custom ringtones in res

2019-03-30 04:43发布

问题:

I have this RingtonePreference (from Android Studio's default SettingsActivity):

pref_notification.xml:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <RingtonePreference
        android:dependency="notifications_alarm"
        android:key="notifications_alarm_ringtone"
        android:title="@string/pref_title_ringtone"
        android:ringtoneType="notification|all"
        android:defaultValue="content://settings/system/notification_sound" />

SettingsActivity.java:

private void setupSimplePreferencesScreen() {
    if (!isSimplePreferences(this)) {
        return;
    }

    // Add 'general' preferences.
    addPreferencesFromResource(R.xml.pref_general);

    // Add 'notifications' preferences, and a corresponding header.
    PreferenceCategory fakeHeader = new PreferenceCategory(this);
    fakeHeader.setTitle(R.string.pref_header_notifications);
    getPreferenceScreen().addPreference(fakeHeader);
    addPreferencesFromResource(R.xml.pref_notification);

    bindPreferenceSummaryToValue(findPreference("notifications_alarm_ringtone"));
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class NotificationPreferenceFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.pref_notification);

        bindPreferenceSummaryToValue(findPreference("notifications_alarm_ringtone"));
    }
}

I would like to add my app's custom ringtones from res/raw folder to the list. (I don't need them to be available for other apps.)

回答1:

Option 1: Copy to device storage

Copy your ringtone files to the device's storage. I did basically this for an app on GitHub, here, where I copied alarm tones from raw resources to the device's alarms directory (you'll need to replace all the instances of "alarms" with "ringtones"). Then we use ContentValues to create metadata telling the system that the files are ringtones and use MediaStore.Audio.Media.getContentUriForPath and then context.getContentResolver().insert(contentUri, contentValues) to add the ringtones to the device's database, so they'll be included in the RingtonePreference's list. You can also set a ringtone as the default using RingtoneManager.setActualDefaultRingtoneUri(), though you'll need the WRITE_SETTINGS permission.

Also, remember to use the URI you get from getContentUriForPath() when calling getContentResolver().insert() and RingtoneManager.setActualDefaultRingtoneUri(). And, make sure you add to your AndroidManifest.xml:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Option 2: Custom preference

Create a custom preference, as shown in this guide, or you can use (or subclass) the ListPreference. You will need to retrieve all the device's ringtones using RingtoneManager.getCursor() (docs) and add them to the list, and include your custom ringtones also.

Then, in your preferences.xml file, instead of RingtonePreference, you would use

<com.yourapp.CustomPreference android:key="your_key"
    ...

In your case, since it's okay if other apps have access to the ringtones as well, I'd recommend using the first method, since in general I feel it's better not to duplicate functionality already provided by the system, since it's best to utilize what the user is already familiar with.



回答2:

Finally I made my own ExtraRingtonePreference based on this answer: In preferences, select my sound just like with RingtonePreference

I'll include it here for future reference:

src/main/java/com/fletech/android/preference/ExtraRingtonePreference.java:

package com.fletech.android.preference;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.preference.DialogPreference;
import android.util.AttributeSet;

import com.fletech.android.redalert.R;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

public class ExtraRingtonePreference extends DialogPreference {

    private Context mContext;
    private String mValue;
    private Ringtone ringtone;
    private int mRingtoneType;
    private boolean mShowSilent;
    private boolean mShowDefault;
    private CharSequence[] mExtraRingtones;
    private CharSequence[] mExtraRingtoneTitles;

    public ExtraRingtonePreference(Context context, AttributeSet attrs) {

        super(context, attrs);

        mContext = context;

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExtraRingtonePreference, 0, 0);

        mRingtoneType = a.getInt(R.styleable.ExtraRingtonePreference_ringtoneType, RingtoneManager.TYPE_RINGTONE);
        mShowDefault = a.getBoolean(R.styleable.ExtraRingtonePreference_showDefault, true);
        mShowSilent = a.getBoolean(R.styleable.ExtraRingtonePreference_showSilent, true);
        mExtraRingtones = a.getTextArray(R.styleable.ExtraRingtonePreference_extraRingtones);
        mExtraRingtoneTitles = a.getTextArray(R.styleable.ExtraRingtonePreference_extraRingtoneTitles);

        a.recycle();
    }

    public ExtraRingtonePreference(Context context) {
        this(context, null);
    }

    public String getValue() {
        return mValue;
    }

    private Map<String, Uri> getSounds(int type) {

        RingtoneManager ringtoneManager = new RingtoneManager(mContext);
        ringtoneManager.setType(type);
        Cursor cursor = ringtoneManager.getCursor();

        Map<String, Uri> list = new TreeMap<String, Uri>();
        while (cursor.moveToNext()) {
            String notificationTitle = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
            Uri notificationUri = ringtoneManager.getRingtoneUri(cursor.getPosition());

            list.put(notificationTitle, notificationUri);
        }

        return list;
    }

    private Uri uriFromRaw(String name) {
        int resId = mContext.getResources().getIdentifier(name, "raw", mContext.getPackageName());
        return Uri.parse("android.resource://" + mContext.getPackageName() + "/" + resId);
    }

    public String getExtraRingtoneTitle(CharSequence name) {
        if (mExtraRingtones != null && mExtraRingtoneTitles != null) {
            int index = Arrays.asList(mExtraRingtones).indexOf(name);
            return mExtraRingtoneTitles[index].toString();
        }

        return null;
    }

    @Override
    public CharSequence getSummary() {

        String ringtoneTitle = null;

        if (mValue != null) {

            if (mValue.length() == 0)
                ringtoneTitle = mContext.getString(R.string.silent);

            Uri mValueUri = Uri.parse(mValue);
            if (ringtoneTitle == null && mExtraRingtones != null && mExtraRingtoneTitles != null) {

                for (int i = 0; i < mExtraRingtones.length; i++) {
                    Uri uriExtra = uriFromRaw(mExtraRingtones[i].toString());
                    if (uriExtra.equals(mValueUri)) {
                        ringtoneTitle = mExtraRingtoneTitles[i].toString();
                        break;
                    }
                }
            }

            if (ringtoneTitle == null) {
                Ringtone ringtone = RingtoneManager.getRingtone(mContext, mValueUri);
                if (ringtone != null) {
                    String title = ringtone.getTitle(mContext);
                    if (title != null && title.length() > 0) {
                        ringtoneTitle = title;
                    }
                }
            }
        }

        CharSequence summary = super.getSummary();

        if (ringtoneTitle != null) {
//            if (summary != null)
//                return String.format(summary.toString(), ringtoneTitle);
//            else
                return ringtoneTitle;
        } else return summary;
    }

    @Override
    protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {

        final Map<String, Uri> sounds = new LinkedHashMap<String, Uri>();

        if (mExtraRingtones != null) {
            for (CharSequence extraRingtone : mExtraRingtones) {
                Uri uri = uriFromRaw(extraRingtone.toString());
                String title = getExtraRingtoneTitle(extraRingtone);

                sounds.put(title, uri);
            }
        }

        if (mShowSilent)
            sounds.put(mContext.getString(R.string.silent), Uri.parse(""));

        if (mShowDefault) {
            Uri uriDefault = RingtoneManager.getDefaultUri(mRingtoneType);
            if (uriDefault != null) {
                Ringtone ringtoneDefault = RingtoneManager.getRingtone(mContext, uriDefault);
                if (ringtoneDefault != null) {
                    sounds.put(ringtoneDefault.getTitle(mContext), uriDefault);
                }
            }
        }

        sounds.putAll(getSounds(mRingtoneType));

        final String[] titleArray = sounds.keySet().toArray(new String[0]);
        final Uri[] uriArray = sounds.values().toArray(new Uri[0]);

        int index = mValue != null ? Arrays.asList(uriArray).indexOf(Uri.parse(mValue)) : -1;

        builder.setSingleChoiceItems(titleArray, index, new DialogInterface.OnClickListener() {

            public void onClick(DialogInterface dialog, int which) {

                if (ringtone != null)
                    ringtone.stop();

                Uri uri = uriArray[which];

                if (uri != null) {
                    if (uri.toString().length() > 0) {
                        ringtone = RingtoneManager.getRingtone(mContext, uri);
                        if (ringtone != null) {
                            ringtone.play();
                        }
                    }
                    mValue = uri.toString();
                } else mValue = null;

            }
        });

        builder.setPositiveButton(R.string.dialog_save, this);
        builder.setNegativeButton(R.string.dialog_cancel, this);

    }

    @Override
    protected void onDialogClosed(boolean positiveResult) {

        super.onDialogClosed(positiveResult);

        if (ringtone != null)
            ringtone.stop();

        if (positiveResult && callChangeListener(mValue)) {
            persistString(mValue);
            notifyChanged();
        }
    }

    @Override
    protected Object onGetDefaultValue(TypedArray a, int index) {
        return a.getString(index);
    }

    @Override
    protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
        if (restoreValue) {
            mValue = getPersistedString("");
        } else {
            if (mExtraRingtones != null && defaultValue != null && defaultValue.toString().length() > 0) {

                int index = Arrays.asList(mExtraRingtones).indexOf((CharSequence) defaultValue);
                if (index >= 0) {
                    mValue = uriFromRaw(defaultValue.toString()).toString();
                } else {
                    mValue = (String)defaultValue;
                }

            } else {
                mValue = (String)defaultValue;
            }

            persistString(mValue);
        }
    }
}

src/main/res/values.attrs.xml:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <declare-styleable name="ExtraRingtonePreference">
        <attr name="ringtoneType"><!-- Should correspond to RingtoneManager -->
            <!-- TYPE_RINGTONE: Ringtones. -->
            <flag name="ringtone" value="1" />
            <!-- TYPE_NOTIFICATION: Notification sounds. -->
            <flag name="notification" value="2" />
            <!-- TYPE_ALARM: Alarm sounds. -->
            <flag name="alarm" value="4" />
            <!-- TYPE_ALL: All available ringtone sounds. -->
            <flag name="all" value="7" />
        </attr>
        <attr name="showSilent" format="boolean"/>
        <attr name="showDefault" format="boolean"/>
        <attr name="extraRingtones" format="reference"/>
        <attr name="extraRingtoneTitles" format="reference"/>
    </declare-styleable>
</resources>

src/res/values/ringtone_preference_strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="ringtone_title">Ringtone</string>
    <string name="silent">Silent</string>
    <string name="dialog_save">Save</string>
    <string name="dialog_cancel">Cancel</string>

    <string-array name="extra_ringtones">
        <item>beep</item>
        <item>beep_beep</item>
        <item>default</item>
        <item>test</item>
    </string-array>

    <string-array name="extra_ringtone_titles">
        <item>Beep</item>
        <item>Beep-Beep</item>
        <item>Default</item>
        <item>Test</item>
    </string-array>
</resources>

and the usage in src/main/res/xml/pref_alarm.xml:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:auto="http://schemas.android.com/apk/res-auto">
    <com.fletech.android.preference.ExtraRingtonePreference
        android:key="notifications_alarm_ringtone"
        android:title="@string/ringtone_title"
        android:defaultValue="default"
        auto:ringtoneType="alarm"
        auto:showSilent="true"
        auto:showDefault="true"
        auto:extraRingtones="@array/extra_ringtones"
        auto:extraRingtoneTitles="@array/extra_ringtone_titles"/>
</PreferenceScreen>