Как установить файл в качестве рингтона для Android 10?

Раньше я использовал следующий код, чтобы сохранить необработанный звуковой файл на внешнее хранилище и установить его в качестве мелодии звонка. Но, похоже, в Android 10 все изменилось. Можете ли вы помочь мне или показать мне некоторые рекомендации, как обновить мой код для работы с Android 10?

Вот код для сохранения файла на внешнее хранилище:

Сохранение файла:

String path = Environment.getExternalStorageDirectory() + "/customsounds";

 public File getFile() {
        boolean exists = (new File(path).exists());
        if (!exists) {
            new File(path).mkdirs();
        }
        File newSoundFile = new File(path, sound.getFileName() + ".mp3");
        Uri mUri = Uri.parse("android.resource://com.example.customsounds/" + sound.getId());
        ContentResolver mCr = getContentResolver();
        AssetFileDescriptor soundFile;
        try {
            soundFile = mCr.openAssetFileDescriptor(mUri, "r");
        } catch (FileNotFoundException e) {
            soundFile = null;
        }

        try {
            byte[] readData = new byte[1024];
            FileInputStream fis = soundFile.createInputStream();
            FileOutputStream fos = new FileOutputStream(newSoundFile);
            int i = fis.read(readData);

            while (i != -1) {
                fos.write(readData, 0, i);
                i = fis.read(readData);
            }

            fos.close();
        } catch (IOException io) {
            Log.e(TAG, "io exception");
            return null;
        }

        return newSoundFile;
    }

Установка в качестве рингтона:

 public void setAsRingtone() {

    File newSoundFile = getFile();
    ContentValues values = new ContentValues();
    values.put(MediaStore.MediaColumns.DATA, newSoundFile.getAbsolutePath());
    values.put(MediaStore.MediaColumns.TITLE, sound.getTitle());
    values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mp3");
    values.put(MediaStore.MediaColumns.SIZE, newSoundFile.length());
    values.put(MediaStore.Audio.Media.ARTIST, R.string.app_name);
    values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
    values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);
    values.put(MediaStore.Audio.Media.IS_ALARM, true);
    values.put(MediaStore.Audio.Media.IS_MUSIC, false);

    Uri uri = MediaStore.Audio.Media.getContentUriForPath(newSoundFile.getAbsolutePath());
    getContentResolver().delete(uri,
            MediaStore.MediaColumns.DATA + "=?", new String[]{newSoundFile.getAbsolutePath()});
    Uri newUri = getContentResolver().insert(uri, values);

    RingtoneUtils.setRingtone(this, newUri, type);
}

Вот RingtoneUtils:

import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;


public class RingtoneUtils {

    private static final String LOG_TAG = "RingtoneUtils";

    public static boolean setRingtone(@NonNull Context context, @NonNull Uri ringtoneUri, int type) {
        Log.v(LOG_TAG, "Setting Ringtone to: " + ringtoneUri);

        if (!hasMarshmallow()) {
            Log.v(LOG_TAG, "On a Lollipop or below device, so go ahead and change device ringtone");
            setActualRingtone(context, ringtoneUri, type);
            return true;
        } else if (hasMarshmallow() && canEditSystemSettings(context)) {
            Log.v(LOG_TAG, "On a marshmallow or above device but app has the permission to edit system settings");
            setActualRingtone(context, ringtoneUri, type);
            return true;
        } else if (hasMarshmallow() && !canEditSystemSettings(context)) {
            Log.d(LOG_TAG, "On android Marshmallow and above but app does not have permission to" +
                    " edit system settings. Opening the manage write settings activity...");
            startManageWriteSettingsActivity(context);
            Toast.makeText(context, "Please allow app to edit settings so your ringtone/notification can be updated", Toast.LENGTH_LONG).show();
            return false;
        }

        return false;
    }

    private static void setActualRingtone(@NonNull Context context, @NonNull Uri ringtoneUri, int type) {
        RingtoneManager.setActualDefaultRingtoneUri(context, type, ringtoneUri);
        String message="";
        if(type == RingtoneManager.TYPE_RINGTONE) {
            message = context.getString(R.string.ringtone_set_success);
        } else if(type == RingtoneManager.TYPE_NOTIFICATION) {
            message = context.getString(R.string.notification_set_success);
        }
        if ((RingtoneManager.getActualDefaultRingtoneUri(context, type)).equals(ringtoneUri)) {
            Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context, context.getString(R.string.operation_failed), Toast.LENGTH_SHORT).show();
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    private static void startManageWriteSettingsActivity(@NonNull Context context) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
        // Passing in the app package here allows the settings app to open the exact app
        intent.setData(Uri.parse("package:" + context.getApplicationContext().getPackageName()));
        // Optional. If you pass in a service context without setting this flag, you will get an exception
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

    private static boolean hasMarshmallow() {
        // returns true if the device is Android Marshmallow or above, false otherwise
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    private static boolean canEditSystemSettings(@NonNull Context context) {
        // returns true if the app can edit system settings, false otherwise
        return Settings.System.canWrite(context.getApplicationContext());
    }

}

Исключение до Android 10:(@greywolf82)

java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.lastIndexOf(int)' on a null object reference
        at android.os.Parcel.readException(Parcel.java:1957)
        at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:183)
        at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:135)
        at android.content.ContentProviderProxy.insert(ContentProviderNative.java:476)
        at android.content.ContentResolver.insert(ContentResolver.java:154

person Figen Güngör    schedule 06.10.2019    source источник
comment
Полезный вопрос! Спасибо   -  person Androider    schedule 18.01.2021


Ответы (2)


Вы больше не можете получить прямой доступ через интерфейс файлов в Android 10, кроме того, вы не можете получить доступ к столбцу DATA. Вы можете удалить свой метод getFile и вам нужно изменить метод setAsRingtone():

 public void setAsRingtone() {


    ContentValues values = new ContentValues();
    values.put(MediaStore.MediaColumns.TITLE, sound.getTitle());
    values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mp3");
    values.put(MediaStore.MediaColumns.SIZE, newSoundFile.length());
    values.put(MediaStore.Audio.Media.ARTIST, R.string.app_name);
    values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
    values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);
    values.put(MediaStore.Audio.Media.IS_ALARM, true);
    values.put(MediaStore.Audio.Media.IS_MUSIC, false);

    Uri newUri = getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
    try (OutputStream os = getContentResolver().openOutputStream(newUri)) {
         //copy your file from asset into os here
    } catch(Exception ignored) {
    }
    RingtoneUtils.setRingtone(this, newUri, type);
}
person greywolf82    schedule 06.10.2019
comment
Эта операция вставки дает сбой на версиях до Android 10. Можете ли вы рассказать мне, о чем она? Я обновлю q с исключением. - person Figen Güngör; 10.10.2019
comment
извините, я понятия не имею, возможно, столбец DATA является обязательным на устройствах pre-q - person greywolf82; 10.10.2019
comment
Я понимаю. Тогда я оставлю свой старый код для версий pre-q. Спасибо. - person Figen Güngör; 10.10.2019
comment
@ greywolf82 greywolf82 как бы вы назначили этот файл в качестве звука для канала уведомлений, вам все еще нужно получить Uri для этого пользовательского звукового файла или который изменился для Android 10 Q? Уже несколько дней пытаюсь решить эту проблему. - person Razvan Emil; 12.10.2019
comment
@greywolf82 greywolf82 Также, как именно вы копируете файл из актива в ОС, например, каков путь в ОС, куда его следует скопировать, пожалуйста? - person Razvan Emil; 13.10.2019

Этот метод работает почти для всех API.

 private boolean SetAsRingtoneOrNotification(File k, int type) {


        ContentValues values = new ContentValues();

        values.put(MediaStore.MediaColumns.TITLE, k.getName());
        values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mp3");
        if (RingtoneManager.TYPE_RINGTONE == type) {
            values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
        } else if (RingtoneManager.TYPE_NOTIFICATION == type) {
            values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);
        }


        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            Uri newUri = this.getContentResolver()
                    .insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
            try (OutputStream os = getContentResolver().openOutputStream(newUri)) {

                int size = (int) k.length();
                byte[] bytes = new byte[size];
                try {
                    BufferedInputStream buf = new BufferedInputStream(new FileInputStream(k));
                    buf.read(bytes, 0, bytes.length);
                    buf.close();

                    os.write(bytes);
                    os.close();
                    os.flush();
                } catch (IOException e) {
                    return false;
                }
            } catch (Exception ignored) {
                return false;
            }
            RingtoneManager.setActualDefaultRingtoneUri(Emotes.this, type,
                    newUri);

            return true;
        } else {
            values.put(MediaStore.MediaColumns.DATA, k.getAbsolutePath());

            Uri uri = MediaStore.Audio.Media.getContentUriForPath(k
                    .getAbsolutePath());

            getContentResolver().delete(uri, MediaStore.MediaColumns.DATA + "=\"" + k.getAbsolutePath() + "\"", null);


            Uri newUri = Emotes.this.getContentResolver().insert(uri, values);
            RingtoneManager.setActualDefaultRingtoneUri(Emotes.this, type,
                    newUri);

            this.getContentResolver()
                    .insert(MediaStore.Audio.Media.getContentUriForPath(k
                            .getAbsolutePath()), values);

            return true;
        }



    }
person Ibrahim    schedule 12.12.2019
comment
Это работало в Android Q и почти во всех API. :) - person RAJESH KUMAR ARUMUGAM; 28.06.2020
comment
Хороший подробный ответ. Очень полезно Спасибо! - person Androider; 18.01.2021