9

I used to use following code to save a raw sound file to external storage and set it as ringtone. But things seems to change in android 10. Can you help me or show me some guideline how to update my code to work with android 10?

Here is the code to save file to external storage:

Saving file:

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

Setting as ringtone:

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

Here is the 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());
    }

}

Pre-Android 10 exception:(@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
Figen Güngör
  • 12,169
  • 14
  • 66
  • 108

3 Answers3

14

This method works for almost all api's.

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



    }
ibrahim
  • 573
  • 1
  • 6
  • 20
5

/working in android 10 and 11/

 private void SetAsRingtoneAndroid(File k) {
    ContentValues values = new ContentValues();
    values.put(MediaStore.MediaColumns.TITLE, k.getName());
    values.put(MediaStore.MediaColumns.MIME_TYPE, getMIMEType(k.getAbsolutePath()));//// getMIMEType(k.getAbsolutePath())
    values.put(MediaStore.MediaColumns.SIZE, k.length());
    values.put(MediaStore.Audio.Media.ARTIST, R.string.app_name);
    values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        Uri newUri = 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) {
            }
        } catch (Exception ignored) {
        }
        RingtoneManager.setActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_RINGTONE, newUri);
        Toast.makeText(this, "Ringtone set", Toast.LENGTH_SHORT).show();
    }
}

public static String getMIMEType(String url) {
    String mType = null;
    String mExtension = MimeTypeMap.getFileExtensionFromUrl(url);
    if (mExtension != null) {
        mType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(mExtension);
    }
    return mType;
}
Dhiraj Ved
  • 81
  • 1
  • 4
4

You can't access directly using a File interface anymore in Android 10, in addition you can't access to the DATA column. You can remove your method getFile and you need to change the method 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);
}
greywolf82
  • 21,813
  • 18
  • 54
  • 108
  • This insert operation crashes on pre Android 10. Can you tell me what it is about? I'll update the q with exception. – Figen Güngör Oct 09 '19 at 21:17
  • 3
    sorry, I have no idea, maybe the DATA column is mandatory on pre-q devices – greywolf82 Oct 10 '19 at 17:24
  • I see. I'll keep my old code for pre-q versions, then. Thank you. – Figen Güngör Oct 10 '19 at 17:47
  • @greywolf82 how would you assign that file as a sound for a notification channel, do you still have to grab the Uri for that custom sound file or that changed for Android 10 Q? Been trying to solve this for days now. – Razvan Emil Oct 11 '19 at 21:01
  • @greywolf82 Also how exactly do you copy file from asset into os, like what is the path in the os where it should be copied please? – Razvan Emil Oct 13 '19 at 04:32
  • @greywolf82 Is there a way to make the set ringtone be associated with itself in the "My Sounds" list in the Settings app? – David Read Mar 11 '22 at 00:07