public class

TrackSelectionView

extends LinearLayout

 java.lang.Object

↳LinearLayout

↳androidx.media3.ui.TrackSelectionView

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-ui', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-ui
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-ui:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

A view for making track selections.

Summary

Constructors
publicTrackSelectionView(Context context)

Creates a track selection view.

publicTrackSelectionView(Context context, AttributeSet attrs)

Creates a track selection view.

publicTrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr)

Creates a track selection view.

Methods
public static java.util.Map<TrackGroup, TrackSelectionOverride>filterOverrides(java.util.Map<TrackGroup, TrackSelectionOverride> overrides, java.util.List<Tracks.Group> trackGroups, boolean allowMultipleOverrides)

Returns the subset of overrides that apply to the specified trackGroups.

public booleangetIsDisabled()

Returns whether the disabled option is selected.

public java.util.Map<TrackGroup, TrackSelectionOverride>getOverrides()

Returns the selected track overrides.

public voidinit(java.util.List<Tracks.Group> trackGroups, boolean isDisabled, java.util.Map<TrackGroup, TrackSelectionOverride> overrides, java.util.Comparator<Format> trackFormatComparator, TrackSelectionView.TrackSelectionListener listener)

Initialize the view to select tracks from a specified list of track groups.

public voidsetAllowAdaptiveSelections(boolean allowAdaptiveSelections)

Sets whether adaptive selections (consisting of more than one track) can be made using this selection view.

public voidsetAllowMultipleOverrides(boolean allowMultipleOverrides)

Sets whether tracks from multiple track groups can be selected.

public voidsetShowDisableOption(boolean showDisableOption)

Sets whether the disabled option can be selected.

public voidsetTrackNameProvider(TrackNameProvider trackNameProvider)

Sets the TrackNameProvider used to generate the user visible name of each track and updates the view with track names queried from the specified provider.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public TrackSelectionView(Context context)

Creates a track selection view.

public TrackSelectionView(Context context, AttributeSet attrs)

Creates a track selection view.

public TrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr)

Creates a track selection view.

Methods

public static java.util.Map<TrackGroup, TrackSelectionOverride> filterOverrides(java.util.Map<TrackGroup, TrackSelectionOverride> overrides, java.util.List<Tracks.Group> trackGroups, boolean allowMultipleOverrides)

Returns the subset of overrides that apply to the specified trackGroups. If allowMultipleOverrides is then at most one override is retained, which will be the one whose track group is first in trackGroups.

Parameters:

overrides: The overrides to filter.
trackGroups: The track groups whose overrides should be retained.
allowMultipleOverrides: Whether more than one override can be retained.

Returns:

The filtered overrides.

public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections)

Sets whether adaptive selections (consisting of more than one track) can be made using this selection view.

For the 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.

Parameters:

allowAdaptiveSelections: Whether adaptive selection is enabled.

public void setAllowMultipleOverrides(boolean allowMultipleOverrides)

Sets whether tracks from multiple track groups can be selected. This results in multiple TrackSelectionOverrides being returned by TrackSelectionView.getOverrides().

Parameters:

allowMultipleOverrides: Whether tracks from multiple track groups can be selected.

public void setShowDisableOption(boolean showDisableOption)

Sets whether the disabled option can be selected.

Parameters:

showDisableOption: Whether the disabled option can be selected.

public void setTrackNameProvider(TrackNameProvider trackNameProvider)

Sets the TrackNameProvider used to generate the user visible name of each track and updates the view with track names queried from the specified provider.

Parameters:

trackNameProvider: The TrackNameProvider to use.

public void init(java.util.List<Tracks.Group> trackGroups, boolean isDisabled, java.util.Map<TrackGroup, TrackSelectionOverride> overrides, java.util.Comparator<Format> trackFormatComparator, TrackSelectionView.TrackSelectionListener listener)

Initialize the view to select tracks from a specified list of track groups.

Parameters:

trackGroups: The .
isDisabled: Whether the disabled option should be initially selected.
overrides: The initially selected track overrides. Any overrides that do not correspond to track groups in trackGroups will be ignored. If TrackSelectionView.setAllowMultipleOverrides(boolean) hasn't been set to true then all but one override will be ignored. The retained override will be the one whose track group is first in trackGroups.
trackFormatComparator: An optional comparator used to determine the display order of the tracks within each track group.
listener: An optional listener to receive selection updates.

public boolean getIsDisabled()

Returns whether the disabled option is selected.

public java.util.Map<TrackGroup, TrackSelectionOverride> getOverrides()

Returns the selected track overrides.

Source

/*
 * Copyright (C) 2018 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 android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckedTextView;
import android.widget.LinearLayout;
import androidx.annotation.AttrRes;
import androidx.annotation.Nullable;
import androidx.media3.common.Format;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** A view for making track selections. */
@UnstableApi
public class TrackSelectionView extends LinearLayout {

  /** Listener for changes to the selected tracks. */
  public interface TrackSelectionListener {

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

  /**
   * Returns the subset of {@code overrides} that apply to the specified {@code trackGroups}. If
   * {@code allowMultipleOverrides} is {@code} then at most one override is retained, which will be
   * the one whose track group is first in {@code trackGroups}.
   *
   * @param overrides The overrides to filter.
   * @param trackGroups The track groups whose overrides should be retained.
   * @param allowMultipleOverrides Whether more than one override can be retained.
   * @return The filtered overrides.
   */
  public static Map<TrackGroup, TrackSelectionOverride> filterOverrides(
      Map<TrackGroup, TrackSelectionOverride> overrides,
      List<Tracks.Group> trackGroups,
      boolean allowMultipleOverrides) {
    HashMap<TrackGroup, TrackSelectionOverride> filteredOverrides = new HashMap<>();
    for (int i = 0; i < trackGroups.size(); i++) {
      Tracks.Group trackGroup = trackGroups.get(i);
      @Nullable TrackSelectionOverride override = overrides.get(trackGroup.getMediaTrackGroup());
      if (override != null && (allowMultipleOverrides || filteredOverrides.isEmpty())) {
        filteredOverrides.put(override.mediaTrackGroup, override);
      }
    }
    return filteredOverrides;
  }

  private final int selectableItemBackgroundResourceId;
  private final LayoutInflater inflater;
  private final CheckedTextView disableView;
  private final CheckedTextView defaultView;
  private final ComponentListener componentListener;
  private final List<Tracks.Group> trackGroups;
  private final Map<TrackGroup, TrackSelectionOverride> overrides;

  private boolean allowAdaptiveSelections;
  private boolean allowMultipleOverrides;

  private TrackNameProvider trackNameProvider;
  private CheckedTextView[][] trackViews;

  private boolean isDisabled;
  @Nullable private Comparator<TrackInfo> trackInfoComparator;
  @Nullable private TrackSelectionListener listener;

  /** Creates a track selection view. */
  public TrackSelectionView(Context context) {
    this(context, null);
  }

  /** Creates a track selection view. */
  public TrackSelectionView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
  }

  /** Creates a track selection view. */
  @SuppressWarnings("nullness")
  public TrackSelectionView(
      Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    setOrientation(LinearLayout.VERTICAL);
    // Don't save view hierarchy as it needs to be reinitialized with a call to init.
    setSaveFromParentEnabled(false);

    TypedArray attributeArray =
        context
            .getTheme()
            .obtainStyledAttributes(new int[] {android.R.attr.selectableItemBackground});
    selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);
    attributeArray.recycle();

    inflater = LayoutInflater.from(context);
    componentListener = new ComponentListener();
    trackNameProvider = new DefaultTrackNameProvider(getResources());
    trackGroups = new ArrayList<>();
    overrides = new HashMap<>();

    // View for disabling the renderer.
    disableView =
        (CheckedTextView)
            inflater.inflate(android.R.layout.simple_list_item_single_choice, this, false);
    disableView.setBackgroundResource(selectableItemBackgroundResourceId);
    disableView.setText(R.string.exo_track_selection_none);
    disableView.setEnabled(false);
    disableView.setFocusable(true);
    disableView.setOnClickListener(componentListener);
    disableView.setVisibility(View.GONE);
    addView(disableView);
    // Divider view.
    addView(inflater.inflate(R.layout.exo_list_divider, this, false));
    // View for clearing the override to allow the selector to use its default selection logic.
    defaultView =
        (CheckedTextView)
            inflater.inflate(android.R.layout.simple_list_item_single_choice, this, false);
    defaultView.setBackgroundResource(selectableItemBackgroundResourceId);
    defaultView.setText(R.string.exo_track_selection_auto);
    defaultView.setEnabled(false);
    defaultView.setFocusable(true);
    defaultView.setOnClickListener(componentListener);
    addView(defaultView);
  }

  /**
   * Sets whether adaptive selections (consisting of more than one track) can be made using this
   * selection view.
   *
   * <p>For the 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.
   */
  public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {
    if (this.allowAdaptiveSelections != allowAdaptiveSelections) {
      this.allowAdaptiveSelections = allowAdaptiveSelections;
      updateViews();
    }
  }

  /**
   * Sets whether tracks from multiple track groups can be selected. This results in multiple {@link
   * TrackSelectionOverride TrackSelectionOverrides} being returned by {@link #getOverrides()}.
   *
   * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected.
   */
  public void setAllowMultipleOverrides(boolean allowMultipleOverrides) {
    if (this.allowMultipleOverrides != allowMultipleOverrides) {
      this.allowMultipleOverrides = allowMultipleOverrides;
      if (!allowMultipleOverrides && overrides.size() > 1) {
        // Re-filter the overrides to retain only one of them.
        Map<TrackGroup, TrackSelectionOverride> filteredOverrides =
            filterOverrides(overrides, trackGroups, /* allowMultipleOverrides= */ false);
        overrides.clear();
        overrides.putAll(filteredOverrides);
      }
      updateViews();
    }
  }

  /**
   * Sets whether the disabled option can be selected.
   *
   * @param showDisableOption Whether the disabled option can be selected.
   */
  public void setShowDisableOption(boolean showDisableOption) {
    disableView.setVisibility(showDisableOption ? View.VISIBLE : View.GONE);
  }

  /**
   * 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.
   */
  public void setTrackNameProvider(TrackNameProvider trackNameProvider) {
    this.trackNameProvider = Assertions.checkNotNull(trackNameProvider);
    updateViews();
  }

  /**
   * Initialize the view to select tracks from a specified list of track groups.
   *
   * @param trackGroups The {@link Tracks.Group track groups}.
   * @param isDisabled Whether the disabled option should be initially selected.
   * @param overrides The initially selected track overrides. Any overrides that do not correspond
   *     to track groups in {@code trackGroups} 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 is first
   *     in {@code trackGroups}.
   * @param trackFormatComparator An optional comparator used to determine the display order of the
   *     tracks within each track group.
   * @param listener An optional listener to receive selection updates.
   */
  public void init(
      List<Tracks.Group> trackGroups,
      boolean isDisabled,
      Map<TrackGroup, TrackSelectionOverride> overrides,
      @Nullable Comparator<Format> trackFormatComparator,
      @Nullable TrackSelectionListener listener) {
    this.isDisabled = isDisabled;
    this.trackInfoComparator =
        trackFormatComparator == null
            ? null
            : (o1, o2) -> trackFormatComparator.compare(o1.getFormat(), o2.getFormat());
    this.listener = listener;

    this.trackGroups.clear();
    this.trackGroups.addAll(trackGroups);
    this.overrides.clear();
    this.overrides.putAll(filterOverrides(overrides, trackGroups, allowMultipleOverrides));
    updateViews();
  }

  /** Returns whether the disabled option is selected. */
  public boolean getIsDisabled() {
    return isDisabled;
  }

  /** Returns the selected track overrides. */
  public Map<TrackGroup, TrackSelectionOverride> getOverrides() {
    return overrides;
  }

  // Private methods.

  private void updateViews() {
    // Remove previous per-track views.
    for (int i = getChildCount() - 1; i >= 3; i--) {
      removeViewAt(i);
    }

    if (trackGroups.isEmpty()) {
      // The view is not initialized.
      disableView.setEnabled(false);
      defaultView.setEnabled(false);
      return;
    }
    disableView.setEnabled(true);
    defaultView.setEnabled(true);

    // Add per-track views.
    trackViews = new CheckedTextView[trackGroups.size()][];
    boolean enableMultipleChoiceForMultipleOverrides = shouldEnableMultiGroupSelection();
    for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.size(); trackGroupIndex++) {
      Tracks.Group trackGroup = trackGroups.get(trackGroupIndex);
      boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(trackGroup);
      trackViews[trackGroupIndex] = new CheckedTextView[trackGroup.length];

      TrackInfo[] trackInfos = new TrackInfo[trackGroup.length];
      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
        trackInfos[trackIndex] = new TrackInfo(trackGroup, trackIndex);
      }
      if (trackInfoComparator != null) {
        Arrays.sort(trackInfos, trackInfoComparator);
      }

      for (int trackIndex = 0; trackIndex < trackInfos.length; trackIndex++) {
        if (trackIndex == 0) {
          addView(inflater.inflate(R.layout.exo_list_divider, this, false));
        }
        int trackViewLayoutId =
            enableMultipleChoiceForAdaptiveSelections || enableMultipleChoiceForMultipleOverrides
                ? android.R.layout.simple_list_item_multiple_choice
                : android.R.layout.simple_list_item_single_choice;
        CheckedTextView trackView =
            (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
        trackView.setBackgroundResource(selectableItemBackgroundResourceId);
        trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].getFormat()));
        trackView.setTag(trackInfos[trackIndex]);
        if (trackGroup.isTrackSupported(trackIndex)) {
          trackView.setFocusable(true);
          trackView.setOnClickListener(componentListener);
        } else {
          trackView.setFocusable(false);
          trackView.setEnabled(false);
        }
        trackViews[trackGroupIndex][trackIndex] = trackView;
        addView(trackView);
      }
    }

    updateViewStates();
  }

  private void updateViewStates() {
    disableView.setChecked(isDisabled);
    defaultView.setChecked(!isDisabled && overrides.size() == 0);
    for (int i = 0; i < trackViews.length; i++) {
      @Nullable
      TrackSelectionOverride override = overrides.get(trackGroups.get(i).getMediaTrackGroup());
      for (int j = 0; j < trackViews[i].length; j++) {
        if (override != null) {
          TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(trackViews[i][j].getTag());
          trackViews[i][j].setChecked(override.trackIndices.contains(trackInfo.trackIndex));
        } else {
          trackViews[i][j].setChecked(false);
        }
      }
    }
  }

  private void onClick(View view) {
    if (view == disableView) {
      onDisableViewClicked();
    } else if (view == defaultView) {
      onDefaultViewClicked();
    } else {
      onTrackViewClicked(view);
    }
    updateViewStates();
    if (listener != null) {
      listener.onTrackSelectionChanged(getIsDisabled(), getOverrides());
    }
  }

  private void onDisableViewClicked() {
    isDisabled = true;
    overrides.clear();
  }

  private void onDefaultViewClicked() {
    isDisabled = false;
    overrides.clear();
  }

  private void onTrackViewClicked(View view) {
    isDisabled = false;
    TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(view.getTag());
    TrackGroup mediaTrackGroup = trackInfo.trackGroup.getMediaTrackGroup();
    int trackIndex = trackInfo.trackIndex;
    @Nullable TrackSelectionOverride override = overrides.get(mediaTrackGroup);
    if (override == null) {
      // Start new override.
      if (!allowMultipleOverrides && overrides.size() > 0) {
        // Removed other overrides if we don't allow multiple overrides.
        overrides.clear();
      }
      overrides.put(
          mediaTrackGroup,
          new TrackSelectionOverride(mediaTrackGroup, ImmutableList.of(trackIndex)));
    } else {
      // An existing override is being modified.
      ArrayList<Integer> trackIndices = new ArrayList<>(override.trackIndices);
      boolean isCurrentlySelected = ((CheckedTextView) view).isChecked();
      boolean isAdaptiveAllowed = shouldEnableAdaptiveSelection(trackInfo.trackGroup);
      boolean isUsingCheckBox = isAdaptiveAllowed || shouldEnableMultiGroupSelection();
      if (isCurrentlySelected && isUsingCheckBox) {
        // Remove the track from the override.
        trackIndices.remove((Integer) trackIndex);
        if (trackIndices.isEmpty()) {
          // The last track has been removed, so remove the whole override.
          overrides.remove(mediaTrackGroup);
        } else {
          overrides.put(mediaTrackGroup, new TrackSelectionOverride(mediaTrackGroup, trackIndices));
        }
      } else if (!isCurrentlySelected) {
        if (isAdaptiveAllowed) {
          // Add new track to adaptive override.
          trackIndices.add(trackIndex);
          overrides.put(mediaTrackGroup, new TrackSelectionOverride(mediaTrackGroup, trackIndices));
        } else {
          // Replace existing track in override.
          overrides.put(
              mediaTrackGroup,
              new TrackSelectionOverride(mediaTrackGroup, ImmutableList.of(trackIndex)));
        }
      }
    }
  }

  private boolean shouldEnableAdaptiveSelection(Tracks.Group trackGroup) {
    return allowAdaptiveSelections && trackGroup.isAdaptiveSupported();
  }

  private boolean shouldEnableMultiGroupSelection() {
    return allowMultipleOverrides && trackGroups.size() > 1;
  }

  // Internal classes.

  private class ComponentListener implements OnClickListener {

    @Override
    public void onClick(View view) {
      TrackSelectionView.this.onClick(view);
    }
  }

  private static final class TrackInfo {
    public final Tracks.Group trackGroup;
    public final int trackIndex;

    public TrackInfo(Tracks.Group trackGroup, int trackIndex) {
      this.trackGroup = trackGroup;
      this.trackIndex = trackIndex;
    }

    public Format getFormat() {
      return trackGroup.getTrackFormat(trackIndex);
    }
  }
}