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ではなく、Media3ExoPlayerを使用します。

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

Media3のExoPlayerを使用する

Media3ExoPlayerを使用するには、モジュールの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)