This is the fifth day of my participation in Gwen Challenge

Bluetooth Starter guide for Android application developers juejin.cn/post/697019…

We briefly introduced the basic usage process of Bluetooth. In this section, we mainly study how the data of audio playback is transmitted between two devices.

A common scenario is that people connect their mobile phones to their cars via Bluetooth, and the car screen displays the current audio message. The car app can also reverse the current music playback on the phone. In the previous section, Bluetooth communicates data through sockets. So is that true of bluetooth in cars? Let’s explore this process.

The demo result:

The phone app transfers information to the car

Vehicle-mounted reverse control phone:

Understanding profile Files

To put this in perspective, profile represents a capability that defines how devices interact with each other. For example, AVRCP, our cross-device invocation, defines how to make cross-device invocation. Each protocol has its own profile(depending on whether the system has implemented the corresponding function, if not necessary, you can not have the corresponding profile).

How do audio messages get sent to bluetooth

Before android5.0, sending audio messages is done via RemoteControlClient. After 5.0 it is done through MediaSession.

MediaSession a class that controls the volume, buttons, and data transfer of the player. In general, there is only one MediaSession per application.

To receive call control from other devices, call setActive (true) and setCallback(Callback).

Sending of audio messages

Mediassession #setMetadata(@nullable MediaMetadata metadata)

There are many keys defined in MediaMetadata. The data sender only needs to add data according to the corresponding keys

Mediassession #setPlaybackState(@nullable PlaybackState) can send information related to the PlaybackState.

Player data sending:

mediaSession = new MediaSession(BluetoothPlayerActivity.this."test");
mediaSession.setActive(true);
mediaSession.setMetadata(new MediaMetadata.Builder()
                         .putString(MediaMetadata.METADATA_KEY_TITLE, "You are the wind and I am the sand")
                         .putString(MediaMetadata.METADATA_KEY_ARTIST, "Who am I?")
                         .build());
Toast.makeText(BluetoothPlayerActivity.this."Sending message already started, reset requires exit reprogress.",Toast.LENGTH_SHORT).show();
AppExecutors.getInstance().networkIO().execute(new Runnable() {
    @Override
    public void run(a) {
        int i = 0;
        while(! destroy){ Log.d(TAG,"update PlaybackState "+mediaPlayer.getCurrentPosition());
            mediaSession.setPlaybackState(new PlaybackState.Builder()
                                          .setState(PlaybackState.STATE_PLAYING, mediaPlayer.getCurrentPosition(), 1.0 f)
                                          .build());
            mediaSession.setMetadata(new MediaMetadata.Builder()
                                     .putLong(MediaMetadata.METADATA_KEY_DURATION, mediaPlayer.getDuration())
                                     .putString(MediaMetadata.METADATA_KEY_TITLE, "You are the wind and I am the sand"+i)
                                     .putString(MediaMetadata.METADATA_KEY_ARTIST, "Who am I?")
                                     .build());
            SystemClock.sleep(500); i++; }}});Copy the code

Vehicle audio message reception

When the system receives a change in the audio message, it sends two broadcasts:

So we also register this broadcast to receive audio messages.

 BroadcastReceiver avrcpBroadcastReceiver = new BroadcastReceiver() {
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onReceive(Context context, Intent intent) {
            if(intent.getAction().equals(AvrcpControllerHelper.ACTION_TRACK_EVENT)){
                final MediaMetadata mediaMetadata = intent.getParcelableExtra(AvrcpControllerHelper.EXTRA_METADATA);
                final PlaybackState playbackState = intent.getParcelableExtra(AvrcpControllerHelper.EXTRA_PLAYBACK);
// if(mediaMetadata == null || playbackState == null){
// Log.e(TAG,"some info null ? = "+mediaMetadata+" "+playbackState);
// return;
/ /}
                if(mediaMetadata ! =null){
                    name = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
                    author = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
                    totalTime = timeCover(+mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION));
                }
                if(playbackState ! =null){
                    currentTime = timeCover(playbackState.getPosition());
                    play = playbackState.getState() == PlaybackState.STATE_PLAYING;
                }
                TextView textView = findViewById(R.id.tvPlayTime);
                textView.setText("Start time"+currentTime);
                textView = findViewById(R.id.tvTotalTime);
                textView.setText("Total Duration"+totalTime);
                textView = findViewById(R.id.tvMusicInfo);
                textView.setText("Title:"+name   +"------------ Author:"+author);
                textView = findViewById(R.id.tvPlayToggle);
                textView.setText("Remote Player"+(play?"Playing now":"Not playing")); }}}; registerReceiver(avrcpBroadcastReceiver,new IntentFilter(AvrcpControllerHelper.ACTION_TRACK_EVENT));
Copy the code

How does bluetooth reverse control the audio player

BluetoothAvrcpController to reverse control the audio player, BluetoothAvrcpController is hidden from normal applications. If there is a framework that can be accessed directly, we can use reflection to handle it if there is not. BluetoothAvrcpController#sendGroupNavigationCmd is used to call the audio player back.

We also set up callback objects for mediaSession

mediaSession.setCallback(new MediaSession.Callback() {
                       
                        @Override
                        public void onCommand(@NonNull String command, @Nullable Bundle args, @Nullable ResultReceiver cb) {
                            Log.e(TAG,"onCommand");
                            super.onCommand(command, args, cb);
                        }
                        
                        @Override
                        public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
                            Log.e(TAG,"onMediaButtonEvent");
                            Toast.makeText(BluetoothPlayerActivity.this."I treated you so well, and you manipulated me.",Toast.LENGTH_SHORT).show();
                            return super.onMediaButtonEvent(mediaButtonIntent); }});Copy the code

Reflection of BluetoothAvrcpController to handle related methods

public class AvrcpControllerHelper {
    private static final String TAG = AvrcpControllerHelper.class.getSimpleName();
    public static int AVRCP_CONTROLLER = 12;

    public static final String ACTION_TRACK_EVENT = "android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT";
    public static final String EXTRA_METADATA = "android.bluetooth.avrcp-controller.profile.extra.METADATA";
    public static final String EXTRA_PLAYBACK = "android.bluetooth.avrcp-controller.profile.extra.PLAYBACK";


    public static void  sendGroupNavigationCmd(BluetoothProfile bluetoothProfile, BluetoothDevice device, int keyCode, int keyState){
        if(bluetoothProfile ! =null) {try {
                Method m = bluetoothProfile.getClass().getMethod("sendGroupNavigationCmd",BluetoothDevice.class,int.class,int.class);
                m.setAccessible(true);
                m.invoke(bluetoothProfile,device,keyCode,keyState);
            } catch(NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); }}}private static void fixSystemHideApi(a){
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            return;
        }
        try {
            Method forName = Class.class.getDeclaredMethod("forName", String.class);
            Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class); Class<? > vmRuntimeClass = (Class<? >) forName.invoke(null."dalvik.system.VMRuntime");
            Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime".null);
            Method setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions".new Class[]{String[].class});
            Object sVmRuntime = getRuntime.invoke(null);
            setHiddenApiExemptions.invoke(sVmRuntime, new Object[]{new String[]{"L"}});
        } catch (Throwable e) {
            Log.e("[error]"."reflect bootstrap failed:", e); }}public static List<BluetoothDevice> getConnectedDevices(BluetoothProfile profile){
        if(profile == null) {return null;
        }
        fixSystemHideApi();
        try {
            Method m = profile.getClass().getDeclaredMethod("getConnectedDevices");
            m.setAccessible(true);
            return (List<BluetoothDevice>) m.invoke(profile);
        } catch (NoSuchMethodException e) {
            Log.w(TAG, "No disconnect method in the " + profile.getClass().getName() +
                    " class, ignoring request.");
            return null;
        } catch (InvocationTargetException | IllegalAccessException e) {
            Log.w(TAG, "Could not execute method 'disconnect' in profile " +
                    profile.getClass().getName() + ", ignoring request.", e);
            return null; }}// BluetoothAvrcp of the system is not available, here is a copy of this
    public static class BluetoothAvrcp {

        /* * State flags for Passthrough commands */
        public static final int PASSTHROUGH_STATE_PRESS = 0;
        public static final int PASSTHROUGH_STATE_RELEASE = 1;

        /* * Operation IDs for Passthrough commands */
        public static final int PASSTHROUGH_ID_SELECT = 0x00;    /* select */
        public static final int PASSTHROUGH_ID_UP = 0x01;    /* up */
        public static final int PASSTHROUGH_ID_DOWN = 0x02;    /* down */
        public static final int PASSTHROUGH_ID_LEFT = 0x03;    /* left */
        public static final int PASSTHROUGH_ID_RIGHT = 0x04;    /* right */
        public static final int PASSTHROUGH_ID_RIGHT_UP = 0x05;    /* right-up */
        public static final int PASSTHROUGH_ID_RIGHT_DOWN = 0x06;    /* right-down */
        public static final int PASSTHROUGH_ID_LEFT_UP = 0x07;    /* left-up */
        public static final int PASSTHROUGH_ID_LEFT_DOWN = 0x08;    /* left-down */
        public static final int PASSTHROUGH_ID_ROOT_MENU = 0x09;    /* root menu */
        public static final int PASSTHROUGH_ID_SETUP_MENU = 0x0A;    /* setup menu */
        public static final int PASSTHROUGH_ID_CONT_MENU = 0x0B;    /* contents menu */
        public static final int PASSTHROUGH_ID_FAV_MENU = 0x0C;    /* favorite menu */
        public static final int PASSTHROUGH_ID_EXIT = 0x0D;    /* exit */
        public static final int PASSTHROUGH_ID_0 = 0x20;    /* 0 */
        public static final int PASSTHROUGH_ID_1 = 0x21;    / * 1 * /
        public static final int PASSTHROUGH_ID_2 = 0x22;    / * 2 * /
        public static final int PASSTHROUGH_ID_3 = 0x23;    / * * / 3
        public static final int PASSTHROUGH_ID_4 = 0x24;    / * * / 4
        public static final int PASSTHROUGH_ID_5 = 0x25;    / * * / 5
        public static final int PASSTHROUGH_ID_6 = 0x26;    / * * / 6
        public static final int PASSTHROUGH_ID_7 = 0x27;    / * * / 7
        public static final int PASSTHROUGH_ID_8 = 0x28;    /* 8 */
        public static final int PASSTHROUGH_ID_9 = 0x29;    /* 9 */
        public static final int PASSTHROUGH_ID_DOT = 0x2A;    /* dot */
        public static final int PASSTHROUGH_ID_ENTER = 0x2B;    /* enter */
        public static final int PASSTHROUGH_ID_CLEAR = 0x2C;    /* clear */
        public static final int PASSTHROUGH_ID_CHAN_UP = 0x30;    /* channel up */
        public static final int PASSTHROUGH_ID_CHAN_DOWN = 0x31;    /* channel down */
        public static final int PASSTHROUGH_ID_PREV_CHAN = 0x32;    /* previous channel */
        public static final int PASSTHROUGH_ID_SOUND_SEL = 0x33;    /* sound select */
        public static final int PASSTHROUGH_ID_INPUT_SEL = 0x34;    /* input select */
        public static final int PASSTHROUGH_ID_DISP_INFO = 0x35;    /* display information */
        public static final int PASSTHROUGH_ID_HELP = 0x36;    /* help */
        public static final int PASSTHROUGH_ID_PAGE_UP = 0x37;    /* page up */
        public static final int PASSTHROUGH_ID_PAGE_DOWN = 0x38;    /* page down */
        public static final int PASSTHROUGH_ID_POWER = 0x40;    /* power */
        public static final int PASSTHROUGH_ID_VOL_UP = 0x41;    /* volume up */
        public static final int PASSTHROUGH_ID_VOL_DOWN = 0x42;    /* volume down */
        public static final int PASSTHROUGH_ID_MUTE = 0x43;    /* mute */
        public static final int PASSTHROUGH_ID_PLAY = 0x44;    /* play */
        public static final int PASSTHROUGH_ID_STOP = 0x45;    /* stop */
        public static final int PASSTHROUGH_ID_PAUSE = 0x46;    /* pause */
        public static final int PASSTHROUGH_ID_RECORD = 0x47;    /* record */
        public static final int PASSTHROUGH_ID_REWIND = 0x48;    /* rewind */
        public static final int PASSTHROUGH_ID_FAST_FOR = 0x49;    /* fast forward */
        public static final int PASSTHROUGH_ID_EJECT = 0x4A;    /* eject */
        public static final int PASSTHROUGH_ID_FORWARD = 0x4B;    /* forward */
        public static final int PASSTHROUGH_ID_BACKWARD = 0x4C;    /* backward */
        public static final int PASSTHROUGH_ID_ANGLE = 0x50;    /* angle */
        public static final int PASSTHROUGH_ID_SUBPICT = 0x51;    /* subpicture */
        public static final int PASSTHROUGH_ID_F1 = 0x71;    /* F1 */
        public static final int PASSTHROUGH_ID_F2 = 0x72;    /* F2 */
        public static final int PASSTHROUGH_ID_F3 = 0x73;    /* F3 */
        public static final int PASSTHROUGH_ID_F4 = 0x74;    /* F4 */
        public static final int PASSTHROUGH_ID_F5 = 0x75;    /* F5 */
        public static final int PASSTHROUGH_ID_VENDOR = 0x7E;    /* vendor unique */
        public static final int PASSTHROUGH_KEYPRESSED_RELEASE = 0x80; }}Copy the code

Overall use of demo (only through the mutual call process between car and mobile app) :

Github.com/xiaolutang/…

Next up:

Analyze the process of Bluetooth interaction from system flow.