android ringtone

Create an Android Ringtone Picker Using the Ringtonemanager Class

In this article, I will show you how to create a ringtone picker using the RingtoneManager class in Android. You will be able to get the list of tones, display them as RadioButton and let the user pick one tone from the list.

I expect you to have a basic knowledge of programming in Android. You should already have set up Android Studio and all the essential components required to build an Android app.

All code in this article will be in Java and not Kotlin.

An introduction to the RingtoneManager class:

Let’s say you are designing an alarm clock app, and you want the user to choose the alarm tone. Android comes with a set of tones that you can display to the user for this purpose. Tones in Android can be classified into a ringtone, alarm tone, and notification tone categories. Android comes with the RingtoneManager class so that you can access these tones.

In order to show the list of tones under one or more categories, you have to send an implicit intent to launch a ringtone picker. The Intent action will be RingtoneManager.ACTION_RINGTONE_PICKER. By default, an Android device comes with an activity that will respond to this intent. You can send the following extras with the intent:

  • EXTRA_RINGTONE_DEFAULT_URI
  • EXTRA_RINGTONE_EXISTING_URI
  • EXTRA_RINGTONE_SHOW_DEFAULT
  • EXTRA_RINGTONE_SHOW_SILENT
  • EXTRA_RINGTONE_TITLE
  • EXTRA_RINGTONE_TYPE

For details on what data these extras should carry, please read the documentation of RingtoneManager class.

When the user has chosen a tone, the ringtone picker will be destroyed and you will get a callback in the onActivityResult(...) method of the caller activity. The ringtone picker will return  RingtoneManager.EXTRA_RINGTONE_PICKED_URI through which you can get the Uri of the tone chosen by the user.

Launching the default ringtone picker using an implicit intent:

The following is a small code snippet showing to invoke the default ringtone picker from an activity using an implicit intent:

Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Select alarm tone:");
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existingToneUri);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
startActivityForResult(intent, RINGTONE_REQUEST_CODE);

In the above snippet, you can see that I have specified the tone type to be alarm by passing TYPE_ALARM with EXTRA_RINGTONE_TYPE. RingtoneManager class allows three types of tones: TYPE_ALARM, TYPE_NOTIFICATION and TYPE_RINGTONE. You can combine these to direct the picker to list tones under more than one category:

intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM | RingtoneManager.TYPE_NOTIFICATION);

Alternatively, you can use TYPE_ALL to list all the available tones.

You can handle the activity result in the following way:

@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
   super.onActivityResult(requestCode, resultCode, data);
   if (requestCode == RINGTONE_REQUEST_CODE) {
      if (resultCode == RESULT_OK && data != null) {
         Uri toneUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
      }
   }
}

Why do you need a custom ringtone picker?

The default ringtone picker will serve your purpose in most cases. However, sometimes you might want to build a custom ringtone picker. Four reasons why I decided to build a custom ringtone picker:

  1. I wanted to allow the user to pick a file from storage and use it as a tone. This is not possible in the default picker. One can do this easily from regular activity, but I wanted to add this option at the end of the list of tones.
  2. If I pass the Uri of a file as EXTRA_RINGTONE_EXISTING_URI that is not among the default tones, I would like the ringtone picker to include it in the list of tones if the file exists.
  3. The default ringtone picker can be a dialog or an activity. If the user cancels the dialog, you might get a RESULT_CANCELLED in onActivityResult(...). As per the requirements of my app, I strictly needed the picker to return a Uri even if the user did not choose any Uri/canceled the picker.
  4. Let’s say the user selects a file from the device storage as the tone. When the user launches the ringtone picker next time, it should display all such files chosen previously along with the default tones, provided the files still exist.

Note that I will not cover any of the above features in this article. It will not only make the article very lengthy but may also bore you if you have come to learn the basics only.

Building a ringtone picker activity:

Now, we shall design a simple ringtone picker activity. The activity will have a RadioButton for each tone. When the user presses the back key or the up button, the activity will return the picked Uri and finish itself. For simplicity, I will not list any import statements and assume that imports are managed by your IDE. I shall also assume that all the static members of RingtoneManager class have been imported using:

import static android.media.RingtoneManager.*;

Therefore, from now on, I will address all the static members of this class without using the class name.

At the end, the ringtone picker would look something like this:

ringtone_picker_activity

Without further ado, let’s start!

Preparing our activity to respond to implicit intents:

Our activity will be called RingtonePickerActivity, and will be a subclass of androidx.appcompat.app.AppCompatActivity.

public class RingtonePickerActivity extends AppCompatActivity implements View.OnClickListener{
...
}

View.OnClickListener will help us to listen to click events. We shall see later where to use it.

We want this activity to respond to any implicit intent that comes with ACTION_RINGTONE_PICKER, as well as any explicit intent. For the latter, we don’t need to do anything, but for the former, we modify the AndroidManifest.xml file as follows:

<activity android:name=".RingtonePickerActivity"
    android:exported="true"
    android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
    <intent-filter>
        <action android:name="android.intent.action.RINGTONE_PICKER"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>
  • When an implicit intent is sent by some app, the IntentFilter will respond to it if the action string matches.
  • android:exported="true" is required if we want other apps to launch this activity.
  • We have set the theme to be Theme.AppCompat.DayNight.NoActionBar. This will have two advantages:
    • The activity will support dark (or night) mode.
    • There will be no ActionBar. We do not want the default ActionBar because we will be creating a custom one.

If the theme of your app is the same as this, you need not set the android:theme attribute separately.

Creating the layout of the activity:

In the layout file of our activity, we will have a Toolbar that will serve as the ActionBar of the activity, and a ScrollView (with vertical scrolling) inside which we will place a RadioGroup. A ScrollView can have only one child. So we make that child a ConstraintLayout inside which we can place the RadioGroup. Note that we are not putting any RadioButton inside the RadioGroup, because we will populate the latter dynamically at runtime.

I always find it convenient to put the child of the ScrollView in a separate xml file, and then include it in the ScrollView. It will not be quite clear how this is helpful in this simple activity, but in a complex layout, where you have multiple views inside the child of the ScrollView, it definitely helps if you separately design the views and include them in the ScrollView.

Therefore, we will have two xml files:

File activity_ringtonepicker:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar4"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="?attr/actionBarTheme"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ScrollView
        android:id="@+id/addAlarmScrollView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="0sp"
        android:layout_marginTop="1dp"
        android:layout_marginEnd="0sp"
        android:fitsSystemWindows="false"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar4"
        app:layout_constraintVertical_bias="0.0">

        <include layout="@layout/activity_ringtonepicker_scrollview"/>

    </ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

A piece of advice: If this is the first time you are working with ScrollView, let me draw your attention to android:fitsSystemWindows attribute in the above code. If this is not set to false, the scrolling won’t work (by default, it is true). Also, make sure you mention the type of scrolling using the android:scrollbars attribute.

File activity_ringtonepicker_scrollview:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RadioGroup
        android:id="@+id/ringtonePickerRadioGroup"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="2dp"
        android:divider="?android:attr/dividerHorizontal"
        android:dividerPadding="30dp"
        android:orientation="vertical"
        android:showDividers="middle"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<!-- You can place more static views here. -->

</androidx.constraintlayout.widget.ConstraintLayout>

Make a note of the android:divider, android:showDividers and android:dividerPadding attributes in the above xml file. Without these, the dividers between the RadioButtons that you see in the above picture won’t appear. Also, make sure you specify android:orientation to be vertical.

The .java file:

We have created the activity layout file and edited the manifest file to suit our needs. Now we can dive into to the real programming part.

Declaring global variables:

I am in no mood to increase the length of this article indefinitely, but I cannot proceed further before giving a list of the global variables that we will be using later.

private Uri defaultUri, existingUri, pickedUri;
private boolean showDefault, showSilent, wasExistingUriGiven;
private CharSequence title;
private int type;
private ArrayList<Uri> toneUriList;
private ArrayList<String> toneNameList;
private ArrayList<Integer> toneIdList;

private RingtoneManager ringtoneManager;

private RadioGroup radioGroup;

private MediaPlayer mediaPlayer;
private AudioAttributes audioAttributes;

private static final int DEFAULT_RADIO_BTN_ID = View.generateViewId(), SILENT_RADIO_BTN_ID = View.generateViewId();

A few words about the global variables (you may skip this if you are not interested):

  • Variables defaultUri, existingUri, showDefault, showSilent, title and type will be read from EXTRA_RINGTONE_DEFAULT_URIEXTRA_RINGTONE_EXISTING_URI, EXTRA_RINGTONE_SHOW_DEFAULT, EXTRA_RINGTONE_SHOW_SILENTEXTRA_RINGTONE_TITLE and EXTRA_RINGTONE_TYPE respectively, or assigned default values if any of these extras are missing.
  • Variable pickedUri will contain the Uri of the tone picked by the user.
  • toneUriList is an ArrayList of the unique Uri of the tone files returned by RingtoneManager. toneNameList contains the corresponding names of the tones that can be displayed to the user in the form of RadioButtons. toneIdList is an ArrayList of ids that will be used for setting the id of RadioButtons.
  • DEFAULT_RADIO_BTN_ID and SILENT_RADIO_BTN_ID are the ids for the “Default” and the “Silent” RadioButton respectively in radioGroup.
  • wasExistingUriGiven will help us determine whether our activity had received  EXTRA_RINGTONE_EXISTING_URI. This variable will be particularly helpful if the Uri passed is null or if the extra is not sent.
  • When the user chooses a tone, we will play the tone using mediaPlayer. The audioAttributes will be required to set the stream of the mediaPlayer.

How to get the list of tones using RingtoneManager:

All the codes in this section will be in the onCreate(...) method of our activity, unless otherwise noted.

We use the constructor of RingtoneManager class that takes an Activity as the argument, and thereby initialise the ringtoneManager:

ringtoneManager = new RingtoneManager(this);

Next, we need to set the type of tone we want. Generally, we will get this using EXTRA_RINGTONE_TYPE:

if (getIntent().hasExtra(EXTRA_RINGTONE_TYPE)) {
   type = (int) Objects.requireNonNull(getIntent().getExtras()).get(EXTRA_RINGTONE_TYPE);
} else {
   type = TYPE_ALL;
}
ringtoneManager.setType(type);

Now we can extract the list of tones using the getCursor() method of RingtoneManager class:

Cursor tonesCursor = ringtoneManager.getCursor();

In Android, the Cursor interface is used to access the result set returned by a database query. You can think of it as a table with several columns. Each column has a unique index (an integer), and the data of that column can be accessed using the column index.

The Cursor returned by RingtoneManager (variable  tonesCursor in our case) has three columns with indices RingtoneManager.TITLE_COLUMN_INDEX, RingtoneManager.URI_COLUMN_INDEX and RingtoneManager.ID_COLUMN_INDEX. The first index will return the displayable name of the tone, the second will give the media provider’s Uri, and the third will give the row ID. We will fill up the variables toneNameList, toneUriList and toneIdList in the following way:

Thread thread = new Thread(() -> {
   if (tonesCursor.moveToFirst()) {
      do {
         int id = tonesCursor.getInt(ID_COLUMN_INDEX);
         String uri = tonesCursor.getString(URI_COLUMN_INDEX);

         toneUriList.add(Uri.parse(uri + "/" + id));
         toneNameList.add(tonesCursor.getString(TITLE_COLUMN_INDEX));
         toneIdList.add(View.generateViewId());
      } while (tonesCursor.moveToNext());
   }
});
thread.start();

Note that URI_COLUMN_INDEX will give you the media provider’s Uri, not the unique Uri to a tone file. You have to append it with ID_COLUMN_INDEXto get the complete Uri of the file, as shown above.

Half of our work is done once we have got the tones. We still have to display the tones as RadioButtons, play the tone when the user chooses one, handle their click events and return the result to the caller activity when the user presses the back/up button.

Initialising the mediaPlayer:

We will initialise the media player in the onCreate(...) of our activity.

mediaPlayer = new MediaPlayer();
audioAttributes = new AudioAttributes.Builder()
      .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
      .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
      .build();

Complete code of the onCreate(...) method of our activity:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_ringtonepicker);

   setSupportActionBar(findViewById(R.id.toolbar4));
   Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);

   radioGroup = findViewById(R.id.ringtonePickerRadioGroup);

   ringtoneManager = new RingtoneManager(this);

   mediaPlayer = new MediaPlayer();
   audioAttributes = new AudioAttributes.Builder()
         .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
         .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
         .build();

   Intent intent = getIntent(); // get the Intent that started this activity.

   Cursor tonesCursor;

   toneNameList = new ArrayList<>();
   toneUriList = new ArrayList<>();
   toneIdList = new ArrayList<>();

   if (Objects.equals(getIntent().getAction(), ACTION_RINGTONE_PICKER)) {

      if (intent.hasExtra(EXTRA_RINGTONE_TYPE)) {
         type = (int) Objects.requireNonNull(intent.getExtras()).get(EXTRA_RINGTONE_TYPE);
      } else {
         type = TYPE_ALL;
      }
      ringtoneManager.setType(type);
      allTonesCursor = ringtoneManager.getCursor();

      Thread thread = new Thread(() -> {
         if (allTonesCursor.moveToFirst()) {
            do {
               int id = tonesCursor.getInt(ID_COLUMN_INDEX);
               String uri = tonesCursor.getString(URI_COLUMN_INDEX);

               toneUriList.add(Uri.parse(uri + "/" + id));
               toneNameList.add(tonesCursor.getString(TITLE_COLUMN_INDEX));
               toneIdList.add(tonesCursor.getInt(ID_COLUMN_INDEX));
            } while (tonesCursor.moveToNext());
         }
      });
      thread.start();

      if (intent.hasExtra(EXTRA_RINGTONE_SHOW_DEFAULT)) {
         showDefault = (boolean) Objects.requireNonNull(intent.getExtras()).get(EXTRA_RINGTONE_SHOW_DEFAULT);
      } else {
         showDefault = true;
      }

      if (intent.hasExtra(EXTRA_RINGTONE_SHOW_SILENT)) {
         showSilent = (boolean) Objects.requireNonNull(intent.getExtras()).get(EXTRA_RINGTONE_SHOW_SILENT);
      } else {
         showSilent = false;
      }

      if (showDefault) {
         if (intent.hasExtra(EXTRA_RINGTONE_DEFAULT_URI)) {
            defaultUri = Objects.requireNonNull(intent.getExtras()).getParcelable(EXTRA_RINGTONE_DEFAULT_URI);
         } else {
            defaultUri = RingtoneManager.getActualDefaultRingtoneUri(this, type);
         }
      } else {
         defaultUri = null;
      }

      if (intent.hasExtra(EXTRA_RINGTONE_EXISTING_URI)) {
         existingUri = Objects.requireNonNull(intent.getExtras()).getParcelable(EXTRA_RINGTONE_EXISTING_URI);
         wasExistingUriGiven = true;
      } else {
         wasExistingUriGiven = false;
         existingUri = null;
      }

      if (intent.hasExtra(EXTRA_RINGTONE_TITLE)) {
         title = (CharSequence) Objects.requireNonNull(intent.getExtras()).get(EXTRA_RINGTONE_TITLE);
      } else {
         title = "Select tone:";
      }

      Objects.requireNonNull(getSupportActionBar()).setTitle(title);

      try { 
          thread.join(); 
      } catch (InterruptedException ignored) { 
      } 
   }
}

Populate the RadioGroup using the tones:

Now that we have the tones, we can start populating radioGroup.

How to add a RadioButton to a RadioGroup dynamically?

We will use the addView(View view) method in RadioGroup class to add RadioButtons. Each RadioButton must have an ID; using this ID we can later find out which button was clicked. To simplify our task, we will be using the values in toneIdList as the id of the RadioButtons. The “Default” and “Silent” buttons, if present, will have the ids DEFAULT_RADIO_BTN_ID and SILENT_RADIO_BTN_ID respectively.

We shall use the following method to create a RadioButton with a given text and id, add it to radioGroup and set the OnClickListener:

/**
 * Creates one {@link RadioButton} and adds it to {@link #radioGroup}.
 *
 * @param id The id to be assigned to the {@link RadioButton}.
 * @param text The text to be set in the {@link RadioButton}.
 */
private void createOneRadioButton(int id, String text) {
   RadioGroup.LayoutParams params = new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
   params.setMargins(5, 24, 5, 24);

   RadioButton radioButton = new RadioButton(this);
   radioButton.setId(id);
   radioButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 17); // set text size to 17sp
   radioButton.setLayoutParams(params);
   radioButton.setText(text);
   radioButton.setOnClickListener(this);
   radioGroup.addView(radioButton);
}
Populate radioGroup:

Now it’s pretty straightforward: For each tone in our list, we call the above method. If showDefault and showSlient are both set to true, we add the “Default” and “Silent” RadioButtons:

if (showDefault) {
   createOneRadioButton(DEFAULT_RADIO_BTN_ID, getResources().getString(R.string.defaultTone));
}

if (showSilent) {
   createOneRadioButton(SILENT_RADIO_BTN_ID, getResources().getString(R.string.silentTone));
}

for (int i = 0; i < toneIdList.size(); i++) {
   createOneRadioButton(toneIdList.get(i), toneNameList.get(i));
}
Check the appropriate RadioButton if the user has passed an existingUri:

If an existingUri has been passed to our activity, we need to check the appropriate RadioButton. We can do this as follows:

if (existingUri != null) {

         if (showDefault && existingUri.equals(defaultUri)) {

            ///////////////////////////////////////////////////////////////////////////
            // The existingUri is same as defaultUri, and showDefault is true.
            // So, we check the "Default" RadioButton.
            //////////////////////////////////////////////////////////////////////////
            ((RadioButton) findViewById(DEFAULT_RADIO_BTN_ID)).setChecked(true);
            pickedUri = defaultUri;

         } else {

            // Find index of existingUri in toneUriList
            int index = toneUriList.indexOf(existingUri);

            if (index != - 1) {

               // toneUriList has existingUri. Check the corresponding RadioButton.
               ((RadioButton) findViewById(toneIdList.get(index))).setChecked(true);
               pickedUri = existingUri;

            }
      }
} else { //existingUri is null.

   if (wasExistingUriGiven) {
      //////////////////////////////////////////////////////////////////////////
      // existingUri was specifically passed as a null value. If showSilent
      // is true, we pre-select the "Silent" RadioButton. Otherwise
      // we do not select any specific RadioButton.
      /////////////////////////////////////////////////////////////////////////
      if (showSilent) {
         ((RadioButton) findViewById(SILENT_RADIO_BTN_ID)).setChecked(true);
      }
   }
   pickedUri = null;
}

We face a problem if existingUri is null, because it can be null for two reasons:

  • The calling activity has deliberately passed null.
  • The calling activity has not passed the EXTRA_EXISTING_URI.

We can distinguish between the first and the second cases using the variable wasExistingUriGiven. As explained earlier, if this yields true, it means that we have received EXTRA_EXISTING_URI, and we have to check whether showSilent is true or not. If it is true, we should check the “Silent” RadioButton. Otherwise, we will just ignore this.

Handling RadioButton click events:

Each of the RadioButtons in radioGroup has an OnClickListener. We have to handle their click events inside the onClick(View view) method, which we override from the View.OnClickListener interface.

@Override
public void onClick(View view) {
   if (view.getId() == DEFAULT_RADIO_BTN_ID) {
      pickedUri = defaultUri;
      playChosenTone();
   } else if (view.getId() == SILENT_RADIO_BTN_ID) {
      pickedUri = null;
   } else {
      pickedUri = toneUriList.get(toneIdList.indexOf(view.getId()));
      playChosenTone();
   }
}
Play the tone chosen by the user:

In the above section, you can see that we are calling a method playChosenTone(). This method is used to play the tone chosen by the user.

private void playChosenTone() {
   if (pickedUri != null) {
      try {
         mediaPlayer.reset();
         mediaPlayer.setDataSource(this, pickedUri);
         mediaPlayer.setLooping(false);
         mediaPlayer.setAudioAttributes(audioAttributes);
         mediaPlayer.prepare();
         mediaPlayer.start();
      } catch (Exception ignored) {
      }
   }
}

The above code will play the chosen tone. However, remember that if the user exits the activity (by pressing back button, up button or home key), we need to stop the media player immediately. Therefore, in the onPause() of our activity, we put the following code:

@Override
protected void onPause() {
   super.onPause();
   try {
      mediaPlayer.stop();
   } catch (Exception ignored) {
   }
}

In some cases, the above may not work. You can try using mediaPlayer.pause() instead of stop().

When the activity finishes, we destroy the media player and release its resources:

@Override
protected void onDestroy() {
   super.onDestroy();
   try {
      mediaPlayer.release();
   } catch (Exception ignored) {
   }
}

Returning the result when the back button is pressed:

We can listen to click events of the back button by overriding the onBackPressed() method in an activity. In this method, we shall set the result. The pickedUri will be returned using EXTRA_RINGTONE_PICKED_URI. If pickedUri is null, we check whether showSilent is true or not. If it is false, it means that the calling activity does not expect a null value, so we return RESULT_CANCELLED. In all other cases, we return RESULT_OK along with pickedUri.

@Override
public void onBackPressed() {

   if (pickedUri == null) {
      if (showSilent) {
         Intent intent = new Intent();
         intent.putExtra(EXTRA_RINGTONE_PICKED_URI, pickedUri);

         setResult(RESULT_OK, intent);
      } else {
         setResult(RESULT_CANCELED);
      }
   } else {
      Intent intent = new Intent();
      intent.putExtra(EXTRA_RINGTONE_PICKED_URI, pickedUri);

      setResult(RESULT_OK, intent);
   }
   finish();
}

Handle up button click events:

In the onCreate(...) method above, we have the following code:

Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);

We need to listen to up button click events. Normally we expect the up button to do the same job as the back button, so we just call onBackPressed():

@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
   if (item.getItemId() == android.R.id.home) {
      this.onBackPressed();
      return true;
   }
   return super.onOptionsItemSelected(item);
}

That’s it! Our simple ringtone picker activity is complete.

Final words:

I hope I have been able to help you build a ringtone picker activity in this article. The activity, in the above form, does the same job as the default ringtone picker. Now you can add as many features to it as you want.

Note that this code is not safe from configuration changes. This means that the activity will malfunction if the screen orientation or night mode changes. You will have to handle configuration changes yourself by making the necessary alterations in the above code. You can have a look at the Android developer guide on how to handle config changes.

I would like to thank Greg Bernhardt, the owner, and admin of Physics Forums, for creating and maintaining the Insights Blog and for constantly motivating us to share our knowledge by writing articles in his blog.

Questions, comments, suggestions for improvements, and bug reports are always welcome.

3 replies
  1. Wrichik Basu says:
    Is it possible for a standard Android app (one built on the regular dev tools) to turn on and turn off the charging current? And if so, would it work without rooting the device?

    Unfortunately Android does not provide any standard way of controlling charge current. All you can do is read the battery status (charging/discharging), battery health, and charge percentage, and for these Android provides the BatteryManager class, and three broadcasts: ACTION_BATTERY_CHANGED, ACTION_BATTERY_LOW and ACTION_BATTERY_OKAY.

    Even if your phone is rooted, there is no standard way of doing this, because rooting itself is considered non-standard by the Android devs. There are Magisk modules that can control charge by modifying the firmware.

  2. Wrichik Basu says:
    May I suggest an Insights tutorial aimed at people who would like to get started with Android programming (assuming they have some prior programming on other platforms).

    Good idea, but it cannot be one tutorial; if I write all the basics of Android in one article, it would become as long as LHC’s technical design report.

    One thing that kills the enthusiasm to some extent is that there is already a huge number of tutorials available on the net that will help you to get started with Android.

    The biggest problem when one starts programming in Android is to set up Android Studio. If you have previously worked with Intellij IDEA and Gradle, then you will face lesser problems, but for someone going from NetBeans environment, it would be a big jump.

    Anyways, thanks for the suggestion; I will surely look into it.

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply