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 index dc2efc911e..daf2df58dd 100644 --- 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 @@ -204,7 +204,8 @@ public class DownloadActivity extends Activity { public RepresentationItem(Parcelable key, String title, float percentDownloaded) { this.key = key; this.title = title; - this.percentDownloaded = (int) percentDownloaded; + this.percentDownloaded = + (int) (percentDownloaded == C.PERCENTAGE_UNSET ? 0 : percentDownloaded); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 045f3bfc6e..79de146f29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -64,6 +64,9 @@ public final class C { */ public static final int LENGTH_UNSET = -1; + /** Represents an unset or unknown percentage. */ + public static final int PERCENTAGE_UNSET = -1; + /** * The number of microseconds in one second. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 85675f6c7e..660b3cfabc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -541,14 +541,11 @@ public final class DownloadManager { /** The state of the task. See {@link State}. */ public final @State int state; /** - * The download percentage, or {@link Float#NaN} if it can't be calculated or the task is for - * removing. + * The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is available + * or if this is a removal task. */ public final float downloadPercentage; - /** - * The downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been calculated yet or the task - * is for removing. - */ + /** The total number of downloaded bytes. */ public final long downloadedBytes; /** If {@link #state} is {@link #STATE_ERROR} then this is the cause, otherwise null. */ public final Throwable error; @@ -648,19 +645,16 @@ public final class DownloadManager { } /** - * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This - * value can be an estimation. + * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is + * available. */ public float getDownloadPercentage() { - return downloader != null ? downloader.getDownloadPercentage() : Float.NaN; + return downloader != null ? downloader.getDownloadPercentage() : C.PERCENTAGE_UNSET; } - /** - * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been - * calculated yet. - */ + /** Returns the total number of downloaded bytes. */ public long getDownloadedBytes() { - return downloader != null ? downloader.getDownloadedBytes() : C.LENGTH_UNSET; + return downloader != null ? downloader.getDownloadedBytes() : 0; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java index 9f4b560ac8..3390a4ed44 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -29,8 +29,6 @@ public interface Downloader { * @throws DownloadException Thrown if the media cannot be downloaded. * @throws InterruptedException If the thread has been interrupted. * @throws IOException Thrown when there is an io error while reading from cache. - * @see #getDownloadedBytes() - * @see #getDownloadPercentage() */ void init() throws InterruptedException, IOException; @@ -50,20 +48,12 @@ public interface Downloader { */ void remove() throws InterruptedException; - /** - * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been - * calculated yet. - * - * @see #init() - */ + /** Returns the total number of downloaded bytes. */ long getDownloadedBytes(); /** - * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This - * value can be an estimation. - * - * @see #init() + * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is + * available. */ float getDownloadPercentage(); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index e7ccc13547..ea38357af5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -83,7 +83,8 @@ public final class ProgressiveDownloader implements Downloader { @Override public float getDownloadPercentage() { long contentLength = cachingCounters.contentLength; - return contentLength == C.LENGTH_UNSET ? Float.NaN + return contentLength == C.LENGTH_UNSET + ? C.PERCENTAGE_UNSET : ((cachingCounters.totalCachedBytes() * 100f) / contentLength); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index fe98bff5f1..2ef9f73e6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.support.annotation.NonNull; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -33,9 +34,6 @@ import java.util.List; /** * Base class for multi segment stream downloaders. * - *

All of the methods are blocking. Also they are not thread safe, except {@link - * #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}. - * * @param The type of the manifest object. * @param The type of the representation key object. */ @@ -101,6 +99,9 @@ public abstract class SegmentDownloader, K> return getManifestIfNeeded(false); } + /** Returns keys for all representations. */ + public abstract K[] getAllRepresentationKeys() throws IOException; + /** * Selects multiple representations pointed to by the keys for downloading, checking status. Any * previous selection is cleared. If keys array is null or empty then all representations are @@ -113,25 +114,13 @@ public abstract class SegmentDownloader, K> } /** - * Returns keys for all representations. + * Initializes the downloader for the selected representations. * - * @see #selectRepresentations(Object[]) - */ - public abstract K[] getAllRepresentationKeys() throws IOException; - - /** - * Initializes the total segments, downloaded segments and downloaded bytes counters for the - * selected representations. - * - * @throws IOException Thrown when there is an io error while reading from cache. - * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws IOException Thrown when there is an error downloading. * @throws InterruptedException If the thread has been interrupted. - * @see #getTotalSegments() - * @see #getDownloadedSegments() - * @see #getDownloadedBytes() */ @Override - public final void init() throws InterruptedException, IOException { + public final void init() throws IOException, InterruptedException { try { getManifestIfNeeded(true); } catch (IOException e) { @@ -140,7 +129,7 @@ public abstract class SegmentDownloader, K> } try { initStatus(true); - } catch (IOException | InterruptedException e) { + } catch (IOException e) { resetCounters(); throw e; } @@ -150,8 +139,7 @@ public abstract class SegmentDownloader, K> * Downloads the content for the selected representations in sync or resumes a previously stopped * download. * - * @throws IOException Thrown when there is an io error while downloading. - * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws IOException Thrown when there is an error downloading. * @throws InterruptedException If the thread has been interrupted. */ @Override @@ -174,32 +162,6 @@ public abstract class SegmentDownloader, K> } } - /** - * Returns the total number of segments in the representations which are selected, or {@link - * C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #init() - */ - public final int getTotalSegments() { - return totalSegments; - } - - /** - * Returns the total number of downloaded segments in the representations which are selected, or - * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #init() - */ - public final int getDownloadedSegments() { - return downloadedSegments; - } - - /** - * Returns the total number of downloaded bytes in the representations which are selected, or - * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #init() - */ @Override public final long getDownloadedBytes() { return downloadedBytes; @@ -211,7 +173,7 @@ public abstract class SegmentDownloader, K> int totalSegments = this.totalSegments; int downloadedSegments = this.downloadedSegments; if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) { - return Float.NaN; + return C.PERCENTAGE_UNSET; } return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments; } @@ -228,7 +190,7 @@ public abstract class SegmentDownloader, K> if (manifest != null) { List segments = null; try { - segments = getSegments(offlineDataSource, manifest, true); + segments = getSegments(offlineDataSource, manifest, true).first; } catch (IOException e) { // Ignore exceptions. We do our best with what's available offline. } @@ -263,16 +225,17 @@ public abstract class SegmentDownloader, K> * @throws InterruptedException Thrown if the thread was interrupted. * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if * the media is not in a form that allows for its segments to be listed. - * @return A list of {@link Segment}s for given keys. + * @return A list of {@link Segment}s for given keys, and a boolean indicating whether the list is + * complete. */ - protected abstract List getSegments( + protected abstract Pair, Boolean> getSegments( DataSource dataSource, M manifest, boolean allowIncompleteIndex) throws InterruptedException, IOException; private void resetCounters() { totalSegments = C.LENGTH_UNSET; - downloadedSegments = C.LENGTH_UNSET; - downloadedBytes = C.LENGTH_UNSET; + downloadedSegments = 0; + downloadedBytes = 0; } private void remove(Uri uri) { @@ -289,9 +252,11 @@ public abstract class SegmentDownloader, K> throws IOException, InterruptedException { DataSource dataSource = getDataSource(offline); M filteredManifest = keys.isEmpty() ? manifest : manifest.copy(keys); - List segments = getSegments(dataSource, filteredManifest, offline); + Pair, Boolean> result = getSegments(dataSource, filteredManifest, offline); + List segments = result.first; + boolean isSegmentListComplete = result.second; CachingCounters cachingCounters = new CachingCounters(); - totalSegments = segments.size(); + totalSegments = isSegmentListComplete ? segments.size() : C.LENGTH_UNSET; downloadedSegments = 0; downloadedBytes = 0; for (int i = segments.size() - 1; i >= 0; i--) { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 23f1ff4f79..2de07c9a82 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.offline.DownloadException; @@ -39,9 +40,6 @@ import java.util.List; /** * Helper class to download DASH streams. * - *

Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. - * *

Example usage: * *

{@code
@@ -89,29 +87,32 @@ public final class DashDownloader extends SegmentDownloader getSegments(
+  protected Pair, Boolean> getSegments(
       DataSource dataSource, DashManifest manifest, boolean allowIndexLoadErrors)
       throws InterruptedException, IOException {
     ArrayList segments = new ArrayList<>();
+    boolean segmentListComplete = true;
     for (int i = 0; i < manifest.getPeriodCount(); i++) {
       Period period = manifest.getPeriod(i);
       long periodStartUs = C.msToUs(period.startMs);
       long periodDurationUs = manifest.getPeriodDurationUs(i);
       List adaptationSets = period.adaptationSets;
       for (int j = 0; j < adaptationSets.size(); j++) {
-        addSegmentsForAdaptationSet(
+        if (!addSegmentsForAdaptationSet(
             dataSource,
             adaptationSets.get(j),
             periodStartUs,
             periodDurationUs,
             allowIndexLoadErrors,
-            segments);
+            segments)) {
+          segmentListComplete = false;
+        }
       }
     }
-    return segments;
+    return Pair., Boolean>create(segments, segmentListComplete);
   }
 
-  private static void addSegmentsForAdaptationSet(
+  private static boolean addSegmentsForAdaptationSet(
       DataSource dataSource,
       AdaptationSet adaptationSet,
       long periodStartUs,
@@ -119,6 +120,7 @@ public final class DashDownloader extends SegmentDownloader out)
       throws IOException, InterruptedException {
+    boolean segmentListComplete = true;
     for (int i = 0; i < adaptationSet.representations.size(); i++) {
       Representation representation = adaptationSet.representations.get(i);
       DashSegmentIndex index;
@@ -129,12 +131,12 @@ public final class DashDownloader extends SegmentDownloader getSegments(
-      DataSource dataSource,
-      HlsMasterPlaylist manifest,
-      boolean allowIndexLoadErrors)
-      throws InterruptedException, IOException {
+  protected Pair, Boolean> getSegments(
+      DataSource dataSource, HlsMasterPlaylist manifest, boolean allowIndexLoadErrors)
+      throws IOException {
     HashSet encryptionKeyUris = new HashSet<>();
     ArrayList renditionUrls = new ArrayList<>();
     renditionUrls.addAll(manifest.variants);
     renditionUrls.addAll(manifest.audios);
     renditionUrls.addAll(manifest.subtitles);
     ArrayList segments = new ArrayList<>();
+
+    boolean segmentListComplete = true;
     for (HlsUrl renditionUrl : renditionUrls) {
       HlsMediaPlaylist mediaPlaylist = null;
       Uri uri = UriUtil.resolveToUri(manifest.baseUri, renditionUrl.url);
@@ -90,6 +91,7 @@ public final class HlsDownloader extends SegmentDownloader, Boolean>create(segments, segmentListComplete);
   }
 
   private static HlsPlaylist loadManifest(DataSource dataSource, Uri uri) throws IOException {
diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
index 00eb2c9059..29eb7c2516 100644
--- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
+++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
@@ -111,8 +111,6 @@ public class HlsDownloaderTest {
     hlsDownloader.selectRepresentations(getKeys(MEDIA_PLAYLIST_1_URI));
     hlsDownloader.download();
 
-    assertThat(hlsDownloader.getTotalSegments()).isEqualTo(4);
-    assertThat(hlsDownloader.getDownloadedSegments()).isEqualTo(4);
     assertThat(hlsDownloader.getDownloadedBytes())
         .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
   }
@@ -126,8 +124,6 @@ public class HlsDownloaderTest {
     newHlsDownloader.selectRepresentations(getKeys(MEDIA_PLAYLIST_1_URI));
     newHlsDownloader.init();
 
-    assertThat(newHlsDownloader.getTotalSegments()).isEqualTo(4);
-    assertThat(newHlsDownloader.getDownloadedSegments()).isEqualTo(4);
     assertThat(newHlsDownloader.getDownloadedBytes())
         .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
   }
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
index 5ec287537d..9a6f52f103 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
@@ -16,6 +16,7 @@
 package com.google.android.exoplayer2.source.smoothstreaming.offline;
 
 import android.net.Uri;
+import android.util.Pair;
 import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
 import com.google.android.exoplayer2.offline.SegmentDownloader;
@@ -34,9 +35,6 @@ import java.util.List;
 /**
  * Helper class to download SmoothStreaming streams.
  *
- * 

Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. - * *

Example usage: * *

{@code
@@ -84,9 +82,8 @@ public final class SsDownloader extends SegmentDownloader
   }
 
   @Override
-  protected List getSegments(
-      DataSource dataSource, SsManifest manifest, boolean allowIndexLoadErrors)
-      throws InterruptedException, IOException {
+  protected Pair, Boolean> getSegments(
+      DataSource dataSource, SsManifest manifest, boolean allowIndexLoadErrors) throws IOException {
     ArrayList segments = new ArrayList<>();
     for (StreamElement streamElement : manifest.streamElements) {
       for (int i = 0; i < streamElement.formats.length; i++) {
@@ -98,7 +95,7 @@ public final class SsDownloader extends SegmentDownloader
         }
       }
     }
-    return segments;
+    return Pair., Boolean>create(segments, true);
   }
 
 }
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java
index e858b37b24..dec28ad5e0 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java
@@ -19,6 +19,7 @@ import android.app.Notification;
 import android.content.Context;
 import android.support.annotation.Nullable;
 import android.support.v4.app.NotificationCompat;
+import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.offline.DownloadManager;
 import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
 import com.google.android.exoplayer2.util.ErrorMessageProvider;
@@ -48,31 +49,31 @@ public final class DownloadNotificationUtil {
       String channelId,
       @Nullable String message) {
     float totalPercentage = 0;
-    int determinatePercentageCount = 0;
-    boolean isAnyDownloadActive = false;
+    int downloadTaskCount = 0;
+    boolean allDownloadPercentagesUnknown = true;
+    boolean haveDownloadedBytes = false;
     for (DownloadState downloadState : downloadStates) {
       if (downloadState.downloadAction.isRemoveAction
           || downloadState.state != DownloadState.STATE_STARTED) {
         continue;
       }
-      float percentage = downloadState.downloadPercentage;
-      if (!Float.isNaN(percentage)) {
-        totalPercentage += percentage;
-        determinatePercentageCount++;
+      if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) {
+        allDownloadPercentagesUnknown = false;
+        totalPercentage += downloadState.downloadPercentage;
       }
-      isAnyDownloadActive = true;
+      haveDownloadedBytes |= downloadState.downloadedBytes > 0;
+      downloadTaskCount++;
     }
 
-    int titleStringId = isAnyDownloadActive ? R.string.exo_download_downloading : NULL_STRING_ID;
+    boolean haveDownloadTasks = downloadTaskCount > 0;
+    int titleStringId = haveDownloadTasks ? R.string.exo_download_downloading : NULL_STRING_ID;
     NotificationCompat.Builder notificationBuilder =
         createNotificationBuilder(context, smallIcon, channelId, message, titleStringId);
 
+    int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0;
+    boolean indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes;
+    notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate);
     notificationBuilder.setOngoing(true);
-    int max = 100;
-    int progress = (int) (totalPercentage / determinatePercentageCount);
-    boolean indeterminate = determinatePercentageCount == 0;
-    notificationBuilder.setProgress(max, progress, indeterminate);
-
     notificationBuilder.setShowWhen(false);
     return notificationBuilder.build();
   }