來電鈴聲播放流程總結


  近期發現不少關於來電鈴聲出現無聲問題,分析這個問題,需要先了解來電的流程,本篇先對該流程做個大概的總結。

一、播放流程准備工作

  來電的時候,通過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對應的資源是否在播放完成后進行了釋放。

 

 
        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM