26

With Android O we get the "Notification Channels".

As far as I understand that means that the user can't set the notification tone or other related Notification settings inside the APP anymore .

The user needs to go to the "Notification Channels Setting" and change the tone or vibration etc. here because all methods from the NotificationBuilder like setSound are getting ignored.

So there is really NO way to change the tone to silent via code?
Or to change the vibration pattern via code?

For example the user have the ability to set the vibration pattern in my app.
Or he can pick tones from the alarm type instead of the notification type.

All this is not possible anymore?
Is this right or is there any way to do this?

chrisonline
  • 6,949
  • 11
  • 42
  • 62
  • I got same problem when I want to update notification channel sound.
    And then I found one solution for update notification channel sound only Thanks to God!!

    I have create Github demo for update different sound in a single notification channel. please check this link => [https://github.com/TejasTrivedi1996/NotificationChannels](https://github.com/TejasTrivedi1996/NotificationChannels)
    – Tejas Trivedi Apr 20 '19 at 21:12
  • See also https://stackoverflow.com/questions/49605594/how-to-change-notification-sound-dynamically-in-android-o – caw Jun 28 '19 at 02:33

5 Answers5

23

You can still offer sound and vibration customization in your app, but it requires a different approach. In short, the idea is to play sound and vibration manually in Android O instead of using the notification channel (it's easier than it seems).

This is how I did it:

NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);

// builder.setSmallIcon(...)
// builder.setContentTitle(...)
// builder.setContentText(...)

if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

    // play vibration
    vibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
    vibrator.vibrate(VibrationEffect.createWaveform(vibrationPattern, -1));

    // play sound
    Intent serviceIntent = new Intent(context, SoundService.class);
    serviceIntent.setAction("ACTION_START_PLAYBACK");
    serviceIntent.putExtra("SOUND_URI", soundUri.toString());
    context.startForegroundService(serviceIntent);

    // the delete intent will stop the sound when the notification is cleared
    Intent deleteIntent = new Intent(context, SoundService.class);
    deleteIntent.setAction("ACTION_STOP_PLAYBACK");
    PendingIntent pendingDeleteIntent =
            PendingIntent.getService(context, 0, deleteIntent, 0);
    builder.setDeleteIntent(pendingDeleteIntent);

} else {

    builder.setVibrate(vibrationPattern);
    builder.setSound(soundUri);

}

notificationManager.notify(notificationId, builder.build());

SoundService.class is the place where I play the sound with MediaPlayer:

public class SoundService extends Service {

    MediaPlayer mMediaPlayer;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    public int onStartCommand(Intent intent, int flags, int startId) {

        // foreground notification
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationCompat.Builder builder =
                new NotificationCompat.Builder(this, otherChannelId);
            builder.setSmallIcon(...)
                    .setContentTitle(...)
                    .setContentText(...)
                    .setAutoCancel(true);
            startForeground(foregroundNotificationId, builder.build());
        }

        // check action
        String action = intent.getAction();
        switch (action) {
            case "ACTION_START_PLAYBACK":
                startSound(intent.getStringExtra("SOUND_URI"));
                break;
            case "ACTION_STOP_PLAYBACK":
                stopSound();
                break;
        }

        // service will not be recreated if abnormally terminated
        return START_NOT_STICKY;
    }

    private void startSound(String uriString) {

        // parse sound
        Uri soundUri;
        try {
            soundUri = Uri.parse(uriString);
        } catch (Exception e) {
            cleanup();
            return;
        }

        // play sound
        if (mMediaPlayer == null) {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                }
            });
            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mediaPlayer) {
                    cleanup();
                }
            });
        }
        try {
            mMediaPlayer.setDataSource(this, soundUri);
            mMediaPlayer.prepareAsync();
        } catch (Exception e) {
            cleanup();
        }

    }

    private void stopSound() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        cleanup();
    }

    private void cleanup() {
        stopSelf();
    }

}

Recommendations

  • Create your notification channel with IMPORTANCE_DEFAULT (for the user this is 'High'), a null sound (setSound(null,null)) and a null vibration (setVibrationPattern(null)) and explain in the channel description that this is the recommended setting in order to avoid conflicts with the app's own customization.
  • Turn the whole thing into your favor: instead of removing a feature, give users a new one. You can give them the chance to use your customization features or the notification channel features (for example, you can check the current channel importance and depending on the level you can use one thing or the other).

Foreground notification

Starting Android O, services started from the background must be started as foreground services. This means SoundService needs a foreground notification. You have some options for this:

  • Create a nice foreground notification with a button that says 'Stop playback' so that the user can stop the sound without removing the notification that started it.

  • Create a simple notification and send it to a disabled channel (you can create disabled channels if you create them with IMPORTANCE_NONE). Doing this, the default system 'App is running in the background' notification will appear instead of your foreground notification, but users can hide this notification from the status bar if they want.

EDIT: in Android 8.1 it seems that creating a disabled channel with IMPORTANCE_NONE is not useful, as the channel will be enabled automatically when you send a notification. It may be better to create it with IMPORTANCE_LOW from the beginning and let users change the importance if they want.

jmart
  • 2,769
  • 21
  • 36
  • is this still working? I tried and I got this errors 1- Use of stream types is deprecated for operations other than volume control 2- See the documentation of setSound() for what to use instead with android.media.AudioAttributes to qualify your playback use case – Eduardo Bonfa Dec 01 '17 at 12:41
  • @EduardoBonfa Yes, it's how I do it in my app. I'd say the errors you see don't have to do with this code, as I don't change the stream for example. – jmart Dec 01 '17 at 12:45
  • So I don't know if I'm doing it right, could you take a look at this test? https://github.com/eduardobonfa/NotificationTest – Eduardo Bonfa Dec 01 '17 at 13:06
  • @EduardoBonfa Take a look at this answer: https://stackoverflow.com/a/45395800/4195337 It seems those warnings come from the library itself. If they don't provoke a crash, I wouldn't worry. – jmart Dec 01 '17 at 13:53
  • @EduardoBonfa now you have to use setUsage() method from AudioAttributes.Builder, as specified in the API. USAGE_NOTIFICATION worked fine for me: https://developer.android.com/reference/android/media/AudioAttributes.Builder.html#setUsage(int) – Samuel Moreno López Dec 28 '17 at 09:59
  • Will this approach play the sounds even if the app is in the background/ not open at all? I work on a messaging app that relies on custom notification sounds for hospitals so it MUST always play when the notification is received. Thanks! – syntakks Dec 11 '18 at 15:30
  • 1
    @syntakks Yes, the sounds will be played. – jmart Dec 11 '18 at 16:15
  • 3
    This is a clever approach, but it can be problematic in instances where user has "Do Not Disturb" enabled, the above posted code will ignore this use case. I think a more user friendly approach would be to delete then re-create the channel based on in-app settings, if any settings were modified outside the app via system notification settings for that app, then those need to be read prior to delete, merged w/ changed in-app settings then re-created in the new the channel. – AlexVPerl Jan 12 '19 at 23:39
  • @AlexVPerl You're right, what I did in my case was checking the 'Do not disturb' mode (and other things like ringer mode, etc) before playing sound or vibration. – jmart Jan 13 '19 at 09:33
6

Might help in your case. If you have one notification displayed you can disable sound when updating this notification by setting .setOnlyAlertOnce(true). This solution works only when updating notification.

Jacek Marchwicki
  • 1,565
  • 15
  • 17
3

That is correct, once a channel is created you cannot make changes to it anymore.

You can't programmatically modify the behavior of a notification channel once it's created and submitted to the notification manager

https://developer.android.com/preview/features/notification-channels.html

What you would have to do delete the channel and create a new one with a different id

tyczj
  • 71,600
  • 54
  • 194
  • 296
  • 4
    OK that is really a step backwards. Because I have to remove some features from my app because this is by way not so flexible as it was before :-( – chrisonline Jul 13 '17 at 14:26
  • This is how whatsapp does it. You can tell because the 'deleted channels' increases – behelit Nov 19 '18 at 03:12
1

For me the right way for Android >= 8.0 is to hide your sound/vibration options and redirect the user to your app notification settings using this:

  Intent intent = new Intent();
                intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
                intent.putExtra("android.provider.extra.APP_PACKAGE", context.getPackageName());
                context.startActivity(intent);
Zoe
  • 27,060
  • 21
  • 118
  • 148
from56
  • 3,976
  • 2
  • 13
  • 23
1

It is not a good idea to play the sound manually. The best approach would be to have two (or more) channels- one for each sound/vibration you would like to play.

In code you can decide which channel to use depending on which sound you would like to play.

Here is my code where I play either the default notification sound or a custom sound depending on the client's settings. The code also takes care of devices running Android prior to API 26:

String sound = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("NotificationsSound", getString(R.string.settingsNotificationSiren));
Uri soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"+ getApplicationContext().getPackageName() + "/" + R.raw.siren);
NotificationManager mNotificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel mChannel;
String channel_id = Utils.CHANNEL_DEFAULT_ID;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    if (sound.toLowerCase().equals(getString(R.string.settingsNotificationSiren).toLowerCase())) {
        channel_id = Utils.CHANNEL_SIREN_ID;
        mChannel = new NotificationChannel(Utils.CHANNEL_SIREN_ID, Utils.CHANNEL_SIREN_NAME, NotificationManager.IMPORTANCE_HIGH);
        mChannel.setLightColor(Color.GRAY);
        mChannel.enableLights(true);
        mChannel.setDescription(Utils.CHANNEL_SIREN_DESCRIPTION);
        AudioAttributes audioAttributes = new AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                .build();
        mChannel.setSound(soundUri, audioAttributes);
    } else {
        mChannel = new NotificationChannel(Utils.CHANNEL_DEFAULT_ID, Utils.CHANNEL_DEFAULT_NAME, NotificationManager.IMPORTANCE_HIGH);
        mChannel.setLightColor(Color.GRAY);
        mChannel.enableLights(true);
        mChannel.setDescription(Utils.CHANNEL_DEFAULT_DESCRIPTION);
    }
    if (mNotificationManager != null) {
        mNotificationManager.createNotificationChannel( mChannel );
    }
}

NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, channel_id)
        .setSmallIcon(R.drawable.ic_stat_maps_local_library)
        .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), R.mipmap.ic_launcher))
        .setTicker(title)
        .setContentTitle(contentTitle)
        .setContentText(contentText)
        .setAutoCancel(true)
        .setLights(0xff0000ff, 300, 1000) // blue color
        .setWhen(System.currentTimeMillis())
        .setPriority(NotificationCompat.PRIORITY_DEFAULT);

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    if (sound.toLowerCase().equals(getString(R.string.settingsNotificationSiren).toLowerCase())) {
        mBuilder.setSound(soundUri);
    } else {
        mBuilder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION));
    }
}

int NOTIFICATION_ID = 1; // Causes to update the same notification over and over again.
if (mNotificationManager != null) {
    mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
}
Dimitar Darazhanski
  • 2,188
  • 20
  • 22