編輯:關於Android編程
距離上篇博客《Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果》 已經快有4個月了,期間有空寫一點,直到今天才完整地寫完。
目前Android的實現是:有來電時,音樂聲音直接停止,鈴聲直接直接使用設置的鈴聲音量進行鈴聲播放。
Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果。
如果要實現這個效果,首先要搞清楚兩大問題;
1、來電時的代碼主要實現流程。
2、主流音樂播放器在播放過程中,如果有來電,到底在收到了什麼事件後將音樂暫停了?
第一大問題,參見:《Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果》。
現在我們分析第二個問題:主流音樂播放器在播放過程中,如果有來電,到底在收到了什麼事件後將音樂暫停了?
先來看看,Ringer.java 中播放鈴聲具體干了啥。第一次mRingtone對象是null,所以會通過r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);對其進行初始化。
private void makeLooper() {
if (mRingThread == null) {
mRingThread = new Worker(ringer);
mRingHandler = new Handler(mRingThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
Ringtone r = null;
switch (msg.what) {
case PLAY_RING_ONCE:
if (DBG) log(mRingHandler: PLAY_RING_ONCE...);
if (mRingtone == null && !hasMessages(STOP_RING)) {
// create the ringtone with the uri
if (DBG) log(creating ringtone: + mCustomRingtoneUri);
r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);
synchronized (Ringer.this) {
if (!hasMessages(STOP_RING)) {
mRingtone = r;
}
}
}
r = mRingtone;
if (r != null && !hasMessages(STOP_RING) && !r.isPlaying()) {
PhoneUtils.setAudioMode();
r.play();
synchronized (Ringer.this) {
if (mFirstRingStartTime < 0) {
mFirstRingStartTime = SystemClock.elapsedRealtime();
}
}
}
break;
下面的邏輯,調用了new Ringtone()來構造一個新的Ringtone對象,其中第二個參數為true,表示調用通過調用遠程對象來播放鈴聲。
private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
...
try {
Ringtone r = new Ringtone(context, true);
if (streamType >= 0) {
r.setStreamType(streamType);
}
...
r.setUri(ringtoneUri);
return r;
} catch (Exception ex) {
Log.e(TAG, Failed to open ringtone + ringtoneUri + : + ex);
}
return null;
}
既然這裡allowRemote為true,則 mRemotePlayer = mAudioManager.getRingtonePlayer(),再來看看mAudioManager中的getRingtonePlayer()干了什麼事。
public Ringtone(Context context, boolean allowRemote) {
mContext = context;
= (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mAllowRemote = allowRemote;
mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
mRemoteToken = allowRemote ? new Binder() : null;
}
再進一步跟蹤,可以發現,mAudioManager其實是到了這個類的實例的引用。AudioService中getRingtonePlayer()的實現如下:
@Override
public void setRingtonePlayer(IRingtonePlayer player) {
mContext.enforceCallingOrSelfPermission(REMOTE_AUDIO_PLAYBACK, null);
mRingtonePlayer = player;
}
@Override
public IRingtonePlayer getRingtonePlayer() {
return mRingtonePlayer;
}
又過了一下代碼,mRingtonePlayer也是外面設置進來的,那我們分析下到底是哪裡設置進來的。
搜索一下setRingtonePlayer這個關鍵字,發現只有在
frameworksasepackagesSystemUIsrccomandroidsystemuimediaRingtonePlayer.java 中有調用,
public class RingtonePlayer extends SystemUI {
private static final String TAG = RingtonePlayer;
private static final boolean LOGD = false;
// TODO: support Uri switching under same IBinder
private IAudioService mAudioService;
private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG);
private final HashMap mClients = Maps.newHashMap();
@Override
public void start() {
mAsyncPlayer.setUsesWakeLock(mContext);
mAudioService = IAudioService.Stub.asInterface(
ServiceManager.getService(Context.AUDIO_SERVICE));
try {
mAudioService.setRingtonePlayer(mCallback);
} catch (RemoteException e) {
Slog.e(TAG, Problem registering RingtonePlayer: + e);
}
}
/**
* Represents an active remote {@link Ringtone} client.
*/
private class Client implements IBinder.DeathRecipient {
private final IBinder mToken;
private final Ringtone mRingtone;
public Client(IBinder token, Uri uri, UserHandle user, int streamType) {
mToken = token;
mRingtone = new Ringtone(getContextForUser(user), false);
mRingtone.setStreamType(streamType);
mRingtone.setUri(uri);
}
@Override
public void binderDied() {
if (LOGD) Slog.d(TAG, binderDied() token= + mToken);
synchronized (mClients) {
mClients.remove(mToken);
}
mRingtone.stop();
}
}
private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() {
@Override
public void play(IBinder token, Uri uri, int streamType) throws RemoteException {
if (LOGD) {
Slog.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, streamType);
token.linkToDeath(client, 0);
mClients.put(token, client);
}
}
client.mRingtone.play();
}
@Override
public void stop(IBinder token) {
if (LOGD) Slog.d(TAG, stop(token= + token + ));
Client client;
synchronized (mClients) {
client = mClients.remove(token);
}
if (client != null) {
client.mToken.unlinkToDeath(client, 0);
client.mRingtone.stop();
}
}
@Override
public boolean isPlaying(IBinder token) {
if (LOGD) Slog.d(TAG, isPlaying(token= + token + ));
Client client;
synchronized (mClients) {
client = mClients.get(token);
}
if (client != null) {
return client.mRingtone.isPlaying();
} else {
return false;
}
}
@Override
public void playAsync(Uri uri, UserHandle user, boolean looping, int streamType) {
if (LOGD) Slog.d(TAG, playAsync(uri= + uri + , user= + user + ));
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
throw new SecurityException(Async playback only available from system UID.);
}
mAsyncPlayer.play(getContextForUser(user), uri, looping, streamType);
}
@Override
public void stopAsync() {
if (LOGD) Slog.d(TAG, stopAsync());
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
throw new SecurityException(Async playback only available from system UID.);
}
mAsyncPlayer.stop();
}
};
private Context getContextForUser(UserHandle user) {
try {
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println(Clients:);
synchronized (mClients) {
for (Client client : mClients.values()) {
pw.print( mToken=);
pw.print(client.mToken);
pw.print( mUri=);
pw.println(client.mRingtone.getUri());
}
}
}
}
最終使用play()方法中,有構造了一個Client對象,這個Client類的實現在上面的代碼也有體現。重點看一下Client的構造函數,
mRingtone = new Ringtone(getContextForUser(user),false);
注意,第二個參數為false,即最終播放鈴聲使用的Ringtone對象,即是SystemUI在初始化時構造出來對象。
public Client(IBinder token, Uri uri, UserHandle user, int streamType) {
mToken = token;
mRingtone = new Ringtone(getContextForUser(user), false);
mRingtone.setStreamType(streamType);
mRingtone.setUri(uri);
}
兜了好大一圖,播放鈴聲使用的即是Ringtone對象,Ringtone中播放鈴聲的實現邏輯如下:
public void play() {
if (mLocalPlayer != null) {
// do not play ringtones if stream volume is 0
// (typically because ringer mode is silent).
if (mAudioManager.getStreamVolume(mStreamType) != 0) {
mLocalPlayer.start();
}
}
...
}
再看看 Ringtone.mLocalPlayer 這個對象是在何時初始化出來的,
public void setUri(Uri uri) {
destroyLocalPlayer();
mUri = uri;
if (mUri == null) {
return;
}
// TODO: detect READ_EXTERNAL and specific content provider case, instead of relying on throwing
// try opening uri locally before delegating to remote player
mLocalPlayer = new MediaPlayer();
try {
mLocalPlayer.setDataSource(mContext, mUri);
mLocalPlayer.setAudioStreamType(mStreamType);
mLocalPlayer.prepare();
}
...
}
好了,mLocalPlayer = new MediaPlayer();,這個就是一個普通的MediaPlayer對象。最後播放鈴音就是調用了MediaPlayer.start() 方法。
鈴聲是如何播放的,基本上都已經說清楚了,下面我們再來分析下到底是什麼事件觸發了第三方音樂播放器在來電時自動暫停了。
在《Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果》中提及的過如下代碼,在 r.play() 之前,有調用 PhoneUtils.setAudioMode(),那再看看這個調用具體做了什麼事情。
private void makeLooper() {
if (mRingThread == null) {
mRingThread = new Worker(ringer);
mRingHandler = new Handler(mRingThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
Ringtone r = null;
switch (msg.what) {
case PLAY_RING_ONCE:
if (DBG) log(mRingHandler: PLAY_RING_ONCE...);
if (mRingtone == null && !hasMessages(STOP_RING)) {
// create the ringtone with the uri
if (DBG) log(creating ringtone: + mCustomRingtoneUri);
r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);
synchronized (Ringer.this) {
if (!hasMessages(STOP_RING)) {
mRingtone = r;
}
}
}
r = mRingtone;
if (r != null && !hasMessages(STOP_RING) && !r.isPlaying()) {
PhoneUtils.setAudioMode();
r.play();
synchronized (Ringer.this) {
if (mFirstRingStartTime < 0) {
mFirstRingStartTime = SystemClock.elapsedRealtime();
}
}
}
break;
...
}
}
};
}
}
一路調用過程如下:
public void CallManager.setAudioMode() {
Context context = getContext();
if (context == null) return;
AudioManager audioManager = (AudioManager)
context.getSystemService(Context.AUDIO_SERVICE);
PhoneConstants.State state = getState();
int lastAudioMode = audioManager.getMode();
// change the audio mode and request/abandon audio focus according to phone state,
// but only on audio mode transitions
switch (state) {
case RINGING:
int curAudioMode = audioManager.getMode();
if (curAudioMode != AudioManager.MODE_RINGTONE) {
// only request audio focus if the ringtone is going to be heard
if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
if (VDBG) Rlog.d(LOG_TAG, requestAudioFocus on STREAM_RING);
audioManager.requestAudioFocusForCall(AudioManager.STREAM_RING,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}
if(!mSpeedUpAudioForMtCall) {
audioManager.setMode(AudioManager.MODE_RINGTONE);
}
}
if (mSpeedUpAudioForMtCall && (curAudioMode != AudioManager.MODE_IN_CALL)) {
audioManager.setMode(AudioManager.MODE_IN_CALL);
}
break;
...
對這個函數調用進行了屏蔽之後,重新編譯出Phone.apk測試後發現,我自己寫的Music測試應用在收到來電時,音樂還是在播放,但是第三方的音樂播放器,還是會暫停,還有哪裡沒有考慮到?
於是找了一份第三方的主流音樂播放器反編譯了一下,發現其Menifest.xml中,有如下權限:
再來看看 TelephonyManager.listen() 的實現:
public void listen(PhoneStateListener listener, int events) {
String pkgForDebug = mContext != null ? mContext.getPackageName() : ;
try {
Boolean notifyNow = true;
sRegistry.listen(pkgForDebug, listener.callback, events, notifyNow);
} catch (RemoteException ex) {
// system process dead
} catch (NullPointerException ex) {
// system process dead
}
}
sRegistry在TelephonyManager的構造函數中有初始化,sRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService(telephony.registry)); 就是telephony.registry服務的一個遠程引用,最終請求會通過Binder走到TelephonyRegistry.listen()中:
public void listen(String pkgForDebug, IPhoneStateListener callback, int events,
boolean notifyNow) {
int callerUid = UserHandle.getCallingUserId();
int myUid = UserHandle.myUserId();
...
if (events != 0) {
/* Checks permission and throws Security exception */
checkListenerPermission(events);
synchronized (mRecords) {
// register
Record r = null;
find_and_add: {
IBinder b = callback.asBinder();
final int N = mRecords.size();
for (int i = 0; i < N; i++) {
r = mRecords.get(i);
if (b == r.binder) {
break find_and_add;
}
}
r = new Record();
r.binder = b;
r.callback = callback;
r.pkgForDebug = pkgForDebug;
r.callerUid = callerUid;
mRecords.add(r);
if (DBG) Slog.i(TAG, listen: add new record= + r);
} ...
mRecords 其中就是一個ArrayList。TelephonyRegistry中保留了所有關注呼叫事件的應用注冊的Listener,在有呼叫事件發生的時候會通知給第三方應用;
TelephonyManager中還有如下方法,將有呼叫事件發生後,此方法會被調用通知給第三方應用:
public void notifyCallState(int state, String incomingNumber) {
if (!checkNotifyPermission(notifyCallState())) {
return;
}
synchronized (mRecords) {
mCallState = state;
mCallIncomingNumber = incomingNumber;
for (Record r : mRecords) {
if ((r.events & PhoneStateListener.LISTEN_CALL_STATE) != 0) {
try {
r.callback.onCallStateChanged(state, incomingNumber);
} catch (RemoteException ex) {
mRemoveList.add(r.binder);
}
}
}
handleRemoveListLocked();
}
broadcastCallStateChanged(state, incomingNumber);
}
OK,搞清楚這個調用流程後,只要延遲對 GsmCallTracker.updatePhoneState() 和 AudioManager.requestAudioFocusForCall() 這兩個函數的調用,即等Music音量下降為0時,再去觸發這兩個函數的調用,即可實現我們需要的功能了。
具體代碼實現,這裡不一一列出了,改動點比較分散有點多。
從 鎖屏服務AIDL線程通信案例看Android 底層啟動
從 鎖屏服務AIDL線程通信案例看Android 底層啟動請確保 你已經閱讀過 我的 Android Window、PhoneWindow、WindowManager、A
Android桌面懸浮窗進階,QQ手機管家小火箭效果實現
今天是2013年的最後一天了,這裡首先提前祝大家新年快樂!同時,本篇文章也是我今年的最後一篇文章了,因此我想要讓它盡量有點特殊性,比起平時的文章要多一些特色
android編譯系統分析(三)-make
這篇博客的目標是摸清楚默認編譯整個android系統時代碼的流程。當我們執行make的時候,會查找當前的Makefie文件或者makefile文件並且執行,在androi
android仿微信聊天界面 語音錄制功能
本例為模仿微信聊天界面UI設計,文字發送以及語言錄制UI。1先看效果圖: 第一:chat.xml設計 <?xml vers