From b9aaf1ebabadecb246856fde9df93df450cd4c01 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 6 May 2018 11:03:31 -0700 Subject: [PATCH] Improve offline support in the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=195593235 --- demos/main/src/main/AndroidManifest.xml | 2 - .../exoplayer2/demo/DemoApplication.java | 30 +- .../exoplayer2/demo/DownloadActivity.java | 196 ------------ .../exoplayer2/demo/DownloadTracker.java | 298 ++++++++++++++++++ .../exoplayer2/demo/PlayerActivity.java | 29 +- .../demo/SampleChooserActivity.java | 123 +++++--- .../main/res/drawable-hdpi/ic_download.png | Bin 0 -> 199 bytes .../res/drawable-hdpi/ic_download_done.png | Bin 0 -> 218 bytes .../ic_offline_pin_white_36dp.png | Bin 551 -> 0 bytes .../main/res/drawable-mdpi/ic_download.png | Bin 0 -> 163 bytes .../res/drawable-mdpi/ic_download_done.png | Bin 0 -> 182 bytes .../ic_offline_pin_white_36dp.png | Bin 388 -> 0 bytes .../main/res/drawable-xhdpi/ic_download.png | Bin 0 -> 187 bytes .../res/drawable-xhdpi/ic_download_done.png | Bin 0 -> 304 bytes .../ic_offline_pin_white_36dp.png | Bin 691 -> 0 bytes .../main/res/drawable-xxhdpi/ic_download.png | Bin 0 -> 303 bytes .../res/drawable-xxhdpi/ic_download_done.png | Bin 0 -> 450 bytes .../ic_offline_pin_white_36dp.png | Bin 1049 -> 0 bytes .../main/res/drawable-xxxhdpi/ic_download.png | Bin 0 -> 304 bytes .../res/drawable-xxxhdpi/ic_download_done.png | Bin 0 -> 575 bytes .../ic_offline_pin_white_36dp.png | Bin 1375 -> 0 bytes .../main/res/layout/downloader_activity.xml | 51 --- .../src/main/res/layout/sample_list_item.xml | 3 +- .../main/res/layout/start_download_dialog.xml | 19 ++ demos/main/src/main/res/values/strings.xml | 10 +- .../exoplayer2/util/ParcelableArray.java | 64 ---- .../dash/manifest/RepresentationKey.java | 35 +- .../source/hls/playlist/RenditionKey.java | 30 +- .../smoothstreaming/manifest/StreamKey.java | 30 +- library/ui/src/main/res/values-hi/strings.xml | 14 +- 30 files changed, 454 insertions(+), 480 deletions(-) delete mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java create mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java create mode 100644 demos/main/src/main/res/drawable-hdpi/ic_download.png create mode 100644 demos/main/src/main/res/drawable-hdpi/ic_download_done.png delete mode 100644 demos/main/src/main/res/drawable-hdpi/ic_offline_pin_white_36dp.png create mode 100644 demos/main/src/main/res/drawable-mdpi/ic_download.png create mode 100644 demos/main/src/main/res/drawable-mdpi/ic_download_done.png delete mode 100644 demos/main/src/main/res/drawable-mdpi/ic_offline_pin_white_36dp.png create mode 100644 demos/main/src/main/res/drawable-xhdpi/ic_download.png create mode 100644 demos/main/src/main/res/drawable-xhdpi/ic_download_done.png delete mode 100644 demos/main/src/main/res/drawable-xhdpi/ic_offline_pin_white_36dp.png create mode 100644 demos/main/src/main/res/drawable-xxhdpi/ic_download.png create mode 100644 demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png delete mode 100644 demos/main/src/main/res/drawable-xxhdpi/ic_offline_pin_white_36dp.png create mode 100644 demos/main/src/main/res/drawable-xxxhdpi/ic_download.png create mode 100644 demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png delete mode 100644 demos/main/src/main/res/drawable-xxxhdpi/ic_offline_pin_white_36dp.png delete mode 100644 demos/main/src/main/res/layout/downloader_activity.xml create mode 100644 demos/main/src/main/res/layout/start_download_dialog.xml delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/ParcelableArray.java diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 25b87f5185..3bedefc60e 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -75,8 +75,6 @@ - - diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 2788684de5..b5c127d2e3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -43,6 +43,7 @@ import java.io.File; public class DemoApplication extends Application { private static final String DOWNLOAD_ACTION_FILE = "actions"; + private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; private static final Deserializer[] DOWNLOAD_DESERIALIZERS = @@ -58,6 +59,7 @@ public class DemoApplication extends Application { private File downloadDirectory; private Cache downloadCache; private DownloadManager downloadManager; + private DownloadTracker downloadTracker; @Override public void onCreate() { @@ -83,20 +85,36 @@ public class DemoApplication extends Application { return "withExtensions".equals(BuildConfig.FLAVOR); } - /** Returns the download manager used by the application. */ - public synchronized DownloadManager getDownloadManager() { + public DownloadManager getDownloadManager() { + initDownloadManager(); + return downloadManager; + } + + public DownloadTracker getDownloadTracker() { + initDownloadManager(); + return downloadTracker; + } + + private synchronized void initDownloadManager() { if (downloadManager == null) { - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory(null)); + DownloaderConstructorHelper downloaderConstructorHelper = + new DownloaderConstructorHelper( + getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null)); downloadManager = new DownloadManager( - constructorHelper, + downloaderConstructorHelper, MAX_SIMULTANEOUS_DOWNLOADS, DownloadManager.DEFAULT_MIN_RETRY_COUNT, new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), DOWNLOAD_DESERIALIZERS); + downloadTracker = + new DownloadTracker( + /* context= */ this, + buildDataSourceFactory(/* listener= */ null), + new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE), + DOWNLOAD_DESERIALIZERS); + downloadManager.addListener(downloadTracker); } - return downloadManager; } private synchronized Cache getDownloadCache() { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java deleted file mode 100644 index 8546cf50e0..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2017 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.google.android.exoplayer2.demo; - -import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Parcelable; -import android.view.View; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.Toast; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -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.ui.DefaultTrackNameProvider; -import com.google.android.exoplayer2.ui.TrackNameProvider; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.util.ParcelableArray; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** An activity for downloading media. */ -public class DownloadActivity extends Activity { - - public static final String PLAYER_INTENT = "player_intent"; - public static final String SAMPLE_NAME = "sample_name"; - - private Intent playerIntent; - private String sampleName; - - private TrackNameProvider trackNameProvider; - private DownloadHelper downloadHelper; - private ListView trackList; - private ArrayAdapter arrayAdapter; - private ArrayList trackKeys; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.downloader_activity); - trackNameProvider = new DefaultTrackNameProvider(getResources()); - - Intent intent = getIntent(); - playerIntent = intent.getParcelableExtra(PLAYER_INTENT); - Uri sampleUri = playerIntent.getData(); - sampleName = intent.getStringExtra(SAMPLE_NAME); - getActionBar().setTitle(sampleName); - - arrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_multiple_choice); - trackList = findViewById(R.id.representation_list); - trackList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - trackList.setAdapter(arrayAdapter); - trackKeys = new ArrayList<>(); - - DemoApplication application = (DemoApplication) getApplication(); - DataSource.Factory manifestDataSourceFactory = - application.buildDataSourceFactory(/* listener= */ null); - String extension = playerIntent.getStringExtra(EXTENSION_EXTRA); - int type = Util.inferContentType(sampleUri, extension); - switch (type) { - case C.TYPE_DASH: - downloadHelper = new DashDownloadHelper(sampleUri, manifestDataSourceFactory); - break; - case C.TYPE_SS: - downloadHelper = new SsDownloadHelper(sampleUri, manifestDataSourceFactory); - break; - case C.TYPE_HLS: - downloadHelper = new HlsDownloadHelper(sampleUri, manifestDataSourceFactory); - break; - case C.TYPE_OTHER: - downloadHelper = new ProgressiveDownloadHelper(sampleUri); - break; - default: - throw new IllegalStateException("Unsupported type: " + type); - } - - downloadHelper.prepare( - new DownloadHelper.Callback() { - @Override - public void onPrepared(DownloadHelper helper) { - DownloadActivity.this.onPrepared(); - } - - @Override - public void onPrepareError(DownloadHelper helper, IOException e) { - DownloadActivity.this.onPrepareError(); - } - }); - } - - private void onPrepared() { - 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++) { - arrayAdapter.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); - trackKeys.add(new TrackKey(i, j, k)); - } - } - } - } - - private void onPrepareError() { - Toast.makeText( - getApplicationContext(), R.string.download_manifest_load_error, Toast.LENGTH_LONG) - .show(); - } - - // This method is referenced in the layout file - public void onClick(View v) { - // switch-case doesn't work as in some compile configurations id definitions aren't constant - int id = v.getId(); - if (id == R.id.download_button) { - startDownload(); - } else if (id == R.id.remove_all_button) { - removeDownload(); - } else if (id == R.id.play_button) { - playDownload(); - } - } - - private void startDownload() { - List selectedTrackKeys = getSelectedTrackKeys(); - if (trackKeys.isEmpty() || !selectedTrackKeys.isEmpty()) { - DownloadService.addDownloadAction( - this, - DemoDownloadService.class, - downloadHelper.getDownloadAction(Util.getUtf8Bytes(sampleName), selectedTrackKeys)); - } - } - - private void removeDownload() { - DownloadService.addDownloadAction( - this, - DemoDownloadService.class, - downloadHelper.getRemoveAction(Util.getUtf8Bytes(sampleName))); - for (int i = 0; i < trackList.getChildCount(); i++) { - trackList.setItemChecked(i, false); - } - } - - private void playDownload() { - DownloadAction action = downloadHelper.getDownloadAction(null, getSelectedTrackKeys()); - List keys = null; - if (action instanceof SegmentDownloadAction) { - keys = ((SegmentDownloadAction) action).keys; - } - if (keys.isEmpty()) { - playerIntent.removeExtra(PlayerActivity.MANIFEST_FILTER_EXTRA); - } else { - playerIntent.putExtra( - PlayerActivity.MANIFEST_FILTER_EXTRA, - new ParcelableArray(keys.toArray(new Parcelable[0]))); - } - startActivity(playerIntent); - } - - private List getSelectedTrackKeys() { - ArrayList selectedTrackKeys = new ArrayList<>(); - for (int i = 0; i < trackList.getChildCount(); i++) { - if (trackList.isItemChecked(i)) { - selectedTrackKeys.add(trackKeys.get(i)); - } - } - return selectedTrackKeys; - } -} 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 new file mode 100644 index 0000000000..29310f8571 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2017 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.google.android.exoplayer2.demo; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.ActionFile; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadManager; +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.SegmentDownloadAction; +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.ui.DefaultTrackNameProvider; +import com.google.android.exoplayer2.ui.TrackNameProvider; +import com.google.android.exoplayer2.upstream.DataSource; +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; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Tracks media that has been downloaded. + * + *

Tracked downloads are persisted using an {@link ActionFile}, however in a real application + * it's expected that state will be stored directly in the application's media database, so that it + * can be queried efficiently together with other information about the media. + */ +public class DownloadTracker implements DownloadManager.Listener { + + /** Listens for changes in the tracked downloads. */ + public interface Listener { + + /** Called when the tracked downloads changed. */ + void onDownloadsChanged(); + } + + private static final String TAG = "DownloadTracker"; + + private final Context context; + private final DataSource.Factory dataSourceFactory; + private final TrackNameProvider trackNameProvider; + private final CopyOnWriteArraySet listeners; + private final HashMap trackedDownloadStates; + private final ActionFile actionFile; + private final Handler actionFileWriteHandler; + + public DownloadTracker( + Context context, + DataSource.Factory dataSourceFactory, + File actionFile, + DownloadAction.Deserializer[] deserializers) { + this.context = context.getApplicationContext(); + this.dataSourceFactory = dataSourceFactory; + this.actionFile = new ActionFile(actionFile); + trackNameProvider = new DefaultTrackNameProvider(context.getResources()); + listeners = new CopyOnWriteArraySet<>(); + trackedDownloadStates = new HashMap<>(); + HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); + actionFileWriteThread.start(); + actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); + loadTrackedActions(deserializers); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public boolean isDownloaded(Uri uri) { + return trackedDownloadStates.containsKey(uri); + } + + @SuppressWarnings("unchecked") + public List getOfflineStreamKeys(Uri uri) { + if (!trackedDownloadStates.containsKey(uri)) { + return Collections.emptyList(); + } + DownloadAction action = trackedDownloadStates.get(uri); + if (action instanceof SegmentDownloadAction) { + return ((SegmentDownloadAction) action).keys; + } + return Collections.emptyList(); + } + + public void toggleDownload(Activity activity, String name, Uri uri, String extension) { + if (isDownloaded(uri)) { + DownloadAction removeAction = + getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name)); + addDownloadAction(removeAction); + } else { + StartDownloadDialogHelper helper = + new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name); + helper.prepare(); + } + } + + // DownloadManager.Listener + + @Override + public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { + DownloadAction action = taskState.action; + Uri uri = action.uri; + if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED) + || (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) { + // A download has been removed, or has failed. Stop tracking it. + if (trackedDownloadStates.remove(uri) != null) { + handleTrackedDownloadStatesChanged(); + } + } + } + + @Override + public void onIdle(DownloadManager downloadManager) { + // Do nothing. + } + + // Internal methods + + private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) { + try { + DownloadAction[] allActions = actionFile.load(deserializers); + for (DownloadAction action : allActions) { + trackedDownloadStates.put(action.uri, action); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load tracked actions", e); + } + } + + private void handleTrackedDownloadStatesChanged() { + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]); + actionFileWriteHandler.post( + new Runnable() { + @Override + public void run() { + try { + actionFile.store(actions); + } catch (IOException e) { + Log.e(TAG, "Failed to store tracked actions", e); + } + } + }); + } + + private void startDownload(DownloadAction action) { + if (trackedDownloadStates.containsKey(action.uri)) { + // This content is already being downloaded. Do nothing. + return; + } + trackedDownloadStates.put(action.uri, action); + handleTrackedDownloadStatesChanged(); + addDownloadAction(action); + } + + private void addDownloadAction(DownloadAction action) { + DownloadService.addDownloadAction(context, DemoDownloadService.class, action); + } + + private DownloadHelper getDownloadHelper(Uri uri, String extension) { + int type = Util.inferContentType(uri, extension); + switch (type) { + case C.TYPE_DASH: + return new DashDownloadHelper(uri, dataSourceFactory); + case C.TYPE_SS: + return new SsDownloadHelper(uri, dataSourceFactory); + case C.TYPE_HLS: + return new HlsDownloadHelper(uri, dataSourceFactory); + case C.TYPE_OTHER: + return new ProgressiveDownloadHelper(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + private final class StartDownloadDialogHelper + implements DownloadHelper.Callback, DialogInterface.OnClickListener { + + private final DownloadHelper downloadHelper; + private final String name; + + private final AlertDialog.Builder builder; + private final View dialogView; + private final List trackKeys; + private final ArrayAdapter trackTitles; + private final ListView representationList; + + public StartDownloadDialogHelper( + Activity activity, DownloadHelper downloadHelper, String name) { + this.downloadHelper = downloadHelper; + this.name = name; + builder = + new AlertDialog.Builder(activity) + .setTitle(R.string.exo_download_description) + .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); + + 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() { + downloadHelper.prepare(this); + } + + @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 (!trackKeys.isEmpty()) { + builder.setView(dialogView); + } + builder.create().show(); + } + } + + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + Toast.makeText( + context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) + .show(); + } + + @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)); + } + } + 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); + } + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 0427de05a8..091e483155 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -21,7 +21,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; @@ -83,7 +82,6 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.EventLogger; -import com.google.android.exoplayer2.util.ParcelableArray; import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Constructor; import java.net.CookieHandler; @@ -104,13 +102,11 @@ public class PlayerActivity extends Activity public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String EXTENSION_EXTRA = "extension"; - public static final String MANIFEST_FILTER_EXTRA = "manifest_filter"; public static final String ACTION_VIEW_LIST = "com.google.android.exoplayer.demo.action.VIEW_LIST"; public static final String URI_LIST_EXTRA = "uri_list"; public static final String EXTENSION_LIST_EXTRA = "extension_list"; - public static final String MANIFEST_FILTER_LIST_EXTRA = "manifest_filter_list"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; @@ -313,11 +309,9 @@ public class PlayerActivity extends Activity String action = intent.getAction(); Uri[] uris; String[] extensions; - Parcelable[] manifestFilters; if (ACTION_VIEW.equals(action)) { uris = new Uri[] {intent.getData()}; extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - manifestFilters = new Parcelable[] {intent.getParcelableExtra(MANIFEST_FILTER_EXTRA)}; } else if (ACTION_VIEW_LIST.equals(action)) { String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); uris = new Uri[uriStrings.length]; @@ -328,10 +322,6 @@ public class PlayerActivity extends Activity if (extensions == null) { extensions = new String[uriStrings.length]; } - manifestFilters = intent.getParcelableArrayExtra(MANIFEST_FILTER_LIST_EXTRA); - if (manifestFilters == null) { - manifestFilters = new Parcelable[uriStrings.length]; - } } else { showToast(getString(R.string.unexpected_intent_action, action)); finish(); @@ -413,9 +403,7 @@ public class PlayerActivity extends Activity MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - ParcelableArray manifestFilter = (ParcelableArray) manifestFilters[i]; - List filter = manifestFilter != null ? manifestFilter.asList() : null; - mediaSources[i] = buildMediaSource(uris[i], extensions[i], filter); + mediaSources[i] = buildMediaSource(uris[i], extensions[i]); } mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -445,12 +433,11 @@ public class PlayerActivity extends Activity } private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null, null); + return buildMediaSource(uri, null); } @SuppressWarnings("unchecked") - private MediaSource buildMediaSource( - Uri uri, @Nullable String overrideExtension, @Nullable List manifestFilter) { + private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { @ContentType int type = Util.inferContentType(uri, overrideExtension); switch (type) { case C.TYPE_DASH: @@ -459,7 +446,7 @@ public class PlayerActivity extends Activity buildDataSourceFactory(false)) .setManifestParser( new FilteringManifestParser<>( - new DashManifestParser(), (List) manifestFilter)) + new DashManifestParser(), (List) getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory( @@ -467,13 +454,13 @@ public class PlayerActivity extends Activity buildDataSourceFactory(false)) .setManifestParser( new FilteringManifestParser<>( - new SsManifestParser(), (List) manifestFilter)) + new SsManifestParser(), (List) getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) .setPlaylistParser( new FilteringManifestParser<>( - new HlsPlaylistParser(), (List) manifestFilter)) + new HlsPlaylistParser(), (List) getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); @@ -483,6 +470,10 @@ public class PlayerActivity extends Activity } } + private List getOfflineStreamKeys(Uri uri) { + return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); + } + private DefaultDrmSessionManager buildDrmSessionManagerV18( UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) throws UnsupportedDrmException { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 04795f4abe..fb0b20f0e4 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -24,7 +24,6 @@ import android.os.AsyncTask; import android.os.Bundle; import android.util.JsonReader; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -47,17 +46,27 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** An activity for selecting from a list of media samples. */ -public class SampleChooserActivity extends Activity { +public class SampleChooserActivity extends Activity + implements DownloadTracker.Listener, OnChildClickListener { private static final String TAG = "SampleChooserActivity"; + private DownloadTracker downloadTracker; + private SampleAdapter sampleAdapter; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sample_chooser_activity); + sampleAdapter = new SampleAdapter(); + ExpandableListView sampleListView = findViewById(R.id.sample_list); + sampleListView.setAdapter(sampleAdapter); + sampleListView.setOnChildClickListener(this); + Intent intent = getIntent(); String dataUri = intent.getDataString(); String[] uris; @@ -80,10 +89,30 @@ public class SampleChooserActivity extends Activity { uriList.toArray(uris); Arrays.sort(uris); } + + downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker(); + startDownloadServiceForeground(); + SampleListLoader loaderTask = new SampleListLoader(); loaderTask.execute(uris); + } - startDownloadServiceForeground(); + @Override + public void onStart() { + super.onStart(); + downloadTracker.addListener(this); + sampleAdapter.notifyDataSetChanged(); + } + + @Override + public void onStop() { + downloadTracker.removeListener(this); + super.onStop(); + } + + @Override + public void onDownloadsChanged() { + sampleAdapter.notifyDataSetChanged(); } private void startDownloadServiceForeground() { @@ -96,28 +125,44 @@ public class SampleChooserActivity extends Activity { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) .show(); } - ExpandableListView sampleList = findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleAdapter(this, groups)); - sampleList.setOnChildClickListener( - new OnChildClickListener() { - @Override - public boolean onChildClick( - ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { - onSampleClicked(groups.get(groupPosition).samples.get(childPosition)); - return true; - } - }); + sampleAdapter.setSampleGroups(groups); } - private void onSampleClicked(Sample sample) { + @Override + public boolean onChildClick( + ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { + Sample sample = (Sample) view.getTag(); startActivity(sample.buildIntent(this)); + return true; } private void onSampleDownloadButtonClicked(Sample sample) { - Intent intent = new Intent(this, DownloadActivity.class); - intent.putExtra(DownloadActivity.SAMPLE_NAME, sample.name); - intent.putExtra(DownloadActivity.PLAYER_INTENT, sample.buildIntent(this)); - startActivity(intent); + int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample); + if (downloadUnsupportedStringId != 0) { + Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) + .show(); + } else { + UriSample uriSample = (UriSample) sample; + downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension); + } + } + + private int getDownloadUnsupportedStringId(Sample sample) { + if (sample instanceof PlaylistSample) { + return R.string.download_playlist_unsupported; + } + UriSample uriSample = (UriSample) sample; + if (uriSample.drmInfo != null) { + return R.string.download_drm_unsupported; + } + if (uriSample.adTagUri != null) { + return R.string.download_ads_unsupported; + } + String scheme = uriSample.uri.getScheme(); + if (!("http".equals(scheme) || "https".equals(scheme))) { + return R.string.download_scheme_unsupported; + } + return 0; } private final class SampleListLoader extends AsyncTask> { @@ -296,12 +341,15 @@ public class SampleChooserActivity extends Activity { private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener { - private final Context context; - private final List sampleGroups; + private List sampleGroups; - public SampleAdapter(Context context, List sampleGroups) { - this.context = context; + public SampleAdapter() { + sampleGroups = Collections.emptyList(); + } + + public void setSampleGroups(List sampleGroups) { this.sampleGroups = sampleGroups; + notifyDataSetChanged(); } @Override @@ -319,7 +367,7 @@ public class SampleChooserActivity extends Activity { View convertView, ViewGroup parent) { View view = convertView; if (view == null) { - view = LayoutInflater.from(context).inflate(R.layout.sample_list_item, parent, false); + view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false); View downloadButton = view.findViewById(R.id.download_button); downloadButton.setOnClickListener(this); downloadButton.setFocusable(false); @@ -348,8 +396,9 @@ public class SampleChooserActivity extends Activity { ViewGroup parent) { View view = convertView; if (view == null) { - view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1, - parent, false); + view = + getLayoutInflater() + .inflate(android.R.layout.simple_expandable_list_item_1, parent, false); } ((TextView) view).setText(getGroup(groupPosition).title); return view; @@ -376,24 +425,18 @@ public class SampleChooserActivity extends Activity { } private void initializeChildView(View view, Sample sample) { + view.setTag(sample); TextView sampleTitle = view.findViewById(R.id.sample_title); sampleTitle.setText(sample.name); + + boolean canDownload = getDownloadUnsupportedStringId(sample) == 0; + boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri); ImageButton downloadButton = view.findViewById(R.id.download_button); downloadButton.setTag(sample); - downloadButton.setColorFilter(0xFFBBBBBB); - downloadButton.setVisibility(canDownload(sample) ? View.VISIBLE : View.GONE); - } - - private boolean canDownload(Sample sample) { - if (!(sample instanceof UriSample)) { - return false; - } - UriSample uriSample = (UriSample) sample; - if (uriSample.drmInfo != null || uriSample.adTagUri != null) { - return false; - } - String scheme = uriSample.uri.getScheme(); - return "http".equals(scheme) || "https".equals(scheme); + downloadButton.setColorFilter( + canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE); + downloadButton.setImageResource( + isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); } } diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download.png b/demos/main/src/main/res/drawable-hdpi/ic_download.png new file mode 100644 index 0000000000000000000000000000000000000000..fa3ebbb31013c4b0710249bda570dc6907f89c98 GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL0wmRZ7KH(+K2I0Nkch)?FYe}TFyLuPbW376 z)pJlo?yPcZ^WudL4T}87nO^?OR<7M-I>5VN zSGv-x+Pqecz(p>M;SU(JI7^xBN~X5I(&E2*)#%EBpV^XG4Bw*~qz?ElcvjfUb;wkU z(=NxSsBxln1dt?FG9Yz75pHICCyV$yXUBLPM;AICXDFOq>d^-WiLGAiiC{)G7_e| z@r4BpSYd0$fS@XN$vtIT(~)B*T;tlDh6HP}Mbd`I6>F0sEv4mxwRxmUkF?8TO;W0s z5|zPD1otXouDN#&J9aIg8U)PPZ*IdbMU+Bmxz7KvfO`Rz-+&+d+jjypHjq=ikAAfr~q&dt={`68eI z!Xnm3*H1o>UlR)`=oYaN`cmHK950+H7#6V!rgk|epkVBR!H#!&N{WrZ%IPV=pRos4 zPEQH`L7HIY-U=uP^*?Gmx5TxKn(c8V&;Gc|A4uMd1+N%!uLLhoVM~|Kf*X{>+^}EI zj0LQ{Vs{*wF~vu(*oEyQ^{Y2*R_612L*ACp4znfifnz(Sh)Aj0j_`0trq-bjgF9NT z$J~ewk{$>p2lxhaT$^z$UrlsaQ?@%UsxMvo++u4|k0wldT1B8K8mZytjh{y4_Q*ZMgP~c#YJsK$U z)~(q1m-m)lF^^vy&zFBx;@>#sNTW$[{bH;=%sZG7QZ>uQBcw?@Ce|ClF7G156^ z#Rt_}Q9HRs{l8sYa>)1&$3+cMLFMTJXY2PZ*sH$V@BeR?lj;@>g(qYZ*L3k@0BvRP MboFyt=akR{0D4_J0ssI2 literal 0 HcmV?d00001 diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download_done.png b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png new file mode 100644 index 0000000000000000000000000000000000000000..08073a2a6dce34c3c57ba728240d4c9b31f519c9 GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBYCT;XLn;{Go;MV1Fko;FR0-h{ zn$RY^<^h9@0;5b#TVCX@f+aFPnTqtCyDu6WQi4vrPx`R*n0xo4Gka1AscENmpO!Z0IxtcdI8Z|K6Y*Z%_H4pbeMP6 z&iMR0ieG>*JE(sc9R?U`EQjMPHHHBlRCwW+dz4s0SxWL$Xu^(ITgEb^F{D(XW02;F z)E4N|kU?sXYkmrvAoa`@D_o-x)MFa2p_E`)Ejp`BY)Y3-&Q66Accy=BO=t0#SZNZ71=CyXcalz*3g=+gWSfg zq2Z}V?VL6}#2)?&QhP+lucLY5yV3FmIbuVsHN(^^J7JZl diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download.png b/demos/main/src/main/res/drawable-xhdpi/ic_download.png new file mode 100644 index 0000000000000000000000000000000000000000..671e0b3eceef10ff0879643331d63bf159e6865b GIT binary patch literal 187 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifXMj(LE0F#V2E`RilYtzjk|4ie zhKAYNeiA@lyr+v}NX4zUXEt&k4q!Oy@cO-diI-?54^#Vta^^dxpY_j8ckLF_ttvjU yv(xUUhmsv`ct{-ro3igcytD1a$&9qRhs>9DvVUH^W!ib5RScf4elF{r5}E*wu0R6- literal 0 HcmV?d00001 diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png new file mode 100644 index 0000000000000000000000000000000000000000..2339c0bf16e704b0a777a7e3c9d5531b0a510f40 GIT binary patch literal 304 zcmV-00nh%4P)IsdXBUJ^aMRZgMdI00mE<+!URGH7!WWZp?4YzLLj#78*sp#%eeQR+5h(h zpU*@@M5K%f6N`gK08*$NPl}P_QkWc)No zp0wzm<4Iq@f-8%H6FwQ5h{)lg0tG2ZK_Vg|du0PUE_qut;+EY20000%R@Xj<9SSWtQ<6SL>K2^YC?7W|#v{NN&drDL}Jt8LpxgFQ|d@yvoX2?=W!JTu~iJsOyDV<Qu%7X!}9q z3OO+Lfg+L$IgrrG1|3(*0qAFg9`W~Ekpt5#P=gp>UwF@p8V5Gi-Jm^ueZ^pS$BPmN zpz8*mkli;7CQTA@VBiLg{G2pN%z>dB^h{QtF=>(l2WD>2f~+i`Qlz&E9GJU7Yw(bI zDUzH6OE)M1KS`Mbu>_PRNjVM3wwAo+6*QCD*P&~HOsazWE?D!an!*Oy%K^^0jqUW4?z!T3OJTp7U#UK9-v zm4WUsc|aX(B_K;XAILDQ1ayhX3yN3;2Q6vk2N@2rK|z-Nq8~la3E63x&%zTz^@G}f z`r$l1O`Za9nzH4~MES59^7Yi2+Is4gn$806fECh~?CY*LuS6@(OIrF%;12hc>5g`| zHF>+>n*2t-CihQ^K2tViwP8v>bxb0Vf9iA@Fl5G@B{4Be=FAu}pi3Q7ZVX{(3)+IV Zpg(ST(F^}*`dyazTESX?MZCat$A76g8UYIu z(-UX!zs+pl`^jMLcG<&iywYZG;%`*%{#KlR|Mph>1iRN|34y`x z9XpSAxZ1j%v31jVWtjBNFe$KKu()5ayQ*hKbx*{VIk1Uc?ekP|Z_Eo~L_>F#D zC%X7m38U*0n^qj1vGeE*SDEZHGTAzhB9iY}Kjw{|@p6rIo9@wPOl`c;oqw4GC)%9+ Tr>>g{^bLcjtDnm{r-UW|-Bg2= literal 0 HcmV?d00001 diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png new file mode 100644 index 0000000000000000000000000000000000000000..b631a00088b192f8793e8e9c721cd133cd7f325d GIT binary patch literal 450 zcmV;z0X_bSP)m=(edlr3EaGus#)?oguD&H&p zIqNikR=;N@(%Q)yu)G+c#_vjPXVtQ0IaWy9wl6I3NXm)z2G)5erPRi)ezQWQwUkx$ zte(IIZ=^IxIkw)z8n1MwLZ4Z@w2HYvX-AfjKdl)m%dtwcgRDSlhgJZd_7W<~vD{MH zffb3TMa%NE20z=k;$n$Lz${M-la)x@wHR2Uj@z*~n5P~v%hTAhcvxi9A`lS~5fNdJ soc0vv6s9nRDNJDs6A=*+5fKrQ4-@T7x~OO3+yDRo07*qoM6N<$g37tT1ONa4 literal 0 HcmV?d00001 diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_offline_pin_white_36dp.png b/demos/main/src/main/res/drawable-xxhdpi/ic_offline_pin_white_36dp.png deleted file mode 100644 index c711b9134f47e45bc3d3513e3180bb504b5ef26d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1049 zcmV+!1m^pRP)-sgORU&8$1+<9M}Yk7+jxjMwn)aCX$*g@qkNo#|^u}3%0On zi|1U95msQ5HPW`qB!zfjz0A?TRSjn84G%lbefDwPKKD2k5_XeKd{yN}7}z;p8+L&;LbOW1PuKvPgsRGbFW3No5waQsK42Hv zBy?5!UBgPO5knPE8e>fMGdon&dV3n)v`Mn3BVmtXKxaygCcYQdpj*C96f#Wx)Y? z8EnX&)uO4g00w2S+o@PBnkEazWw5!FtQJj=1v4_(BJ%#AggKd}!h!`EY=tDfKbfY) zf@K+O102!5QenZm47LM_X~h!=8Z4;GV26;NrpSV(40a6ZX^Jd3lEdt2a+W&m5bSAk zmj0jO4)(pHsLq1A{OvwHO^*fZ@(<{gGzm-p19}nC(R@+(8tw4`2HsE<1#0FWsDT|0R2?1-!9Qf4QpI+>-UK+In3~!PU$Ho z3G6a~^8Faeuu3OX(Roy>--NG+>(dxdXPWly@U~;a=3R zI~ZHoDXP)JKG0(iGu((4HkS5>n6ILQ{ftQq>!ThW>@#Q6!wla?2fOO(=jrpr)FX^5 zSdKEW^oE?<5s+14=ue8Shj#j-7}`1SXUwWb$ZA~lHyBqXR8=ne9A)gM66m#c-uLin zkuuu7p{Ow~oZ~T?JtC(!;C+p4H2Ta{?UDRGUgEo-I2&e(!gW64s}GEYTzk>OUG{L@ z9(U*ozo_I4581_4yZlHgrqwbNR7hKe2?{YT0~+NSHEjCCQ%2)n-_*r0QF`o2}}_}t%%${nijyLSKM`^zAAsKnfb Sjb}E{YYd*QelF{r5}E)O=6ezV literal 0 HcmV?d00001 diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png new file mode 100644 index 0000000000000000000000000000000000000000..52fe8f69907ddd3cb364be0d13278919d7c32afa GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q1xWh(YZ)^zFtK^MIEGX(zP-elel=0T;bVHa ztAV$mt6-;+qmPRKhYANvi%^%qp%>LH5(Y;#lsgm-97$3V>)x2N%tvc(%f=6OcmLn5 ze!O??eRm5#k4Y+?5R}Dv*~h(e$NBv4PM&hYPb+<n!&;B2AjaGHus}W8=sWkcGlSLi_P>!sxl9B ztJ++Bm`_%HNy3yt@tDafP z>ptX>esalW@x1gd-(QBOBU)|dN1rZk)cs|8_{O@=bGo;EXwj29F2BQ7Jbp&kwGHj! zC5FZyY-j(pT|D{4#fHjrZh2FLO3!V+=QfY!{L4qGu{kDl)%UMxD)n=p*m-S}jat;> zlr7qxDf3h=8o!Fjwe)ZLsMQ%}Dd(ydb@J1UjMn79ltm`}ozpf0RX+0&nW=F$dC_;} zdFq~Dwyd{le^l@KetqXOkTH)EL}s3_$z0_6Ii*wVro5|~k^iiWgEo8J7fR58qfv`6#0Uwsyv~JZQ&L*TvX#GZPJ0xvWUydoR$l;l}AWujI9!Zj#5HO3mlOJl;tvY zBvt1U87V+{{zPIE-o@#qtsvH`S(MPWSpL}l8{1KkqH zqfhj!X`tQI1oG$;)!1nk=($*)#+)EYQzn51JmC4BKS)-eIiObo^1$DwfChr(0q8RW zG!ZBdjGF-J43-C=vw5KF0rSAsW`Q>FPry8|LQ8W%g`jx=&Nc^B44elZHv`nhm%w?T zN=_eWFnAt-fTF65)MJ{bK_rX^ zU|({e^X7RPM8bGrFgef&`kh3fzD^{H2Zoaa-9f)E{LKGd<;@7y|nOGL-S@xZXw4Kw>h?s@cYnDu~tBKJJ{w_IDnE|DOfIvcef`&cGAA%q9! zTs=7SrBQc}p8aS@52y?IIemE{%QD|WOJwwb6!(yy!xyL&=a8S%SMc&wk)OktI2BI_ zy~pxjBzN1HWM5e(j7Qp@+1~T>^QEGA=7| zQu-RvHh!aqi`Kb8-j7AJJ9$RhJ!OYqtBm`2Nm{+&fS=2-`xwV|Mf!YSr+$?RHd^5d zonb8GZ{aMDsbZ>sdBho7L@(smm^=mUvP3g2ahC!)R9d4*d$R0di2KY@PjKNd$0$SW zA%jYH6z*md`xs=H+e|XedzM*copn}O<~`F)a+_fW*~dndt5L*>KoKYcMW6^2fg(@@ hiW7k%P})Fo{sV{ggnx-Sz*YbN002ovPDHLkV1kyMZ@K^g diff --git a/demos/main/src/main/res/layout/downloader_activity.xml b/demos/main/src/main/res/layout/downloader_activity.xml deleted file mode 100644 index 64975e8b98..0000000000 --- a/demos/main/src/main/res/layout/downloader_activity.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - -