近期發現不少關於來電鈴聲出現無聲問題,分析這個問題,需要先了解來電的流程,本篇先對該流程做個大概的總結。
一、播放流程准備工作
來電的時候,通過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對應的資源是否在播放完成后進行了釋放。
