2022. 6. 27. 16:44ㆍAndroid
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
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://daldalhanstory.tistory.com/186
https://goodtogreate.tistory.com/entry/Activity와-Service간의-통신
https://code.tutsplus.com/tutorials/background-audio-in-android-with-mediasessioncompat--cms-27030
https://www.youtube.com/watch?v=FBC1FgWe5X4
'Android' 카테고리의 다른 글
Coil 라이브러리를 사용하여 Jetpack Compose에 Gif 이미지 넣기 (1) | 2024.01.03 |
---|---|
[Android] MediaBrowserServiceCompat이용하여 MediaButtonClickEvent 구현 (feat. AudioFocus받아오기) (0) | 2022.08.17 |
[Kotlin] Android에서 Kotlin과 JAVA연결 (0) | 2022.01.24 |
[Android] Exoplayer with Seekbar example in VR Player / VR에서 ExoPlayer 영상 재생 시간 SeekBar로 컨트롤 (0) | 2022.01.11 |
[Android & OpenGL] Click or Touch없이 Button Event 효과 구현 (0) | 2022.01.05 |