こんにちは、まっさん(@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 onReceive(Context context, Intent intent1) {
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
Notification notification;
String command = intent1.getStringExtra("COMMAND");
byte[] binary;
switch (command) {
case "MUSIC": // 曲レスポンス(Uri)
mount = intent1.getStringExtra("VALUE");
musicUri = Uri.parse(mount);
List<MediaItem> mediaItemList = new ArrayList<>();
mediaItemList.add(MediaItem.fromUri(musicUri));
// Android 13対応で SEEK_TO_NEXT の アイコンを出すため
Uri silence = Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE
+ File.pathSeparator + File.separator + File.separator
+ context.getPackageName()
+ File.separator
+ R.raw.silence
);
mediaItemList.add(MediaItem.fromUri(silence));
try {
exoPlayer.stop();
exoPlayer.setMediaItems(mediaItemList);
exoPlayer.prepare();
exoPlayer.seekTo(intent1.getLongExtra("CURRENT", 0));
exoPlayer.play();
pause = false;
} catch (Exception e) {
e.printStackTrace();
}
mediaMetadataRetriever.setDataSource(context, musicUri);
binary = mediaMetadataRetriever.getEmbeddedPicture();
notification = new NotificationCompat.Builder(context, TAG)
.setContentTitle(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE))
.setContentText(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST))
.setContentIntent(pendingIntentList)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_timer)
.setLargeIcon(binary != null ? BitmapFactory.decodeByteArray(binary, 0, binary.length) : 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();
notificationManager.notify(ID, notification);
break;
:
case PAUSE: // 中断
case PLAY: // 再生
if (command.equals(PAUSE)) {
pause = true;
exoPlayer.pause();
} else {
pause = false;
exoPlayer.play();
}
mediaMetadataRetriever.setDataSource(context, musicUri);
binary = mediaMetadataRetriever.getEmbeddedPicture();
notification = new NotificationCompat.Builder(context, TAG)
.setContentTitle(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE))
.setContentText(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST))
.setContentIntent(pendingIntentList)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_timer)
.setLargeIcon(binary != null ? BitmapFactory.decodeByteArray(binary, 0, binary.length) : 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();
notificationManager.notify(ID, notification);
break;
default:
}
}
};
localBroadcastManager = LocalBroadcastManager.getInstance(context);
final IntentFilter filter = new IntentFilter();
filter.addAction("RECEIVE_MESSAGE");
localBroadcastManager.registerReceiver(broadcastReceiver, filter);
:
メディアコントールの再生と一時停止は、メディアコントールが直接メディアセッションを操作しています。
Android 12以前の場合は、通知からのアクションを受けて、再生と一時停止の処理と、通知を更新します。
注意点としては、MediaItemが1つしかない場合、メディアコントロールの曲送りが非表示となるため、MediaItemにダミーを追加する必要があります。
◎メッセージ受信(Activity)
// ======================================================================
// メッセージ受信
// ======================================================================
if (broadcastReceiver == null) {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent1) {
String music = intent1.getStringExtra("MUSIC") != null ? intent1.getStringExtra("MUSIC") : "NONE";
:
Intent intent = new Intent();
switch (music) {
:
default:
if (music.equals(RWD)) {
// SEEK_TO_PREVIOUS
musicPos = musicPos - 1 < 0 ? musicList.size() - 1 : musicPos - 1;
} else {
//SEEK_TO_NEXT
musicPos = musicPos + 1 < musicList.size() ? musicPos + 1 : 0;
}
// 頭出し
if (layoutManager != null) layoutManager.scrollToPositionWithOffset(musicPos, 0);
intent.setAction("RECEIVE_MESSAGE");
intent.putExtra("COMMAND", "MUSIC");
intent.putExtra("VALUE", musicList.get(musicPos).musicUri.toString());
intent.putExtra("CURRENT", 0);
localBroadcastManager.sendBroadcast(intent);
source = musicList.get(musicPos).source;
}
:
}
};
localBroadcastManager = LocalBroadcastManager.getInstance(context);
final IntentFilter filter = new IntentFilter();
filter.addAction("SEND_MESSAGE");
localBroadcastManager.registerReceiver(broadcastReceiver, filter);
}
:
曲送り・曲戻しは、あらかじめプリセットしている曲リストから、MediaItemのセットに必要な 音楽ファイルの Uriを、サービス(Service)にメッセージ通知しています。
今回は、ここまでです。
参考 : PlaybackState から派生するメディア コントロール
Android 13のメディアコントロールに対応しているAndroidアプリです。
誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にあるコメントか、
こちらから、お願いいたします♪
ポチッとして頂けると、
次のコンテンツを作成する励みになります♪
これからAndroidのアプリ開発やJavaでの開発を始めたい方へ
初めての Android のアプリ開発では、アプリケーション開発経験がない方や、
アプリケーション開発経験がある方でも、Java や C# などのオブジェクト指向言語が初めての方は、
書籍などによる独学ではアプリ開発できるようになるには、
かなりの時間がかかりますので、オンラインスクールでの習得をおススメします。
未経験者からシステムエンジニアを目指すのに最適かと、
まずは無料相談から♪
未経験者からプログラマーを目指すのに最適かと、
まずは無料カウンセリングから♪
カリキュラムとサポートがしっかりしています、
お得なキャンペーンとかいろいろやっています♪
ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適かと、
まずは無料オンライン相談から♪
参考になったら、💛をポッチとしてね♪
コメント欄