1. 程式人生 > >寫一個APP控制第三方播放器播放,以及獲取正在播放的歌曲資訊

寫一個APP控制第三方播放器播放,以及獲取正在播放的歌曲資訊

      最近遇到這麼一個需求,就是在自己的應用中控制第三方播放器播放,以及獲取正在播放的歌曲資訊,包括名字,歌手,專輯,顯示出來。一開始覺得很簡單,但實際上遇到了不少的麻煩,最終實現了兩種方案,讀者可根據自己需要選擇。

      先講如何控制第三方播放器播放的問題,可以使用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的,但卻獲取到了,非常奇怪)。程式碼如下:


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;

        }

    }

}
      關於歌曲的資訊就在上面的metadataEditor中,接著在Activity中實現OnClientUpdateListener方法來獲取:
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就可以了,我這裡不貼出程式碼。所以概括下這兩種方案:第一種基本能獲取資訊,但需要授權;第二種不需要授權,但很多播放器獲取不到。所以就根據自己需要作出選擇吧!目前我找不到更好的實現方法了,如果各位有好的方案的,歡迎提出探討。如有問題的,在下方評論處留言,我看到就回復。