diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index df549a92ae..b746757fca 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -19,13 +19,17 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.content.res.Resources; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; +import android.support.annotation.Nullable; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; -import android.widget.ArrayAdapter; -import android.widget.ListView; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.RenderersFactory; @@ -37,20 +41,21 @@ import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.offline.TrackKey; -import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; import com.google.android.exoplayer2.ui.TrackNameProvider; +import com.google.android.exoplayer2.ui.TrackSelectionView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -126,10 +131,8 @@ public class DownloadTracker implements DownloadManager.Listener { getDownloadHelper(uri, extension, renderersFactory).getRemoveAction(); startServiceWithAction(removeAction); } else { - StartDownloadDialogHelper helper = - new StartDownloadDialogHelper( - activity, getDownloadHelper(uri, extension, renderersFactory), name); - helper.prepare(); + new StartDownloadDialogHelper( + activity, getDownloadHelper(uri, extension, renderersFactory), name); } } @@ -217,61 +220,56 @@ public class DownloadTracker implements DownloadManager.Listener { } } + @SuppressWarnings("UngroupedOverloads") private final class StartDownloadDialogHelper - implements DownloadHelper.Callback, DialogInterface.OnClickListener { + implements DownloadHelper.Callback, + DialogInterface.OnClickListener, + View.OnClickListener, + TrackSelectionView.DialogCallback { private final DownloadHelper downloadHelper; private final String name; + private final LayoutInflater dialogInflater; + private final AlertDialog dialog; + private final LinearLayout selectionList; - private final AlertDialog.Builder builder; - private final View dialogView; - private final List trackKeys; - private final ArrayAdapter trackTitles; - private final ListView representationList; + private MappedTrackInfo mappedTrackInfo; + private DefaultTrackSelector.Parameters parameters; - public StartDownloadDialogHelper( + private StartDownloadDialogHelper( Activity activity, DownloadHelper downloadHelper, String name) { this.downloadHelper = downloadHelper; this.name = name; - builder = + AlertDialog.Builder builder = new AlertDialog.Builder(activity) - .setTitle(R.string.exo_download_description) + .setTitle(R.string.download_preparing) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, null); // Inflate with the builder's context to ensure the correct style is used. - LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); - dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null); + dialogInflater = LayoutInflater.from(builder.getContext()); + selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null); + builder.setView(selectionList); + dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - trackKeys = new ArrayList<>(); - trackTitles = - new ArrayAdapter<>( - builder.getContext(), android.R.layout.simple_list_item_multiple_choice); - representationList = dialogView.findViewById(R.id.representation_list); - representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - representationList.setAdapter(trackTitles); - } - - public void prepare() { + parameters = DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS; downloadHelper.prepare(this); } + // DownloadHelper.Callback implementation. + @Override public void onPrepared(DownloadHelper helper) { - for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { - TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i); - for (int j = 0; j < trackGroups.length; j++) { - TrackGroup trackGroup = trackGroups.get(j); - for (int k = 0; k < trackGroup.length; k++) { - trackKeys.add(new TrackKey(i, j, k)); - trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); - } - } + if (helper.getPeriodCount() < 1) { + onPrepareError(downloadHelper, new IOException("Content is empty.")); + return; } - if (!trackKeys.isEmpty()) { - builder.setView(dialogView); - } - builder.create().show(); + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + updateSelectionList(); + dialog.setTitle(R.string.exo_download_description); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); } @Override @@ -280,21 +278,107 @@ public class DownloadTracker implements DownloadManager.Listener { context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) .show(); Log.e(TAG, "Failed to start download", e); + dialog.cancel(); } + // View.OnClickListener implementation. + + @Override + public void onClick(View v) { + Integer rendererIndex = (Integer) v.getTag(); + String dialogTitle = getTrackTypeString(mappedTrackInfo.getRendererType(rendererIndex)); + Pair dialogPair = + TrackSelectionView.getDialog( + dialog.getContext(), + dialogTitle, + mappedTrackInfo, + rendererIndex, + parameters, + /* callback= */ this); + dialogPair.second.setShowDisableOption(true); + dialogPair.second.setAllowAdaptiveSelections(false); + dialogPair.first.show(); + } + + // TrackSelectionView.DialogCallback implementation. + + @Override + public void onTracksSelected(DefaultTrackSelector.Parameters parameters) { + for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { + downloadHelper.replaceTrackSelections(/* periodIndex= */ i, parameters); + } + this.parameters = parameters; + updateSelectionList(); + } + + // DialogInterface.OnClickListener implementation. + @Override public void onClick(DialogInterface dialog, int which) { - ArrayList selectedTrackKeys = new ArrayList<>(); - for (int i = 0; i < representationList.getChildCount(); i++) { - if (representationList.isItemChecked(i)) { - selectedTrackKeys.add(trackKeys.get(i)); + DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name)); + startDownload(downloadAction); + } + + // Internal methods. + + private void updateSelectionList() { + selectionList.removeAllViews(); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); + if (trackGroupArray.length == 0) { + continue; + } + String trackTypeString = + getTrackTypeString(mappedTrackInfo.getRendererType(/* rendererIndex= */ i)); + if (trackTypeString == null) { + return; + } + String trackSelectionsString = getTrackSelectionString(/* rendererIndex= */ i); + View view = dialogInflater.inflate(R.layout.download_track_item, selectionList, false); + TextView trackTitleView = view.findViewById(R.id.track_title); + TextView trackDescView = view.findViewById(R.id.track_desc); + ImageButton editButton = view.findViewById(R.id.edit_button); + trackTitleView.setText(trackTypeString); + trackDescView.setText(trackSelectionsString); + editButton.setTag(i); + editButton.setOnClickListener(this); + selectionList.addView(view); + } + } + + private String getTrackSelectionString(int rendererIndex) { + List trackSelections = + downloadHelper.getTrackSelections(/* periodIndex= */ 0, rendererIndex); + String selectedTracks = ""; + Resources resources = selectionList.getResources(); + for (int i = 0; i < trackSelections.size(); i++) { + TrackSelection selection = trackSelections.get(i); + for (int j = 0; j < selection.length(); j++) { + String trackName = trackNameProvider.getTrackName(selection.getFormat(j)); + if (i == 0 && j == 0) { + selectedTracks = trackName; + } else { + selectedTracks = resources.getString(R.string.exo_item_list, selectedTracks, trackName); + } } } - if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) { - // We have selected keys, or we're dealing with single stream content. - DownloadAction downloadAction = - downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys); - startDownload(downloadAction); + return selectedTracks.isEmpty() + ? resources.getString(R.string.exo_track_selection_none) + : selectedTracks; + } + + @Nullable + private String getTrackTypeString(int trackType) { + Resources resources = selectionList.getResources(); + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + return resources.getString(R.string.exo_track_selection_title_video); + case C.TRACK_TYPE_AUDIO: + return resources.getString(R.string.exo_track_selection_title_audio); + case C.TRACK_TYPE_TEXT: + return resources.getString(R.string.exo_track_selection_title_text); + default: + return null; } } } diff --git a/demos/main/src/main/res/drawable-hdpi/ic_edit.png b/demos/main/src/main/res/drawable-hdpi/ic_edit.png new file mode 100755 index 0000000000..25678d6de9 Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_edit.png differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_edit.png b/demos/main/src/main/res/drawable-mdpi/ic_edit.png new file mode 100755 index 0000000000..dffcd9f61a Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_edit.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xhdpi/ic_edit.png new file mode 100755 index 0000000000..82f8563d1e Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_edit.png differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png new file mode 100755 index 0000000000..f00b4b68c5 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png new file mode 100755 index 0000000000..a9f99417fb Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png differ diff --git a/demos/main/src/main/res/layout/download_track_item.xml b/demos/main/src/main/res/layout/download_track_item.xml new file mode 100644 index 0000000000..fe1c62b391 --- /dev/null +++ b/demos/main/src/main/res/layout/download_track_item.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + diff --git a/demos/main/src/main/res/layout/start_download_dialog.xml b/demos/main/src/main/res/layout/start_download_dialog.xml index acb9af5d97..c182047ff8 100644 --- a/demos/main/src/main/res/layout/start_download_dialog.xml +++ b/demos/main/src/main/res/layout/start_download_dialog.xml @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index 40f065b18e..7ac5a65a49 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -51,6 +51,10 @@ Playing sample without ads, as the IMA extension was not loaded + Edit selection + + Preparing download… + Failed to start download This demo app does not support downloading playlists diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index 3f09ac2427..fd78631337 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.ui; -import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -33,13 +32,25 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.util.Assertions; import java.util.Arrays; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A view for making track selections. */ public class TrackSelectionView extends LinearLayout { + /** Callback which is invoked when a track selection has been made. */ + public interface DialogCallback { + + /** + * Called when track are selected. + * + * @param parameters The {@link DefaultTrackSelector.Parameters} for the selected tracks. + */ + void onTracksSelected(DefaultTrackSelector.Parameters parameters); + } + private final int selectableItemBackgroundResourceId; private final LayoutInflater inflater; private final CheckedTextView disableView; @@ -51,35 +62,64 @@ public class TrackSelectionView extends LinearLayout { private TrackNameProvider trackNameProvider; private CheckedTextView[][] trackViews; - private DefaultTrackSelector trackSelector; + private @MonotonicNonNull MappedTrackInfo mappedTrackInfo; private int rendererIndex; + private DefaultTrackSelector.Parameters parameters; private TrackGroupArray trackGroups; private boolean isDisabled; - private @Nullable SelectionOverride override; + @Nullable private SelectionOverride override; /** * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it. * - * @param activity The parent activity. + *

The dialog shows the current configuration of the provided {@code TrackSelector} and updates + * the parameters when closing the dialog. + * + * @param context The parent context. * @param title The dialog's title. * @param trackSelector The track selector. * @param rendererIndex The index of the renderer. * @return The dialog and the {@link TrackSelectionView} that will be shown by it. */ public static Pair getDialog( - Activity activity, + Context context, CharSequence title, DefaultTrackSelector trackSelector, int rendererIndex) { + return getDialog( + context, + title, + Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()), + rendererIndex, + trackSelector.getParameters(), + trackSelector::setParameters); + } + + /** + * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it. + * + * @param context The parent context. + * @param title The dialog's title. + * @param mappedTrackInfo The {@link MappedTrackInfo}. + * @param rendererIndex The index of the renderer. + * @param parameters The {@link DefaultTrackSelector.Parameters}. + * @param callback The {@link DialogCallback} invoked when the dialog is closed successfully. + * @return The dialog and the {@link TrackSelectionView} that will be shown by it. + */ + public static Pair getDialog( + Context context, CharSequence title, - DefaultTrackSelector trackSelector, - int rendererIndex) { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); + MappedTrackInfo mappedTrackInfo, + int rendererIndex, + DefaultTrackSelector.Parameters parameters, + DialogCallback callback) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); // 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, null); - final TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view); - selectionView.init(trackSelector, rendererIndex); - Dialog.OnClickListener okClickListener = (dialog, which) -> selectionView.applySelection(); + TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view); + selectionView.init(mappedTrackInfo, rendererIndex, parameters); + Dialog.OnClickListener okClickListener = + (dialog, which) -> callback.onTracksSelected(selectionView.getSelectionParameters()); AlertDialog dialog = builder @@ -113,6 +153,8 @@ public class TrackSelectionView extends LinearLayout { inflater = LayoutInflater.from(context); componentListener = new ComponentListener(); trackNameProvider = new DefaultTrackNameProvider(getResources()); + parameters = DefaultTrackSelector.Parameters.DEFAULT; + trackGroups = TrackGroupArray.EMPTY; // View for disabling the renderer. disableView = @@ -176,18 +218,35 @@ public class TrackSelectionView extends LinearLayout { } /** - * Initialize the view to select tracks for a specified renderer using a {@link - * DefaultTrackSelector}. + * Initialize the view to select tracks for a specified renderer using {@link MappedTrackInfo} and + * a set of {@link DefaultTrackSelector.Parameters}. * - * @param trackSelector The {@link DefaultTrackSelector}. + * @param mappedTrackInfo The {@link MappedTrackInfo}. * @param rendererIndex The index of the renderer. + * @param parameters The {@link DefaultTrackSelector.Parameters}. */ - public void init(DefaultTrackSelector trackSelector, int rendererIndex) { - this.trackSelector = trackSelector; + public void init( + MappedTrackInfo mappedTrackInfo, + int rendererIndex, + DefaultTrackSelector.Parameters parameters) { + this.mappedTrackInfo = mappedTrackInfo; this.rendererIndex = rendererIndex; + this.parameters = parameters; updateViews(); } + /** Returns the {@link DefaultTrackSelector.Parameters} for the current selection. */ + public DefaultTrackSelector.Parameters getSelectionParameters() { + DefaultTrackSelector.ParametersBuilder parametersBuilder = parameters.buildUpon(); + parametersBuilder.setRendererDisabled(rendererIndex, isDisabled); + if (override != null) { + parametersBuilder.setSelectionOverride(rendererIndex, trackGroups, override); + } else { + parametersBuilder.clearSelectionOverrides(rendererIndex); + } + return parametersBuilder.build(); + } + // Private methods. private void updateViews() { @@ -196,9 +255,7 @@ public class TrackSelectionView extends LinearLayout { removeViewAt(i); } - MappingTrackSelector.MappedTrackInfo trackInfo = - trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo(); - if (trackSelector == null || trackInfo == null) { + if (mappedTrackInfo == null) { // The view is not initialized. disableView.setEnabled(false); defaultView.setEnabled(false); @@ -207,9 +264,8 @@ public class TrackSelectionView extends LinearLayout { disableView.setEnabled(true); defaultView.setEnabled(true); - trackGroups = trackInfo.getTrackGroups(rendererIndex); + trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); isDisabled = parameters.getRendererDisabled(rendererIndex); override = parameters.getSelectionOverride(rendererIndex, trackGroups); @@ -220,7 +276,7 @@ public class TrackSelectionView extends LinearLayout { boolean enableAdaptiveSelections = allowAdaptiveSelections && trackGroups.get(groupIndex).length > 1 - && trackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) + && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; trackViews[groupIndex] = new CheckedTextView[group.length]; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { @@ -235,7 +291,7 @@ public class TrackSelectionView extends LinearLayout { (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false); trackView.setBackgroundResource(selectableItemBackgroundResourceId); trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex))); - if (trackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) + if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) == RendererCapabilities.FORMAT_HANDLED) { trackView.setFocusable(true); trackView.setTag(Pair.create(groupIndex, trackIndex)); @@ -263,17 +319,6 @@ public class TrackSelectionView extends LinearLayout { } } - private void applySelection() { - DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters(); - parametersBuilder.setRendererDisabled(rendererIndex, isDisabled); - if (override != null) { - parametersBuilder.setSelectionOverride(rendererIndex, trackGroups, override); - } else { - parametersBuilder.clearSelectionOverrides(rendererIndex); - } - trackSelector.setParameters(parametersBuilder); - } - private void onClick(View view) { if (view == disableView) { onDisableViewClicked();