TrackSelectionDialogBuilder.java

/*
 * Copyright (C) 2019 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.ui;

import static androidx.media3.common.Player.COMMAND_GET_TRACKS;
import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Player;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/** Builder for a dialog with a {@link TrackSelectionView}. */
@UnstableApi
public final class TrackSelectionDialogBuilder {

  /** Callback which is invoked when a track selection has been made. */
  public interface DialogCallback {

    /**
     * Called when tracks are selected.
     *
     * @param isDisabled Whether the disabled option is selected.
     * @param overrides The selected track overrides.
     */
    void onTracksSelected(boolean isDisabled, Map<TrackGroup, TrackSelectionOverride> overrides);
  }

  private final Context context;
  private final CharSequence title;
  private final List<Tracks.Group> trackGroups;
  private final DialogCallback callback;

  @StyleRes private int themeResId;
  private boolean allowAdaptiveSelections;
  private boolean allowMultipleOverrides;
  private boolean showDisableOption;
  @Nullable private TrackNameProvider trackNameProvider;
  private boolean isDisabled;
  private ImmutableMap<TrackGroup, TrackSelectionOverride> overrides;
  @Nullable private Comparator<Format> trackFormatComparator;

  /**
   * Creates a builder for a track selection dialog.
   *
   * @param context The context of the dialog.
   * @param title The title of the dialog.
   * @param trackGroups The {@link Tracks.Group track groups}.
   * @param callback The {@link DialogCallback} invoked when a track selection has been made.
   */
  public TrackSelectionDialogBuilder(
      Context context,
      CharSequence title,
      List<Tracks.Group> trackGroups,
      DialogCallback callback) {
    this.context = context;
    this.title = title;
    this.trackGroups = ImmutableList.copyOf(trackGroups);
    this.callback = callback;
    overrides = ImmutableMap.of();
  }

  /**
   * Creates a builder for a track selection dialog.
   *
   * @param context The context of the dialog.
   * @param title The title of the dialog.
   * @param player The {@link Player} whose tracks should be selected.
   * @param trackType The type of tracks to show for selection.
   */
  public TrackSelectionDialogBuilder(
      Context context, CharSequence title, Player player, @C.TrackType int trackType) {
    this.context = context;
    this.title = title;
    Tracks tracks =
        player.isCommandAvailable(COMMAND_GET_TRACKS) ? player.getCurrentTracks() : Tracks.EMPTY;
    List<Tracks.Group> allTrackGroups = tracks.getGroups();
    trackGroups = new ArrayList<>();
    for (int i = 0; i < allTrackGroups.size(); i++) {
      Tracks.Group trackGroup = allTrackGroups.get(i);
      if (trackGroup.getType() == trackType) {
        trackGroups.add(trackGroup);
      }
    }
    overrides = player.getTrackSelectionParameters().overrides;
    callback =
        (isDisabled, overrides) -> {
          if (!player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
            return;
          }
          TrackSelectionParameters.Builder parametersBuilder =
              player.getTrackSelectionParameters().buildUpon();
          parametersBuilder.setTrackTypeDisabled(trackType, isDisabled);
          parametersBuilder.clearOverridesOfType(trackType);
          for (TrackSelectionOverride override : overrides.values()) {
            parametersBuilder.addOverride(override);
          }
          player.setTrackSelectionParameters(parametersBuilder.build());
        };
  }

  /**
   * Sets the resource ID of the theme used to inflate this dialog.
   *
   * @param themeResId The resource ID of the theme.
   * @return This builder, for convenience.
   */
  public TrackSelectionDialogBuilder setTheme(@StyleRes int themeResId) {
    this.themeResId = themeResId;
    return this;
  }

  /**
   * Sets whether the selection is initially shown as disabled.
   *
   * @param isDisabled Whether the selection is initially shown as disabled.
   * @return This builder, for convenience.
   */
  public TrackSelectionDialogBuilder setIsDisabled(boolean isDisabled) {
    this.isDisabled = isDisabled;
    return this;
  }

  /**
   * Sets the single initial override.
   *
   * @param override The initial override, or {@code null} for no override.
   * @return This builder, for convenience.
   */
  public TrackSelectionDialogBuilder setOverride(@Nullable TrackSelectionOverride override) {
    return setOverrides(
        override == null
            ? Collections.emptyMap()
            : ImmutableMap.of(override.mediaTrackGroup, override));
  }

  /**
   * Sets the initial track overrides. Any overrides that do not correspond to track groups that
   * were passed to the constructor will be ignored. If {@link #setAllowMultipleOverrides(boolean)}
   * hasn't been set to {@code true} then all but one override will be ignored. The retained
   * override will be the one whose track group was first in the list of track groups passed to the
   * constructor.
   *
   * @param overrides The initially selected track overrides.
   * @return This builder, for convenience.
   */
  public TrackSelectionDialogBuilder setOverrides(
      Map<TrackGroup, TrackSelectionOverride> overrides) {
    this.overrides = ImmutableMap.copyOf(overrides);
    return this;
  }

  /**
   * Sets whether adaptive selections (consisting of more than one track) can be made.
   *
   * <p>For the selection view to enable adaptive selection it is necessary both for this feature to
   * be enabled, and for the target renderer to support adaptation between the available tracks.
   *
   * @param allowAdaptiveSelections Whether adaptive selection is enabled.
   * @return This builder, for convenience.
   */
  public TrackSelectionDialogBuilder setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {
    this.allowAdaptiveSelections = allowAdaptiveSelections;
    return this;
  }

  /**
   * Sets whether multiple overrides can be set and selected, i.e. tracks from multiple track groups
   * can be selected.
   *
   * @param allowMultipleOverrides Whether multiple track selection overrides are allowed.
   * @return This builder, for convenience.
   */
  public TrackSelectionDialogBuilder setAllowMultipleOverrides(boolean allowMultipleOverrides) {
    this.allowMultipleOverrides = allowMultipleOverrides;
    return this;
  }

  /**
   * Sets whether an option is available for disabling the renderer.
   *
   * @param showDisableOption Whether the disable option is shown.
   * @return This builder, for convenience.
   */
  public TrackSelectionDialogBuilder setShowDisableOption(boolean showDisableOption) {
    this.showDisableOption = showDisableOption;
    return this;
  }

  /**
   * Sets a {@link Comparator} used to determine the display order of the tracks within each track
   * group.
   *
   * @param trackFormatComparator The comparator, or {@code null} to use the original order.
   */
  public void setTrackFormatComparator(@Nullable Comparator<Format> trackFormatComparator) {
    this.trackFormatComparator = trackFormatComparator;
  }

  /**
   * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and
   * updates the view with track names queried from the specified provider.
   *
   * @param trackNameProvider The {@link TrackNameProvider} to use, or null to use the default.
   */
  public TrackSelectionDialogBuilder setTrackNameProvider(
      @Nullable TrackNameProvider trackNameProvider) {
    this.trackNameProvider = trackNameProvider;
    return this;
  }

  /** Builds the dialog. */
  public Dialog build() {
    @Nullable Dialog dialog = buildForAndroidX();
    return dialog == null ? buildForPlatform() : dialog;
  }

  private Dialog buildForPlatform() {
    AlertDialog.Builder builder = new AlertDialog.Builder(context, themeResId);

    // Inflate with the builder's context to ensure the correct style is used.
    LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
    View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, /* root= */ null);
    Dialog.OnClickListener okClickListener = setUpDialogView(dialogView);

    return builder
        .setTitle(title)
        .setView(dialogView)
        .setPositiveButton(android.R.string.ok, okClickListener)
        .setNegativeButton(android.R.string.cancel, null)
        .create();
  }

  // Reflection calls can't verify null safety of return values or parameters.
  @SuppressWarnings("nullness:argument")
  @Nullable
  private Dialog buildForAndroidX() {
    try {
      // This method uses reflection to avoid a dependency on AndroidX appcompat that adds 800KB to
      // the APK size even with shrinking. See https://issuetracker.google.com/161514204.
      Class<?> builderClazz = Class.forName("androidx.appcompat.app.AlertDialog$Builder");
      Constructor<?> builderConstructor = builderClazz.getConstructor(Context.class, int.class);
      Object builder = builderConstructor.newInstance(context, themeResId);

      // Inflate with the builder's context to ensure the correct style is used.
      Context builderContext = (Context) builderClazz.getMethod("getContext").invoke(builder);
      LayoutInflater dialogInflater = LayoutInflater.from(builderContext);
      View dialogView =
          dialogInflater.inflate(R.layout.exo_track_selection_dialog, /* root= */ null);
      Dialog.OnClickListener okClickListener = setUpDialogView(dialogView);

      builderClazz.getMethod("setTitle", CharSequence.class).invoke(builder, title);
      builderClazz.getMethod("setView", View.class).invoke(builder, dialogView);
      builderClazz
          .getMethod("setPositiveButton", int.class, DialogInterface.OnClickListener.class)
          .invoke(builder, android.R.string.ok, okClickListener);
      builderClazz
          .getMethod("setNegativeButton", int.class, DialogInterface.OnClickListener.class)
          .invoke(builder, android.R.string.cancel, null);
      return (Dialog) builderClazz.getMethod("create").invoke(builder);
    } catch (ClassNotFoundException e) {
      // Expected if the AndroidX compat library is not available.
      return null;
    } catch (Exception e) {
      throw new IllegalStateException(e);
    }
  }

  private Dialog.OnClickListener setUpDialogView(View dialogView) {
    TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view);
    selectionView.setAllowMultipleOverrides(allowMultipleOverrides);
    selectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
    selectionView.setShowDisableOption(showDisableOption);
    if (trackNameProvider != null) {
      selectionView.setTrackNameProvider(trackNameProvider);
    }
    selectionView.init(
        trackGroups, isDisabled, overrides, trackFormatComparator, /* listener= */ null);
    return (dialog, which) ->
        callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides());
  }
}