Androidアプリ開発

Android13対応(メディアコントロール)

この記事は約38分で読めます。
スポンサーリンク

※当サイトではアフィリエイト広告を利用しています

こんにちは、まっさん(@Tera_Msaki)です。

この記事は Androidスマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


Android13で新しくなったメディアコントロールに対応する

Android 13(API レベル 33)では、
音楽ファイルの
ジャケット画像通知に全面で表示したり、
リズムに合わせて動く再生バーなど、新しいメディアコントロールを採用しています。
このため、従来の通知で使用していた
MediaStyleのアクションは、
Android 13 では動作しなくなっています。

Android 13 で採用された新しいメディアコントロール

Android 13 の新しいメディアコントロールでは、
通知( Notification )のアクション( addAction )は無効(非表示)になります。

音楽ファイルのジャケット画像 について、詳しく知りたい方はこちらです↓↓↓

Android 13 の新しいメディアコントロールを使用する

◎ポイント
Android 13 の新しいメディアコントロールを使用するには、
Media3 のメディアアプリのアーキテクチャに準ずる必要があり、
動画や音楽を扱う場合に使用する
ExoPlayerを使ったアプリでは、
Media3のメディアセッションに対応する必要があります。
具体的には、オープンソース プロジェクトの
ExoPlayerではなく、
Media3 ExoPlayerを使用します。

Android13の新しいメディアコントロールで曲送りと曲戻りを操作

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 の通知に関する権限の追加 ユーザ承認リクエスト の実装について、
詳しく知りたい方はこちらです↓↓↓

Media3ExoPlayerについて、詳しく知りたい方はこちらです↓↓↓

ExoPlayer で曲戻し・曲送りをコントロールする

Media3MediaSessionを使用することで、
新しいメディアコントロールが
MediaSessionの状態に応じて、
曲戻し・曲送りボタンを表示、制御するようになります。
しかし、
ExoPlayer曲戻し・曲送りをコントロールしたい場合、
少し工夫が必要です。

ExoPlayerのインスタンス化

オープンソース プロジェクトのExoPlayerと、
Media3ExoPlayer の実装上の違いはそれほどありません。

    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を指定します。
曲戻し・曲送りを検出するには、
addListenerPlayerリスナーを追加します。
Playerリスナーでは、曲戻しはonEventsを使用して、
曲戻しを行った際に発生するイベントの発生順を判定に使用しています。
曲送りは
onMediaItemTransitionを使用して、
MediaItemのインデックスを判定に使用しています。
Playerリスナーには、これ以外にオーバライドできるメソッドがありますので、
用途に応じた実装を行えばよいでしょう。
曲送りで
onPlaybackStateChangedを使用する場合の注意点として、
MediaItemのすべてが終了した場合でしか、
Player.STATE_ENDEDが発生しないことです。
また、
MediaItemの最後を再生している場合、
新しいメディアコントロールの曲送りは非表示になります。

Android12以前の対応

新しいメディアコントロールは、Android 12 以前では使用できないため、
MediaStyleを使って、通知を実装する必要があります。

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;
    }

Media3MediaSessionを使用する場合、
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# などのオブジェクト指向言語が初めての方は、
書籍などによる独学ではアプリ開発できるようになるには、
かなりの時間がかかりますので、オンラインスクールでの習得をおススメします。

未経験者からシステムエンジニアを目指すのに最適かと、
まずは無料相談から♪

無料でJava言語を学べるのは、かなり魅力的♪
でも、応募資格は35歳以下です、、、
2022年12月から説明会が土曜日開催が追加されていますよ♪
説明会では、希望者に対してプログラミング体験もできるよ♪

未経験者からプログラマーを目指すのに最適かと、
まずは無料カウンセリングから♪

カリキュラムとサポートがしっかりしています、
お得なキャンペーンとかいろいろやっています♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適かと、
まずは無料オンライン相談から♪

参考になったら、💛をポッチとしてね♪

コメント欄