来电铃声播放流程总结
近期发现不少关于来电铃声出现无声问题,分析这个问题,需要先了解来电的流程,本篇先对该流程做个大概的总结。
一、播放流程准备工作
来电的时候,通过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对应的资源是否在播放完成后进行了释放。