蓝蓝的天 2019-06-28
我们在做Android开发的时候,免不了会使用到Notification,而且在android设备的设置中还可以设置通知音的优先级,以及播放的声音种类。那么通知音是如何播放的呢,今天我们就来谈谈这个。
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。
以上。