那些不太热门的Android知识——RemoteController

前言

要不是团队中有这么个需求,我估计永远也不会去接触这么个东西。首先要从需求说起,需求是通过自己的控件来控制第三方的播放器,市面上的音乐播放器有多种,且其内部的实现方式多种多样,长久以来没有统一的标准,但大部分都是通过开启一个服务在后台,接着通知栏上会有一个常驻的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属性,三处务必统一,且小写字母部分务必和包名相同(笔者也不懂为何)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容