最近接到一个需求,如题,给友盟推送添加定制化声音。

android 通知 自定义UI 安卓自定义提示音_自定义


描述很简洁,由上图可知,本次需求的重点工作是8.0以上版本的兼容问题。友盟给出的示例代码如下:

android 通知 自定义UI 安卓自定义提示音_android_02


由以上代码可知,其实就是设置一个自定义Notification,但是以上代码用在8.0以上系统中并不可行。原因大家应该都清楚了,8.0以上通知栏新增了一个NotificationChannel的特性,如果没有设置channel通知渠道的话,就会导致通知无法展示。

设置本地声音

现在再来回到我们本次需求的重点:定制声音。通过以上分析我们已经知道了友盟自定义声音其实就是自定义通知,那么定制声音也就是自定义通知的一部分,该怎么做呢?通过查找NotificationChannel的API可以找到setSound(Uri sound, AudioAttributes audioAttributes)方法。那么赶快上代码吧,我们先来设置一个本地声音吧:

//Android 8.0 以上需包添加渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
	NotificationManager manager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
	NotificationChannel notificationChannel = new NotificationChannel(newChannel, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
	Uri sound = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.ding);
	//只能在create一个渠道之前修改铃声,在创建之后不支持修改
	notificationChannel.setSound(sound, Notification.AUDIO_ATTRIBUTES_DEFAULT);

	manager.createNotificationChannel(notificationChannel);
}

注意:
这里要特别注意上述代码中的注释,只能在create一个渠道之前修改铃声,在创建之后不支持修改。
具体原因setSound方法的注释中已经给出了描述:

android 通知 自定义UI 安卓自定义提示音_自定义_03

声音可配置

看到这里有人可能有疑问了,那我如果要想更改声音,让定制声音可配置,这怎么办呢???

既然一个NotificationChannel只能绑定一个声音,那我们可不可以再新建一个Channel来绑定新的声音呢?答案是可以的。由于之前的声音不再使用,所以我们需要删掉(deleteNotificationChannel(String channelId))之前的NotificationChannel,这里有个坑,你什么时候去删除呢?第一次测试我是在修改铃声或者振动的时候创建一个新的渠道,把之前所有旧的渠道都删除,但是这样会有一个bug,之前渠道上还在状态栏显示的Notification都会删除掉,所有要做一个判断,如果当前渠道在状态栏没有notification显示则删除,否则继续保存,代码如下:

注:
这里补充下,deleteNotificationChannel方法并不是真的删除了该渠道,只是设置了一个删除标记。具体的分析过程可以看下源码PreferencesHelper.deleteNotificationChannel()。

private static void deleteNoNumberNotification(NotificationManager nm, String newChannelId) {
	List<NotificationChannel> notificationChannels = nm.getNotificationChannels();
	if (Utils.isEmpty(notificationChannels) || notificationChannels.size() < 5) {
		return;
	}
	
	for (NotificationChannel channel : notificationChannels) {
		if (channel.getId() == null || channel.getId().equals(newChannelId)) {
			continue;
		}

		int notificationNumbers = getNotificationNumbers(nm, channel.getId());
		Logger.i(TAG, "notificationNumbers: " + notificationNumbers + " channelId:" + channel.getId());
		if (notificationNumbers == 0) {
			Log.i(TAG, "deleteNoNumberNotification: " + channel.getId());
            nm.deleteNotificationChannel(channel.getId());
        }
    }
}

 /**
  * 获取某个渠道下状态栏上通知显示个数
  *
  * @param mNotificationManager NotificationManager
  * @param channelId            String
  * @return int
  */
@RequiresApi(api = Build.VERSION_CODES.O)
private static int getNotificationNumbers(NotificationManager mNotificationManager, String channelId) {
	if (mNotificationManager == null || TextUtils.isEmpty(channelId)) {
		return -1;
	}
	int numbers = 0;
	StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications();
	for (StatusBarNotification item : activeNotifications) {
		Notification notification = item.getNotification();
        if (notification != null) {
        	if (channelId.equals(notification.getChannelId())) {
            	numbers++;
            }
        }
    }
    return numbers;
}

以上就是定制本地声音的所有问题了,那么有人又有问题了,我如果想要配置一个网络音频,该如何设置呢???

设置网络音频

参考友盟SDK里面UmengMessageHandler的getSound方法:

android 通知 自定义UI 安卓自定义提示音_android_04

android 通知 自定义UI 安卓自定义提示音_Notification_05


如果是网络音频的话,友盟先将音频文件下载到本地缓存目录,在进行设置的。

这里我一开始有个偷懒的想法,既然友盟已经为我们考虑的如此周全了,那我可不可以直接用这个getSound方法呢?通过试验,很遗憾,直接用友盟的方法声音并没有播放出来,这里留个大大的问号给大家,如果有哪位伙伴知道原因可以评论分享出来一起探讨。

既然直接用友盟的消息不可靠那就只有直接动手了,按照友盟提供的思路,将文件下载到本地,再进行设置吧。这里我下载的目录是getExternalCacheDir(),奇怪竟然可以播放( •̀ ω •́ )y。

通知音频播放的一点点分析

这里还要插播一条:在找不到友盟下载的文件为什么不能播放的时候,我还有个偷懒的想法直接将http音频地址设置给Uri,测试发现在小米手机上Https的地址竟然可以播放,Http的地址不可以播放。这里稍微说下吧,其实这并不是一个解决方案,甚至本来就是一个错误的做法。为什么呢?

通知声音的播放是在NotificationPlayer类中实现,看代码:

android 通知 自定义UI 安卓自定义提示音_android 通知 自定义UI_06


最终声音的播放还是由MediaPlayer来执行的。之前我们给Notification设置的Sound就是MediaPlayer的DataSource,如果是网络音频,然后被Uri包裹,那么播放器就认为这是一个本地文件,在解析的时候就会报找不到文件错误,很神奇,这里明明报错了,有些手机上你还是可以听到声音的播放。所以并不建议大家直接将Http地址塞给Sound。

由于上面提到的源码部分只是某个点,需要了解更多的同学,请打开源码详细对照理解哈。

本文源码如下:

public static String UMENG_INTERNET_SOUND_DOWNPATH = MyApplication.getInstance().getExternalCacheDir() +  "/sound_cache";

//8.0版本以上自定义通知兼容
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
	mPushAgent.setMessageHandler(messageHandler);
}

//自定义消息进行处理
private static UmengMessageHandler messageHandler = new UmengMessageHandler() {

	@Override
    public void dealWithNotificationMessage(Context context, UMessage uMessage) {
		if (uMessage.hasResourceFromInternet() && !MessageSharedPrefs.getInstance(context).hasMessageResourceDownloaded(uMessage.msg_id)) {
        	downInternetSound(context, uMessage);
            return;
        }
        super.dealWithNotificationMessage(context, uMessage);
    }

    @Override
    public Uri getSound(Context context, UMessage uMessage) {
		return getCustomSound(context, uMessage);
    }

    @Override
    public Notification getNotification(Context context, UMessage uMessage) {

        if (uMessage.builder_id == 1) {
			long curTime = System.currentTimeMillis();
            String CHANNEL_ID = AppUtils.getAppName();//应用频道Id唯一值, 长度若太长可能会被截断,
            String newChannel = CHANNEL_ID + curTime;
            String CHANNEL_NAME = AppUtils.getAppName();//最长40个字符,太长会被截断

			Uri sound = getSound(context, uMessage);

            Intent hangIntent = new Intent(context, MainActivity.class);
            PendingIntent hangPendingIntent = PendingIntent.getActivity(context, 1001, hangIntent,  PendingIntent.FLAG_UPDATE_CURRENT);

            Notification notification = new NotificationCompat.Builder(context, newChannel)
                        .setContentTitle(uMessage.title)
                        .setContentText(uMessage.text)
                        .setSmallIcon(R.mipmap.app_logo)
                        .setContentIntent(hangPendingIntent)
                        //.setFullScreenIntent(hangPendingIntent,true)
                        .setSound(sound)
                        .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.app_logo))
                        .setAutoCancel(true)
                        .build();

            //Android 5.0 以上锁屏通知
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            	notification.visibility = Notification.VISIBILITY_PUBLIC;
            }

            //Android 8.0 以上需包添加渠道
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            	NotificationManager manager =  (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

                //只能在create一个渠道之前修改铃声,在创建之后不支持修改
                //只能去重新创建一个渠道设置铃声振动
                //对于之前创建的渠道,通过deleteNotificationChannel(String channelId)去删除
                List<NotificationChannel> channelList = manager.getNotificationChannels();
                if (channelList != null && channelList.size() > 0) {
                	for (NotificationChannel channel : channelList) {
                    	if (!TextUtils.isEmpty(channel.getId()) && channel.getId().startsWith(CHANNEL_ID)) {
                        	manager.deleteNotificationChannel(channel.getId());
                        }
                    }
                }

                NotificationChannel notificationChannel = new NotificationChannel(newChannel, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
                //只能在create一个渠道之前修改铃声,在创建之后不支持修改
                notificationChannel.setSound(sound, Notification.AUDIO_ATTRIBUTES_DEFAULT);

                manager.createNotificationChannel(notificationChannel);
		}

        Log.d("umeng", "notificationDefault: " + getNotificationDefaults(context, uMessage));

        return notification;
            }
        //默认为0,若填写的builder_id并不存在,也使用默认。
        return super.getNotification(context, uMessage);
    }
};

    /**
     * 自定义通知声音
     *
     * @param context
     * @param uMessage
     * @return
     */
    private static Uri getCustomSound(Context context, UMessage uMessage) {
        String soundPath = uMessage.sound;

        try {
            if (soundPath == null) {
                int assetsSound = com.umeng.message.common.d.a(context).j("umeng_push_notification_default_sound");

                if (assetsSound > 0) {
                    soundPath = "android.resource://" + context.getPackageName() + "/" + assetsSound;
                }
            } else {
                if (uMessage.isSoundFromInternet()) {
                    soundPath = UMENG_INTERNET_SOUND_DOWNPATH + "/" + uMessage.sound.hashCode();
                } else {
                    int assetsSound = com.umeng.message.common.d.a(context).j(uMessage.sound);

                    if (assetsSound > 0) {
                        soundPath = "android.resource://" + context.getPackageName() + "/" + assetsSound;
                    }
                }
            }

            if (soundPath != null) {
                Uri soundUri = Uri.parse(soundPath);
                return soundUri;
            }
        } catch (Throwable throwable) {
            throwable.toString();
        }
        return null;
    }

    private static void downInternetSound(Context context, UMessage uMessage) {
        String downPath = UMENG_INTERNET_SOUND_DOWNPATH;
        String downFileName = uMessage.sound.hashCode() + "";
        OkGoRequest.downLoad(context, uMessage.sound, new FileCallback(downPath, downFileName) {
            @Override
            public void onSuccess(Response<File> response) {
                MessageSharedPrefs.getInstance(context).setMessageResourceDownloaded(uMessage.msg_id);
                messageHandler.dealWithNotificationMessage(context, uMessage);
            }

            @Override
            public void onError(Response<File> response) {
                super.onError(response);
                MessageSharedPrefs.getInstance(context).setMessageResourceDownloaded(uMessage.msg_id);
                messageHandler.dealWithNotificationMessage(context, uMessage);
            }
        });
    }

代码仅提供思路,具体实现自己写完整代码哈。

补充:

最近又遇到说Android11上面有手机自定义声音不发声的,打开系统配置里面铃声配置,发现通知铃声为无,就挺诡异的。有同事提出仿QQ,微信的自定义铃声方案,自己去用MediaPlayer来播放音乐,这也是一种解决方案吧,遇到此问题的同学可以一试。