With Android O we get the \"Notification Channels\".
As far as I understand that means that the user can\'t set
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
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.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.
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());
}
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
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.
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);