public class

TrackSelectionView

extends LinearLayout

 java.lang.Object

↳LinearLayout

↳androidx.media3.ui.TrackSelectionView

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-ui', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-ui
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-ui:1.0.0-alpha03 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 booleangetIsDisabled()

Returns whether the renderer is disabled.

public java.util.List<DefaultTrackSelector.SelectionOverride>getOverrides()

Returns the list of selected track selection overrides.

public voidinit(MappingTrackSelector.MappedTrackInfo mappedTrackInfo, int rendererIndex, boolean isDisabled, java.util.List<DefaultTrackSelector.SelectionOverride> overrides, java.util.Comparator<Format> trackFormatComparator, TrackSelectionView.TrackSelectionListener listener)

Initialize the view to select tracks for a specified renderer using MappingTrackSelector.MappedTrackInfo and a set of .

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 an option is available for disabling the renderer.

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 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 SelectionOverrides to be returned by TrackSelectionView.getOverrides().

Parameters:

allowMultipleOverrides: Whether multiple track selection overrides can be selected.

public void setShowDisableOption(boolean showDisableOption)

Sets whether an option is available for disabling the renderer.

Parameters:

showDisableOption: Whether the disable option is shown.

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(MappingTrackSelector.MappedTrackInfo mappedTrackInfo, int rendererIndex, boolean isDisabled, java.util.List<DefaultTrackSelector.SelectionOverride> overrides, java.util.Comparator<Format> trackFormatComparator, TrackSelectionView.TrackSelectionListener listener)

Initialize the view to select tracks for a specified renderer using MappingTrackSelector.MappedTrackInfo and a set of .

Parameters:

mappedTrackInfo: The MappingTrackSelector.MappedTrackInfo.
rendererIndex: The index of the renderer.
isDisabled: Whether the renderer should be initially shown as disabled.
overrides: List of initial overrides to be shown for this renderer. There must be at most one override for each track group. If TrackSelectionView.setAllowMultipleOverrides(boolean) hasn't been set to true, only the first override is used.
trackFormatComparator: An optional comparator used to determine the display order of the tracks within each track group.
listener: An optional listener for track selection updates.

public boolean getIsDisabled()

Returns whether the renderer is disabled.

public java.util.List<DefaultTrackSelector.SelectionOverride> getOverrides()

Returns the list of selected track selection overrides. There will be at most one override for each track group.

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.util.SparseArray;
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.C;
import androidx.media3.common.Format;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/** 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 renderer is disabled.
     * @param overrides List of selected track selection overrides for the renderer.
     */
    void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides);
  }

  private final int selectableItemBackgroundResourceId;
  private final LayoutInflater inflater;
  private final CheckedTextView disableView;
  private final CheckedTextView defaultView;
  private final ComponentListener componentListener;
  private final SparseArray<SelectionOverride> overrides;

  private boolean allowAdaptiveSelections;
  private boolean allowMultipleOverrides;

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

  private @MonotonicNonNull MappedTrackInfo mappedTrackInfo;
  private int rendererIndex;
  private TrackGroupArray trackGroups;
  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);

    overrides = new SparseArray<>();

    // 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 = TrackGroupArray.EMPTY;

    // 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
   * SelectionOverride SelectionOverrides} to be returned by {@link #getOverrides()}.
   *
   * @param allowMultipleOverrides Whether multiple track selection overrides can be selected.
   */
  public void setAllowMultipleOverrides(boolean allowMultipleOverrides) {
    if (this.allowMultipleOverrides != allowMultipleOverrides) {
      this.allowMultipleOverrides = allowMultipleOverrides;
      if (!allowMultipleOverrides && overrides.size() > 1) {
        for (int i = overrides.size() - 1; i > 0; i--) {
          overrides.remove(i);
        }
      }
      updateViews();
    }
  }

  /**
   * Sets whether an option is available for disabling the renderer.
   *
   * @param showDisableOption Whether the disable option is shown.
   */
  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 for a specified renderer using {@link MappedTrackInfo} and
   * a set of {@link DefaultTrackSelector.Parameters}.
   *
   * @param mappedTrackInfo The {@link MappedTrackInfo}.
   * @param rendererIndex The index of the renderer.
   * @param isDisabled Whether the renderer should be initially shown as disabled.
   * @param overrides List of initial overrides to be shown for this renderer. There must be at most
   *     one override for each track group. If {@link #setAllowMultipleOverrides(boolean)} hasn't
   *     been set to {@code true}, only the first override is used.
   * @param trackFormatComparator An optional comparator used to determine the display order of the
   *     tracks within each track group.
   * @param listener An optional listener for track selection updates.
   */
  public void init(
      MappedTrackInfo mappedTrackInfo,
      int rendererIndex,
      boolean isDisabled,
      List<SelectionOverride> overrides,
      @Nullable Comparator<Format> trackFormatComparator,
      @Nullable TrackSelectionListener listener) {
    this.mappedTrackInfo = mappedTrackInfo;
    this.rendererIndex = rendererIndex;
    this.isDisabled = isDisabled;
    this.trackInfoComparator =
        trackFormatComparator == null
            ? null
            : (o1, o2) -> trackFormatComparator.compare(o1.format, o2.format);
    this.listener = listener;
    int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1);
    for (int i = 0; i < maxOverrides; i++) {
      SelectionOverride override = overrides.get(i);
      this.overrides.put(override.groupIndex, override);
    }
    updateViews();
  }

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

  /**
   * Returns the list of selected track selection overrides. There will be at most one override for
   * each track group.
   */
  public List<SelectionOverride> getOverrides() {
    List<SelectionOverride> overrideList = new ArrayList<>(overrides.size());
    for (int i = 0; i < overrides.size(); i++) {
      overrideList.add(overrides.valueAt(i));
    }
    return overrideList;
  }

  // Private methods.

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

    if (mappedTrackInfo == null) {
      // The view is not initialized.
      disableView.setEnabled(false);
      defaultView.setEnabled(false);
      return;
    }
    disableView.setEnabled(true);
    defaultView.setEnabled(true);

    trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);

    // Add per-track views.
    trackViews = new CheckedTextView[trackGroups.length][];
    boolean enableMultipleChoiceForMultipleOverrides = shouldEnableMultiGroupSelection();
    for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
      TrackGroup group = trackGroups.get(groupIndex);
      boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex);
      trackViews[groupIndex] = new CheckedTextView[group.length];

      TrackInfo[] trackInfos = new TrackInfo[group.length];
      for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
        trackInfos[trackIndex] = new TrackInfo(groupIndex, trackIndex, group.getFormat(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].format));
        trackView.setTag(trackInfos[trackIndex]);
        if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
            == C.FORMAT_HANDLED) {
          trackView.setFocusable(true);
          trackView.setOnClickListener(componentListener);
        } else {
          trackView.setFocusable(false);
          trackView.setEnabled(false);
        }
        trackViews[groupIndex][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++) {
      SelectionOverride override = overrides.get(i);
      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.containsTrack(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());
    int groupIndex = trackInfo.groupIndex;
    int trackIndex = trackInfo.trackIndex;
    SelectionOverride override = overrides.get(groupIndex);
    Assertions.checkNotNull(mappedTrackInfo);
    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(groupIndex, new SelectionOverride(groupIndex, trackIndex));
    } else {
      // An existing override is being modified.
      int overrideLength = override.length;
      int[] overrideTracks = override.tracks;
      boolean isCurrentlySelected = ((CheckedTextView) view).isChecked();
      boolean isAdaptiveAllowed = shouldEnableAdaptiveSelection(groupIndex);
      boolean isUsingCheckBox = isAdaptiveAllowed || shouldEnableMultiGroupSelection();
      if (isCurrentlySelected && isUsingCheckBox) {
        // Remove the track from the override.
        if (overrideLength == 1) {
          // The last track is being removed, so the override becomes empty.
          overrides.remove(groupIndex);
        } else {
          int[] tracks = getTracksRemoving(overrideTracks, trackIndex);
          overrides.put(groupIndex, new SelectionOverride(groupIndex, tracks));
        }
      } else if (!isCurrentlySelected) {
        if (isAdaptiveAllowed) {
          // Add new track to adaptive override.
          int[] tracks = getTracksAdding(overrideTracks, trackIndex);
          overrides.put(groupIndex, new SelectionOverride(groupIndex, tracks));
        } else {
          // Replace existing track in override.
          overrides.put(groupIndex, new SelectionOverride(groupIndex, trackIndex));
        }
      }
    }
  }

  @RequiresNonNull("mappedTrackInfo")
  private boolean shouldEnableAdaptiveSelection(int groupIndex) {
    return allowAdaptiveSelections
        && trackGroups.get(groupIndex).length > 1
        && mappedTrackInfo.getAdaptiveSupport(
                rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false)
            != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED;
  }

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

  private static int[] getTracksAdding(int[] tracks, int addedTrack) {
    tracks = Arrays.copyOf(tracks, tracks.length + 1);
    tracks[tracks.length - 1] = addedTrack;
    return tracks;
  }

  private static int[] getTracksRemoving(int[] tracks, int removedTrack) {
    int[] newTracks = new int[tracks.length - 1];
    int trackCount = 0;
    for (int track : tracks) {
      if (track != removedTrack) {
        newTracks[trackCount++] = track;
      }
    }
    return newTracks;
  }

  // Internal classes.

  private class ComponentListener implements OnClickListener {

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

  private static final class TrackInfo {
    public final int groupIndex;
    public final int trackIndex;
    public final Format format;

    public TrackInfo(int groupIndex, int trackIndex, Format format) {
      this.groupIndex = groupIndex;
      this.trackIndex = trackIndex;
      this.format = format;
    }
  }
}