こんにちは、まっさん(@Tera_Msaki)です。
この記事は Androidスマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、記事の最後で紹介します)
Android 13(API レベル 33)では、音楽ファイルのジャケット画像を通知に全面で表示したり、リズムに合わせて動く再生バーなど、新しいメディアコントロールを採用しています。
このため、従来の通知で使用していたMediaStyleのアクションは、Android 13では動作しなくなっています。
音楽ファイルのジャケット画像について、詳しく知りたい方はこちらです↓↓↓
Android 13 の新しいメディアコントロールを使用する
◎ポイント
Android 13の新しいメディアコントロールを使用するには、Media3のメディアアプリのアーキテクチャに準ずる必要があり、動画や音楽を扱う場合に使用するExoPlayerを使ったアプリでは、Media3のメディアセッションに対応する必要があります。
具体的には、オープンソース プロジェクトのExoPlayerではなく、Media3のExoPlayerを使用します。
Media3のExoPlayerを使用する
Media3のExoPlayerを使用するには、モジュールのbuild.gradleファイルに定義の追加が必要です。
◎build.gradle(モジュール)
2023年3月現在の最新バージョンは1.0.0です。
dependencies {
:
implementation 'androidx.media3:media3-exoplayer:1.0.0-rc01'
implementation 'androidx.media3:media3-ui:1.0.0-rc01'
implementation 'androidx.media3:media3-session:1.0.0-rc01'
}
Android 13で新しいメディアコントールを通知で使用するには、マニフェストファイル(AndroidManifest.xml)に権限の追加と、ユーザ承認リクエストが必要です。
Android 13の通知に関する権限の追加とユーザ承認リクエストの実装について、詳しく知りたい方はこちらです↓↓↓
Media3のExoPlayerについて、詳しく知りたい方はこちらです↓↓↓
ExoPlayerで曲戻し・曲送りをコントロールする
Media3のMediaSessionを使用することで、新しいメディアコントロールがMediaSessionの状態に応じて、曲戻し・曲送りボタンを表示、制御するようになります。
しかし、ExoPlayerで曲戻し・曲送りをコントロールしたい場合、少し工夫が必要です。
ExoPlayerのインスタンス化
オープンソース プロジェクトのExoPlayerと、Media3のExoPlayerの実装上の違いはそれほどありません。
private NotificationManager notificationManager;
private MediaSession mediaSession;
private androidx.media3.session.MediaStyleNotificationHelper.MediaStyle
mediaStyle;
private ExoPlayer exoPlayer;
:
@Override
public void onCreate() {
super.onCreate();
:
// EXOPLAYER
exoPlayer = new ExoPlayer.Builder(context)
.setHandleAudioBecomingNoisy(true)
.build();
exoPlayer.addListener(new Player.Listener() {
@Override
public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
List<Integer> eventList = new ArrayList<>();
for (int i = 0 ; i < events.size(); i++)
eventList.add(events.get(i));
if (eventList.size() > 2) {
if (eventList.get(0) == 4 && eventList.get(1) == 7 && eventList.get(2) == 11 && exoPlayer.getContentPosition() == 0) {
// SEEK_TO_PREVIOUS
Intent intent = new Intent("SEND_MESSAGE");
intent.putExtra("MUSIC", RWD);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
}
Player.Listener.super.onEvents(player, events);
}
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
if (exoPlayer.getCurrentMediaItemIndex() == 1) {
// SEEK_TO_NEXT
Intent intent = new Intent("SEND_MESSAGE");
intent.putExtra("MUSIC", FWD);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
Player.Listener.super.onMediaItemTransition(mediaItem, reason);
}
});
mediaSession = new MediaSession.Builder(context, exoPlayer).build();
:
setHandleAudioBecomingNoisyはイヤホンなどの外部スピーカーが外れた時に、再生を停止したい場合はTrueを指定します。
曲戻し・曲送りを検出するには、addListenerでPlayerリスナーを追加します。
Playerリスナーでは、曲戻しはonEventsを使用して、曲戻しを行った際に発生するイベントの発生順を判定に使用しています。
曲送りはonMediaItemTransitionを使用して、MediaItemのインデックスを判定に使用しています。
Playerリスナーには、これ以外にオーバライドできるメソッドがありますので、用途に応じた実装を行えばよいでしょう。
曲送りでonPlaybackStateChangedを使用する場合の注意点として、MediaItemのすべてが終了した場合でしか、Player.STATE_ENDEDが発生しないことです。
また、MediaItemの最後を再生している場合、新しいメディアコントロールの曲送りは非表示になります。
Android12以前の対応
新しいメディアコントロールは、Android 12 以前では使用できないため、MediaStyleを使って、通知を実装する必要があります。
private NotificationManager notificationManager;
private MediaSession mediaSession;
private androidx.media3.session.MediaStyleNotificationHelper.MediaStyle
mediaStyle;
private PendingIntent pendingIntentList;
private static final String TAG = service.class.getSimpleName();
:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
:
// mediaStyle
mediaStyle = new MediaStyleNotificationHelper.MediaStyle(mediaSession);
mediaStyle.setShowActionsInCompactView(0,1,2);
// PendingIntentList
pendingIntentList = PendingIntent.getActivity(context, REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE);
// Notification
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("Silent Notification");
channel.setSound(null, null);
channel.enableLights(false);
channel.setLightColor(R.color.blue);
channel.enableVibration(false);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
Notification notification;
notification = new NotificationCompat.Builder(context, TAG)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(TAG)
.setContentIntent(pendingIntentList)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_timer)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.logo))
.setStyle(mediaStyle)
.addAction(new NotificationCompat.Action(R.drawable.ic_round_rwd, RWD, CustomMediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)))
.addAction(pause ? new NotificationCompat.Action(R.drawable.ic_round_play, PLAY, CustomMediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY)) :
new NotificationCompat.Action(R.drawable.ic_round_pause, PAUSE, CustomMediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PAUSE)))
.addAction(new NotificationCompat.Action(R.drawable.ic_round_fwd, FWD, CustomMediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_SKIP_TO_NEXT)))
.build();
startForeground(ID, notification);
}
:
return START_NOT_STICKY;
}
Media3のMediaSession を使用する場合、MediaStyleは、MediaStyleNotificationHelperを使用します。
それ以外については、従来と同じ実装です。
MediaButtonReceiver の実装
メディアコントロールの操作または、通知のアクションをハンドリングするには、MediaButtonReceiverを実装する必要があります。
マニフェストファイル(AndroidManifest.xml)にreceiverの使用を登録します。
:
<receiver android:name=".CustomMediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
:
MediaButtonReceiverを継承したCustomMediaButtonReceiverクラスを実装します。
:
public class CustomMediaButtonReceiver extends MediaButtonReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) && intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (keyEvent.getKeyCode() == PlaybackStateCompat.toKeyCode(PlaybackStateCompat.ACTION_PLAY)) {
// ACTION_PLAY
intent = new Intent("RECEIVE_MESSAGE");
intent.putExtra("COMMAND", PLAY);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
} else if (keyEvent.getKeyCode() == PlaybackStateCompat.toKeyCode(PlaybackStateCompat.ACTION_PAUSE)) {
// ACTION_PAUSE
intent = new Intent("RECEIVE_MESSAGE");
intent.putExtra("COMMAND", PAUSE);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
} else if (keyEvent.getKeyCode() == PlaybackStateCompat.toKeyCode(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
// ACTION_SKIP_TO_PREVIOUS
intent = new Intent("SEND_MESSAGE");
intent.putExtra("MUSIC", RWD);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
} else if (keyEvent.getKeyCode() == PlaybackStateCompat.toKeyCode(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
// ACTION_SKIP_TO_NEXT
intent = new Intent("SEND_MESSAGE");
intent.putExtra("MUSIC", FWD);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
}
}
}
MediaButtonReceiverでは、Intentでセットされたアクションを取り出し、アクションに応じた処理を実装します。
上記のCustomMediaButtonReceiverでは、LocalBroadcastManagerを使用して、メッセージを通知するだけで、処理自体は通知先で行っています。
メッセージの通知先は、曲送り・曲戻しを処理する画面(Activity)と、再生・一時停止を処理するサービス(Service)に分けています。
◎メッセージ受信(Servise)
:
// ======================================================================
// メッセージ受信
// ======================================================================
if (broadcastReceiver == null) {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void on