寫一個APP控制第三方播放器播放,以及獲取正在播放的歌曲資訊
阿新 • • 發佈:2019-02-08
最近遇到這麼一個需求,就是在自己的應用中控制第三方播放器播放,以及獲取正在播放的歌曲資訊,包括名字,歌手,專輯,顯示出來。一開始覺得很簡單,但實際上遇到了不少的麻煩,最終實現了兩種方案,讀者可根據自己需要選擇。
先講如何控制第三方播放器播放的問題,可以使用RemoteControlService,裡面有相關的方法,但究其原理,還是模擬耳機訊號來控制。我們知道當耳機按下一曲時,無論是系統自帶播放器還是第三方播放器都會做出相應的反應,所以這裡我直接模擬發出耳機的keycode,就可以控制播放、暫停、上一曲、下一曲了,這個實現得比較輕鬆,程式碼如下:
public static void sendKeyEvent(final int KeyCode) {
new Thread() { //不可在主執行緒中呼叫
public void run() {
try {
Instrumentation inst = new Instrumentation();
inst.sendKeyDownUpSync(KeyCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
然後呼叫這個方法:
sendKeyEvent(85); //暫停
sendKeyEvent(87); //下一曲
sendKeyEvent(88); //上一曲
接著說如何獲取正在播放的歌曲資訊的問題,第一個方案是使用RemoteControlService,繼承NotificationListenerService,實現RemoteController.OnClientUpdateListener介面,原理就是播放器在切換歌曲的時候,會給系統發一個Notification,告訴系統歌曲切換了,而其中就包含了歌曲的資訊,可以攔截Notification來獲取到裡面的資訊,但是有個弊端,你需要開啟一個Service來監聽,而且需要系統授權,這是最大的問題,如果使用者不授權,你將無法獲取資訊,非常被動,但這個方法幾乎能獲取到所有第三方播放器的歌曲資訊(酷狗不行,不知道為何,可能他發出的Notification裡沒有包含相關資訊。而網易雲我是監聽不到Notification的,但卻獲取到了,非常奇怪)。程式碼如下:
關於歌曲的資訊就在上面的metadataEditor中,接著在Activity中實現OnClientUpdateListener方法來獲取:import android.annotation.TargetApi; import android.content.Intent; import android.media.AudioManager; import android.media.RemoteController; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; import android.view.KeyEvent; /** * Created by wangzhaopeng on 2017/11/20. */ @TargetApi(Build.VERSION_CODES.KITKAT) public class RemoteControlService extends NotificationListenerService implements RemoteController.OnClientUpdateListener { public RemoteController remoteController; private RemoteController.OnClientUpdateListener mExternalClientUpdateListener; private IBinder mBinder = new RCBinder(); @Override public void onCreate() { registerRemoteController(); } @Override public IBinder onBind(Intent intent) { if (intent.getAction().equals("com.example.wangzhaopeng.music.BIND_RC_CONTROL_SERVICE")) {//這裡要根據實際進行替換 return mBinder; } else { return super.onBind(intent); } } @Override public void onNotificationPosted(StatusBarNotification sbn) { // Log.e(TAG, "onNotificationPosted..."); if (sbn.getPackageName().contains("music")) { Log.e(TAG, "音樂軟體正在播放..."); Log.e(TAG, sbn.getPackageName()); Log.e(TAG, sbn.getNotification().toString()); } Log.e("正在播放", sbn.getPackageName()); } @Override public void onNotificationRemoved(StatusBarNotification sbn) { Log.e(TAG, "onNotificationRemoved..."); } public void registerRemoteController() { remoteController = new RemoteController(this, this); boolean registered; try { registered = ((AudioManager) getSystemService(AUDIO_SERVICE)) .registerRemoteController(remoteController); } catch (NullPointerException e) { registered = false; } if (registered) { try { remoteController.setArtworkConfiguration( 100, 100); remoteController.setSynchronizationMode(RemoteController.POSITION_SYNCHRONIZATION_CHECK); } catch (IllegalArgumentException e) { e.printStackTrace(); } } } public void setClientUpdateListener(RemoteController.OnClientUpdateListener listener) { mExternalClientUpdateListener = listener; } @Override public void onClientChange(boolean clearing) { if (mExternalClientUpdateListener != null) { mExternalClientUpdateListener.onClientChange(clearing); } } @Override public void onClientPlaybackStateUpdate(int state) { if (mExternalClientUpdateListener != null) { mExternalClientUpdateListener.onClientPlaybackStateUpdate(state); } } @Override public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, long currentPosMs, float speed) { if (mExternalClientUpdateListener != null) { mExternalClientUpdateListener.onClientPlaybackStateUpdate(state, stateChangeTimeMs, currentPosMs, speed); } } @Override public void onClientTransportControlUpdate(int transportControlFlags) { if (mExternalClientUpdateListener != null) { mExternalClientUpdateListener.onClientTransportControlUpdate(transportControlFlags); } } @Override public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) { if (mExternalClientUpdateListener != null) { mExternalClientUpdateListener.onClientMetadataUpdate(metadataEditor); } } public boolean sendMusicKeyEvent(int keyCode) { if (remoteController != null) { KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); boolean down = remoteController.sendMediaKeyEvent(keyEvent); keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); boolean up = remoteController.sendMediaKeyEvent(keyEvent); return down && up; } else { long eventTime = SystemClock.uptimeMillis(); KeyEvent key = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0); dispatchMediaKeyToAudioService(key); dispatchMediaKeyToAudioService(KeyEvent.changeAction(key, KeyEvent.ACTION_UP)); } return false; } private void dispatchMediaKeyToAudioService(KeyEvent event) { AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); if (audioManager != null) { try { audioManager.dispatchMediaKeyEvent(event); } catch (Exception e) { e.printStackTrace(); } } } public class RCBinder extends Binder { public RemoteControlService getService() { return RemoteControlService.this; } } }
RemoteController.OnClientUpdateListener mExternalClientUpdateListener = new RemoteController.OnClientUpdateListener() {
@Override
public void onClientChange(boolean clearing) {
// Log.e(TAG, "onClientChange()...");
}
@Override
public void onClientPlaybackStateUpdate(int state) {
// Log.e(TAG, "onClientPlaybackStateUpdate()...");
}
@Override
public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, long currentPosMs, float speed) {
// Log.e(TAG, "onClientPlaybackStateUpdate()...");
}
@Override
public void onClientTransportControlUpdate(int transportControlFlags) {
// Log.e(TAG, "onClientTransportControlUpdate()...");
}
@Override
public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) {
String artist = metadataEditor.
getString(MediaMetadataRetriever.METADATA_KEY_ARTIST, "null");
String album = metadataEditor.
getString(MediaMetadataRetriever.METADATA_KEY_ALBUM, "null");
String title = metadataEditor.
getString(MediaMetadataRetriever.METADATA_KEY_TITLE, "null");
Long duration = metadataEditor.
getLong(MediaMetadataRetriever.METADATA_KEY_DURATION, -1);
Bitmap defaultCover = BitmapFactory.decodeResource(getResources(), android.R.drawable.ic_menu_compass);
Bitmap bitmap = metadataEditor.
getBitmap(RemoteController.MetadataEditor.BITMAP_KEY_ARTWORK, defaultCover);
//這裡便獲取到資訊了,下面只是我設定給元件的方法,可以自己呼叫自己的設定方法,這裡只要獲取到結果就可以了
setCoverImage(bitmap);
setContentString(artist);
setTitleString(title);
malbum.setText(album);
Log.e("結果為:", "artist:" + artist
+ "album:" + album
+ "title:" + title
+ "duration:" + duration);
}
};
不要忘記在Androidmanifest中宣告service: <service
android:name=".RemoteControlService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
>
<intent-filter>
<action android:name="com.example.wangzhaopeng.music.BIND_RC_CONTROL_SERVICE" />
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
至此,該方案就能夠獲取到正在播放的音樂資訊了,但要注意到,只有在歌曲狀態發生改變時才能獲取到Notification,並且需要使用者授權。那有沒有不需要使用者授權的方法呢?答案是肯定的,這裡提出第二種方案。我注意到播放器在播放音樂的時候,會發出一個廣播(可能是讓藍芽裝置接收歌曲資訊的),只要我們找到這個廣播的Action,寫一個廣播接收者,就可以收到播放器發出的廣播了,也就能獲取到廣播中的歌曲資訊。但是怎麼找這個Action呢?答案是反編譯APK,事實證明這種方法實在是費盡,我反編譯了數十款播放器,有的做了程式碼混淆,有的根本找不到發出廣播的位置,有的是發出的廣播中沒有歌曲資訊,也就是說這種方法也有著它自身很大的侷限性,但也可以嘗試下吧!系統自帶的播放器發出的是com.android.music.metachanged,QQ音樂也是這個,所以這兩個很容易就獲取到了,kugou的是com.kugou.android.music.metachanged,但很遺憾裡面沒資訊,其他的國內的基本都找不到發廣播的位置了,但我要做的主要是國外的播放器,所以嘗試了國外幾款主流播放器,類似Spotify、Google
play music、SoundClound、poweramp,都成功了, 部分失敗,這也沒辦法。具體程式碼如下:import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.ImageView;
import android.widget.TextView;
/**
* Created by wangzhaopeng on 2017/11/22.
*/
public class MyReceiver extends BroadcastReceiver {
private ImageView mCover;
private TextView mTitle ;
private TextView singer;
private TextView malbum;
private DataCallBack mDataCallBack;
public MyReceiver(DataCallBack callBack) {
mDataCallBack = callBack;
}
@Override
public void onReceive(Context context, Intent intent) {
String albumName = intent.getStringExtra("album");
String artist = intent.getStringExtra("artist");
String trackName = intent.getStringExtra("track");
String xiaMiName=intent.getStringExtra("widget_song_name");
System.out.println("最新的結果是!: " + albumName + " artist: "
+ artist + " Track:" + trackName+" xiaMiName:"+xiaMiName);
if(albumName!=null || artist!=null || trackName!= null) {
mDataCallBack.onDataChanged(trackName, artist, albumName);
}
}
public interface DataCallBack {
void onDataChanged(String trackName,String artist,String albumName );
}
}
先寫一個廣播,再去Activity中動態註冊:
private void receive(){
IntentFilter iF = new IntentFilter();
//這麼多Action是我嘗試出來的,有的播放器可以,有的不行,但基本可以的都在這
iF.addAction("com.android.music.metachanged");
iF.addAction("com.android.music.playstatechanged");
iF.addAction("com.android.music.queuechanged");
iF.addAction("com.htc.music.metachanged");
iF.addAction("fm.last.android.metachanged");
iF.addAction("com.sec.android.app.music.metachanged");
iF.addAction("com.nullsoft.winamp.metachanged");
iF.addAction("com.amazon.mp3.metachanged");
iF.addAction("com.miui.player.metachanged");
iF.addAction("com.real.IMP.metachanged");
iF.addAction("com.sonyericsson.music.metachanged");
iF.addAction("com.rdio.android.metachanged");
iF.addAction("com.samsung.sec.android.MusicPlayer.metachanged");
iF.addAction("com.andrew.apollo.metachanged");
iF.addAction("com.kugou.android.music.metachanged");
iF.addAction("com.ting.mp3.playinfo_changed");
iF.addAction("com.spotify.music.playbackstatechanged");
iF.addAction("com.spotify.music.metadatachanged");
iF.addAction("com.rhapsody.playstatechanged");
iF.addAction("com.spotify.music.metadatachanged");
iF.addAction("com.xiami.music.horizontalplayer");
iF.addAction("com.android.music.musicservicecommand");
iF.addAction("com.jiubang.go.music");
iF.addAction("com.google.android.gms.measurement.UPLOAD");
iF.addAction("com.google.android.gms.measurement.AppMeasurementService");
iF.addAction("com.google.android.gms");
iF.addAction("com.google.android.gms.common.account.CHOOSE_ACCOUNT");
iF.addAction("com.Project100Pi.themusicplayer");
iF.addAction("com.tutorialsface.audioplayer.next");
iF.addAction("com.sec.android.automotive.drivelink");
iF.addAction("com.apsalar.sdk.INITIALIZE");
registerReceiver(new MyReceiver(new MyReceiver.DataCallBack() {
@Override
public void onDataChanged(String albumName,String singer,String zhuanji) {
mTitle.setText(albumName);
msinger.setText(singer);
malbum.setText(zhuanji);
}
}), iF);
}
至此,基本就實現了第二種方案,佈局的話自己寫幾個按鈕,放三個Textview和一個Imageview就可以了,我這裡不貼出程式碼。所以概括下這兩種方案:第一種基本能獲取資訊,但需要授權;第二種不需要授權,但很多播放器獲取不到。所以就根據自己需要作出選擇吧!目前我找不到更好的實現方法了,如果各位有好的方案的,歡迎提出探討。如有問題的,在下方評論處留言,我看到就回復。