DefaultActionFactory.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 android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT;
import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS;
import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND;
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
import static android.view.KeyEvent.KEYCODE_UNKNOWN;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;

import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;

/** The default {@link MediaNotification.ActionFactory}. */
@UnstableApi
/* package */ final class DefaultActionFactory implements MediaNotification.ActionFactory {

  private static final String ACTION_CUSTOM = "androidx.media3.session.CUSTOM_NOTIFICATION_ACTION";
  private static final String EXTRAS_KEY_ACTION_CUSTOM =
      "androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION";
  public static final String EXTRAS_KEY_ACTION_CUSTOM_EXTRAS =
      "androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION_EXTRAS";

  private final Service service;

  private int customActionPendingIntentRequestCode = 0;

  public DefaultActionFactory(Service service) {
    this.service = service;
  }

  @Override
  public NotificationCompat.Action createMediaAction(
      MediaSession mediaSession, IconCompat icon, CharSequence title, @Player.Command int command) {
    return new NotificationCompat.Action(
        icon, title, createMediaActionPendingIntent(mediaSession, command));
  }

  @Override
  public NotificationCompat.Action createCustomAction(
      MediaSession mediaSession,
      IconCompat icon,
      CharSequence title,
      String customAction,
      Bundle extras) {
    return new NotificationCompat.Action(
        icon, title, createCustomActionPendingIntent(mediaSession, customAction, extras));
  }

  @Override
  public NotificationCompat.Action createCustomActionFromCustomCommandButton(
      MediaSession mediaSession, CommandButton customCommandButton) {
    checkArgument(
        customCommandButton.sessionCommand != null
            && customCommandButton.sessionCommand.commandCode
                == SessionCommand.COMMAND_CODE_CUSTOM);
    SessionCommand customCommand = checkNotNull(customCommandButton.sessionCommand);
    return new NotificationCompat.Action(
        IconCompat.createWithResource(service, customCommandButton.iconResId),
        customCommandButton.displayName,
        createCustomActionPendingIntent(
            mediaSession, customCommand.customAction, customCommand.customExtras));
  }

  @Override
  public PendingIntent createMediaActionPendingIntent(
      MediaSession mediaSession, @Player.Command long command) {
    int keyCode = toKeyCode(command);
    Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    intent.setData(mediaSession.getImpl().getUri());
    intent.setComponent(new ComponentName(service, service.getClass()));
    intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
    if (Util.SDK_INT >= 26
        && command == COMMAND_PLAY_PAUSE
        && !mediaSession.getPlayer().getPlayWhenReady()) {
      return Api26.createForegroundServicePendingIntent(service, keyCode, intent);
    } else {
      return PendingIntent.getService(
          service,
          /* requestCode= */ keyCode,
          intent,
          Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0);
    }
  }

  private int toKeyCode(@Player.Command long action) {
    if (action == COMMAND_SEEK_TO_NEXT_MEDIA_ITEM || action == COMMAND_SEEK_TO_NEXT) {
      return KEYCODE_MEDIA_NEXT;
    } else if (action == COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
        || action == COMMAND_SEEK_TO_PREVIOUS) {
      return KEYCODE_MEDIA_PREVIOUS;
    } else if (action == Player.COMMAND_STOP) {
      return KEYCODE_MEDIA_STOP;
    } else if (action == COMMAND_SEEK_FORWARD) {
      return KEYCODE_MEDIA_FAST_FORWARD;
    } else if (action == COMMAND_SEEK_BACK) {
      return KEYCODE_MEDIA_REWIND;
    } else if (action == COMMAND_PLAY_PAUSE) {
      return KEYCODE_MEDIA_PLAY_PAUSE;
    }
    return KEYCODE_UNKNOWN;
  }

  private PendingIntent createCustomActionPendingIntent(
      MediaSession mediaSession, String action, Bundle extras) {
    Intent intent = new Intent(ACTION_CUSTOM);
    intent.setData(mediaSession.getImpl().getUri());
    intent.setComponent(new ComponentName(service, service.getClass()));
    intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM, action);
    intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS, extras);
    // Custom actions always start the service in the background.
    return PendingIntent.getService(
        service,
        /* requestCode= */ ++customActionPendingIntentRequestCode,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT
            | (Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
  }

  /** Returns whether {@code intent} was part of a {@link #createMediaAction media action}. */
  public boolean isMediaAction(Intent intent) {
    return Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction());
  }

  /** Returns whether {@code intent} was part of a {@link #createCustomAction custom action }. */
  public boolean isCustomAction(Intent intent) {
    return ACTION_CUSTOM.equals(intent.getAction());
  }

  /**
   * Returns the {@link KeyEvent} that was included in the media action, or {@code null} if no
   * {@link KeyEvent} is found in the {@code intent}.
   */
  @Nullable
  public KeyEvent getKeyEvent(Intent intent) {
    @Nullable Bundle extras = intent.getExtras();
    if (extras != null && extras.containsKey(Intent.EXTRA_KEY_EVENT)) {
      return extras.getParcelable(Intent.EXTRA_KEY_EVENT);
    }
    return null;
  }

  /**
   * Returns the custom action that was included in the {@link #createCustomAction custom action},
   * or {@code null} if no custom action is found in the {@code intent}.
   */
  @Nullable
  public String getCustomAction(Intent intent) {
    @Nullable Bundle extras = intent.getExtras();
    @Nullable Object customAction = extras != null ? extras.get(EXTRAS_KEY_ACTION_CUSTOM) : null;
    return customAction instanceof String ? (String) customAction : null;
  }

  /**
   * Returns extras that were included in the {@link #createCustomAction custom action}, or {@link
   * Bundle#EMPTY} is no extras are found.
   */
  public Bundle getCustomActionExtras(Intent intent) {
    @Nullable Bundle extras = intent.getExtras();
    @Nullable
    Object customExtras = extras != null ? extras.get(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS) : null;
    return customExtras instanceof Bundle ? (Bundle) customExtras : Bundle.EMPTY;
  }

  @RequiresApi(26)
  private static final class Api26 {
    private Api26() {}

    public static PendingIntent createForegroundServicePendingIntent(
        Service service, int keyCode, Intent intent) {
      return PendingIntent.getForegroundService(
          service, /* requestCode= */ keyCode, intent, PendingIntent.FLAG_IMMUTABLE);
    }
  }
}