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 android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
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.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;

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

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

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

  @Override
  public PendingIntent createMediaActionPendingIntent(@Command long command) {
    int keyCode = PlaybackStateCompat.toKeyCode(command);
    Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    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_PAUSE && command != COMMAND_STOP) {
      return Api26.createPendingIntent(service, /* requestCode= */ keyCode, intent);
    } else {
      return PendingIntent.getService(
          service,
          /* requestCode= */ keyCode,
          intent,
          Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0);
    }
  }

  private PendingIntent createCustomActionPendingIntent(String action, Bundle extras) {
    Intent intent = new Intent(ACTION_CUSTOM);
    intent.setComponent(new ComponentName(service, service.getClass()));
    intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM, action);
    intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS, extras);
    if (Util.SDK_INT >= 26) {
      return Api26.createPendingIntent(
          service, /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, intent);
    } else {
      return PendingIntent.getService(
          service,
          /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN,
          intent,
          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 createPendingIntent(Service service, int keyCode, Intent intent) {
      return PendingIntent.getForegroundService(
          service, /* requestCode= */ keyCode, intent, PendingIntent.FLAG_IMMUTABLE);
    }
  }
}