[Android] MediaBrowserServiceCompat 이용하여 Bluetooth Earbuds Button Click Event 받기

2022. 6. 27. 16:44Android

VR 특성상 사용자 입력을 받아 이벤트를 처리하는 것이 2D Display에 비해 제한적이기 때문에 

어떻게 컨트롤러의 버튼 onClick 이벤트를 받을 것인지 고민이 되었다.

 

모바일 디바이스에서 VR 앱이라 오큘러스나 VIVE와 같은 standalone 디바이스와는 다르게

사용자가 별도의 컨트롤러를 구비하고 있지 않은 상태인 것이 한계점이었다.

 

지금까지는 모바일 디바이스에 부착되어 있는 자이로 센서를 이용하여 입력을 받았다.

HMD에 휴대전화를 끼운 상태로 옆면을 더블 탭하면 그 행위를 센서를 통해 입력 신호로 활용하는 것이다.

 

그러나 움직임을 감지하는 센서를 통해 구현한 이벤트가 항상 사용자의 의도를 반영하고 있지는 않다.

HMD에 끼우다가 혹은 움직이다가 센서가 더블 탭으로 인지하는 경우가 종종 있었다.

 

좀 더 편하게 사용자의 의도로 이벤트를 처리할 수 없을까 고민하던 중,

모바일 디바이스 사용자들이 대부분 가지고 있는 블루투스 이어폰을 통해 입력을 받아보면

어떨까하는 아이디어로 이 기능을 구현하게 되었다.

 

1. AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="_______________">
    
    ...
    
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application
        android:largeHeap="true"
        android:usesCleartextTraffic="true">
        <activity
            ...>
            ...
        </activity>

        <receiver android:name="androidx.media.session.MediaButtonReceiver"
            android:exported="true"
            android:enabled="true">
            <intent-filter>
                <action android:name="android.intent.action.MEDIA_BUTTON" />
                <action android:name="android.media.browse.MediaBrowserService" />
            </intent-filter>
        </receiver>

        <service android:name=".MediaButtonService"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MEDIA_BUTTON" />
                <action android:name="android.media.browse.MediaBrowserService" />
            </intent-filter>
        </service>
        
    </application>
</manifest>

 

 

 

 

 

2.  MediaButtonService.java

package kr.co.alphacircle.alphavr;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.media.browse.MediaBrowser;
import android.os.Bundle;
import android.service.media.MediaBrowserService;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.session.MediaButtonReceiver;

import java.util.List;

import kr.co.alphacircle.alphavr.quadview.AcControlPanel;
import kr.co.alphacircle.alphavr.utils.AcC;

public class MediaButtonService extends MediaBrowserServiceCompat {
    private static final String TAG = "MediaButtonService";
    private static final int MEDIA_BUTTON_NOTIFICATION_ID = 2;

    private MediaSessionCompat mediaSession;
    private PlaybackStateCompat.Builder stateBuilder;
    private boolean isFirstServiceCall = false; // To receive only one event at a time <--onStartCommand() of Service calling multiple times in Android System)
    private static AcControlPanel controlPanel;

    public static void setControlPanel(AcControlPanel controlPanel) {
        MediaButtonService.controlPanel = controlPanel;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        String CHANNEL_ID = "media_button_channel";
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "MediaButtonClick",
                NotificationManager.IMPORTANCE_DEFAULT);
        ((NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);

        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("")
                .setContentText("").build();
        startForeground(MEDIA_BUTTON_NOTIFICATION_ID, notification);

        // Create a MediaSessionCompat
        mediaSession = new MediaSessionCompat(getBaseContext(), TAG);

        // Enable callbacks from MediaButtons and TransportControls
        mediaSession.setFlags(
                MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
                        MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

        // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
        stateBuilder = new PlaybackStateCompat.Builder()
                .setActions(
                        PlaybackStateCompat.ACTION_PLAY |
                                PlaybackStateCompat.ACTION_PLAY_PAUSE);
        mediaSession.setPlaybackState(stateBuilder.build());

        // MySessionCallback() has methods that handle callbacks from a media controller
        mediaSession.setCallback(mediaSessionCallback);

        // Set the session's token so that client activities can communicate with it.
        setSessionToken(mediaSession.getSessionToken());
        mediaSession.setActive(true);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
        return null;
    }

    @Override
    public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        MediaButtonReceiver.handleIntent(mediaSession, intent);
        return super.onStartCommand(intent, flags, startId);
    }

    public MediaSessionCompat.Callback mediaSessionCallback = new MediaSessionCompat.Callback() {
        @Override
        public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
        	Log.i(TAG, "❤️ XIO, media button event!!!! ");
            if(controlPanel==null) return false;
            isFirstServiceCall=!isFirstServiceCall;
            if(isFirstServiceCall) controlPanel.onDoubleTap();
            Toast.makeText(getBaseContext(), "MEDIA BUTTON EVENT", Toast.LENGTH_SHORT).show();
            return false;
        }
    };
}

 

onStartCommand() 메소드는 Activity의 onResume()과 같은 lifecycle 메소드이다.

mediaSessionCallback의 onMediaButtonEvent() 메소드의 리턴값은 항상 false로 바꿔주었다. 

원래는 super()의 메소드를 호출한 값이었으나 저것때문에 이벤트가 받아와지지 않을 수 있다는 글을 발견했기 때문이다.

댓글을 참조하여 리턴값을 바꿔 테스트 해보았더니 이벤트가 잘 받아졌다.

https://stackoverflow.com/questions/65808985/android-mediasessioncompat-callbacks-not-firing

 

Android MediaSessionCompat Callbacks not firing

I'm creating an audiobook player, and I'm using MediaSessionCompat related classes to handle notifications. My code is heavily inspired by the android-MediaBrowserService samples ( https://github.com/

stackoverflow.com

 

 

 

 

3.  MainActivity.java :: onResume()

@Override
protected void onResume() {
    ...
    startService(new Intent(this, MediaButtonService.class));
}

동적 서비스 등록

 

 

 

 

 


Unused

4.   Activity <-> Service Communication

MainActivity.java

- Declare & Initialize this field(member variable)

/** To receive only one event at a time
 * <--onStartCommand() of Service calling multiple times in Android System) */
private boolean isFirstServiceCall = false; 

/** for MediaButtonService receive bluetooth button click event **
 * Our handler for received Intents. This will be called whenever an Intent
 * with an action named <G.MEDIA_BUTTON_CLICK_EVENT> is broad-casted. 
 * G means global. It has only public static fields. */
private BroadcastReceiver mediaButtonBroadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        isFirstServiceCall=!isFirstServiceCall;
        Log.i(TAG, "⭐️️, mediaButtonBroadcastReceiver on!!! " + isFirstServiceCall);
        if(accFile.isMultiCamVideoType() && isFirstServiceCall) ((AcControlPanel) controlView).onDoubleTap();
    }
};

- Regist & Unregist the your BroadcastReceiver in LifeCycle method of Activity

@Override
protected void onResume() {
    ...

    LocalBroadcastManager.getInstance(this).registerReceiver(
            mediaButtonBroadcastReceiver, new IntentFilter(G.MEDIA_BUTTON_CLICK_EVENT));
}

@Override
protected void onPause() {
    ...

    LocalBroadcastManager.getInstance(this).unregisterReceiver(mediaButtonBroadcastReceiver);
}

MediaButtonService.java

- send broadcast in onStartCommand

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    MediaButtonReceiver.handleIntent(null, intent); //first parameter should be null
    String event = intent.getAction();
        Intent serviceIntent = new Intent(AcC.MEDIA_BUTTON_CLICK_EVENT);
    if(event!=null) {
        Log.i(TAG, "❤️, media button event!!!! " + event);
        serviceIntent.putExtra(AcC.MEDIA_BUTTON_CLICK_EVENT, "MediaButton Clicked!");
    }
    LocalBroadcastManager.getInstance(this).sendBroadcast(serviceIntent);
    return super.onStartCommand(intent, flags, startId);
}

 

 

 

 

 

 

 

 


Reference.

https://jhshjs.tistory.com/48

 

[Android Service] 안드로이드 서비스와 바인딩 개념 및 기본 예제 코드

안드로이드 개발 공부 Android Service 서비스와 바인딩 개념 및 기본 예제 코드 1. 서비스란? Service는 백그라운드 작업을 위한 애플리케이션 구성 요소이다. Activity와 비교하면 이해하기 쉽다. Activity

jhshjs.tistory.com

https://daldalhanstory.tistory.com/186

 

android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord

오늘은 살짝 힘들었다. 만들고 있는 앱이 계속 말썽을 부렸기 때문이다. 기존에 프래그먼트 A 위에 뷰 페이저를 통해 프래그먼트 B를 올렸는데, 프래그먼트 A에서 B에 있던 버튼을 참조해서 버튼

daldalhanstory.tistory.com

https://github.com/tutsplus/background-audio-in-android-with-mediasessioncompat/blob/master/app/src/main/java/com/tutsplus/backgroundaudio/BackgroundAudioService.java

 

GitHub - tutsplus/background-audio-in-android-with-mediasessioncompat

Contribute to tutsplus/background-audio-in-android-with-mediasessioncompat development by creating an account on GitHub.

github.com

https://goodtogreate.tistory.com/entry/Activity와-Service간의-통신

 

Activity와 Service간의 통신

Activity와 Service간의 통신 Local Broadcast Manager를 이용한 방법 (BR) 그냥 broadcast receiver 를 등록해서 사용하는 방법은 다른앱에서도 해당 방송을 들을 수 있기 때문에 private data leak 문제가 생긴..

goodtogreate.tistory.com

https://code.tutsplus.com/tutorials/background-audio-in-android-with-mediasessioncompat--cms-27030

 

Background Audio in Android With MediaSessionCompat

One of the most popular uses for mobile devices is playing back audio through music streaming services, downloaded podcasts, or any other number of audio sources. While this is a fairly common...

code.tutsplus.com

https://developer.android.com/guide/topics/media-apps/mediabuttons?hl=ko#mediabuttons-and-active-mediasessions 

 

미디어 버튼에 응답  |  Android 개발자  |  Android Developers

미디어 버튼에 응답 미디어 버튼은 블루투스 헤드셋의 일시중지/재생 버튼 등 Android 기기 및 기타 주변기기에서 볼 수 있는 하드웨어 버튼입니다. 사용자가 미디어 버튼을 누르면 Android는 KeyEvent

developer.android.com

https://social.msdn.microsoft.com/Forums/en-US/b3bd1acf-fd0e-4d0a-906c-b2dbb9deda81/onstartcommand-calling-multiple-time-how-to-solve-this?forum=xamarinforms 

 

OnStartCommand calling multiple time how to solve this?

User371688 posted Whenever we start a service from any activity , Android system calls the onStartCommand() method of the service. And if the service is not running, the system will first call onCreate() method, and then call onStartCommand() method. 1. Th

social.msdn.microsoft.com

 

https://stackoverflow.com/questions/12758681/android-how-can-i-put-my-notification-on-top-of-notification-area

 

Android: How can I put my notification on top of notification area?

I'm trying to put my notification on top of notification area. A solution is to set the parameter "when" to my notification object with a future time like: notification.when = System.

stackoverflow.com

https://stackoverflow.com/questions/63577592/how-to-detect-and-override-media-button-key-events-on-bluetooth-headset

 

How to detect and override media button key events on bluetooth headset?

Okay so I know many similar questions have been asked but nothing is working so far. I have tried all the mediaSessionCompat methods and callbacks but to no luck. I have gone through the documentat...

stackoverflow.com

https://www.youtube.com/watch?v=FBC1FgWe5X4