前言
我们在做Android开发的时候,免不了会使用到Notification,而且在android设备的设置中还可以设置通知音的优先级,以及播放的声音种类。那么通知音是如何播放的呢,今天我们就来谈谈这个。
Notification的使用
NotificationManager notificationManager=(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
//重点:先创建通知渠道
if(android.os.Build.VERSION.SDK_INT>=android.os.Build.VERSION_CODES.O){
NotificationChannel mChannel=new NotificationChannel(getString(R.string.app_name),getString(R.string.app_name),NotificationManager.IMPORTANCE_MAX);
NotificationChannel channel=new NotificationChannel(channelId,
channelName,NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(true); //设置开启指示灯,如果设备有的话
channel.setLightColor(Color.RED); //设置指示灯颜色
channel.setShowBadge(true); //设置是否显示角标
channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);//设置是否应在锁定屏幕上显示此频道的通知
channel.setDescription(channelDescription);//设置渠道描述
channel.setVibrationPattern(new long[]{100,200,300,400,500,600});//设置震动频率
channel.setBypassDnd(true);//设置是否绕过免打扰模式
notificationManager.createNotificationChannel(mChannel);
}
//再创建通知
NotificationCompat.Builder builder=new NotificationCompat.Builder(this,getString(R.string.app_name));
//设置通知栏大图标,上图中右边的大图
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
// 设置状态栏和通知栏小图标
.setSmallIcon(R.drawable.ic_launcher_background)
// 设置通知栏应用名称
.setTicker("通知栏应用名称")
// 设置通知栏显示时间
.setWhen(System.currentTimeMillis())
// 设置通知栏标题
.setContentTitle("通知栏标题")
// 设置通知栏内容
.setContentText("通知栏内")
// 设置通知栏点击后是否清除,设置为true,当点击此通知栏后,它会自动消失
.setAutoCancel(false)
// 将Ongoing设为true 那么左滑右滑将不能删除通知栏
.setOngoing(true)
// 设置通知栏点击意图
.setContentIntent(pendingIntent)
// 铃声、闪光、震动均系统默认
.setDefaults(Notification.DEFAULT_ALL)
//设置通知时间
.setWhen(System.currentTimeMillis())
// 设置为public后,通知栏将在锁屏界面显示
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
//发送通知
notificationManager.notify(10, builder.build());
ANdroid O主要增加了NotificationChannel,详细用法可参照其API。
正文
那么就从发送通知的notify开始入手
public void notify(String tag, int id, Notification notification)
{
//当我们调用Notification的notify()发送通知时,会继续调到notifyAsUser
notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
}
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
//得到NotificationManagerService
INotificationManager service = getService();
//…………
ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
boolean isLowRam = am.isLowRamDevice();
final Notification copy = Builder.maybeCloneStrippedForDelivery(notification, isLowRam);
try {
//把Nofitication copy到了NofificationManagerservie中
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
copy, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
而enqueueNotificationWithTag()又调用了 enqueueNotificationInternal()
在enqueueNotificationInternal()中需要注意下面这行代码:
final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
我们来看下NotificationRecord的初始化,在NotificationRecoder的构造方法中
mAttributes = calculateAttributes();
mAttributes 是不是很熟悉,就是audio播放时需要传入的那个AudioAttributes,在
calculateAttributes()中会取我们在创建channel时传入的AudioAttributes,如果没有则使用默认的,如果ANdroid O之前的版本,没有channel,则会使用Notification中默认的AudioAttributes,(NotificationChannel中的AudioAttributes是通过setSounde()方法设置下来的)。默认的AudioAttributes
是什么呢?就是
//通知中默认的AudioAttributes
public static final AudioAttributes AUDIO_ATTRIBUTES_DEFAULT = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build();
在enqueueNotificationInternal()中有将notificationRecord放到了EnqueueNotificationRunnable中线程运行代码如下:
mHandler.post(new EnqueueNotificationRunnable(userId, r));
而在EnqueueNotificationRunnable线程中又调用的PostNotificationRunnable线程中执行,代码如下
mHandler.post(new PostNotificationRunnable(r.getKey()));
在PostNotificationRunnable中 通过buzzBeepBlinkLocked(r)方法播放
if (hasValidSound) {
mSoundNotificationKey = key;
//如果电话中则playInCallNotification()
if (mInCall) {
playInCallNotification();
beep = true;
} else {
//否则调用playSound(),
beep = playSound(record, soundUri);
}
}
无论哪个方法方法都是一样的,只是 AudioAttributes不同,playInCallNotification()使用的mInCallNotificationAudioAttributes即
mInCallNotificationAudioAttributes = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build();
而playSound()使用的AudioAttributes如果未通过channel传入,则使用上面提到的默认的,那么看看到底是如何播放的呢,通知音的播放是通过
final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
来播放的,代码略过,简单说下RingTonePlayer的play()和playerAsync()这俩方法,还是有点区别的,play使用的Ringtone来播放的,源码:
client.mRingtone.setLooping(looping);
client.mRingtone.setVolume(volume);
client.mRingtone.play();
而playerAsync()是通过NotificationPlayer来播放的,对于NotificationPlayer的play()会通过enqueueLocked()创建CmdThread线程。在CmdThread线程中startSound(),在startSound()中创建CreationAndCompletionThread线程
mCompletionThread = new CreationAndCompletionThread(cmd);
synchronized (mCompletionThread) {
mCompletionThread.start();
mCompletionThread.wait();
}
在CreationAndCompletionThread线程中通过mediaplayer播放
private final class CreationAndCompletionThread extends Thread {
public Command mCmd;
public CreationAndCompletionThread(Command cmd) {
super();
mCmd = cmd;
}
public void run() {
Looper.prepare();
// ok to modify mLooper as here we are
// synchronized on mCompletionHandlingLock due to the Object.wait() in startSound(cmd)
mLooper = Looper.myLooper();
if (DEBUG) Log.d(mTag, "in run: new looper " + mLooper);
synchronized(this) {
AudioManager audioManager =
(AudioManager) mCmd.context.getSystemService(Context.AUDIO_SERVICE);
try {
//饶了一大圈竟然也用mediaplayer来播放
MediaPlayer player = new MediaPlayer();
//attributes 就是从NotificationChannel传下来的attributes
if (mCmd.attributes == null) {
mCmd.attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
}
player.setAudioAttributes(mCmd.attributes);
player.setDataSource(mCmd.context, mCmd.uri);
player.setLooping(mCmd.looping);
player.setOnCompletionListener(NotificationPlayer.this);
player.setOnErrorListener(NotificationPlayer.this);
player.prepare();
if ((mCmd.uri != null) && (mCmd.uri.getEncodedPath() != null)
&& (mCmd.uri.getEncodedPath().length() > 0)) {
if (!audioManager.isMusicActiveRemotely()) {
synchronized (mQueueAudioFocusLock) {
if (mAudioManagerWithAudioFocus == null) {
if (DEBUG) Log.d(mTag, "requesting AudioFocus");
int focusGain = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
if (mCmd.looping) {
focusGain = AudioManager.AUDIOFOCUS_GAIN;
}
mNotificationRampTimeMs = audioManager.getFocusRampTimeMs(
focusGain, mCmd.attributes);
//需要注意i,通知音是会申请焦点的。
audioManager.requestAudioFocus(null, mCmd.attributes,
focusGain, 0);
mAudioManagerWithAudioFocus = audioManager;
} else {
if (DEBUG) Log.d(mTag, "AudioFocus was previously requested");
}
}
}
}
// FIXME Having to start a new thread so we can receive completion callbacks
// is wrong, as we kill this thread whenever a new sound is to be played. This
// can lead to AudioFocus being released too early, before the second sound is
// done playing. This class should be modified to use a single thread, on which
// command are issued, and on which it receives the completion callbacks.
if (DEBUG) { Log.d(mTag, "notification will be delayed by "
+ mNotificationRampTimeMs + "ms"); }
try {
Thread.sleep(mNotificationRampTimeMs);
player.start();
} catch (InterruptedException e) {
Log.e(mTag, "Exception while sleeping to sync notification playback"
+ " with ducking", e);
}
if (DEBUG) { Log.d(mTag, "player.start"); }
if (mPlayer != null) {
if (DEBUG) { Log.d(mTag, "mPlayer.release"); }
mPlayer.release();
}
mPlayer = player;
}
catch (Exception e) {
Log.w(mTag, "error loading sound for " + mCmd.uri, e);
}
this.notify();
}
Looper.loop();
}
};
到此over,代码逻辑很复杂,涉及的类也比较多,有兴趣的可以去看看源码,我就不多说了。
总结
Notification从创建到播放的流程基本就这样,至于声音的区分是否电话中,如果incall则使用RingTone播放,反之mediaplayer播放。
而使用的attributes也区分是否incall。
以上。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。