来电铃声播放流程总结

  近期发现不少关于来电铃声出现无声问题,分析这个问题,需要先了解来电的流程,本篇先对该流程做个大概的总结。

一、播放流程准备工作

  来电的时候,通过telecom那边的Ringer类启动播放:

packages/services/Telecomm/src/com/android/server/telecom/Ringer.java

mRingtonePlayer.play(mRingtoneFactory, foregroundCall);

  而mRingtonePlayer通过AsyncRingtonePlayer构造的:

packages/services/Telecomm/src/com/android/server/telecom/AsyncRingtonePlayer.java

    /** Plays the ringtone. */
    public void play(RingtoneFactory factory, Call incomingCall) {
        Log.d(this, "Posting play.");
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = factory;
        args.arg2 = incomingCall;
        postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
    }

  这里通过消息传递在子线程中调用了handlePlaye方法:

            @Override
            public void handleMessage(Message msg) {
                switch(msg.what) {
                    case EVENT_PLAY:
                        handlePlay((SomeArgs) msg.obj);
                        break;
                    case EVENT_REPEAT:
                        handleRepeat();
                        break;
                    case EVENT_STOP:
                        handleStop();
                        break;
                }
            }

  handlePlay方法中通过调用factory.getRingtone初始化了铃声播放的设置:

        if (mRingtone == null) {
            mRingtone = factory.getRingtone(incomingCall);
            if (mRingtone == null) {
                Uri ringtoneUri = incomingCall.getRingtone();
                String ringtoneUriString = (ringtoneUri == null) ? "null" :
                        ringtoneUri.toSafeString();
                Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " +
                        "factory. Skipping ringing. Uri was: " + ringtoneUriString);
                return;
            }
        }

  在factory.getRingtone中,初始化了默认的uri以及stream类型:

packages/services/Telecomm/src/com/android/server/telecom/RingtoneFactory.java

    public Ringtone getRingtone(Call incomingCall) {
        // Use the default ringtone of the work profile if the contact is a work profile contact.
        Context userContext = isWorkContact(incomingCall) ?
                getWorkProfileContextForUser(mCallsManager.getCurrentUserHandle()) :
                getContextForUserHandle(mCallsManager.getCurrentUserHandle());
        Uri ringtoneUri = incomingCall.getRingtone();
        Ringtone ringtone = null;

        if(ringtoneUri != null && userContext != null) {
            // Ringtone URI is explicitly specified. First, try to create a Ringtone with that.
            ringtone = RingtoneManager.getRingtone(userContext, ringtoneUri);
        }
        if(ringtone == null) {
            // Contact didn't specify ringtone or custom Ringtone creation failed. Get default
            // ringtone for user or profile.
            Context contextToUse = hasDefaultRingtoneForUser(userContext) ? userContext : mContext;
            Uri defaultRingtoneUri;
            if (UserManager.get(contextToUse).isUserUnlocked(contextToUse.getUserId())) {
                defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(contextToUse,
                        RingtoneManager.TYPE_RINGTONE);
            } else {
                defaultRingtoneUri = Settings.System.DEFAULT_RINGTONE_URI;
            }
            if (defaultRingtoneUri == null) {
                return null;
            }
            ringtone = RingtoneManager.getRingtone(contextToUse, defaultRingtoneUri);
        }
        if (ringtone != null) {
            ringtone.setStreamType(AudioManager.STREAM_RING);
        }
        return ringtone;
    }

  这里通过获取ringtone,传入了默认的uri,进而在RingtoneManager的getRingtone方法中进行了设置:

frameworks/base/media/java/android/media/RingtoneManager.java

    private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
        try {
            final Ringtone r = new Ringtone(context, true); //set allowRemote true
            if (streamType >= 0) {
                //FIXME deprecated call
                r.setStreamType(streamType);
            }
            r.setUri(ringtoneUri);
            return r;
        } catch (Exception ex) {
            Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
        }

        return null;
    }

  ringtone的setUri创建了了mediaplayer,并设置了相应的参数:

frameworks/base/media/java/android/media/Ringtone.java

        try {
            mLocalPlayer.setDataSource(mContext, mUri);
            mLocalPlayer.setAudioAttributes(mAudioAttributes);
            synchronized (mPlaybackSettingsLock) {
                applyPlaybackProperties_sync();
            }
            mLocalPlayer.prepare();

        } catch (SecurityException | IOException e) {
            destroyLocalPlayer();
            if (!mAllowRemote) {
                Log.w(TAG, "Remote playback not allowed: " + e);
            }
        }

  这里需要注意,执行setDataSource的时候,传入的uri在外置存储的路径下,会引发SecurityException,不能直接通过system_server进程播放,需要通过远程调用其他进程进行播放,这点后面再说明下。

这里远程调用和本地调用的播放器设置在applyPlayerProperties_sync()中:

        if (mLocalPlayer != null) {
            mLocalPlayer.setVolume(mVolume);
            mLocalPlayer.setLooping(mIsLooping);
        } else if (mAllowRemote && (mRemotePlayer != null)) {
            try {
                mRemotePlayer.setPlaybackProperties(mRemoteToken, mVolume, mIsLooping);
            } catch (RemoteException e) {
                Log.w(TAG, "Problem setting playback properties: ", e);
            }
        } 

以上基本完成完成了播放的准备工作。

 

二、播放流程

  在AsyncRingtonePlayer的handlePlay方法最后又调用了handleRepeat流程,而该方法中通过调用ringtone的play方法进行播放:

    public void play() {
        if (mLocalPlayer != null) {
            // do not play ringtones if stream volume is 0
            // (typically because ringer mode is silent).
            if (mAudioManager.getStreamVolume(
                    AudioAttributes.toLegacyStreamType(mAudioAttributes)) != 0) {
                startLocalPlayer();
            }
        } else if (mAllowRemote && (mRemotePlayer != null)) {
            final Uri canonicalUri = mUri.getCanonicalUri();
            final boolean looping;
            final float volume;
            synchronized (mPlaybackSettingsLock) {
                looping = mIsLooping;
                volume = mVolume;
            }
            try {
                mRemotePlayer.play(mRemoteToken, canonicalUri, mAudioAttributes, volume, looping);
            } catch (RemoteException e) {
                if (!playFallbackRingtone()) {
                    Log.w(TAG, "Problem playing ringtone: " + e);
                }
            }
        } else {
            if (!playFallbackRingtone()) {
                Log.w(TAG, "Neither local nor remote playback available");
            }
        }
    }

  这个涉及到本地和远程播放流程,其判断依据为mAllowRemote的逻辑,而这个判断与ringtone对象的初始化有关,前面通过RingtoneManager的getRingtone的时候已经设置为true,因而创建了mRemotePlayer对象:

    public Ringtone(Context context, boolean allowRemote) {
        mContext = context;
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        mAllowRemote = allowRemote;
        mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
        mRemoteToken = allowRemote ? new Binder() : null;
    }

  下面来分析本地、远程、以及异常播放的流程:

1)若传入的uri为系统内置的音频资源,这个时候在setUri的时候就会成功创建mLocalPlayer,这个时候走的是系统进程播放的流程,会调用本地播放方法:

    private void startLocalPlayer() {
        if (mLocalPlayer == null) {
            return;
        }
        synchronized (sActiveRingtones) {
            sActiveRingtones.add(this);
        }
        mLocalPlayer.setOnCompletionListener(mCompletionListener);
        mLocalPlayer.start();
    }

  接下来就是Mediaplayer的start方法了,这里的具体逻辑涉及到native的mediaplayerserver的实现,目前我们暂时不关注其实现;

2)若传入的uri为外置存储的音频资源,这个时候在setUri的时候因为抛了SecurityException会执行destroyLocalPlayer,这个时候就会进入远程播放的流程:

            try {
                mRemotePlayer.play(mRemoteToken, canonicalUri, mAudioAttributes, volume, looping);
            } catch (RemoteException e) {
                if (!playFallbackRingtone()) {
                    Log.w(TAG, "Problem playing ringtone: " + e);
                }
            }

   前面我们已经分析了mRemotePlayer的回调方法是通过systemui的RingtonePlayer播放的,这里RingtonePlayer 启动的时候,会注册audioservice的回调:

frameworks/base/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java

    @Override
    public void start() {
        mAsyncPlayer.setUsesWakeLock(mContext);

        mAudioService = IAudioService.Stub.asInterface(
                ServiceManager.getService(Context.AUDIO_SERVICE));
        try {
            mAudioService.setRingtonePlayer(mCallback);
        } catch (RemoteException e) {
            Log.e(TAG, "Problem registering RingtonePlayer: " + e);
        }
    }

  play的方法在实现回调接口中:

    private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() {
        @Override
        public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping)
                throws RemoteException {
            if (LOGD) {
                Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
                        + Binder.getCallingUid() + ")");
            }
            Client client;
            synchronized (mClients) {
                client = mClients.get(token);
                if (client == null) {
                    final UserHandle user = Binder.getCallingUserHandle();
                    client = new Client(token, uri, user, aa);
                    token.linkToDeath(client, 0);
                    mClients.put(token, client);
                }
            }
            client.mRingtone.setLooping(looping);
            client.mRingtone.setVolume(volume);
            client.mRingtone.play();
        }
......

  这里的mRingtone又通过client的实现来得到的:

        public Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa) {
            mToken = token;

            mRingtone = new Ringtone(getContextForUser(user), false);
            mRingtone.setAudioAttributes(aa);
            mRingtone.setUri(uri);
        }

  这里可以看到client内部构造Ringtone时,关闭了远程调用,通过传入自己的uri成功调用了本地播放器,所以这里的client.mRingtone.play()最终通过startLocalPlayer启动播放器。

这里需要注意token的状态,每次播放的时候创建的token会到通过mClients的HashMap保存,以便在binderDied和回调stop的时候释放对应的资源,这里的token是在构造ringtone的时候创建的:

mRemoteToken = allowRemote ? new Binder() : null;

 

 3)若本地和远程出现问题,此时会进入下面流程:

  在Ringtone的play方法中,若远程和本地均播放失败时,均会执行playFallbackRingtone方法:

                    AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
                            com.android.internal.R.raw.fallbackring);
                    if (afd != null) {
                        mLocalPlayer = new MediaPlayer();
                        if (afd.getDeclaredLength() < 0) {
                            mLocalPlayer.setDataSource(afd.getFileDescriptor());
                        } else {
                            mLocalPlayer.setDataSource(afd.getFileDescriptor(),
                                    afd.getStartOffset(),
                                    afd.getDeclaredLength());
                        }

  这里会通过本地调用播放系统预装的fallbackring.ogg音频资源,其路径如下:

frameworks/base/core/res/res/raw/fallbackring.ogg

至此播放流程已大致走完。

 

三、总结

通过上面大概的流程可以总结如下:

1、播放铃声的时候,根据传入的uri会有不同的策略播放,内置资源通过系统进程播放,外置资源的通过systemui传入新的uri播放,本地和远程均异常时系统会尝试播放系统默认资源;

2、播放外置或内置铃声的判断在于设置数据源的安全异常,若关闭selinux的权限,这两种方式都可以通过系统进程播放;

3、通过RingtonePlayer远程播放铃声时,需要注意传入的token对应的资源是否在播放完成后进行了释放。

 


 

posted @ 2018-09-20 17:47  elenin  阅读(1847)  评论(0编辑  收藏  举报