/* * 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 com.example.exoplayertv; import android.app.Dialog; import android.content.DialogInterface; import android.content.res.Resources; import android.os.Bundle; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import androidx.media3.common.C; import androidx.media3.common.Player; import androidx.media3.common.Tracks; import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.ui.TrackSelectionView; import com.google.android.material.tabs.TabLayout; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** Dialog to select tracks. */ public final class TrackSelectionDialog extends DialogFragment { /** Called when tracks are selected. */ public interface TrackSelectionListener { /** * Called when tracks are selected. * * @param trackSelectionParameters A {@link TrackSelectionParameters} representing the selected * tracks. Any manual selections are defined by {@link * TrackSelectionParameters#disabledTrackTypes} and {@link * TrackSelectionParameters#overrides}. */ void onTracksSelected(TrackSelectionParameters trackSelectionParameters); } public static final ImmutableList SUPPORTED_TRACK_TYPES = ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT); private final SparseArray tabFragments; private final ArrayList tabTrackTypes; private int titleId; private DialogInterface.OnClickListener onClickListener; private DialogInterface.OnDismissListener onDismissListener; /** * Returns whether a track selection dialog will have content to display if initialized with the * specified {@link Player}. */ public static boolean willHaveContent(Player player) { return willHaveContent(player.getCurrentTracks()); } /** * Returns whether a track selection dialog will have content to display if initialized with the * specified {@link Tracks}. */ public static boolean willHaveContent(Tracks tracks) { for (Tracks.Group trackGroup : tracks.getGroups()) { if (SUPPORTED_TRACK_TYPES.contains(trackGroup.getType())) { return true; } } return false; } /** * Creates a dialog for a given {@link Player}, whose parameters will be automatically updated * when tracks are selected. * * @param player The {@link Player}. * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is * dismissed. */ public static TrackSelectionDialog createForPlayer( Player player, DialogInterface.OnDismissListener onDismissListener) { return createForTracksAndParameters( R.string.track_selection_title, player.getCurrentTracks(), player.getTrackSelectionParameters(), /* allowAdaptiveSelections= */ true, /* allowMultipleOverrides= */ false, player::setTrackSelectionParameters, onDismissListener); } /** * Creates a dialog for given {@link Tracks} and {@link TrackSelectionParameters}. * * @param titleId The resource id of the dialog title. * @param tracks The {@link Tracks} describing the tracks to display. * @param trackSelectionParameters The initial {@link TrackSelectionParameters}. * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) * can be made. * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. * @param trackSelectionListener Called when tracks are selected. * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is * dismissed. */ public static TrackSelectionDialog createForTracksAndParameters( int titleId, Tracks tracks, TrackSelectionParameters trackSelectionParameters, boolean allowAdaptiveSelections, boolean allowMultipleOverrides, TrackSelectionListener trackSelectionListener, DialogInterface.OnDismissListener onDismissListener) { TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); trackSelectionDialog.init( tracks, trackSelectionParameters, titleId, allowAdaptiveSelections, allowMultipleOverrides, /* onClickListener= */ (dialog, which) -> { TrackSelectionParameters.Builder builder = trackSelectionParameters.buildUpon(); for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { int trackType = SUPPORTED_TRACK_TYPES.get(i); builder.setTrackTypeDisabled(trackType, trackSelectionDialog.getIsDisabled(trackType)); builder.clearOverridesOfType(trackType); Map overrides = trackSelectionDialog.getOverrides(trackType); for (TrackSelectionOverride override : overrides.values()) { builder.addOverride(override); } } trackSelectionListener.onTracksSelected(builder.build()); }, onDismissListener); return trackSelectionDialog; } public TrackSelectionDialog() { tabFragments = new SparseArray<>(); tabTrackTypes = new ArrayList<>(); // Retain instance across activity re-creation to prevent losing access to init data. setRetainInstance(true); } private void init( Tracks tracks, TrackSelectionParameters trackSelectionParameters, int titleId, boolean allowAdaptiveSelections, boolean allowMultipleOverrides, DialogInterface.OnClickListener onClickListener, DialogInterface.OnDismissListener onDismissListener) { this.titleId = titleId; this.onClickListener = onClickListener; this.onDismissListener = onDismissListener; for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { @C.TrackType int trackType = SUPPORTED_TRACK_TYPES.get(i); ArrayList trackGroups = new ArrayList<>(); for (Tracks.Group trackGroup : tracks.getGroups()) { if (trackGroup.getType() == trackType) { trackGroups.add(trackGroup); } } if (!trackGroups.isEmpty()) { TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); tabFragment.init( trackGroups, trackSelectionParameters.disabledTrackTypes.contains(trackType), trackSelectionParameters.overrides, allowAdaptiveSelections, allowMultipleOverrides); tabFragments.put(trackType, tabFragment); tabTrackTypes.add(trackType); } } } /** * Returns whether the disabled option is selected for the specified track type. * * @param trackType The track type. * @return Whether the disabled option is selected for the track type. */ public boolean getIsDisabled(int trackType) { TrackSelectionViewFragment trackView = tabFragments.get(trackType); return trackView != null && trackView.isDisabled; } /** * Returns the selected track overrides for the specified track type. * * @param trackType The track type. * @return The track overrides for the track type. */ public Map getOverrides(int trackType) { TrackSelectionViewFragment trackView = tabFragments.get(trackType); return trackView == null ? Collections.emptyMap() : trackView.overrides; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { // We need to own the view to let tab layout work correctly on all API levels. We can't use // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using // the AlertDialog theme overlay with force-enabled title. AppCompatDialog dialog = new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay); dialog.setTitle(titleId); return dialog; } @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); onDismissListener.onDismiss(dialog); } @Override public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button); Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button); viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager())); tabLayout.setupWithViewPager(viewPager); tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE); cancelButton.setOnClickListener(view -> dismiss()); okButton.setOnClickListener( view -> { onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); dismiss(); }); return dialogView; } private static String getTrackTypeString(Resources resources, @C.TrackType int trackType) { switch (trackType) { case C.TRACK_TYPE_VIDEO: return resources.getString(com.google.android.exoplayer2.ui.R.string.exo_track_selection_title_video); case C.TRACK_TYPE_AUDIO: return resources.getString(com.google.android.exoplayer2.ui.R.string.exo_track_selection_title_audio); case C.TRACK_TYPE_TEXT: return resources.getString(com.google.android.exoplayer2.ui.R.string.exo_track_selection_title_text); default: throw new IllegalArgumentException(); } } private final class FragmentAdapter extends FragmentPagerAdapter { public FragmentAdapter(FragmentManager fragmentManager) { super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } @Override public Fragment getItem(int position) { return tabFragments.get(tabTrackTypes.get(position)); } @Override public int getCount() { return tabTrackTypes.size(); } @Override public CharSequence getPageTitle(int position) { return getTrackTypeString(getResources(), tabTrackTypes.get(position)); } } /** Fragment to show a track selection in tab of the track selection dialog. */ public static final class TrackSelectionViewFragment extends Fragment implements TrackSelectionView.TrackSelectionListener { private List trackGroups; private boolean allowAdaptiveSelections; private boolean allowMultipleOverrides; /* package */ boolean isDisabled; /* package */ Map overrides; public TrackSelectionViewFragment() { // Retain instance across activity re-creation to prevent losing access to init data. setRetainInstance(true); } public void init( List trackGroups, boolean isDisabled, Map overrides, boolean allowAdaptiveSelections, boolean allowMultipleOverrides) { this.trackGroups = trackGroups; this.isDisabled = isDisabled; this.allowAdaptiveSelections = allowAdaptiveSelections; this.allowMultipleOverrides = allowMultipleOverrides; // TrackSelectionView does this filtering internally, but we need to do it here as well to // handle the case where the TrackSelectionView is never created. this.overrides = new HashMap<>( TrackSelectionView.filterOverrides(overrides, trackGroups, allowMultipleOverrides)); } @Override public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate( com.google.android.exoplayer2.ui.R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false); TrackSelectionView trackSelectionView = rootView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_track_selection_view); trackSelectionView.setShowDisableOption(true); trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); trackSelectionView.init( trackGroups, isDisabled, overrides, /* trackFormatComparator= */ null, /* listener= */ this); return rootView; } @Override public void onTrackSelectionChanged( boolean isDisabled, Map overrides) { this.isDisabled = isDisabled; this.overrides = overrides; } } }