那些不太熱門的Android知識(shí)——RemoteController

前言

要不是團(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ù)必和包名相同(筆者也不懂為何)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容