DefaultMediaNotificationProvider.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */
package androidx.media3.session;

import static androidx.media3.common.util.Assertions.checkStateNotNull;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;

/**
 * The default {@link MediaNotification.Provider}.
 *
 * <h2>Actions</h2>
 *
 * The following actions are included in the provided notifications:
 *
 * <ul>
 *   <li>{@link MediaNotification.ActionFactory#COMMAND_PLAY} to start playback. Included when
 *       {@link MediaController#getPlayWhenReady()} returns {@code false}.
 *   <li>{@link MediaNotification.ActionFactory#COMMAND_PAUSE}, to pause playback. Included when
 *       ({@link MediaController#getPlayWhenReady()} returns {@code true}.
 *   <li>{@link MediaNotification.ActionFactory#COMMAND_SKIP_TO_PREVIOUS} to skip to the previous
 *       item.
 *   <li>{@link MediaNotification.ActionFactory#COMMAND_SKIP_TO_NEXT} to skip to the next item.
 * </ul>
 *
 * <h2>Drawables</h2>
 *
 * The drawables used can be overridden by drawables with the same names defined the application.
 * The drawables are:
 *
 * <ul>
 *   <li><b>{@code media3_notification_play}</b> - The play icon.
 *   <li><b>{@code media3_notification_pause}</b> - The pause icon.
 *   <li><b>{@code media3_notification_seek_to_previous}</b> - The previous icon.
 *   <li><b>{@code media3_notification_seek_to_next}</b> - The next icon.
 * </ul>
 */
@UnstableApi
/* package */ final class DefaultMediaNotificationProvider implements MediaNotification.Provider {

  private static final int NOTIFICATION_ID = 1001;
  private static final String NOTIFICATION_CHANNEL_ID = "default_channel_id";
  private static final String NOTIFICATION_CHANNEL_NAME = "Now playing";

  private final Context context;
  private final NotificationManager notificationManager;

  /** Creates an instance. */
  public DefaultMediaNotificationProvider(Context context) {
    this.context = context.getApplicationContext();
    notificationManager =
        checkStateNotNull(
            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
  }

  @Override
  public MediaNotification createNotification(
      MediaController mediaController,
      MediaNotification.ActionFactory actionFactory,
      Callback onNotificationChangedCallback) {
    ensureNotificationChannel();

    NotificationCompat.Builder builder =
        new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
    // TODO(b/193193926): Filter actions depending on the player's available commands.
    // Skip to previous action.
    builder.addAction(
        actionFactory.createMediaAction(
            IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_previous),
            context.getString(R.string.media3_controls_seek_to_previous_description),
            MediaNotification.ActionFactory.COMMAND_SKIP_TO_PREVIOUS));
    if (mediaController.getPlaybackState() == Player.STATE_ENDED
        || !mediaController.getPlayWhenReady()) {
      // Play action.
      builder.addAction(
          actionFactory.createMediaAction(
              IconCompat.createWithResource(context, R.drawable.media3_notification_play),
              context.getString(R.string.media3_controls_play_description),
              MediaNotification.ActionFactory.COMMAND_PLAY));
    } else {
      // Pause action.
      builder.addAction(
          actionFactory.createMediaAction(
              IconCompat.createWithResource(context, R.drawable.media3_notification_pause),
              context.getString(R.string.media3_controls_pause_description),
              MediaNotification.ActionFactory.COMMAND_PAUSE));
    }
    // Skip to next action.
    builder.addAction(
        actionFactory.createMediaAction(
            IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next),
            context.getString(R.string.media3_controls_seek_to_next_description),
            MediaNotification.ActionFactory.COMMAND_SKIP_TO_NEXT));

    // Set metadata info in the notification.
    MediaMetadata metadata = mediaController.getMediaMetadata();
    builder.setContentTitle(metadata.title).setContentText(metadata.artist);
    if (metadata.artworkData != null) {
      Bitmap artworkBitmap =
          BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length);
      builder.setLargeIcon(artworkBitmap);
    }

    androidx.media.app.NotificationCompat.MediaStyle mediaStyle =
        new androidx.media.app.NotificationCompat.MediaStyle()
            .setCancelButtonIntent(
                actionFactory.createMediaActionPendingIntent(
                    MediaNotification.ActionFactory.COMMAND_STOP))
            .setShowActionsInCompactView(1 /* Show play/pause button only in compact view */);

    Notification notification =
        builder
            .setContentIntent(mediaController.getSessionActivity())
            .setDeleteIntent(
                actionFactory.createMediaActionPendingIntent(
                    MediaNotification.ActionFactory.COMMAND_STOP))
            .setOnlyAlertOnce(true)
            .setSmallIcon(getSmallIconResId(context))
            .setStyle(mediaStyle)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
            .setOngoing(false)
            .build();
    return new MediaNotification(NOTIFICATION_ID, notification);
  }

  @Override
  public void handleCustomAction(MediaController mediaController, String action, Bundle extras) {
    // We don't handle custom commands.
  }

  private void ensureNotificationChannel() {
    if (Util.SDK_INT < 26
        || notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null) {
      return;
    }
    NotificationChannel channel =
        new NotificationChannel(
            NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
    notificationManager.createNotificationChannel(channel);
  }

  private static int getSmallIconResId(Context context) {
    int appIcon = context.getApplicationInfo().icon;
    if (appIcon != 0) {
      return appIcon;
    } else {
      return Util.SDK_INT >= 21 ? R.drawable.media_session_service_notification_ic_music_note : 0;
    }
  }
}