前言
要不是团队中有这么个需求,我估计永远也不会去接触这么个东西。首先要从需求说起,需求是通过自己的控件来控制第三方的播放器,市面上的音乐播放器有多种,且其内部的实现方式多种多样,长久以来没有统一的标准,但大部分都是通过开启一个服务在后台,接着通知栏上会有一个常驻的Notification来方便用户的控制。
反编译大多数音乐APP,你会发现它们都有注册耳机插拔的广播,还有就是你可以通过控制耳机按键来控制音乐播放,而耳机按键事件是可以模拟的,这就为控制第三方音乐播放器提供可能。
接着就是关于接收音乐信息的问题,这里指的是接收专辑、歌手专辑封面等等,前面说了,通知栏会有常驻的Notification来显示当前一些歌曲的信息,那如何获取呢,一种方式是通过反射,但是普遍性比较差。
在Android API 19中,谷歌为我们提供了RemoteController,现在这个API已经被MediaSession代替,然而网上对MediaSession的资料几乎为零,所以本篇文章只讲讲RemoteController的使用,如果有关于MediaSession的资料demo或者有关对第三方音乐播放器控制好的方法,欢迎私信留言,本篇文章有欠妥的地方,欢迎指出,笔者加以改正,共同学习。
一些储备知识:
1、NotificationListenerService
相信做过和Notification有关的同学对这个东西多少都有些了解,这是谷歌官方提供的用于监听和处理消息通知的API。使用方式也很简单,继承它,重写其中的几个方法就好,系统会在后台开启一个服务专门用于监听系统消息,当然这需要手动去开启权限。
2、按键事件:
关于按键事件来控制Media,看下面两个方法即可
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,这里有两个相对比较重要的方法
@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...");
}
这里我们可以通过关键字看到当前正在后台播放的是哪一款播放器。
part2
接着让这个继承于NotificationListenerService的服务实现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);};
在最后一个方法中,我们需要的专辑封面、歌手、歌曲名等等资料都能在metadataEditor参数里拿到,这个放在后面说。
part3
接下来就是获取合法的RemoteController对象以及其他一些设置,比如设置获取封面时封面的大小等,在onCreate()中执行再合适不过了,这段获取及配置的代码为:
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();
}
}
}
还有就是通过回调把一些具体实现放在外部去,前面说到,我们的service是继承自系统的NotificationListenerService ,所以终究来说,它还是一个服务,你可以在正在运行的后台服务中看到,这是作为单独一个服务进行的。
所以就涉及到了与service通信的问题,我们使用Binder,服务的完整代码如下:
@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中进行了,写一个音乐控制的view很简单,并且在onAttachedToWindow()的时候绑定这个服务,接下来附上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);
}
};
}
布局文件的代码我就不粘贴了
一些注意事项
1、不要忘了在配置文件里加上服务的代码,且这个服务需要加权限:
<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、首先要到安全里开启消息通知权限,首先要到安全里开启消息通知权限,首先要到安全里开启消息通知权限,重要的事情说三遍,否则死活都不会启用的,而且会报:
java.lang.SecurityException: Missing permission to control media.
3、代码中有三处用到
RemoteControlService的name属性,三处务必统一,且小写字母部分务必和包名相同(笔者也不懂为何)
网友评论