前言

我们在做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。

以上。


我是李睿
41 声望4 粉丝

移动端原生和混合开发