前言
要不是團(tuán)隊(duì)中有這么個(gè)需求,我估計(jì)永遠(yuǎn)也不會(huì)去接觸這么個(gè)東西。首先要從需求說起,需求是通過自己的控件來控制第三方的播放器,市面上的音樂播放器有多種,且其內(nèi)部的實(shí)現(xiàn)方式多種多樣,長(zhǎng)久以來沒有統(tǒng)一的標(biāo)準(zhǔn),但大部分都是通過開啟一個(gè)服務(wù)在后臺(tái),接著通知欄上會(huì)有一個(gè)常駐的Notification來方便用戶的控制。
反編譯大多數(shù)音樂APP,你會(huì)發(fā)現(xiàn)它們都有注冊(cè)耳機(jī)插拔的廣播,還有就是你可以通過控制耳機(jī)按鍵來控制音樂播放,而耳機(jī)按鍵事件是可以模擬的,這就為控制第三方音樂播放器提供可能。
接著就是關(guān)于接收音樂信息的問題,這里指的是接收專輯、歌手專輯封面等等,前面說了,通知欄會(huì)有常駐的Notification來顯示當(dāng)前一些歌曲的信息,那如何獲取呢,一種方式是通過反射,但是普遍性比較差。
在Android API 19中,谷歌為我們提供了RemoteController,現(xiàn)在這個(gè)API已經(jīng)被MediaSession代替,然而網(wǎng)上對(duì)MediaSession的資料幾乎為零,所以本篇文章只講講RemoteController的使用,如果有關(guān)于MediaSession的資料demo或者有關(guān)對(duì)第三方音樂播放器控制好的方法,歡迎私信留言,本篇文章有欠妥的地方,歡迎指出,筆者加以改正,共同學(xué)習(xí)。
一些儲(chǔ)備知識(shí):
1、NotificationListenerService
相信做過和Notification有關(guān)的同學(xué)對(duì)這個(gè)東西多少都有些了解,這是谷歌官方提供的用于監(jiān)聽和處理消息通知的API。使用方式也很簡(jiǎn)單,繼承它,重寫其中的幾個(gè)方法就好,系統(tǒng)會(huì)在后臺(tái)開啟一個(gè)服務(wù)專門用于監(jiān)聽系統(tǒng)消息,當(dāng)然這需要手動(dòng)去開啟權(quán)限。
2、按鍵事件:
關(guān)于按鍵事件來控制Media,看下面兩個(gè)方法即可
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();
}
}
}
那么接下來我們來說說RemotController來控制及獲取第三方音樂信息:
part1
首先我們需要繼承NotificationListenerService,這里有兩個(gè)相對(duì)比較重要的方法
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
Log.e(TAG, "onNotificationPosted...");
if (sbn.getPackageName().contains("music"))
{
Log.e(TAG, "音樂軟件正在播放...");
Log.e(TAG, sbn.getPackageName());
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
Log.e(TAG, "onNotificationRemoved...");
}
這里我們可以通過關(guān)鍵字看到當(dāng)前正在后臺(tái)播放的是哪一款播放器。
part2
接著讓這個(gè)繼承于NotificationListenerService的服務(wù)實(shí)現(xiàn)RemoteController.OnClientUpdateListener接口,以下是接口中的方法:
/**
* Interface definition for the callbacks to be invoked whenever media events, metadata * and playback status are available. */
public interface OnClientUpdateListener {
/**
* Called whenever all information, previously received through the other
* methods of the listener, is no longer valid and is about to be refreshed.
* This is typically called whenever a new {@link RemoteControlClient} has been selected
* by the system to have its media information published.
* @param clearing true if there is no selected RemoteControlClient and no information
* is available.
*/public void onClientChange(boolean clearing);
/**
* Called whenever the playback state has changed.
* It is called when no information is known about the playback progress in the media and
* the playback speed.
* @param state one of the playback states authorized
* in {@link RemoteControlClient#setPlaybackState(int)}.
*/public void onClientPlaybackStateUpdate(int state);
/**
* Called whenever the playback state has changed, and playback position
* and speed are known.
* @param state one of the playback states authorized
* in {@link RemoteControlClient#setPlaybackState(int)}.
* @param stateChangeTimeMs the system time at which the state change was reported,
* expressed in ms. Based on {@link android.os.SystemClock#elapsedRealtime()}.
* @param currentPosMs a positive value for the current media playback position expressed
* in ms, a negative value if the position is temporarily unknown.
* @param speed a value expressed as a ratio of 1x playback: 1.0f is normal playback,
* 2.0f is 2x, 0.5f is half-speed, -2.0f is rewind at 2x speed. 0.0f means nothing is
* playing (e.g. when state is {@link RemoteControlClient#PLAYSTATE_ERROR}). */
public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, long currentPosMs, float speed); /**
* Called whenever the transport control flags have changed.
* @param transportControlFlags one of the flags authorized
* in {@link RemoteControlClient#setTransportControlFlags(int)}. */
public void onClientTransportControlUpdate(int transportControlFlags);
/**
* Called whenever new metadata is available.
* See the {@link MediaMetadataEditor#putLong(int, long)},
* {@link MediaMetadataEditor#putString(int, String)},
* {@link MediaMetadataEditor#putBitmap(int, Bitmap)}, and
* {@link MediaMetadataEditor#putObject(int, Object)} methods for the various keys that
* can be queried.
* @param metadataEditor the container of the new metadata. */
public void onClientMetadataUpdate(MetadataEditor metadataEditor);};
在最后一個(gè)方法中,我們需要的專輯封面、歌手、歌曲名等等資料都能在metadataEditor參數(shù)里拿到,這個(gè)放在后面說。
part3
接下來就是獲取合法的RemoteController對(duì)象以及其他一些設(shè)置,比如設(shè)置獲取封面時(shí)封面的大小等,在onCreate()中執(zhí)行再合適不過了,這段獲取及配置的代碼為:
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();
}
}
}
還有就是通過回調(diào)把一些具體實(shí)現(xiàn)放在外部去,前面說到,我們的service是繼承自系統(tǒng)的NotificationListenerService ,所以終究來說,它還是一個(gè)服務(wù),你可以在正在運(yùn)行的后臺(tái)服務(wù)中看到,這是作為單獨(dú)一個(gè)服務(wù)進(jìn)行的。
所以就涉及到了與service通信的問題,我們使用Binder,服務(wù)的完整代碼如下:
@TargetApi(Build.VERSION_CODES.KITKAT)
public class RemoteControlService extends NotificationListenerService implements RemoteController.OnClientUpdateListener {
String TAG = "Yankee";
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.yankee.musicview.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());
}
}
@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;
}
}
}
part4
那么接下來的操作就是在我們自己的view中進(jìn)行了,寫一個(gè)音樂控制的view很簡(jiǎn)單,并且在onAttachedToWindow()的時(shí)候綁定這個(gè)服務(wù),接下來附上view的代碼;
MusicView:
/**
* Created by Yankee on 2016/12/20.
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public class MusicView extends LinearLayout {
private ImageView mCover;
private ImageView mPre;
private ImageView mPause;
private ImageView mNext;
private TextView mTitle;
private TextView mContent;
private Context mContext;
private boolean isPlaying = true;
private RemoteControlService mRCService;
private static final String TAG = "Yankee";
public MusicView(Context context) {
this(context, null);
}
public MusicView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
LayoutInflater.from(context).inflate(R.layout.layout_music_view, this);
initView();
initListener();
}
private void initView() {
mCover = (ImageView) findViewById(R.id.music_view_cover);
mPre = (ImageView) findViewById(R.id.music_view_previous);
mPause = (ImageView) findViewById(R.id.music_view_pause);
mNext = (ImageView) findViewById(R.id.music_view_next);
mTitle = (TextView) findViewById(R.id.music_view_title);
mContent = (TextView) findViewById(R.id.music_view_content);
}
private void initListener() {
mPre.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
mRCService.sendMusicKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
isPlaying = true;
mPause.setImageResource(android.R.drawable.ic_media_pause);
}
});
mPause.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
mRCService.sendMusicKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
if (isPlaying) {
isPlaying = false;
mPause.setImageResource(android.R.drawable.ic_media_play);
} else {
isPlaying = true;
mPause.setImageResource(android.R.drawable.ic_media_pause);
}
}
});
mNext.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
mRCService.sendMusicKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT);
isPlaying = true;
mPause.setImageResource(android.R.drawable.ic_media_pause);
}
});
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Intent intent = new Intent("com.yankee.musicview.BIND_RC_CONTROL_SERVICE");
intent.setPackage(mContext.getPackageName());
mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
public void setCoverImage(Bitmap bitmap) {
mCover.setImageBitmap(bitmap);
}
public void setTitleString(String title) {
mTitle.setText(title);
}
public void setContentString(String content) {
mContent.setText(content);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
RemoteControlService.RCBinder binder = (RemoteControlService.RCBinder) service;
mRCService = binder.getService();
mRCService.setClientUpdateListener(mExternalClientUpdateListener);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
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);
Log.e(TAG, "artist:" + artist
+ "album:" + album
+ "title:" + title
+ "duration:" + duration);
}
};
}
布局文件的代碼我就不粘貼了
一些注意事項(xiàng)
1、不要忘了在配置文件里加上服務(wù)的代碼,且這個(gè)服務(wù)需要加權(quán)限:
<service
android:name=".RemoteControlService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
>
<intent-filter>
<action android:name="com.yankee.musicview.BIND_RC_CONTROL_SERVICE" />
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
2、首先要到安全里開啟消息通知權(quán)限,首先要到安全里開啟消息通知權(quán)限,首先要到安全里開啟消息通知權(quán)限,重要的事情說三遍,否則死活都不會(huì)啟用的,而且會(huì)報(bào):
java.lang.SecurityException: Missing permission to control media.
3、代碼中有三處用到
RemoteControlService的name屬性,三處務(wù)必統(tǒng)一,且小寫字母部分務(wù)必和包名相同(筆者也不懂為何)