From 55154ad475eb3f498fcf31a7430bf6354b526fdb Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Fri, 12 Apr 2019 10:49:48 +0200 Subject: [PATCH 01/95] Allow hex format tags when parsing url templates --- .../android/exoplayer2/source/dash/manifest/UrlTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java index a7ce7eb9a0..370db184b0 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java @@ -139,7 +139,7 @@ public final class UrlTemplate { String formatTag = DEFAULT_FORMAT_TAG; if (formatTagIndex != -1) { formatTag = identifier.substring(formatTagIndex); - if (!formatTag.endsWith("d")) { + if (!formatTag.endsWith("d") && !formatTag.endsWith("x")) { formatTag += "d"; } identifier = identifier.substring(0, formatTagIndex); From af5131e393dd5c0de31de2144bf2238d65f2820e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 16 Apr 2019 18:56:09 +0100 Subject: [PATCH 02/95] Rename Shadow*Looper classes (PR#4868) ShadowLooper -> ShadowLegacyLooper ShadowRealisticLooper -> ShadowPausedLooper ShadowBaseLooper -> ShadowLooper And all public methods from ShadowLegacyLooper get pushed up to ShadowLooper Pull Request: https://github.com/robolectric/robolectric/pull/4868 Copybara: OK Also adjust Google3 tests using custom looper shadows where necessary. Convert exoplayer to paused looper to eliminate reliance on custom shadows PiperOrigin-RevId: 243839311 --- constants.gradle | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 5 +- .../analytics/AnalyticsCollectorTest.java | 6 +- .../drm/OfflineLicenseHelperTest.java | 5 +- .../offline/DownloadHelperTest.java | 9 +- .../offline/DownloadManagerTest.java | 6 +- .../source/ClippingMediaSourceTest.java | 6 +- .../source/ConcatenatingMediaSourceTest.java | 5 +- .../source/LoopingMediaSourceTest.java | 5 +- .../source/MergingMediaSourceTest.java | 5 +- .../source/dash/DashMediaPeriodTest.java | 5 +- .../dash/offline/DownloadManagerDashTest.java | 5 +- .../dash/offline/DownloadServiceDashTest.java | 5 +- .../source/hls/HlsMediaPeriodTest.java | 5 +- .../smoothstreaming/SsMediaPeriodTest.java | 5 +- .../exoplayer2/testutil/FakeClockTest.java | 4 +- testutils_robolectric/build.gradle | 1 + .../exoplayer2/testutil/RobolectricUtil.java | 231 ------------------ 18 files changed, 38 insertions(+), 278 deletions(-) delete mode 100644 testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java diff --git a/constants.gradle b/constants.gradle index 5063c59141..fd61b6f08e 100644 --- a/constants.gradle +++ b/constants.gradle @@ -20,8 +20,9 @@ project.ext { compileSdkVersion = 28 dexmakerVersion = '2.21.0' mockitoVersion = '2.25.0' - robolectricVersion = '4.2' + robolectricVersion = '4.3-alpha-2' autoValueVersion = '1.6' + autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' androidXTestVersion = '1.1.0' modulePrefix = ':' diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index a715289a04..4172a61271 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -50,7 +50,6 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; @@ -68,11 +67,11 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit test for {@link ExoPlayer}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public final class ExoPlayerTest { /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index af9591d1b7..d175a5eab3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -48,7 +48,6 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -58,11 +57,12 @@ import java.util.Iterator; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; /** Integration test for {@link AnalyticsCollector}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(Mode.PAUSED) public final class AnalyticsCollectorTest { private static final int EVENT_PLAYER_STATE_CHANGED = 0; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 30dde1db57..83ca752114 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -24,7 +24,6 @@ import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import java.util.HashMap; import org.junit.After; import org.junit.Before; @@ -32,11 +31,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Tests {@link OfflineLicenseHelper}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public class OfflineLicenseHelperTest { private OfflineLicenseHelper offlineLicenseHelper; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index f06e90dc48..3b78a2e3ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.shadows.ShadowBaseLooper.shadowMainLooper; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -34,7 +35,6 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; @@ -51,12 +51,11 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowLooper; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link DownloadHelper}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public class DownloadHelperTest { private static final String TEST_DOWNLOAD_TYPE = "downloadType"; @@ -426,7 +425,7 @@ public class DownloadHelperTest { } }); while (!preparedCondition.block(0)) { - ShadowLooper.runMainLooperToNextTask(); + shadowMainLooper().idleFor(shadowMainLooper().getNextScheduledTaskTime()); } if (prepareException.get() != null) { throw prepareException.get(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index f23248952c..5b2f47d2e5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; @@ -41,12 +40,13 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; import org.robolectric.shadows.ShadowLog; /** Tests {@link DownloadManager}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(Mode.PAUSED) public class DownloadManagerTest { /** Used to check if condition becomes true in this time interval. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index fc29e134d9..846600f243 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -43,11 +42,12 @@ import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; /** Unit tests for {@link ClippingMediaSource}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(Mode.PAUSED) public final class ClippingMediaSourceTest { private static final long TEST_PERIOD_DURATION_US = 1000000; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 2c4ca7c334..17cdd9e7ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; import java.util.ArrayList; @@ -44,11 +43,11 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link ConcatenatingMediaSource}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public final class ConcatenatingMediaSourceTest { private ConcatenatingMediaSource mediaSource; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 68de5fef18..df6506ed52 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -23,17 +23,16 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link LoopingMediaSource}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public class LoopingMediaSourceTest { private FakeTimeline multiWindowTimeline; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java index 130ba3c2dd..5ea15ac2e8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -26,15 +26,14 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link MergingMediaSource}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public class MergingMediaSourceTest { @Test diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java index 9f92e7df3c..fa077df209 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegm import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -45,11 +44,11 @@ import java.util.Arrays; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link DashMediaPeriod}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public final class DashMediaPeriodTest { @Test diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 76356cf3a8..6d7e2a881b 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -36,7 +36,6 @@ import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource.Factory; @@ -52,12 +51,12 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowLog; /** Tests {@link DownloadManager}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public class DownloadManagerDashTest { private static final int ASSERT_TRUE_TIMEOUT = 1000; diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index a35b6d1ea4..05d8979666 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -40,7 +40,6 @@ import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource; @@ -58,11 +57,11 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link DownloadService}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public class DownloadServiceDashTest { private SimpleCache cache; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index 5988391213..dc9c0e0644 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; @@ -43,11 +42,11 @@ import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit test for {@link HlsMediaPeriod}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public final class HlsMediaPeriodTest { @Test diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java index cb1dd2b700..787659fffe 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory; -import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -36,11 +35,11 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link SsMediaPeriod}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public class SsMediaPeriodTest { @Test diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java index 65b0efa72a..c82980d7a4 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -26,11 +26,11 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Unit test for {@link FakeClock}. */ @RunWith(AndroidJUnit4.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +@LooperMode(LooperMode.Mode.PAUSED) public final class FakeClockTest { private static final long TIMEOUT_MS = 10000; diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index 44459ea272..feee9536bb 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -42,4 +42,5 @@ dependencies { api project(modulePrefix + 'testutils') implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.1' + annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java deleted file mode 100644 index ad1fa6bb29..0000000000 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.testutil; - -import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.util.ReflectionHelpers.callInstanceMethod; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.MessageQueue; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Util; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.atomic.AtomicLong; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; -import org.robolectric.shadows.ShadowLooper; -import org.robolectric.shadows.ShadowMessageQueue; - -/** Collection of shadow classes used to run tests with Robolectric which require Loopers. */ -public final class RobolectricUtil { - - private static final AtomicLong sequenceNumberGenerator = new AtomicLong(0); - private static final int ANY_MESSAGE = Integer.MIN_VALUE; - - private RobolectricUtil() {} - - /** - * A custom implementation of Robolectric's ShadowLooper which runs all scheduled messages in the - * loop method of the looper. Also ensures to correctly emulate the message order of the real - * message loop and to avoid blocking caused by Robolectric's default implementation. - * - *

Only works in conjunction with {@link CustomMessageQueue}. Note that the test's {@code - * SystemClock} is not advanced automatically. - */ - @Implements(Looper.class) - public static final class CustomLooper extends ShadowLooper { - - private final PriorityBlockingQueue pendingMessages; - private final CopyOnWriteArraySet removedMessages; - - public CustomLooper() { - pendingMessages = new PriorityBlockingQueue<>(); - removedMessages = new CopyOnWriteArraySet<>(); - } - - @Implementation - public static void loop() { - Looper looper = Looper.myLooper(); - if (shadowOf(looper) instanceof CustomLooper) { - ((CustomLooper) shadowOf(looper)).doLoop(); - } - } - - @Implementation - @Override - public void quitUnchecked() { - super.quitUnchecked(); - // Insert message at the front of the queue to quit loop as soon as possible. - addPendingMessage(/* message= */ null, /* when= */ Long.MIN_VALUE); - } - - private void addPendingMessage(@Nullable Message message, long when) { - pendingMessages.put(new PendingMessage(message, when)); - } - - private void removeMessages(Handler handler, int what, Object object) { - RemovedMessage newRemovedMessage = new RemovedMessage(handler, what, object); - removedMessages.add(newRemovedMessage); - for (RemovedMessage removedMessage : removedMessages) { - if (removedMessage != newRemovedMessage - && removedMessage.handler == handler - && removedMessage.what == what - && removedMessage.object == object) { - removedMessages.remove(removedMessage); - } - } - } - - private void doLoop() { - boolean wasInterrupted = false; - while (true) { - try { - PendingMessage pendingMessage = pendingMessages.take(); - if (pendingMessage.message == null) { - // Null message is signal to end message loop. - return; - } - // Call through to real {@code Message.markInUse()} and {@code Message.recycle()} to - // ensure message recycling works. This is also done in Robolectric's own implementation - // of the message queue. - callInstanceMethod(pendingMessage.message, "markInUse"); - Handler target = pendingMessage.message.getTarget(); - if (target != null) { - boolean isRemoved = false; - for (RemovedMessage removedMessage : removedMessages) { - if (removedMessage.handler == target - && (removedMessage.what == ANY_MESSAGE - || removedMessage.what == pendingMessage.message.what) - && (removedMessage.object == null - || removedMessage.object == pendingMessage.message.obj) - && pendingMessage.sequenceNumber < removedMessage.sequenceNumber) { - isRemoved = true; - } - } - if (!isRemoved) { - try { - if (wasInterrupted) { - wasInterrupted = false; - // Restore the interrupt status flag, so long-running messages will exit early. - Thread.currentThread().interrupt(); - } - target.dispatchMessage(pendingMessage.message); - } catch (Throwable t) { - // Interrupt the main thread to terminate the test. Robolectric's HandlerThread will - // print the rethrown error to standard output. - Looper.getMainLooper().getThread().interrupt(); - throw t; - } - } - } - if (Util.SDK_INT >= 21) { - callInstanceMethod(pendingMessage.message, "recycleUnchecked"); - } else { - callInstanceMethod(pendingMessage.message, "recycle"); - } - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - } - } - - /** - * Custom implementation of Robolectric's ShadowMessageQueue which is needed to let {@link - * CustomLooper} work as intended. - */ - @Implements(MessageQueue.class) - public static final class CustomMessageQueue extends ShadowMessageQueue { - - private final Thread looperThread; - - public CustomMessageQueue() { - looperThread = Thread.currentThread(); - } - - @Implementation - @Override - public boolean enqueueMessage(Message msg, long when) { - Looper looper = ShadowLooper.getLooperForThread(looperThread); - if (shadowOf(looper) instanceof CustomLooper - && shadowOf(looper) != shadowOf(Looper.getMainLooper())) { - ((CustomLooper) shadowOf(looper)).addPendingMessage(msg, when); - } else { - super.enqueueMessage(msg, when); - } - return true; - } - - @Implementation - public void removeMessages(Handler handler, int what, Object object) { - Looper looper = ShadowLooper.getLooperForThread(looperThread); - if (shadowOf(looper) instanceof CustomLooper - && shadowOf(looper) != shadowOf(Looper.getMainLooper())) { - ((CustomLooper) shadowOf(looper)).removeMessages(handler, what, object); - } - } - - @Implementation - public void removeCallbacksAndMessages(Handler handler, Object object) { - Looper looper = ShadowLooper.getLooperForThread(looperThread); - if (shadowOf(looper) instanceof CustomLooper - && shadowOf(looper) != shadowOf(Looper.getMainLooper())) { - ((CustomLooper) shadowOf(looper)).removeMessages(handler, ANY_MESSAGE, object); - } - } - } - - private static final class PendingMessage implements Comparable { - - public final @Nullable Message message; - public final long when; - public final long sequenceNumber; - - public PendingMessage(@Nullable Message message, long when) { - this.message = message; - this.when = when; - sequenceNumber = sequenceNumberGenerator.getAndIncrement(); - } - - @Override - public int compareTo(@NonNull PendingMessage other) { - int res = Util.compareLong(this.when, other.when); - if (res == 0 && this != other) { - res = Util.compareLong(this.sequenceNumber, other.sequenceNumber); - } - return res; - } - } - - private static final class RemovedMessage { - - public final Handler handler; - public final int what; - public final Object object; - public final long sequenceNumber; - - public RemovedMessage(Handler handler, int what, Object object) { - this.handler = handler; - this.what = what; - this.object = object; - this.sequenceNumber = sequenceNumberGenerator.get(); - } - } -} From d92e6bfaf82e068133aa0fd5cec4b82d8e0c791b Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 17 Apr 2019 09:30:25 +0100 Subject: [PATCH 03/95] Assert customCacheKey is null for DASH, HLS and SmoothStreaming downloads PiperOrigin-RevId: 243954989 --- .../exoplayer2/offline/DownloadRequest.java | 9 ++++++++- .../action_file_for_download_index_upgrade.exi | Bin 161 -> 161 bytes .../offline/ActionFileUpgradeUtilTest.java | 12 ++++++------ .../exoplayer2/offline/DownloadRequestTest.java | 5 +++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java index 5acefd6f93..7ff43ceacd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java @@ -52,7 +52,10 @@ public final class DownloadRequest implements Parcelable { public final Uri uri; /** Stream keys to be downloaded. If empty, all streams will be downloaded. */ public final List streamKeys; - /** Custom key for cache indexing, or null. */ + /** + * Custom key for cache indexing, or null. Must be null for DASH, HLS and SmoothStreaming + * downloads. + */ @Nullable public final String customCacheKey; /** Application defined data associated with the download. May be empty. */ public final byte[] data; @@ -72,6 +75,10 @@ public final class DownloadRequest implements Parcelable { List streamKeys, @Nullable String customCacheKey, @Nullable byte[] data) { + if (TYPE_DASH.equals(type) || TYPE_HLS.equals(type) || TYPE_SS.equals(type)) { + Assertions.checkArgument( + customCacheKey == null, "customCacheKey must be null for type: " + type); + } this.id = id; this.type = type; this.uri = uri; diff --git a/library/core/src/test/assets/offline/action_file_for_download_index_upgrade.exi b/library/core/src/test/assets/offline/action_file_for_download_index_upgrade.exi index 888ba4af4467a3d7a0077afad8ea24bbd48f8be0..0bf49b133a1c91e9542671bad37539141a8f953d 100644 GIT binary patch delta 33 ecmZ3;xR8;L0Ros9SV~fhOD6JpKxyTPwJHE Date: Wed, 17 Apr 2019 11:47:48 +0100 Subject: [PATCH 04/95] Reset playback info but not position/state in release ImaAdsLoader gets the player position after the app releases the player to support resuming ads at their current position if the same ads loader is reused. PiperOrigin-RevId: 243969916 --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 8e5a6d2a9b..15deb8ea47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -403,8 +403,8 @@ import java.util.concurrent.CopyOnWriteArrayList; eventHandler.removeCallbacksAndMessages(null); playbackInfo = getResetPlaybackInfo( - /* resetPosition= */ true, - /* resetState= */ true, + /* resetPosition= */ false, + /* resetState= */ false, /* playbackState= */ Player.STATE_IDLE); } From 2feadc97626a9ddea4c8d44687205c547084b9a7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 13:32:17 +0100 Subject: [PATCH 05/95] Add WritableDownloadIndex interface One goal we forgot about a little bit was to allow applications to provide their own index implementation. This requires the writable side to also be defined by an interface. PiperOrigin-RevId: 243979660 --- .../offline/DefaultDownloadIndex.java | 36 ++--------- .../exoplayer2/offline/DownloadIndex.java | 2 +- .../exoplayer2/offline/DownloadManager.java | 15 +++-- .../offline/WritableDownloadIndex.java | 59 +++++++++++++++++++ 4 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index d7ab4201a5..fc1518e5c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -38,7 +38,7 @@ import java.util.List; *

Database access may take a long time, do not call methods of this class from * the application main thread. */ -public final class DefaultDownloadIndex implements DownloadIndex { +public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads"; @@ -185,12 +185,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { return new DownloadCursorImpl(cursor); } - /** - * Adds or replaces a {@link Download}. - * - * @param download The {@link Download} to be added. - * @throws DatabaseIOException If an error occurs setting the state. - */ + @Override public void putDownload(Download download) throws DatabaseIOException { ensureInitialized(); ContentValues values = new ContentValues(); @@ -218,12 +213,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { } } - /** - * Removes the {@link Download} with the given {@code id}. - * - * @param id ID of a {@link Download}. - * @throws DatabaseIOException If an error occurs removing the state. - */ + @Override public void removeDownload(String id) throws DatabaseIOException { ensureInitialized(); try { @@ -233,13 +223,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { } } - /** - * Sets the manual stop reason of the downloads in a terminal state ({@link - * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). - * - * @param manualStopReason The manual stop reason. - * @throws DatabaseIOException If an error occurs updating the state. - */ + @Override public void setManualStopReason(int manualStopReason) throws DatabaseIOException { ensureInitialized(); try { @@ -252,17 +236,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { } } - /** - * Sets the manual stop reason of the download with the given {@code id} in a terminal state - * ({@link Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). - * - *

If there's no {@link Download} with the given {@code id} or it isn't in a terminal state, - * then nothing happens. - * - * @param id ID of a {@link Download}. - * @param manualStopReason The manual stop reason. - * @throws DatabaseIOException If an error occurs updating the state. - */ + @Override public void setManualStopReason(String id, int manualStopReason) throws DatabaseIOException { ensureInitialized(); try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java index 90d0fa1b51..3de1b7b212 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.offline; import androidx.annotation.Nullable; import java.io.IOException; -/** Persists {@link Download}s. */ +/** An index of {@link Download Downloads}. */ public interface DownloadIndex { /** 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 03c33b6aad..fdb3ca1840 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 @@ -34,7 +34,6 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; @@ -155,7 +154,7 @@ public final class DownloadManager { private final int maxSimultaneousDownloads; private final int minRetryCount; private final Context context; - private final DefaultDownloadIndex downloadIndex; + private final WritableDownloadIndex downloadIndex; private final DownloaderFactory downloaderFactory; private final Handler mainHandler; private final HandlerThread internalThread; @@ -231,7 +230,7 @@ public final class DownloadManager { * Constructs a {@link DownloadManager}. * * @param context Any context. - * @param downloadIndex The {@link DefaultDownloadIndex} that holds the downloads. + * @param downloadIndex The download index used to hold the download information. * @param downloaderFactory A factory for creating {@link Downloader}s. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. * @param minRetryCount The minimum number of times a download must be retried before failing. @@ -239,7 +238,7 @@ public final class DownloadManager { */ public DownloadManager( Context context, - DefaultDownloadIndex downloadIndex, + WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory, int maxSimultaneousDownloads, int minRetryCount, @@ -651,7 +650,7 @@ public final class DownloadManager { } else { downloadIndex.setManualStopReason(manualStopReason); } - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "setManualStopReason failed", e); } } @@ -734,7 +733,7 @@ public final class DownloadManager { logd("Download state is changed", downloadInternal); try { downloadIndex.putDownload(download); - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "Failed to update index", e); } if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { @@ -747,7 +746,7 @@ public final class DownloadManager { logd("Download is removed", downloadInternal); try { downloadIndex.removeDownload(download.request.id); - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "Failed to remove from index", e); } downloadInternals.remove(downloadInternal); @@ -805,7 +804,7 @@ public final class DownloadManager { private Download loadDownload(String id) { try { return downloadIndex.getDownload(id); - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "loadDownload failed", e); } return null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java new file mode 100644 index 0000000000..24f4421bc4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -0,0 +1,59 @@ +/* + * 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.google.android.exoplayer2.offline; + +import java.io.IOException; + +/** An writable index of {@link Download Downloads}. */ +public interface WritableDownloadIndex extends DownloadIndex { + + /** + * Adds or replaces a {@link Download}. + * + * @param download The {@link Download} to be added. + * @throws throws IOException If an error occurs setting the state. + */ + void putDownload(Download download) throws IOException; + + /** + * Removes the {@link Download} with the given {@code id}. + * + * @param id ID of a {@link Download}. + * @throws throws IOException If an error occurs removing the state. + */ + void removeDownload(String id) throws IOException; + /** + * Sets the manual stop reason of the downloads in a terminal state ({@link + * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). + * + * @param manualStopReason The manual stop reason. + * @throws throws IOException If an error occurs updating the state. + */ + void setManualStopReason(int manualStopReason) throws IOException; + + /** + * Sets the manual stop reason of the download with the given {@code id} in a terminal state + * ({@link Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). + * + *

If there's no {@link Download} with the given {@code id} or it isn't in a terminal state, + * then nothing happens. + * + * @param id ID of a {@link Download}. + * @param manualStopReason The manual stop reason. + * @throws throws IOException If an error occurs updating the state. + */ + void setManualStopReason(String id, int manualStopReason) throws IOException; +} From c9470296ab59d0c66ac07f46d988841c357abbe8 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 14:45:18 +0100 Subject: [PATCH 06/95] Fix playback of badly clipped MP3 streams Issue: #5772 PiperOrigin-RevId: 243987497 --- RELEASENOTES.md | 6 ++++-- .../exoplayer2/extractor/mp3/Mp3Extractor.java | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 765244ac1a..182701ec34 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,14 +32,16 @@ replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). * Extractors: - * MP3: Add support for SHOUTcast ICY metadata - ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * MP4/FMP4: Add support for Dolby Vision. * MP4: Fix issue handling meta atoms in some streams ([#5698](https://github.com/google/ExoPlayer/issues/5698), [#5694](https://github.com/google/ExoPlayer/issues/5694)). + * MP3: Add support for SHOUTcast ICY metadata + ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * MP3: Fix ID3 frame unsychronization ([#5673](https://github.com/google/ExoPlayer/issues/5673)). + * MP3: Fix playback of badly clipped files + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default (i.e. if the flag is not set), the 0x82 elementary stream type is now treated as an SCTE subtitle track diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 4db715f53e..c65ad0bc67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -341,9 +341,19 @@ public final class Mp3Extractor implements Extractor { */ private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput) throws IOException, InterruptedException { - return (seeker != null && extractorInput.getPeekPosition() == seeker.getDataEndPosition()) - || !extractorInput.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true); + if (seeker != null) { + long dataEndPosition = seeker.getDataEndPosition(); + if (dataEndPosition != C.POSITION_UNSET + && extractorInput.getPeekPosition() > dataEndPosition - 4) { + return true; + } + } + try { + return !extractorInput.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true); + } catch (EOFException e) { + return true; + } } /** From a0fe7ace833000dbb92cacf5a42cca25b318c077 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 17 Apr 2019 14:50:40 +0100 Subject: [PATCH 07/95] Upgrade IMA to 3.11.2 PiperOrigin-RevId: 243988105 --- extensions/ima/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index c80fb26124..a91bbbd981 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,9 +32,9 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.9' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') - implementation 'com.google.android.gms:play-services-ads:17.2.0' + implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } From d656782bd4deaba624ab47da4d4f4e44d56c0786 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 16:46:42 +0100 Subject: [PATCH 08/95] Don't start download if user explicitly deselects all tracks PiperOrigin-RevId: 244003817 --- .../android/exoplayer2/demo/DownloadTracker.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 34282fc389..4a7a810314 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 @@ -240,7 +240,12 @@ public class DownloadTracker { } } } - startDownload(); + DownloadRequest downloadRequest = buildDownloadRequest(); + if (downloadRequest.streamKeys.isEmpty()) { + // All tracks were deselected in the dialog. Don't start the download. + return; + } + startDownload(downloadRequest); } // DialogInterface.OnDismissListener implementation. @@ -254,9 +259,16 @@ public class DownloadTracker { // Internal methods. private void startDownload() { - DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); + startDownload(buildDownloadRequest()); + } + + private void startDownload(DownloadRequest downloadRequest) { DownloadService.startWithNewDownload( context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); } + + private DownloadRequest buildDownloadRequest() { + return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); + } } } From e290f883d1ede6523b00854c3fdf439b0e4b851f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 16:59:11 +0100 Subject: [PATCH 09/95] Disable cache span touching for offline Currently SimpleCache will touch cache spans whenever it reads from them. With legacy SimpleCache setups this involves a potentially expensive file rename. With new SimpleCache setups it involves a more efficient but still non-free database write. For offline use cases, and more generally any use case where the eviction policy doesn't use last access timestamps, touching is not useful. This change allows the evictor to specify whether it needs cache spans to be touched or not. SimpleCache will only touch spans if the evictor requires it. Note: There is a potential change in behavior in cases where a cache uses an evictor that doesn't need cache spans to be touched, but then later switches to an evictor that does. The new evictor may temporarily make sub-optimal eviction decisions as a result. I think this is a very fair trade-off, since this scenario is unlikely to occur much, if at all, in practice, and even if it does occur the result isn't that bad. PiperOrigin-RevId: 244005682 --- .../exoplayer2/upstream/cache/Cache.java | 11 ++++---- .../upstream/cache/CacheEvictor.java | 7 +++++ .../upstream/cache/CacheFileMetadata.java | 6 ++-- .../cache/CacheFileMetadataIndex.java | 18 ++++++------ .../exoplayer2/upstream/cache/CacheSpan.java | 15 +++++----- .../upstream/cache/CachedContent.java | 18 ++++++------ .../cache/LeastRecentlyUsedCacheEvictor.java | 11 ++++++-- .../upstream/cache/NoOpCacheEvictor.java | 5 ++++ .../upstream/cache/SimpleCache.java | 27 ++++++++++-------- .../upstream/cache/SimpleCacheSpan.java | 28 +++++++++---------- .../cache/CachedContentIndexTest.java | 4 +-- .../cache/CachedRegionTrackerTest.java | 4 +-- .../upstream/cache/SimpleCacheSpanTest.java | 21 ++++++-------- 13 files changed, 96 insertions(+), 79 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 91349e9284..12905f908c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -49,19 +49,18 @@ public interface Cache { void onSpanRemoved(Cache cache, CacheSpan span); /** - * Called when an existing {@link CacheSpan} is accessed, causing it to be replaced. The new + * Called when an existing {@link CacheSpan} is touched, causing it to be replaced. The new * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however - * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed. - *

- * Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and - * {@link #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method. + * {@link CacheSpan#file} and {@link CacheSpan#lastTouchTimestamp} may have changed. + * + *

Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and {@link + * #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method. * * @param cache The source of the event. * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache. * @param newSpan The new {@link CacheSpan}, which has been added to the cache. */ void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan); - } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java index dbec4b78fc..6ebfe01df4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java @@ -23,6 +23,13 @@ import com.google.android.exoplayer2.C; */ public interface CacheEvictor extends Cache.Listener { + /** + * Returns whether the evictor requires the {@link Cache} to touch {@link CacheSpan CacheSpans} + * when it accesses them. Implementations that do not use {@link CacheSpan#lastTouchTimestamp} + * should return {@code false}. + */ + boolean requiresCacheSpanTouches(); + /** * Called when cache has been initialized. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java index 492b98a0de..7ac80325a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java @@ -19,10 +19,10 @@ package com.google.android.exoplayer2.upstream.cache; /* package */ final class CacheFileMetadata { public final long length; - public final long lastAccessTimestamp; + public final long lastTouchTimestamp; - public CacheFileMetadata(long length, long lastAccessTimestamp) { + public CacheFileMetadata(long length, long lastTouchTimestamp) { this.length = length; - this.lastAccessTimestamp = lastAccessTimestamp; + this.lastTouchTimestamp = lastTouchTimestamp; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index 084c02b11b..027172e090 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -36,17 +36,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private static final String COLUMN_NAME = "name"; private static final String COLUMN_LENGTH = "length"; - private static final String COLUMN_LAST_ACCESS_TIMESTAMP = "last_access_timestamp"; + private static final String COLUMN_LAST_TOUCH_TIMESTAMP = "last_touch_timestamp"; private static final int COLUMN_INDEX_NAME = 0; private static final int COLUMN_INDEX_LENGTH = 1; - private static final int COLUMN_INDEX_LAST_ACCESS_TIMESTAMP = 2; + private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2; private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; private static final String[] COLUMNS = new String[] { - COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_ACCESS_TIMESTAMP, + COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_TOUCH_TIMESTAMP, }; private static final String TABLE_SCHEMA = "(" @@ -54,7 +54,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + " TEXT PRIMARY KEY NOT NULL," + COLUMN_LENGTH + " INTEGER NOT NULL," - + COLUMN_LAST_ACCESS_TIMESTAMP + + COLUMN_LAST_TOUCH_TIMESTAMP + " INTEGER NOT NULL)"; private final DatabaseProvider databaseProvider; @@ -141,8 +141,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; while (cursor.moveToNext()) { String name = cursor.getString(COLUMN_INDEX_NAME); long length = cursor.getLong(COLUMN_INDEX_LENGTH); - long lastAccessTimestamp = cursor.getLong(COLUMN_INDEX_LAST_ACCESS_TIMESTAMP); - fileMetadata.put(name, new CacheFileMetadata(length, lastAccessTimestamp)); + long lastTouchTimestamp = cursor.getLong(COLUMN_INDEX_LAST_TOUCH_TIMESTAMP); + fileMetadata.put(name, new CacheFileMetadata(length, lastTouchTimestamp)); } return fileMetadata; } catch (SQLException e) { @@ -155,17 +155,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * * @param name The name of the file. * @param length The file length. - * @param lastAccessTimestamp The file last access timestamp. + * @param lastTouchTimestamp The file last touch timestamp. * @throws DatabaseIOException If an error occurs setting the metadata. */ - public void set(String name, long length, long lastAccessTimestamp) throws DatabaseIOException { + public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(COLUMN_NAME, name); values.put(COLUMN_LENGTH, length); - values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp); + values.put(COLUMN_LAST_TOUCH_TIMESTAMP, lastTouchTimestamp); writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); } catch (SQLException e) { throw new DatabaseIOException(e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index 7dbcd4a922..1e8cf1517d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -45,13 +45,12 @@ public class CacheSpan implements Comparable { * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */ public final @Nullable File file; - /** - * The last access timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. - */ - public final long lastAccessTimestamp; + /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */ + public final long lastTouchTimestamp; /** - * Creates a hole CacheSpan which isn't cached, has no last access time and no file associated. + * Creates a hole CacheSpan which isn't cached, has no last touch timestamp and no file + * associated. * * @param key The cache key that uniquely identifies the original stream. * @param position The position of the {@link CacheSpan} in the original stream. @@ -69,18 +68,18 @@ public class CacheSpan implements Comparable { * @param position The position of the {@link CacheSpan} in the original stream. * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an * open-ended hole. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link * #isCached} is false. * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ public CacheSpan( - String key, long position, long length, long lastAccessTimestamp, @Nullable File file) { + String key, long position, long length, long lastTouchTimestamp, @Nullable File file) { this.key = key; this.position = position; this.length = length; this.isCached = file != null; this.file = file; - this.lastAccessTimestamp = lastAccessTimestamp; + this.lastTouchTimestamp = lastTouchTimestamp; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index e244163bc8..7abb9b3896 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -141,30 +141,30 @@ import java.util.TreeSet; } /** - * Sets the given span's last access timestamp. The passed span becomes invalid after this call. + * Sets the given span's last touch timestamp. The passed span becomes invalid after this call. * * @param cacheSpan Span to be copied and updated. - * @param lastAccessTimestamp The new last access timestamp. + * @param lastTouchTimestamp The new last touch timestamp. * @param updateFile Whether the span file should be renamed to have its timestamp match the new - * last access time. - * @return A span with the updated last access timestamp. + * last touch time. + * @return A span with the updated last touch timestamp. */ - public SimpleCacheSpan setLastAccessTimestamp( - SimpleCacheSpan cacheSpan, long lastAccessTimestamp, boolean updateFile) { + public SimpleCacheSpan setLastTouchTimestamp( + SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) { Assertions.checkState(cachedSpans.remove(cacheSpan)); File file = cacheSpan.file; if (updateFile) { File directory = file.getParentFile(); long position = cacheSpan.position; - File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastAccessTimestamp); + File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp); if (file.renameTo(newFile)) { file = newFile; } else { - Log.w(TAG, "Failed to rename " + file + " to " + newFile + "."); + Log.w(TAG, "Failed to rename " + file + " to " + newFile); } } SimpleCacheSpan newCacheSpan = - cacheSpan.copyWithFileAndLastAccessTimestamp(file, lastAccessTimestamp); + cacheSpan.copyWithFileAndLastTouchTimestamp(file, lastTouchTimestamp); cachedSpans.add(newCacheSpan); return newCacheSpan; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java index aa40c1d2fd..44a735f144 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java @@ -35,6 +35,11 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar this.leastRecentlyUsed = new TreeSet<>(this); } + @Override + public boolean requiresCacheSpanTouches() { + return true; + } + @Override public void onCacheInitialized() { // Do nothing. @@ -68,12 +73,12 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar @Override public int compare(CacheSpan lhs, CacheSpan rhs) { - long lastAccessTimestampDelta = lhs.lastAccessTimestamp - rhs.lastAccessTimestamp; - if (lastAccessTimestampDelta == 0) { + long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; + if (lastTouchTimestampDelta == 0) { // Use the standard compareTo method as a tie-break. return lhs.compareTo(rhs); } - return lhs.lastAccessTimestamp < rhs.lastAccessTimestamp ? -1 : 1; + return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; } private void evictCache(Cache cache, long requiredSpace) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java index b0c8c7e087..da89dc1cb3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java @@ -24,6 +24,11 @@ package com.google.android.exoplayer2.upstream.cache; */ public final class NoOpCacheEvictor implements CacheEvictor { + @Override + public boolean requiresCacheSpanTouches() { + return false; + } + @Override public void onCacheInitialized() { // Do nothing. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 14f659855b..b31d3b66f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -70,6 +70,7 @@ public final class SimpleCache implements Cache { @Nullable private final CacheFileMetadataIndex fileIndex; private final HashMap> listeners; private final Random random; + private final boolean touchCacheSpans; private long uid; private long totalSpace; @@ -279,6 +280,7 @@ public final class SimpleCache implements Cache { this.fileIndex = fileIndex; listeners = new HashMap<>(); random = new Random(); + touchCacheSpans = evictor.requiresCacheSpanTouches(); uid = UID_UNSET; // Start cache initialization. @@ -408,23 +410,26 @@ public final class SimpleCache implements Cache { // Read case. if (span.isCached) { + if (!touchCacheSpans) { + return span; + } String fileName = Assertions.checkNotNull(span.file).getName(); long length = span.length; - long lastAccessTimestamp = System.currentTimeMillis(); + long lastTouchTimestamp = System.currentTimeMillis(); boolean updateFile = false; if (fileIndex != null) { try { - fileIndex.set(fileName, length, lastAccessTimestamp); + fileIndex.set(fileName, length, lastTouchTimestamp); } catch (IOException e) { - throw new CacheException(e); + Log.w(TAG, "Failed to update index with new touch timestamp."); } } else { - // Updating the file itself to incorporate the new last access timestamp is much slower than + // Updating the file itself to incorporate the new last touch timestamp is much slower than // updating the file index. Hence we only update the file if we don't have a file index. updateFile = true; } SimpleCacheSpan newSpan = - contentIndex.get(key).setLastAccessTimestamp(span, lastAccessTimestamp, updateFile); + contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); notifySpanTouched(span, newSpan); return newSpan; } @@ -459,8 +464,8 @@ public final class SimpleCache implements Cache { if (!fileDir.exists()) { fileDir.mkdir(); } - long lastAccessTimestamp = System.currentTimeMillis(); - return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastAccessTimestamp); + long lastTouchTimestamp = System.currentTimeMillis(); + return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastTouchTimestamp); } @Override @@ -488,7 +493,7 @@ public final class SimpleCache implements Cache { if (fileIndex != null) { String fileName = file.getName(); try { - fileIndex.set(fileName, span.length, span.lastAccessTimestamp); + fileIndex.set(fileName, span.length, span.lastTouchTimestamp); } catch (IOException e) { throw new CacheException(e); } @@ -674,14 +679,14 @@ public final class SimpleCache implements Cache { continue; } long length = C.LENGTH_UNSET; - long lastAccessTimestamp = C.TIME_UNSET; + long lastTouchTimestamp = C.TIME_UNSET; CacheFileMetadata metadata = fileMetadata != null ? fileMetadata.remove(fileName) : null; if (metadata != null) { length = metadata.length; - lastAccessTimestamp = metadata.lastAccessTimestamp; + lastTouchTimestamp = metadata.lastTouchTimestamp; } SimpleCacheSpan span = - SimpleCacheSpan.createCacheEntry(file, length, lastAccessTimestamp, contentIndex); + SimpleCacheSpan.createCacheEntry(file, length, lastTouchTimestamp, contentIndex); if (span != null) { addSpan(span); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 7235830019..7d9f0c9ff1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -96,7 +96,7 @@ import java.util.regex.Pattern; */ @Nullable public static SimpleCacheSpan createCacheEntry(File file, long length, CachedContentIndex index) { - return createCacheEntry(file, length, /* lastAccessTimestamp= */ C.TIME_UNSET, index); + return createCacheEntry(file, length, /* lastTouchTimestamp= */ C.TIME_UNSET, index); } /** @@ -106,14 +106,14 @@ import java.util.regex.Pattern; * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the * underlying file system. Querying the underlying file system can be expensive, so callers * that already know the length of the file should pass it explicitly. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} to use the file + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} to use the file * timestamp. * @return The span, or null if the file name is not correctly formatted, or if the id is not * present in the content index, or if the length is 0. */ @Nullable public static SimpleCacheSpan createCacheEntry( - File file, long length, long lastAccessTimestamp, CachedContentIndex index) { + File file, long length, long lastTouchTimestamp, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { file = upgradeFile(file, index); @@ -142,10 +142,10 @@ import java.util.regex.Pattern; } long position = Long.parseLong(matcher.group(2)); - if (lastAccessTimestamp == C.TIME_UNSET) { - lastAccessTimestamp = Long.parseLong(matcher.group(3)); + if (lastTouchTimestamp == C.TIME_UNSET) { + lastTouchTimestamp = Long.parseLong(matcher.group(3)); } - return new SimpleCacheSpan(key, position, length, lastAccessTimestamp, file); + return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file); } /** @@ -187,26 +187,26 @@ import java.util.regex.Pattern; * @param position The position of the {@link CacheSpan} in the original stream. * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an * open-ended hole. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link * #isCached} is false. * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ private SimpleCacheSpan( - String key, long position, long length, long lastAccessTimestamp, @Nullable File file) { - super(key, position, length, lastAccessTimestamp, file); + String key, long position, long length, long lastTouchTimestamp, @Nullable File file) { + super(key, position, length, lastTouchTimestamp, file); } /** - * Returns a copy of this CacheSpan with a new file and last access timestamp. + * Returns a copy of this CacheSpan with a new file and last touch timestamp. * * @param file The new file. - * @param lastAccessTimestamp The new last access time. - * @return A copy with the new file and last access timestamp. + * @param lastTouchTimestamp The new last touch time. + * @return A copy with the new file and last touch timestamp. * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false). */ - public SimpleCacheSpan copyWithFileAndLastAccessTimestamp(File file, long lastAccessTimestamp) { + public SimpleCacheSpan copyWithFileAndLastTouchTimestamp(File file, long lastTouchTimestamp) { Assertions.checkState(isCached); - return new SimpleCacheSpan(key, position, length, lastAccessTimestamp, file); + return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index bebcf0ec12..cee5703ff8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -108,7 +108,7 @@ public class CachedContentIndexTest { cachedContent1.id, /* offset= */ 10, cacheFileLength, - /* lastAccessTimestamp= */ 30); + /* lastTouchTimestamp= */ 30); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, cacheFileLength, index); assertThat(span).isNotNull(); cachedContent1.addSpan(span); @@ -293,7 +293,7 @@ public class CachedContentIndexTest { cachedContent.id, /* offset= */ 10, cacheFileLength, - /* lastAccessTimestamp= */ 30); + /* lastTouchTimestamp= */ 30); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); cachedContent.addSpan(span); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 5efdf36191..b00ee73f0f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -134,8 +134,8 @@ public final class CachedRegionTrackerTest { } public static File createCacheSpanFile( - File cacheDir, int id, long offset, int length, long lastAccessTimestamp) throws IOException { - File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp); + File cacheDir, int id, long offset, int length, long lastTouchTimestamp) throws IOException { + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp); createTestFile(cacheFile, length); return cacheFile; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 028937dc5a..39be9fbcd8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -38,9 +38,8 @@ import org.junit.runner.RunWith; public class SimpleCacheSpanTest { public static File createCacheSpanFile( - File cacheDir, int id, long offset, long length, long lastAccessTimestamp) - throws IOException { - File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp); + File cacheDir, int id, long offset, long length, long lastTouchTimestamp) throws IOException { + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp); createTestFile(cacheFile, length); return cacheFile; } @@ -117,7 +116,7 @@ public class SimpleCacheSpanTest { SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, file.length(), index); if (cacheSpan != null) { assertThat(cacheSpan.key).isEqualTo(key); - cachedPositions.put(cacheSpan.position, cacheSpan.lastAccessTimestamp); + cachedPositions.put(cacheSpan.position, cacheSpan.lastTouchTimestamp); } } @@ -140,12 +139,11 @@ public class SimpleCacheSpanTest { return file; } - private void assertCacheSpan(String key, long offset, long lastAccessTimestamp) + private void assertCacheSpan(String key, long offset, long lastTouchTimestamp) throws IOException { int id = index.assignIdForKey(key); long cacheFileLength = 1; - File cacheFile = - createCacheSpanFile(cacheDir, id, offset, cacheFileLength, lastAccessTimestamp); + File cacheFile = createCacheSpanFile(cacheDir, id, offset, cacheFileLength, lastTouchTimestamp); SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); String message = cacheFile.toString(); assertWithMessage(message).that(cacheSpan).isNotNull(); @@ -155,14 +153,13 @@ public class SimpleCacheSpanTest { assertWithMessage(message).that(cacheSpan.length).isEqualTo(1); assertWithMessage(message).that(cacheSpan.isCached).isTrue(); assertWithMessage(message).that(cacheSpan.file).isEqualTo(cacheFile); - assertWithMessage(message).that(cacheSpan.lastAccessTimestamp).isEqualTo(lastAccessTimestamp); + assertWithMessage(message).that(cacheSpan.lastTouchTimestamp).isEqualTo(lastTouchTimestamp); } - private void assertNullCacheSpan(File parent, String key, long offset, - long lastAccessTimestamp) { + private void assertNullCacheSpan(File parent, String key, long offset, long lastTouchTimestamp) { long cacheFileLength = 0; - File cacheFile = SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset, - lastAccessTimestamp); + File cacheFile = + SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset, lastTouchTimestamp); CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); assertWithMessage(cacheFile.toString()).that(cacheSpan).isNull(); } From f001e492956c42f8d4ae1eb5124e84331342d017 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 17 Apr 2019 17:20:18 +0100 Subject: [PATCH 10/95] Small javadoc fix for DownloadManager constructors PiperOrigin-RevId: 244009343 --- .../google/android/exoplayer2/offline/DownloadManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fdb3ca1840..915f375027 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 @@ -186,7 +186,7 @@ public final class DownloadManager { * Constructs a {@link DownloadManager}. * * @param context Any context. - * @param databaseProvider Provides the {@link DownloadIndex} that holds the downloads. + * @param databaseProvider Provides the database that holds the downloads. * @param downloaderFactory A factory for creating {@link Downloader}s. */ public DownloadManager( @@ -204,7 +204,7 @@ public final class DownloadManager { * Constructs a {@link DownloadManager}. * * @param context Any context. - * @param databaseProvider Provides the {@link DownloadIndex} that holds the downloads. + * @param databaseProvider Provides the database that holds the downloads. * @param downloaderFactory A factory for creating {@link Downloader}s. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. * @param minRetryCount The minimum number of times a download must be retried before failing. From 4d6aca7629445cb8ac8c73b29f7ba0229274c0a8 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 20:45:03 +0100 Subject: [PATCH 11/95] Remove TODOs we're not going to do 1. customCacheKey for DASH/HLS/SS is now asserted against in DownloadRequest 2. Merging of event delivery in DownloadManager is very tricky to get right and probably not a good idea PiperOrigin-RevId: 244048392 --- .../android/exoplayer2/offline/DefaultDownloaderFactory.java | 1 - .../com/google/android/exoplayer2/offline/DownloadManager.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java index 9a4e5925ee..ca20c769dc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java @@ -98,7 +98,6 @@ public class DefaultDownloaderFactory implements DownloaderFactory { throw new IllegalStateException("Module missing for: " + request.type); } try { - // TODO: Support customCacheKey in DASH/HLS/SS, for completeness. return constructor.newInstance(request.uri, request.streamKeys, downloaderConstructorHelper); } catch (Exception e) { throw new RuntimeException("Failed to instantiate downloader for: " + request.type, e); 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 915f375027..df958f8691 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 @@ -485,8 +485,6 @@ public final class DownloadManager { return true; } - // TODO: Merge these three events into a single MSG_STATE_CHANGE that can carry all updates. This - // allows updating idle at the same point as the downloads that can be queried changes. private void onInitialized(List downloads) { initialized = true; this.downloads.addAll(downloads); From 92269ff774c59cc8f0af0210720a0e743f951a57 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Apr 2019 21:31:21 +0100 Subject: [PATCH 12/95] Make MediaCodecRenderer#onInputFormatChanged take a FormatHolder PiperOrigin-RevId: 244056421 --- .../exoplayer2/audio/MediaCodecAudioRenderer.java | 6 ++++-- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 9 +++++---- .../exoplayer2/video/MediaCodecVideoRenderer.java | 6 ++++-- .../exoplayer2/testutil/DebugRenderersFactory.java | 5 +++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 07769e7d85..edaa9e1474 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; @@ -419,8 +420,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); + Format newFormat = formatHolder.format; eventDispatcher.inputFormatChanged(newFormat); // If the input format is anything other than PCM then we assume that the audio decoder will // output 16-bit PCM. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index f7855810d4..06b76781b4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -726,7 +726,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } else if (result == C.RESULT_BUFFER_READ && flagsOnlyBuffer.isEndOfStream()) { inputStreamEnded = true; @@ -1029,7 +1029,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { buffer.clear(); codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; } - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } @@ -1141,11 +1141,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Called when a new format is read from the upstream {@link MediaPeriod}. * - * @param newFormat The new format. + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. */ - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { Format oldFormat = inputFormat; + Format newFormat = formatHolder.format; inputFormat = newFormat; waitingForFirstSampleInFormat = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 193fbddfec..e693af2bd1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; @@ -631,8 +632,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); + Format newFormat = formatHolder.format; eventDispatcher.inputFormatChanged(newFormat); pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio; pendingRotationDegrees = newFormat.rotationDegrees; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 70059114db..2b479c549a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -141,8 +142,8 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { } @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); // Ensure timestamps of buffers queued after this format change are never inserted into the // queue of expected output timestamps before those of buffers that have already been queued. minimumInsertIndex = startIndex + queueSize; From 4c967895e9a493d0f59dc593fa39563c7a9d3b48 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 00:58:44 +0100 Subject: [PATCH 13/95] [libvpx] permalaunch number of buffers. PiperOrigin-RevId: 244094942 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 952e15aad6..d5da9a011d 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -221,8 +221,8 @@ public class LibvpxVideoRenderer extends BaseRenderer { disableLoopFilter, /* enableRowMultiThreadMode= */ false, getRuntime().availableProcessors(), - /* numInputBuffers= */ 8, - /* numOutputBuffers= */ 8); + /* numInputBuffers= */ 4, + /* numOutputBuffers= */ 4); } /** From a985ca93c5c7dcf9aef38eca93f5f907d4de871a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Apr 2019 08:35:07 +0100 Subject: [PATCH 14/95] Extend Bluetooth dead audio track workaround to Q PiperOrigin-RevId: 244139959 --- .../android/exoplayer2/audio/AudioTrackPositionTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index 2ce9b8bdbe..e87e49d2da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -517,7 +517,7 @@ import java.lang.reflect.Method; rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; } - if (Util.SDK_INT <= 28) { + if (Util.SDK_INT <= 29) { if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0 && state == PLAYSTATE_PLAYING) { From 2347bd2c9901cb679979dd4b292c6b1ef18cd65c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Apr 2019 13:03:14 +0100 Subject: [PATCH 15/95] Prioritize decoders with format support PiperOrigin-RevId: 244167456 --- RELEASENOTES.md | 3 + .../audio/MediaCodecAudioRenderer.java | 11 ++- .../exoplayer2/mediacodec/MediaCodecUtil.java | 91 ++++++++++--------- .../video/MediaCodecVideoRenderer.java | 30 ++++-- 4 files changed, 83 insertions(+), 52 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 182701ec34..514528698f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### dev-v2 (not yet released) ### +* Decoders: prefer codecs that advertise format support over ones that do not, + even if they are listed lower in the `MediaCodecList`. + ### 2.10.0 ### * Core library: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index edaa9e1474..a38304d2b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; import com.google.android.exoplayer2.util.Log; @@ -290,8 +291,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } List decoderInfos = - mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, requiresSecureDecryption, /* requiresTunnelingDecoder= */ false); + getDecoderInfos(mediaCodecSelector, format, requiresSecureDecryption); if (decoderInfos.isEmpty()) { return requiresSecureDecryption && !mediaCodecSelector @@ -327,8 +327,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return Collections.singletonList(passthroughDecoderInfo); } } - return mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + List decoderInfos = + mediaCodecSelector.getDecoderInfos( + format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + return Collections.unmodifiableList(decoderInfos); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 3211f7ea8e..1742e9ebf3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -20,16 +20,17 @@ import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; +import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseIntArray; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -185,6 +186,26 @@ public final class MediaCodecUtil { return unmodifiableDecoderInfos; } + /** + * Returns a copy of the provided decoder list sorted such that decoders with format support are + * listed first. The returned list is modifiable for convenience. + */ + @CheckResult + public static List getDecoderInfosSortedByFormatSupport( + List decoderInfos, Format format) { + decoderInfos = new ArrayList<>(decoderInfos); + sortByScore( + decoderInfos, + decoderInfo -> { + try { + return decoderInfo.isFormatSupported(format) ? 1 : 0; + } catch (DecoderQueryException e) { + return -1; + } + }); + return decoderInfos; + } + /** * Returns the maximum frame size supported by the default H264 decoder. * @@ -484,7 +505,22 @@ public final class MediaCodecUtil { */ private static void applyWorkarounds(String mimeType, List decoderInfos) { if (MimeTypes.AUDIO_RAW.equals(mimeType)) { - Collections.sort(decoderInfos, new RawAudioCodecComparator()); + // Work around inconsistent raw audio decoding behavior across different devices. + sortByScore( + decoderInfos, + decoderInfo -> { + String name = decoderInfo.name; + if (name.startsWith("OMX.google") || name.startsWith("c2.android")) { + // Prefer generic decoders over ones provided by the device. + return 1; + } + if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This decoder may modify the audio, so any other compatible decoders take + // precedence. See [Internal: b/62337687]. + return -1; + } + return 0; + }); } else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { String firstCodecName = decoderInfos.get(0).name; if ("OMX.SEC.mp3.dec".equals(firstCodecName) @@ -494,7 +530,7 @@ public final class MediaCodecUtil { // OMX.brcm.audio.mp3.decoder on older devices. See: // https://github.com/google/ExoPlayer/issues/398 and // https://github.com/google/ExoPlayer/issues/4519. - Collections.sort(decoderInfos, new PreferOmxGoogleCodecComparator()); + sortByScore(decoderInfos, decoderInfo -> decoderInfo.name.startsWith("OMX.google") ? 1 : 0); } } } @@ -676,6 +712,17 @@ public final class MediaCodecUtil { return null; } + /** Stably sorts the provided {@code list} in-place, in order of decreasing score. */ + private static void sortByScore(List list, ScoreProvider scoreProvider) { + Collections.sort(list, (a, b) -> scoreProvider.getScore(b) - scoreProvider.getScore(a)); + } + + /** Interface for providers of item scores. */ + private interface ScoreProvider { + /** Returns the score of the provided item. */ + int getScore(T t); + } + private interface MediaCodecListCompat { /** @@ -826,44 +873,6 @@ public final class MediaCodecUtil { } - /** - * Comparator for ordering media codecs that handle {@link MimeTypes#AUDIO_RAW} to work around - * possible inconsistent behavior across different devices. A list sorted with this comparator has - * more preferred codecs first. - */ - private static final class RawAudioCodecComparator implements Comparator { - @Override - public int compare(MediaCodecInfo a, MediaCodecInfo b) { - return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b); - } - - private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) { - String name = mediaCodecInfo.name; - if (name.startsWith("OMX.google") || name.startsWith("c2.android")) { - // Prefer generic decoders over ones provided by the device. - return -1; - } - if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { - // This decoder may modify the audio, so any other compatible decoders take precedence. See - // [Internal: b/62337687]. - return 1; - } - return 0; - } - } - - /** Comparator for preferring OMX.google media codecs. */ - private static final class PreferOmxGoogleCodecComparator implements Comparator { - @Override - public int compare(MediaCodecInfo a, MediaCodecInfo b) { - return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b); - } - - private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) { - return mediaCodecInfo.name.startsWith("OMX.google") ? -1 : 0; - } - } - static { AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index e693af2bd1..8e61388647 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -306,12 +306,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } List decoderInfos = - getDecoderInfos(mediaCodecSelector, format, requiresSecureDecryption); + getDecoderInfos( + mediaCodecSelector, + format, + requiresSecureDecryption, + /* requiresTunnelingDecoder= */ false); if (decoderInfos.isEmpty()) { return requiresSecureDecryption - && !mediaCodecSelector - .getDecoderInfos( - format.sampleMimeType, + && !getDecoderInfos( + mediaCodecSelector, + format, /* requiresSecureDecoder= */ false, /* requiresTunnelingDecoder= */ false) .isEmpty() @@ -331,8 +335,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = - mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, + getDecoderInfos( + mediaCodecSelector, + format, requiresSecureDecryption, /* requiresTunnelingDecoder= */ true); if (!tunnelingDecoderInfos.isEmpty()) { @@ -351,8 +356,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { + return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling); + } + + private static List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, + Format format, + boolean requiresSecureDecoder, + boolean requiresTunnelingDecoder) + throws DecoderQueryException { List decoderInfos = - mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecoder, tunneling); + mediaCodecSelector.getDecoderInfos( + format.sampleMimeType, requiresSecureDecoder, requiresTunnelingDecoder); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); return Collections.unmodifiableList(decoderInfos); } From f6de1aa24203681e5999898cd9b07f06ddf77a33 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 18 Apr 2019 13:17:04 +0100 Subject: [PATCH 16/95] Add test for HlsTrackMetadataEntry population in the HlsPlaylistParser PiperOrigin-RevId: 244168713 --- .../playlist/HlsMasterPlaylistParserTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 97d330cdaa..095739271e 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -23,10 +23,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry; import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -146,6 +150,50 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n" + "http://example.com/{$tricky}\n"; + private static final String PLAYLIST_WITH_MATCHING_STREAM_INF_URLS = + "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2227464," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" + + "v5/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=6453202," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" + + "v8/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=5054232," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" + + "v7/prog_index.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2448841," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud2\",SUBTITLES=\"sub1\"\n" + + "v5/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8399417," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud2\",SUBTITLES=\"sub1\"\n" + + "v9/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=5275609," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud2\",SUBTITLES=\"sub1\"\n" + + "v7/prog_index.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2256841," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud3\",SUBTITLES=\"sub1\"\n" + + "v5/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8207417," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud3\",SUBTITLES=\"sub1\"\n" + + "v9/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=6482579," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud3\",SUBTITLES=\"sub1\"\n" + + "v8/prog_index.m3u8\n" + + "\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",URI=\"a1/index.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",NAME=\"English\",URI=\"a2/index.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud3\",NAME=\"English\",URI=\"a3/index.m3u8\"\n" + + "\n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS," + + "GROUP-ID=\"cc1\",NAME=\"English\",INSTREAM-ID=\"CC1\"\n" + + "\n" + + "#EXT-X-MEDIA:TYPE=SUBTITLES," + + "GROUP-ID=\"sub1\",NAME=\"English\",URI=\"s1/en/prog_index.m3u8\"\n"; + @Test public void testParseMasterPlaylist() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -296,6 +344,61 @@ public class HlsMasterPlaylistParserTest { .isEqualTo(Uri.parse("http://example.com/This/{$nested}/reference/shouldnt/work")); } + @Test + public void testHlsMetadata() throws IOException { + HlsMasterPlaylist playlist = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS); + assertThat(playlist.variants).hasSize(4); + assertThat(playlist.variants.get(0).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 2227464, /* audioGroupId= */ "aud1"), + createVariantInfo(/* bitrate= */ 2448841, /* audioGroupId= */ "aud2"), + createVariantInfo(/* bitrate= */ 2256841, /* audioGroupId= */ "aud3"))); + assertThat(playlist.variants.get(1).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 6453202, /* audioGroupId= */ "aud1"), + createVariantInfo(/* bitrate= */ 6482579, /* audioGroupId= */ "aud3"))); + assertThat(playlist.variants.get(2).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 5054232, /* audioGroupId= */ "aud1"), + createVariantInfo(/* bitrate= */ 5275609, /* audioGroupId= */ "aud2"))); + assertThat(playlist.variants.get(3).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 8399417, /* audioGroupId= */ "aud2"), + createVariantInfo(/* bitrate= */ 8207417, /* audioGroupId= */ "aud3"))); + + assertThat(playlist.audios).hasSize(3); + assertThat(playlist.audios.get(0).format.metadata) + .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud1", /* name= */ "English")); + assertThat(playlist.audios.get(1).format.metadata) + .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud2", /* name= */ "English")); + assertThat(playlist.audios.get(2).format.metadata) + .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English")); + } + + private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) { + return new Metadata( + new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos))); + } + + private static Metadata createExtXMediaMetadata(String groupId, String name) { + return new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList())); + } + + private static HlsTrackMetadataEntry.VariantInfo createVariantInfo( + long bitrate, String audioGroupId) { + return new HlsTrackMetadataEntry.VariantInfo( + bitrate, + /* videoGroupId= */ null, + audioGroupId, + /* subtitleGroupId= */ "sub1", + /* captionGroupId= */ "cc1"); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); From 50c9fe629486c1d417913176fa379c1fb1d19429 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 18 Apr 2019 13:34:52 +0100 Subject: [PATCH 17/95] Fix flaky DownloadManagerDashTest PiperOrigin-RevId: 244170179 --- .../offline/DownloadManagerTest.java | 2 +- .../dash/offline/DownloadManagerDashTest.java | 41 +++++++++++-------- .../testutil/TestDownloadManagerListener.java | 9 +++- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 5b2f47d2e5..c8ec02160c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -552,7 +552,7 @@ public class DownloadManagerTest { } } - private void runOnMainThread(final TestRunnable r) { + private void runOnMainThread(TestRunnable r) { dummyMainThread.runTestOnMainThread(r); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 6d7e2a881b..b140bf8d05 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; +import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; @@ -99,8 +100,8 @@ public class DownloadManagerDashTest { } @After - public void tearDown() throws Exception { - downloadManager.release(); + public void tearDown() { + runOnMainThread(() -> downloadManager.release()); Util.recursiveDelete(tempFolder); dummyMainThread.release(); } @@ -128,10 +129,11 @@ public class DownloadManagerDashTest { // Run DM accessing code on UI/main thread as it should be. Also not to block handling of loaded // actions. - dummyMainThread.runOnMainThread( + runOnMainThread( () -> { // Setup an Action and immediately release the DM. - handleDownloadRequest(fakeStreamKey1, fakeStreamKey2); + DownloadRequest request = getDownloadRequest(fakeStreamKey1, fakeStreamKey2); + downloadManager.addDownload(request); downloadManager.release(); }); @@ -228,25 +230,28 @@ public class DownloadManagerDashTest { } private void handleDownloadRequest(StreamKey... keys) { + DownloadRequest request = getDownloadRequest(keys); + runOnMainThread(() -> downloadManager.addDownload(request)); + } + + private DownloadRequest getDownloadRequest(StreamKey... keys) { ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - DownloadRequest action = - new DownloadRequest( - TEST_ID, - DownloadRequest.TYPE_DASH, - TEST_MPD_URI, - keysList, - /* customCacheKey= */ null, - null); - downloadManager.addDownload(action); + return new DownloadRequest( + TEST_ID, + DownloadRequest.TYPE_DASH, + TEST_MPD_URI, + keysList, + /* customCacheKey= */ null, + null); } private void handleRemoveAction() { - downloadManager.removeDownload(TEST_ID); + runOnMainThread(() -> downloadManager.removeDownload(TEST_ID)); } private void createDownloadManager() { - dummyMainThread.runTestOnMainThread( + runOnMainThread( () -> { Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); downloadManager = @@ -260,9 +265,13 @@ public class DownloadManagerDashTest { new Requirements(0)); downloadManagerListener = - new TestDownloadManagerListener(downloadManager, dummyMainThread); + new TestDownloadManagerListener( + downloadManager, dummyMainThread, /* timeout= */ 3000); downloadManager.startDownloads(); }); } + private void runOnMainThread(TestRunnable r) { + dummyMainThread.runTestOnMainThread(r); + } } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java index b74e539fd6..9d6223b8b1 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -40,14 +40,21 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen private final DummyMainThread dummyMainThread; private final HashMap> downloadStates; private final ConditionVariable initializedCondition; + private final int timeout; private CountDownLatch downloadFinishedCondition; @Download.FailureReason private int failureReason; public TestDownloadManagerListener( DownloadManager downloadManager, DummyMainThread dummyMainThread) { + this(downloadManager, dummyMainThread, TIMEOUT); + } + + public TestDownloadManagerListener( + DownloadManager downloadManager, DummyMainThread dummyMainThread, int timeout) { this.downloadManager = downloadManager; this.dummyMainThread = dummyMainThread; + this.timeout = timeout; downloadStates = new HashMap<>(); initializedCondition = new ConditionVariable(); downloadManager.addListener(this); @@ -110,7 +117,7 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen downloadFinishedCondition.countDown(); } }); - assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(downloadFinishedCondition.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); } private ArrayBlockingQueue getStateQueue(String taskId) { From 6665af5b7b332587138d90a185cc2a8043c65132 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Apr 2019 13:37:08 +0100 Subject: [PATCH 18/95] Support additional DV profiles that require fallback PiperOrigin-RevId: 244170391 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 3 +-- .../video/MediaCodecVideoRenderer.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 0185a6d8af..18ca81c72f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -897,8 +897,7 @@ import java.util.List; out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); - // TODO: Support profiles 4, 8 and 9 once we have a way to fall back to AVC/HEVC decoding. - if (dolbyVisionConfig != null && dolbyVisionConfig.profile == 5) { + if (dolbyVisionConfig != null) { codecs = dolbyVisionConfig.codecs; mimeType = MimeTypes.VIDEO_DOLBY_VISION; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 8e61388647..611a906a9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -369,6 +369,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { mediaCodecSelector.getDecoderInfos( format.sampleMimeType, requiresSecureDecoder, requiresTunnelingDecoder); decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + // Fallback to primary decoders for H.265/HEVC or H.264/AVC for the relevant DV profiles. + Pair codecProfileAndLevel = + MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + if (codecProfileAndLevel != null) { + int profile = codecProfileAndLevel.first; + if (profile == 4 || profile == 8) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder)); + } else if (profile == 9) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder)); + } + } + } return Collections.unmodifiableList(decoderInfos); } From fe65f002a50898314d15b8e37fd3f7e5e812ac94 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Apr 2019 14:15:38 +0100 Subject: [PATCH 19/95] Move E-AC3 workaround out of MediaCodecUtil PiperOrigin-RevId: 244173887 --- .../android/exoplayer2/audio/MediaCodecAudioRenderer.java | 7 +++++++ .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index a38304d2b2..a2fe8244f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -331,6 +331,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaCodecSelector.getDecoderInfos( format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + if (MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType)) { + // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. + List eac3DecoderInfos = + mediaCodecSelector.getDecoderInfos( + MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + decoderInfos.addAll(eac3DecoderInfos); + } return Collections.unmodifiableList(decoderInfos); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 1742e9ebf3..1c3bdd95a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -173,13 +173,6 @@ public final class MediaCodecUtil { + ". Assuming: " + decoderInfos.get(0).name); } } - if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { - // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. - CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure, key.tunneling); - ArrayList eac3DecoderInfos = - getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType); - decoderInfos.addAll(eac3DecoderInfos); - } applyWorkarounds(mimeType, decoderInfos); List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); decoderInfosCache.put(key, unmodifiableDecoderInfos); From 4ea2463856566818008a1441941e20be7cd7956a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 18 Apr 2019 14:41:45 +0100 Subject: [PATCH 20/95] Avoid selecting a forced text track that doesn't match the audio selection Assuming there is no text language preference. PiperOrigin-RevId: 244176667 --- RELEASENOTES.md | 4 +- .../trackselection/DefaultTrackSelector.java | 44 +++++++++---------- .../DefaultTrackSelectorTest.java | 36 +++++++-------- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 514528698f..c3ddb1d5e0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,7 +44,7 @@ * MP3: Fix ID3 frame unsychronization ([#5673](https://github.com/google/ExoPlayer/issues/5673)). * MP3: Fix playback of badly clipped files - ([#5772](https://github.com/google/ExoPlayer/issues/5772)). + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default (i.e. if the flag is not set), the 0x82 elementary stream type is now treated as an SCTE subtitle track @@ -55,6 +55,8 @@ * Update `TrackSelection.Factory` interface to support creating all track selections together. * Allow to specify a selection reason for a `SelectionOverride`. + * When no text language preference matches, only select forced text tracks + whose language matches the selected audio language. * UI: * Update `DefaultTimeBar` based on duration of media and add parameter to set the minimum update interval to control the smoothness of the updates diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index f25f1a979c..3200e40495 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -2070,29 +2070,25 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage); - if (languageScore > 0 - || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { + boolean trackHasNoLanguage = formatHasNoLanguage(format); + if (languageScore > 0 || (params.selectUndeterminedTextLanguage && trackHasNoLanguage)) { if (isDefault) { - trackScore = 17; + trackScore = 11; } else if (!isForced) { // Prefer non-forced to forced if a preferred text language has been specified. Where // both are provided the non-forced track will usually contain the forced subtitles as // a subset. - trackScore = 13; + trackScore = 7; } else { - trackScore = 9; + trackScore = 3; } trackScore += languageScore; } else if (isDefault) { - trackScore = 8; - } else if (isForced) { - int preferredAudioLanguageScore = - getFormatLanguageScore(format, params.preferredAudioLanguage); - if (preferredAudioLanguageScore > 0) { - trackScore = 4 + preferredAudioLanguageScore; - } else { - trackScore = 1 + getFormatLanguageScore(format, selectedAudioLanguage); - } + trackScore = 2; + } else if (isForced + && (getFormatLanguageScore(format, selectedAudioLanguage) > 0 + || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { + trackScore = 1; } else { // Track should not be selected. continue; @@ -2281,15 +2277,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } - /** - * Returns whether a {@link Format} does not define a language. - * - * @param format The {@link Format}. - * @return Whether the {@link Format} does not define a language. - */ + /** Equivalent to {@link #stringDefinesNoLanguage stringDefinesNoLanguage(format.language)}. */ protected static boolean formatHasNoLanguage(Format format) { - return TextUtils.isEmpty(format.language) - || TextUtils.equals(format.language, C.LANGUAGE_UNDETERMINED); + return stringDefinesNoLanguage(format.language); + } + + /** + * Returns whether the given string does not define a language. + * + * @param language The string. + * @return Whether the given string does not define a language. + */ + protected static boolean stringDefinesNoLanguage(@Nullable String language) { + return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED); } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 3091e46456..83fe34db97 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -910,13 +910,8 @@ public final class DefaultTrackSelectorTest { result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly); - // With no language preference and no text track flagged as default, the first forced should be + // Default flags are disabled and no language preference is provided, so no text track is // selected. - trackGroups = wrapFormats(forcedOnly, noFlag); - result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedOnly); - - // Default flags are disabled, so the first track flagged as forced should be selected. trackGroups = wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault); trackSelector.setParameters( Parameters.DEFAULT @@ -924,15 +919,7 @@ public final class DefaultTrackSelectorTest { .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) .build()); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedOnly); - - // Default flags are disabled, but there is a text track flagged as forced whose language - // matches the preferred audio language. - trackGroups = wrapFormats(forcedDefault, forcedOnly, defaultOnly, noFlag, forcedOnlySpanish); - trackSelector.setParameters( - trackSelector.getParameters().buildUpon().setPreferredTextLanguage("spa").build()); - result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedOnlySpanish); + assertNoSelection(result.selections.get(0)); // All selection flags are disabled and there is no language preference, so nothing should be // selected. @@ -977,6 +964,11 @@ public final class DefaultTrackSelectorTest { buildTextFormat(/* id= */ "forcedEnglish", /* language= */ "eng", C.SELECTION_FLAG_FORCED); Format forcedGerman = buildTextFormat(/* id= */ "forcedGerman", /* language= */ "deu", C.SELECTION_FLAG_FORCED); + Format forcedNoLanguage = + buildTextFormat( + /* id= */ "forcedNoLanguage", + /* language= */ C.LANGUAGE_UNDETERMINED, + C.SELECTION_FLAG_FORCED); Format audio = buildAudioFormat(/* id= */ "audio"); Format germanAudio = buildAudioFormat( @@ -994,16 +986,18 @@ public final class DefaultTrackSelectorTest { ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES }; - // The audio declares no language. The first forced text track should be selected. - TrackGroupArray trackGroups = wrapFormats(audio, forcedEnglish, forcedGerman); + // Neither the audio nor the forced text track define a language. We select them both under the + // assumption that they have matching language. + TrackGroupArray trackGroups = wrapFormats(audio, forcedNoLanguage); TrackSelectorResult result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(1), trackGroups, forcedEnglish); + assertFixedSelection(result.selections.get(1), trackGroups, forcedNoLanguage); - // Ditto. - trackGroups = wrapFormats(audio, forcedGerman, forcedEnglish); + // No forced text track should be selected because none of the forced text tracks' languages + // matches the selected audio language. + trackGroups = wrapFormats(audio, forcedEnglish, forcedGerman); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(1), trackGroups, forcedGerman); + assertNoSelection(result.selections.get(1)); // The audio declares german. The german forced track should be selected. trackGroups = wrapFormats(germanAudio, forcedGerman, forcedEnglish); From f9f009645d8978d87da45d5527184aa042b39eb9 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 18 Apr 2019 17:04:31 +0100 Subject: [PATCH 21/95] Add missing DownloadService build*Intent and startWith* methods PiperOrigin-RevId: 244196081 --- demos/ima/build.gradle | 2 +- demos/main/build.gradle | 2 +- .../exoplayer2/demo/DownloadTracker.java | 4 +- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 2 +- extensions/gvr/build.gradle | 2 +- extensions/leanback/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- extensions/rtmp/build.gradle | 2 +- extensions/vp9/build.gradle | 2 +- library/core/build.gradle | 2 +- .../exoplayer2/offline/DownloadService.java | 127 ++++++++++++++---- library/dash/build.gradle | 2 +- library/hls/build.gradle | 2 +- library/smoothstreaming/build.gradle | 2 +- library/ui/build.gradle | 2 +- playbacktests/build.gradle | 2 +- testutils/build.gradle | 2 +- testutils_robolectric/build.gradle | 2 +- 21 files changed, 123 insertions(+), 46 deletions(-) diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 1d2068e5f7..33161b4121 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-ima') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 84a8a4087c..7089d4d731 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -62,7 +62,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' implementation 'androidx.fragment:fragment:1.0.0' implementation 'com.google.android.material:material:1.0.0' 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 4a7a810314..a860d96e43 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 @@ -101,7 +101,7 @@ public class DownloadTracker { RenderersFactory renderersFactory) { Download download = downloads.get(uri); if (download != null) { - DownloadService.startWithRemoveDownload( + DownloadService.sendRemoveDownload( context, DemoDownloadService.class, download.request.id, /* foreground= */ false); } else { if (startDownloadDialogHelper != null) { @@ -263,7 +263,7 @@ public class DownloadTracker { } private void startDownload(DownloadRequest downloadRequest) { - DownloadService.startWithNewDownload( + DownloadService.sendNewDownload( context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); } diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 573426df2e..4dc463ff81 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -32,7 +32,7 @@ android { dependencies { api 'com.google.android.gms:play-services-cast-framework:16.1.2' - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index baf925acbd..ad45f61d98 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -33,7 +33,7 @@ android { dependencies { api 'org.chromium.net:cronet-embedded:72.3626.96' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index ee3358d21a..ffecdcd16f 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -38,7 +38,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 9a247c3f8f..06a5888404 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 6c4bfa469a..50acd6c040 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index a86eedc2d4..c6f5a216ce 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.leanback:leanback:1.0.0' } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index eddd364370..db2e073c8a 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion api 'com.squareup.okhttp3:okhttp:3.12.1' } diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index e7c7fce164..ca734c3657 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 4b2ba26ca2..02b68b831d 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/library/core/build.gradle b/library/core/build.gradle index deb9f24dce..68ff8cc977 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -58,7 +58,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index c206a94d6d..6922d6a787 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -117,8 +117,8 @@ public abstract class DownloadService extends Service { public static final String KEY_DOWNLOAD_REQUEST = "download_request"; /** - * Key for the content id in {@link #ACTION_START}, {@link #ACTION_STOP} and {@link - * #ACTION_REMOVE} intents. + * Key for the content id in {@link #ACTION_SET_MANUAL_STOP_REASON} and {@link #ACTION_REMOVE} + * intents. */ public static final String KEY_CONTENT_ID = "content_id"; @@ -265,10 +265,9 @@ public abstract class DownloadService extends Service { DownloadRequest downloadRequest, int manualStopReason, boolean foreground) { - return getIntent(context, clazz, ACTION_ADD) + return getIntent(context, clazz, ACTION_ADD, foreground) .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest) - .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason) - .putExtra(KEY_FOREGROUND, foreground); + .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason); } /** @@ -282,9 +281,7 @@ public abstract class DownloadService extends Service { */ public static Intent buildRemoveDownloadIntent( Context context, Class clazz, String id, boolean foreground) { - return getIntent(context, clazz, ACTION_REMOVE) - .putExtra(KEY_CONTENT_ID, id) - .putExtra(KEY_FOREGROUND, foreground); + return getIntent(context, clazz, ACTION_REMOVE, foreground).putExtra(KEY_CONTENT_ID, id); } /** @@ -295,55 +292,122 @@ public abstract class DownloadService extends Service { * @param clazz The concrete download service being targeted by the intent. * @param id The content id, or {@code null} to set the manual stop reason for all downloads. * @param manualStopReason An application defined stop reason. + * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ public static Intent buildSetManualStopReasonIntent( Context context, Class clazz, @Nullable String id, - int manualStopReason) { - return getIntent(context, clazz, ACTION_STOP) + int manualStopReason, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_MANUAL_STOP_REASON, foreground) .putExtra(KEY_CONTENT_ID, id) .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason); } /** - * Starts the service, adding a new download. + * Builds an {@link Intent} for starting all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return Created Intent. + */ + public static Intent buildStartDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_START, foreground); + } + + /** + * Builds an {@link Intent} for stopping all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return Created Intent. + */ + public static Intent buildStopDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_STOP, foreground); + } + + /** + * Starts the service if not started already and adds a new download. * * @param context A {@link Context}. * @param clazz The concrete download service to be started. * @param downloadRequest The request to be executed. * @param foreground Whether the service is started in the foreground. */ - public static void startWithNewDownload( + public static void sendNewDownload( Context context, Class clazz, DownloadRequest downloadRequest, boolean foreground) { Intent intent = buildAddRequestIntent(context, clazz, downloadRequest, foreground); - if (foreground) { - Util.startForegroundService(context, intent); - } else { - context.startService(intent); - } + startService(context, intent, foreground); } /** - * Starts the service to remove a download. + * Starts the service if not started already and removes a download. * * @param context A {@link Context}. * @param clazz The concrete download service to be started. * @param id The content id. * @param foreground Whether the service is started in the foreground. */ - public static void startWithRemoveDownload( + public static void sendRemoveDownload( Context context, Class clazz, String id, boolean foreground) { Intent intent = buildRemoveDownloadIntent(context, clazz, id, foreground); - if (foreground) { - Util.startForegroundService(context, intent); - } else { - context.startService(intent); - } + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and sets the manual stop reason for one or all + * downloads. To clear manual stop reason, pass {@link Download#MANUAL_STOP_REASON_NONE}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param id The content id, or {@code null} to set the manual stop reason for all downloads. + * @param manualStopReason An application defined stop reason. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendManualStopReason( + Context context, + Class clazz, + @Nullable String id, + int manualStopReason, + boolean foreground) { + Intent intent = + buildSetManualStopReasonIntent(context, clazz, id, manualStopReason, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and starts all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendStartDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildStartDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and stops all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendStopDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildStopDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); } /** @@ -367,7 +431,7 @@ public abstract class DownloadService extends Service { * @see #start(Context, Class) */ public static void startForeground(Context context, Class clazz) { - Intent intent = getIntent(context, clazz, ACTION_INIT).putExtra(KEY_FOREGROUND, true); + Intent intent = getIntent(context, clazz, ACTION_INIT, true); Util.startForegroundService(context, intent); } @@ -588,11 +652,24 @@ public abstract class DownloadService extends Service { } } + private static Intent getIntent( + Context context, Class clazz, String action, boolean foreground) { + return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground); + } + private static Intent getIntent( Context context, Class clazz, String action) { return new Intent(context, clazz).setAction(action); } + private static void startService(Context context, Intent intent, boolean foreground) { + if (foreground) { + Util.startForegroundService(context, intent); + } else { + context.startService(intent); + } + } + private final class ForegroundNotificationUpdater { private final int notificationId; diff --git a/library/dash/build.gradle b/library/dash/build.gradle index c7e68f548a..f6981a2220 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 99619bf750..8e9696af70 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index ba3b4ab65d..a2e81fb304 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 9c47f3684d..49446b25de 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.media:media:1.0.0' - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 0e1c8a1268..dd5cfa64a7 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -34,7 +34,7 @@ android { dependencies { androidTestImplementation 'androidx.test:rules:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.0.1' + androidTestImplementation 'androidx.annotation:annotation:1.0.2' androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') diff --git a/testutils/build.gradle b/testutils/build.gradle index ab78e6673f..bdc26d5c19 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -41,7 +41,7 @@ dependencies { api 'org.mockito:mockito-core:' + mockitoVersion api 'androidx.test.ext:junit:' + androidXTestVersion api 'androidx.test.ext:truth:' + androidXTestVersion - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index feee9536bb..78fa5dbd87 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -41,6 +41,6 @@ dependencies { api 'org.robolectric:robolectric:' + robolectricVersion api project(modulePrefix + 'testutils') implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion } From 82af6899a0903cb6638ff1776cbcd7e2b89521c2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 18:26:00 +0100 Subject: [PATCH 22/95] Rename manualStopReason to stopReason PiperOrigin-RevId: 244210737 --- .../offline/ActionFileUpgradeUtil.java | 4 +- .../offline/DefaultDownloadIndex.java | 20 ++-- .../android/exoplayer2/offline/Download.java | 20 ++-- .../exoplayer2/offline/DownloadManager.java | 93 +++++++++---------- .../exoplayer2/offline/DownloadService.java | 89 ++++++++---------- .../offline/WritableDownloadIndex.java | 16 ++-- .../offline/DefaultDownloadIndexTest.java | 47 +++++----- .../exoplayer2/offline/DownloadBuilder.java | 8 +- .../offline/DownloadManagerTest.java | 28 +++--- 9 files changed, 153 insertions(+), 172 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index 0a37fe3a80..51996ed284 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -90,7 +90,7 @@ public final class ActionFileUpgradeUtil { DownloadRequest request, DefaultDownloadIndex downloadIndex) throws IOException { Download download = downloadIndex.getDownload(request.id); if (download != null) { - download = DownloadManager.mergeRequest(download, request, download.manualStopReason); + download = DownloadManager.mergeRequest(download, request, download.stopReason); } else { long nowMs = System.currentTimeMillis(); download = @@ -98,7 +98,7 @@ public final class ActionFileUpgradeUtil { request, STATE_QUEUED, Download.FAILURE_REASON_NONE, - Download.MANUAL_STOP_REASON_NONE, + Download.STOP_REASON_NONE, /* startTimeMs= */ nowMs, /* updateTimeMs= */ nowMs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index fc1518e5c3..a2caff3ff1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -57,7 +57,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes"; private static final String COLUMN_TOTAL_BYTES = "total_bytes"; private static final String COLUMN_FAILURE_REASON = "failure_reason"; - private static final String COLUMN_MANUAL_STOP_REASON = "manual_stop_reason"; + private static final String COLUMN_STOP_REASON = "manual_stop_reason"; private static final String COLUMN_START_TIME_MS = "start_time_ms"; private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; @@ -82,7 +82,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 8; private static final int COLUMN_INDEX_TOTAL_BYTES = 9; private static final int COLUMN_INDEX_FAILURE_REASON = 10; - private static final int COLUMN_INDEX_MANUAL_STOP_REASON = 11; + private static final int COLUMN_INDEX_STOP_REASON = 11; private static final int COLUMN_INDEX_START_TIME_MS = 12; private static final int COLUMN_INDEX_UPDATE_TIME_MS = 13; @@ -103,7 +103,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { COLUMN_DOWNLOADED_BYTES, COLUMN_TOTAL_BYTES, COLUMN_FAILURE_REASON, - COLUMN_MANUAL_STOP_REASON, + COLUMN_STOP_REASON, COLUMN_START_TIME_MS, COLUMN_UPDATE_TIME_MS }; @@ -135,7 +135,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { + " INTEGER NOT NULL," + COLUMN_NOT_MET_REQUIREMENTS + " INTEGER NOT NULL," - + COLUMN_MANUAL_STOP_REASON + + COLUMN_STOP_REASON + " INTEGER NOT NULL," + COLUMN_START_TIME_MS + " INTEGER NOT NULL," @@ -202,7 +202,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { values.put(COLUMN_FAILURE_REASON, download.failureReason); values.put(COLUMN_STOP_FLAGS, 0); values.put(COLUMN_NOT_MET_REQUIREMENTS, 0); - values.put(COLUMN_MANUAL_STOP_REASON, download.manualStopReason); + values.put(COLUMN_STOP_REASON, download.stopReason); values.put(COLUMN_START_TIME_MS, download.startTimeMs); values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); try { @@ -224,11 +224,11 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } @Override - public void setManualStopReason(int manualStopReason) throws DatabaseIOException { + public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); try { ContentValues values = new ContentValues(); - values.put(COLUMN_MANUAL_STOP_REASON, manualStopReason); + values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update(TABLE_NAME, values, WHERE_STATE_TERMINAL, /* whereArgs= */ null); } catch (SQLException e) { @@ -237,11 +237,11 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } @Override - public void setManualStopReason(String id, int manualStopReason) throws DatabaseIOException { + public void setStopReason(String id, int stopReason) throws DatabaseIOException { ensureInitialized(); try { ContentValues values = new ContentValues(); - values.put(COLUMN_MANUAL_STOP_REASON, manualStopReason); + values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update( TABLE_NAME, values, WHERE_STATE_TERMINAL + " AND " + WHERE_ID_EQUALS, new String[] {id}); @@ -332,7 +332,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { request, cursor.getInt(COLUMN_INDEX_STATE), cursor.getInt(COLUMN_INDEX_FAILURE_REASON), - cursor.getInt(COLUMN_INDEX_MANUAL_STOP_REASON), + cursor.getInt(COLUMN_INDEX_STOP_REASON), cursor.getLong(COLUMN_INDEX_START_TIME_MS), cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), cachingCounters); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index b29abde24b..343b9d6a49 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -46,7 +46,7 @@ public final class Download { // Important: These constants are persisted into DownloadIndex. Do not change them. /** The download is waiting to be started. */ public static final int STATE_QUEUED = 0; - /** The download is stopped for a specified {@link #manualStopReason}. */ + /** The download is stopped for a specified {@link #stopReason}. */ public static final int STATE_STOPPED = 1; /** The download is currently started. */ public static final int STATE_DOWNLOADING = 2; @@ -69,8 +69,8 @@ public final class Download { /** The download is failed because of unknown reason. */ public static final int FAILURE_REASON_UNKNOWN = 1; - /** The download isn't manually stopped. */ - public static final int MANUAL_STOP_REASON_NONE = 0; + /** The download isn't stopped. */ + public static final int STOP_REASON_NONE = 0; /** Returns the state string for the given state value. */ public static String getStateString(@State int state) { @@ -108,8 +108,8 @@ public final class Download { * #FAILURE_REASON_NONE}. */ @FailureReason public final int failureReason; - /** The reason the download is manually stopped, or {@link #MANUAL_STOP_REASON_NONE}. */ - public final int manualStopReason; + /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */ + public final int stopReason; /* package */ CachingCounters counters; @@ -117,14 +117,14 @@ public final class Download { DownloadRequest request, @State int state, @FailureReason int failureReason, - int manualStopReason, + int stopReason, long startTimeMs, long updateTimeMs) { this( request, state, failureReason, - manualStopReason, + stopReason, startTimeMs, updateTimeMs, new CachingCounters()); @@ -134,19 +134,19 @@ public final class Download { DownloadRequest request, @State int state, @FailureReason int failureReason, - int manualStopReason, + int stopReason, long startTimeMs, long updateTimeMs, CachingCounters counters) { Assertions.checkNotNull(counters); Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); - if (manualStopReason != 0) { + if (stopReason != 0) { Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); } this.request = request; this.state = state; this.failureReason = failureReason; - this.manualStopReason = manualStopReason; + this.stopReason = stopReason; this.startTimeMs = startTimeMs; this.updateTimeMs = updateTimeMs; this.counters = counters; 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 df958f8691..497e3476af 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.offline; import static com.google.android.exoplayer2.offline.Download.FAILURE_REASON_NONE; import static com.google.android.exoplayer2.offline.Download.FAILURE_REASON_UNKNOWN; -import static com.google.android.exoplayer2.offline.Download.MANUAL_STOP_REASON_NONE; import static com.google.android.exoplayer2.offline.Download.STATE_COMPLETED; import static com.google.android.exoplayer2.offline.Download.STATE_DOWNLOADING; import static com.google.android.exoplayer2.offline.Download.STATE_FAILED; @@ -25,6 +24,7 @@ import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED; import static com.google.android.exoplayer2.offline.Download.STATE_REMOVING; import static com.google.android.exoplayer2.offline.Download.STATE_RESTARTING; import static com.google.android.exoplayer2.offline.Download.STATE_STOPPED; +import static com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE; import android.content.Context; import android.os.Handler; @@ -128,7 +128,7 @@ public final class DownloadManager { private static final int MSG_INITIALIZE = 0; private static final int MSG_SET_DOWNLOADS_STARTED = 1; private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; - private static final int MSG_SET_MANUAL_STOP_REASON = 3; + private static final int MSG_SET_STOP_REASON = 3; private static final int MSG_ADD_DOWNLOAD = 4; private static final int MSG_REMOVE_DOWNLOAD = 5; private static final int MSG_DOWNLOAD_THREAD_STOPPED = 6; @@ -346,10 +346,7 @@ public final class DownloadManager { return Collections.unmodifiableList(new ArrayList<>(downloads)); } - /** - * Starts all downloads except those that are manually stopped (i.e. have a non-zero {@link - * Download#manualStopReason}). - */ + /** Starts all downloads except those that have a non-zero {@link Download#stopReason}. */ public void startDownloads() { pendingMessages++; internalHandler @@ -366,17 +363,17 @@ public final class DownloadManager { } /** - * Sets the manual stop reason for one or all downloads. To clear the manual stop reason, pass - * {@link Download#MANUAL_STOP_REASON_NONE}. + * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link + * Download#STOP_REASON_NONE}. * - * @param id The content id of the download to update, or {@code null} to set the manual stop - * reason for all downloads. - * @param manualStopReason The manual stop reason, or {@link Download#MANUAL_STOP_REASON_NONE}. + * @param id The content id of the download to update, or {@code null} to set the stop reason for + * all downloads. + * @param stopReason The stop reason, or {@link Download#STOP_REASON_NONE}. */ - public void setManualStopReason(@Nullable String id, int manualStopReason) { + public void setStopReason(@Nullable String id, int stopReason) { pendingMessages++; internalHandler - .obtainMessage(MSG_SET_MANUAL_STOP_REASON, manualStopReason, /* unused */ 0, id) + .obtainMessage(MSG_SET_STOP_REASON, stopReason, /* unused */ 0, id) .sendToTarget(); } @@ -386,20 +383,20 @@ public final class DownloadManager { * @param request The download request. */ public void addDownload(DownloadRequest request) { - addDownload(request, Download.MANUAL_STOP_REASON_NONE); + addDownload(request, Download.STOP_REASON_NONE); } /** - * Adds a download defined by the given request and with the specified manual stop reason. + * Adds a download defined by the given request and with the specified stop reason. * * @param request The download request. - * @param manualStopReason An initial manual stop reason for the download, or {@link - * Download#MANUAL_STOP_REASON_NONE} if the download should be started. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. */ - public void addDownload(DownloadRequest request, int manualStopReason) { + public void addDownload(DownloadRequest request, int stopReason) { pendingMessages++; internalHandler - .obtainMessage(MSG_ADD_DOWNLOAD, manualStopReason, /* unused */ 0, request) + .obtainMessage(MSG_ADD_DOWNLOAD, stopReason, /* unused */ 0, request) .sendToTarget(); } @@ -552,15 +549,15 @@ public final class DownloadManager { notMetRequirements = message.arg1; setNotMetRequirementsInternal(notMetRequirements); break; - case MSG_SET_MANUAL_STOP_REASON: + case MSG_SET_STOP_REASON: String id = (String) message.obj; - int manualStopReason = message.arg1; - setManualStopReasonInternal(id, manualStopReason); + int stopReason = message.arg1; + setStopReasonInternal(id, stopReason); break; case MSG_ADD_DOWNLOAD: DownloadRequest request = (DownloadRequest) message.obj; - manualStopReason = message.arg1; - addDownloadInternal(request, manualStopReason); + stopReason = message.arg1; + addDownloadInternal(request, stopReason); break; case MSG_REMOVE_DOWNLOAD: id = (String) message.obj; @@ -629,34 +626,34 @@ public final class DownloadManager { } } - private void setManualStopReasonInternal(@Nullable String id, int manualStopReason) { + private void setStopReasonInternal(@Nullable String id, int stopReason) { if (id != null) { DownloadInternal downloadInternal = getDownload(id); if (downloadInternal != null) { - logd("download manual stop reason is set to : " + manualStopReason, downloadInternal); - downloadInternal.setManualStopReason(manualStopReason); + logd("download stop reason is set to : " + stopReason, downloadInternal); + downloadInternal.setStopReason(stopReason); return; } } else { for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).setManualStopReason(manualStopReason); + downloadInternals.get(i).setStopReason(stopReason); } } try { if (id != null) { - downloadIndex.setManualStopReason(id, manualStopReason); + downloadIndex.setStopReason(id, stopReason); } else { - downloadIndex.setManualStopReason(manualStopReason); + downloadIndex.setStopReason(stopReason); } } catch (IOException e) { - Log.e(TAG, "setManualStopReason failed", e); + Log.e(TAG, "setStopReason failed", e); } } - private void addDownloadInternal(DownloadRequest request, int manualStopReason) { + private void addDownloadInternal(DownloadRequest request, int stopReason) { DownloadInternal downloadInternal = getDownload(request.id); if (downloadInternal != null) { - downloadInternal.addRequest(request, manualStopReason); + downloadInternal.addRequest(request, stopReason); logd("Request is added to existing download", downloadInternal); } else { Download download = loadDownload(request.id); @@ -665,14 +662,14 @@ public final class DownloadManager { download = new Download( request, - manualStopReason != Download.MANUAL_STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, + stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, Download.FAILURE_REASON_NONE, - manualStopReason, + stopReason, /* startTimeMs= */ nowMs, /* updateTimeMs= */ nowMs); logd("Download state is created for " + request.id); } else { - download = mergeRequest(download, request, manualStopReason); + download = mergeRequest(download, request, stopReason); logd("Download state is loaded for " + request.id); } addDownloadForState(download); @@ -820,11 +817,11 @@ public final class DownloadManager { } /* package */ static Download mergeRequest( - Download download, DownloadRequest request, int manualStopReason) { + Download download, DownloadRequest request, int stopReason) { @Download.State int state = download.state; if (state == STATE_REMOVING || state == STATE_RESTARTING) { state = STATE_RESTARTING; - } else if (manualStopReason != MANUAL_STOP_REASON_NONE) { + } else if (stopReason != STOP_REASON_NONE) { state = STATE_STOPPED; } else { state = STATE_QUEUED; @@ -835,7 +832,7 @@ public final class DownloadManager { download.request.copyWithMergedRequest(request), state, FAILURE_REASON_NONE, - manualStopReason, + stopReason, startTimeMs, /* updateTimeMs= */ nowMs, download.counters); @@ -846,7 +843,7 @@ public final class DownloadManager { download.request, state, FAILURE_REASON_NONE, - download.manualStopReason, + download.stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), download.counters); @@ -882,21 +879,21 @@ public final class DownloadManager { // TODO: Get rid of these and use download directly. @Download.State private int state; - private int manualStopReason; + private int stopReason; @MonotonicNonNull @Download.FailureReason private int failureReason; private DownloadInternal(DownloadManager downloadManager, Download download) { this.downloadManager = downloadManager; this.download = download; - manualStopReason = download.manualStopReason; + stopReason = download.stopReason; } private void initialize() { initialize(download.state); } - public void addRequest(DownloadRequest newRequest, int manualStopReason) { - download = mergeRequest(download, newRequest, manualStopReason); + public void addRequest(DownloadRequest newRequest, int stopReason) { + download = mergeRequest(download, newRequest, stopReason); initialize(); } @@ -910,7 +907,7 @@ public final class DownloadManager { download.request, state, state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, - manualStopReason, + stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), download.counters); @@ -934,8 +931,8 @@ public final class DownloadManager { } } - public void setManualStopReason(int manualStopReason) { - this.manualStopReason = manualStopReason; + public void setStopReason(int stopReason) { + this.stopReason = stopReason; updateStopState(); } @@ -981,7 +978,7 @@ public final class DownloadManager { } private boolean canStart() { - return downloadManager.canStartDownloads() && manualStopReason == MANUAL_STOP_REASON_NONE; + return downloadManager.canStartDownloads() && stopReason == STOP_REASON_NONE; } private void startOrQueue() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 6922d6a787..fa74afacb3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.offline; -import static com.google.android.exoplayer2.offline.Download.MANUAL_STOP_REASON_NONE; +import static com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE; import android.app.Notification; import android.app.Service; @@ -58,16 +58,15 @@ public abstract class DownloadService extends Service { *

*/ public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; /** - * Starts all downloads except those that are manually stopped (i.e. have a non-zero {@link - * Download#manualStopReason}). Extras: + * Starts all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * * */ - public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; + public static final String ACTION_ADD_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD"; /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: @@ -72,8 +73,8 @@ public abstract class DownloadService extends Service { *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. * */ - public static final String ACTION_RESUME = - "com.google.android.exoplayer.downloadService.action.RESUME"; + public static final String ACTION_RESUME_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.RESUME_DOWNLOADS"; /** * Pauses all downloads. Extras: @@ -82,8 +83,8 @@ public abstract class DownloadService extends Service { *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. * */ - public static final String ACTION_PAUSE = - "com.google.android.exoplayer.downloadService.action.PAUSE"; + public static final String ACTION_PAUSE_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.PAUSE_DOWNLOADS"; /** * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link @@ -98,7 +99,7 @@ public abstract class DownloadService extends Service { * */ public static final String ACTION_SET_STOP_REASON = - "com.google.android.exoplayer.downloadService.action.SET_MANUAL_STOP_REASON"; + "com.google.android.exoplayer.downloadService.action.SET_STOP_REASON"; /** * Removes a download. Extras: @@ -108,18 +109,22 @@ public abstract class DownloadService extends Service { *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. * */ - public static final String ACTION_REMOVE = - "com.google.android.exoplayer.downloadService.action.REMOVE"; + public static final String ACTION_REMOVE_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; - /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD} intents. */ + /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD_DOWNLOAD} intents. */ public static final String KEY_DOWNLOAD_REQUEST = "download_request"; /** - * Key for the content id in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_REMOVE} intents. + * Key for the content id in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_REMOVE_DOWNLOAD} + * intents. */ public static final String KEY_CONTENT_ID = "content_id"; - /** Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD} intents. */ + /** + * Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD_DOWNLOAD} + * intents. + */ public static final String KEY_STOP_REASON = "manual_stop_reason"; /** @@ -233,12 +238,12 @@ public abstract class DownloadService extends Service { * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent buildAddRequestIntent( + public static Intent buildAddDownloadIntent( Context context, Class clazz, DownloadRequest downloadRequest, boolean foreground) { - return buildAddRequestIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground); + return buildAddDownloadIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground); } /** @@ -252,13 +257,13 @@ public abstract class DownloadService extends Service { * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent buildAddRequestIntent( + public static Intent buildAddDownloadIntent( Context context, Class clazz, DownloadRequest downloadRequest, int stopReason, boolean foreground) { - return getIntent(context, clazz, ACTION_ADD, foreground) + return getIntent(context, clazz, ACTION_ADD_DOWNLOAD, foreground) .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest) .putExtra(KEY_STOP_REASON, stopReason); } @@ -274,7 +279,8 @@ public abstract class DownloadService extends Service { */ public static Intent buildRemoveDownloadIntent( Context context, Class clazz, String id, boolean foreground) { - return getIntent(context, clazz, ACTION_REMOVE, foreground).putExtra(KEY_CONTENT_ID, id); + return getIntent(context, clazz, ACTION_REMOVE_DOWNLOAD, foreground) + .putExtra(KEY_CONTENT_ID, id); } /** @@ -287,7 +293,7 @@ public abstract class DownloadService extends Service { */ public static Intent buildResumeDownloadsIntent( Context context, Class clazz, boolean foreground) { - return getIntent(context, clazz, ACTION_RESUME, foreground); + return getIntent(context, clazz, ACTION_RESUME_DOWNLOADS, foreground); } /** @@ -300,7 +306,7 @@ public abstract class DownloadService extends Service { */ public static Intent buildPauseDownloadsIntent( Context context, Class clazz, boolean foreground) { - return getIntent(context, clazz, ACTION_PAUSE, foreground); + return getIntent(context, clazz, ACTION_PAUSE_DOWNLOADS, foreground); } /** @@ -333,12 +339,12 @@ public abstract class DownloadService extends Service { * @param downloadRequest The request to be executed. * @param foreground Whether the service is started in the foreground. */ - public static void sendNewDownload( + public static void sendAddDownload( Context context, Class clazz, DownloadRequest downloadRequest, boolean foreground) { - Intent intent = buildAddRequestIntent(context, clazz, downloadRequest, foreground); + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, foreground); startService(context, intent, foreground); } @@ -352,13 +358,13 @@ public abstract class DownloadService extends Service { * if the download should be started. * @param foreground Whether the service is started in the foreground. */ - public static void sendNewDownload( + public static void sendAddDownload( Context context, Class clazz, DownloadRequest downloadRequest, int stopReason, boolean foreground) { - Intent intent = buildAddRequestIntent(context, clazz, downloadRequest, stopReason, foreground); + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, stopReason, foreground); startService(context, intent, foreground); } @@ -412,7 +418,7 @@ public abstract class DownloadService extends Service { * @param stopReason An application defined stop reason. * @param foreground Whether the service is started in the foreground. */ - public static void sendStopReason( + public static void sendSetStopReason( Context context, Class clazz, @Nullable String id, @@ -488,7 +494,7 @@ public abstract class DownloadService extends Service { case ACTION_RESTART: // Do nothing. break; - case ACTION_ADD: + case ACTION_ADD_DOWNLOAD: DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { Log.e(TAG, "Ignored ADD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); @@ -497,10 +503,10 @@ public abstract class DownloadService extends Service { downloadManager.addDownload(downloadRequest, stopReason); } break; - case ACTION_RESUME: + case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; - case ACTION_PAUSE: + case ACTION_PAUSE_DOWNLOADS: downloadManager.pauseDownloads(); break; case ACTION_SET_STOP_REASON: @@ -512,7 +518,7 @@ public abstract class DownloadService extends Service { downloadManager.setStopReason(contentId, stopReason); } break; - case ACTION_REMOVE: + case ACTION_REMOVE_DOWNLOAD: String contentId = intent.getStringExtra(KEY_CONTENT_ID); if (contentId == null) { Log.e(TAG, "Ignored REMOVE: Missing " + KEY_CONTENT_ID + " extra"); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index c0da9251ec..1527c5a544 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -214,7 +214,7 @@ public class DownloadServiceDashTest { dummyMainThread.runOnMainThread( () -> { Intent startIntent = - DownloadService.buildAddRequestIntent( + DownloadService.buildAddDownloadIntent( context, DownloadService.class, action, /* foreground= */ false); dashDownloadService.onStartCommand(startIntent, 0, 0); }); From b01075f325ca48e6ad24928330610621989dd174 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 23 Apr 2019 09:18:03 +0100 Subject: [PATCH 30/95] Play out remaining data on reconfiguration Before this change we'd release the audio track and create a new one as soon as audio processors had drained when reconfiguring. Fix this behavior by stop()ing the AudioTrack to play out all written data. Issue: #2446 PiperOrigin-RevId: 244812402 --- RELEASENOTES.md | 2 + extensions/mediasession/build.gradle | 2 +- .../exoplayer2/audio/DefaultAudioSink.java | 57 +++++++++++-------- library/ui/build.gradle | 2 +- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c3ddb1d5e0..3282bb25d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,8 @@ * Decoders: prefer codecs that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. +* Audio: fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). ### 2.10.0 ### diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 186fdb1621..6c6ddf4ce4 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - api 'androidx.media:media:1.0.0' + api 'androidx.media:media:1.0.1' } ext { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ffcd893e7b..a90fc41df5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -272,6 +272,7 @@ public final class DefaultAudioSink implements AudioSink { private int preV21OutputBufferOffset; private int drainingAudioProcessorIndex; private boolean handledEndOfStream; + private boolean stoppedAudioTrack; private boolean playing; private int audioSessionId; @@ -465,19 +466,15 @@ public final class DefaultAudioSink implements AudioSink { processingEnabled, canApplyPlaybackParameters, availableAudioProcessors); - if (isInitialized()) { - if (!pendingConfiguration.canReuseAudioTrack(configuration)) { - // We need a new AudioTrack before we can handle more input. We should first stop() the - // track and wait for audio to play out (tracked by [Internal: b/33161961]), but for now we - // discard the audio track immediately. - flush(); - } else if (flushAudioProcessors) { - // We don't need a new AudioTrack but audio processors need to be drained and flushed. - this.pendingConfiguration = pendingConfiguration; - return; - } + // If we have a pending configuration already, we always drain audio processors as the preceding + // configuration may have required it (even if this one doesn't). + boolean drainAudioProcessors = flushAudioProcessors || this.pendingConfiguration != null; + if (isInitialized() + && (!pendingConfiguration.canReuseAudioTrack(configuration) || drainAudioProcessors)) { + this.pendingConfiguration = pendingConfiguration; + } else { + configuration = pendingConfiguration; } - configuration = pendingConfiguration; } private void setupAudioProcessors() { @@ -579,12 +576,21 @@ public final class DefaultAudioSink implements AudioSink { Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); if (pendingConfiguration != null) { - // We are waiting for audio processors to drain before applying a the new configuration. if (!drainAudioProcessorsToEndOfStream()) { + // There's still pending data in audio processors to write to the track. return false; + } else if (!pendingConfiguration.canReuseAudioTrack(configuration)) { + playPendingData(); + if (hasPendingData()) { + // We're waiting for playout on the current audio track to finish. + return false; + } + flush(); + } else { + // The current audio track can be reused for the new configuration. + configuration = pendingConfiguration; + pendingConfiguration = null; } - configuration = pendingConfiguration; - pendingConfiguration = null; playbackParameters = configuration.canApplyPlaybackParameters ? audioProcessorChain.applyPlaybackParameters(playbackParameters) @@ -786,15 +792,8 @@ public final class DefaultAudioSink implements AudioSink { @Override public void playToEndOfStream() throws WriteException { - if (handledEndOfStream || !isInitialized()) { - return; - } - - if (drainAudioProcessorsToEndOfStream()) { - // The audio processors have drained, so drain the underlying audio track. - audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); - audioTrack.stop(); - bytesUntilNextAvSync = 0; + if (!handledEndOfStream && isInitialized() && drainAudioProcessorsToEndOfStream()) { + playPendingData(); handledEndOfStream = true; } } @@ -976,6 +975,7 @@ public final class DefaultAudioSink implements AudioSink { flushAudioProcessors(); inputBuffer = null; outputBuffer = null; + stoppedAudioTrack = false; handledEndOfStream = false; drainingAudioProcessorIndex = C.INDEX_UNSET; avSyncHeader = null; @@ -1223,6 +1223,15 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.setStereoVolume(volume, volume); } + private void playPendingData() { + if (!stoppedAudioTrack) { + stoppedAudioTrack = true; + audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); + audioTrack.stop(); + bytesUntilNextAvSync = 0; + } + } + /** Stores playback parameters with the position and media time at which they apply. */ private static final class PlaybackParametersCheckpoint { diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 49446b25de..6384bf920f 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -40,7 +40,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.media:media:1.0.0' + implementation 'androidx.media:media:1.0.1' implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') From d89f3eeb29884f02bc91e7cbadf6327bbbcb7a74 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Apr 2019 09:58:52 +0100 Subject: [PATCH 31/95] Update dependency versions PiperOrigin-RevId: 244816212 --- extensions/cronet/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index ad45f61d98..76972a3530 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:72.3626.96' + api 'org.chromium.net:cronet-embedded:73.3683.76' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'library') From 9725132e3c09280c2532f2de3077e9cd6ba28b1d Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 11:00:55 +0100 Subject: [PATCH 32/95] Update default min duration for playbacks with video to match max duration. Experiments show this is beneficial for rebuffers with only minor impact on battery usage. Configurations which explicitly set a minimum buffer duration are unaffected. Issue:#2083 PiperOrigin-RevId: 244823642 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultLoadControl.java | 77 +++++++++++++++---- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3282bb25d7..f4bbe39ae8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -120,6 +120,8 @@ order when in shuffle mode. * Allow handling of custom commands via `registerCustomCommandReceiver`. * Add ability to include an extras `Bundle` when reporting a custom error. +* LoadControl: Set minimum buffer for playbacks with video equal to maximum + buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)). ### 2.9.6 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 83cb5b723c..972f651a41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -29,12 +29,14 @@ public class DefaultLoadControl implements LoadControl { /** * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. + * times, in milliseconds. This value is only applied to playbacks without video. */ public static final int DEFAULT_MIN_BUFFER_MS = 15000; /** * The default maximum duration of media that the player will attempt to buffer, in milliseconds. + * For playbacks with video, this is also the default minimum duration of media that the player + * will attempt to ensure is buffered. */ public static final int DEFAULT_MAX_BUFFER_MS = 50000; @@ -69,7 +71,8 @@ public class DefaultLoadControl implements LoadControl { public static final class Builder { private DefaultAllocator allocator; - private int minBufferMs; + private int minBufferAudioMs; + private int minBufferVideoMs; private int maxBufferMs; private int bufferForPlaybackMs; private int bufferForPlaybackAfterRebufferMs; @@ -81,7 +84,8 @@ public class DefaultLoadControl implements LoadControl { /** Constructs a new instance. */ public Builder() { - minBufferMs = DEFAULT_MIN_BUFFER_MS; + minBufferAudioMs = DEFAULT_MIN_BUFFER_MS; + minBufferVideoMs = DEFAULT_MAX_BUFFER_MS; maxBufferMs = DEFAULT_MAX_BUFFER_MS; bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; @@ -125,7 +129,18 @@ public class DefaultLoadControl implements LoadControl { int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs) { Assertions.checkState(!createDefaultLoadControlCalled); - this.minBufferMs = minBufferMs; + assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); + assertGreaterOrEqual( + bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); + assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferMs, + bufferForPlaybackAfterRebufferMs, + "minBufferMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); + this.minBufferAudioMs = minBufferMs; + this.minBufferVideoMs = minBufferMs; this.maxBufferMs = maxBufferMs; this.bufferForPlaybackMs = bufferForPlaybackMs; this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; @@ -173,6 +188,7 @@ public class DefaultLoadControl implements LoadControl { */ public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { Assertions.checkState(!createDefaultLoadControlCalled); + assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); this.backBufferDurationMs = backBufferDurationMs; this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; return this; @@ -187,7 +203,8 @@ public class DefaultLoadControl implements LoadControl { } return new DefaultLoadControl( allocator, - minBufferMs, + minBufferAudioMs, + minBufferVideoMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -200,7 +217,8 @@ public class DefaultLoadControl implements LoadControl { private final DefaultAllocator allocator; - private final long minBufferUs; + private final long minBufferAudioUs; + private final long minBufferVideoUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; @@ -211,6 +229,7 @@ public class DefaultLoadControl implements LoadControl { private int targetBufferSize; private boolean isBuffering; + private boolean hasVideo; /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ @SuppressWarnings("deprecation") @@ -220,16 +239,18 @@ public class DefaultLoadControl implements LoadControl { /** @deprecated Use {@link Builder} instead. */ @Deprecated - @SuppressWarnings("deprecation") public DefaultLoadControl(DefaultAllocator allocator) { this( allocator, - DEFAULT_MIN_BUFFER_MS, + /* minBufferAudioMs= */ DEFAULT_MIN_BUFFER_MS, + /* minBufferVideoMs= */ DEFAULT_MAX_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, DEFAULT_TARGET_BUFFER_BYTES, - DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); + DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS, + DEFAULT_BACK_BUFFER_DURATION_MS, + DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); } /** @deprecated Use {@link Builder} instead. */ @@ -244,7 +265,8 @@ public class DefaultLoadControl implements LoadControl { boolean prioritizeTimeOverSizeThresholds) { this( allocator, - minBufferMs, + /* minBufferAudioMs= */ minBufferMs, + /* minBufferVideoMs= */ minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -256,7 +278,8 @@ public class DefaultLoadControl implements LoadControl { protected DefaultLoadControl( DefaultAllocator allocator, - int minBufferMs, + int minBufferAudioMs, + int minBufferVideoMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, @@ -267,17 +290,27 @@ public class DefaultLoadControl implements LoadControl { assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); assertGreaterOrEqual( bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); - assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); assertGreaterOrEqual( - minBufferMs, + minBufferAudioMs, bufferForPlaybackMs, "minBufferAudioMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferVideoMs, bufferForPlaybackMs, "minBufferVideoMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferAudioMs, bufferForPlaybackAfterRebufferMs, - "minBufferMs", + "minBufferAudioMs", "bufferForPlaybackAfterRebufferMs"); - assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); + assertGreaterOrEqual( + minBufferVideoMs, + bufferForPlaybackAfterRebufferMs, + "minBufferVideoMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferAudioMs, "maxBufferMs", "minBufferAudioMs"); + assertGreaterOrEqual(maxBufferMs, minBufferVideoMs, "maxBufferMs", "minBufferVideoMs"); assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); this.allocator = allocator; - this.minBufferUs = C.msToUs(minBufferMs); + this.minBufferAudioUs = C.msToUs(minBufferAudioMs); + this.minBufferVideoUs = C.msToUs(minBufferVideoMs); this.maxBufferUs = C.msToUs(maxBufferMs); this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); @@ -295,6 +328,7 @@ public class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + hasVideo = hasVideo(renderers, trackSelections); targetBufferSize = targetBufferBytesOverwrite == C.LENGTH_UNSET ? calculateTargetBufferSize(renderers, trackSelections) @@ -330,7 +364,7 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; - long minBufferUs = this.minBufferUs; + long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; if (playbackSpeed > 1) { // The playback speed is faster than real time, so scale up the minimum required media // duration to keep enough media buffered for a playout duration of minBufferUs. @@ -384,6 +418,15 @@ public class DefaultLoadControl implements LoadControl { } } + private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + for (int i = 0; i < renderers.length; i++) { + if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) { + return true; + } + } + return false; + } + private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) { Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2); } From 3e9a45b9d360760cd82216e6cc072a3107b18a9a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Apr 2019 11:23:34 +0100 Subject: [PATCH 33/95] Fix DownloadManagerDashTest tests Also re-enable two of them, although note that the fix here is not related to the flakiness that caused them to be disabled. I'm re-enabling them since much has changed in DownloadManager, and the hope is that these tests are no longer flaky. PiperOrigin-RevId: 244826225 --- .../source/dash/offline/DownloadManagerDashTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 8cd6154373..56fedbefd0 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -190,8 +191,6 @@ public class DownloadManagerDashTest { assertCacheEmpty(cache); } - // Disabled due to flakiness. - @Ignore @Test public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { handleDownloadRequest(fakeStreamKey1); @@ -202,8 +201,6 @@ public class DownloadManagerDashTest { assertCacheEmpty(cache); } - // Disabled due to flakiness [Internal: b/122290449]. - @Ignore @Test public void testHandleInterferingRemoveAction() throws Throwable { final ConditionVariable downloadInProgressCondition = new ConditionVariable(); @@ -259,6 +256,7 @@ public class DownloadManagerDashTest { downloadIndex, new DefaultDownloaderFactory( new DownloaderConstructorHelper(cache, fakeDataSourceFactory))); + downloadManager.setRequirements(new Requirements(0)); downloadManagerListener = new TestDownloadManagerListener( From 9441f7244520e3e17ea8dce4f6372e108d3ecd03 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 13:35:58 +0100 Subject: [PATCH 34/95] Always update loading period in handleSourceInfoRefreshed. This ensures we keep the loading period in sync with the the playing period in PlybackInfo, when the latter changes to something new. PiperOrigin-RevId: 244838123 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a7ee6eb86e..37774bccb5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1321,7 +1321,6 @@ import java.util.concurrent.atomic.AtomicBoolean; if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } - handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } else { // Something changed. Seek to new start position. MediaPeriodHolder periodHolder = queue.getFrontPeriod(); @@ -1341,6 +1340,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo.copyWithNewPosition( newPeriodId, seekedToPositionUs, newContentPositionUs, getTotalBufferedDurationUs()); } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } private long getMaxRendererReadPositionUs() { From 3acd8a6048882902d835d5a6ea999a12ce585027 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 13:36:24 +0100 Subject: [PATCH 35/95] Remove unused PlaybackInfo.resetToNewPosition PiperOrigin-RevId: 244838165 --- .../android/exoplayer2/ExoPlayerImpl.java | 7 +++-- .../android/exoplayer2/PlaybackInfo.java | 30 +------------------ 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 15deb8ea47..96b9072c5c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -628,8 +628,11 @@ import java.util.concurrent.CopyOnWriteArrayList; if (playbackInfo.startPositionUs == C.TIME_UNSET) { // Replace internal unset start position with externally visible start position of zero. playbackInfo = - playbackInfo.resetToNewPosition( - playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs); + playbackInfo.copyWithNewPosition( + playbackInfo.periodId, + /* positionUs= */ 0, + playbackInfo.contentPositionUs, + playbackInfo.totalBufferedDurationUs); } if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) { // Update the masking variables, which are used when the timeline becomes empty. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 0792bf0c7d..f45e61fb37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -170,35 +170,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; } /** - * Copies playback info and resets playing and loading position. - * - * @param periodId New playing and loading {@link MediaPeriodId}. - * @param startPositionUs New start position. See {@link #startPositionUs}. - * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored - * if {@code periodId.isAd()} is true. - * @return Copied playback info with reset position. - */ - @CheckResult - public PlaybackInfo resetToNewPosition( - MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { - return new PlaybackInfo( - timeline, - manifest, - periodId, - startPositionUs, - periodId.isAd() ? contentPositionUs : C.TIME_UNSET, - playbackState, - isLoading, - trackGroups, - trackSelectorResult, - periodId, - startPositionUs, - /* totalBufferedDurationUs= */ 0, - startPositionUs); - } - - /** - * Copied playback info with new playing position. + * Copies playback info with new playing position. * * @param periodId New playing media period. See {@link #periodId}. * @param positionUs New position. See {@link #positionUs}. From 4507c6870deec9715802d31e433b2756bcaaae59 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 15:09:57 +0100 Subject: [PATCH 36/95] Move away from AndroidX bundled Truth to be closer to head revision. The AndroidX bundled version (0.42) lags behind the most up-to-date public release (0.44) making it more difficult to stay close to the actual head revision which is used internally. PiperOrigin-RevId: 244848568 --- constants.gradle | 1 + extensions/vp9/build.gradle | 2 +- library/core/build.gradle | 4 ++-- testutils/build.gradle | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/constants.gradle b/constants.gradle index fd61b6f08e..0e668d2464 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,6 +25,7 @@ project.ext { autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' androidXTestVersion = '1.1.0' + truthVersion = '0.44' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 02b68b831d..fe1f220af6 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -43,7 +43,7 @@ dependencies { testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion - androidTestImplementation 'androidx.test.ext:truth:' + androidXTestVersion + androidTestImplementation 'com.google.truth:truth:' + truthVersion } ext { diff --git a/library/core/build.gradle b/library/core/build.gradle index 68ff8cc977..f532ae0e6a 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -63,7 +63,7 @@ dependencies { compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion - androidTestImplementation 'androidx.test.ext:truth:' + androidXTestVersion + androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion @@ -71,7 +71,7 @@ dependencies { androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion testImplementation 'androidx.test:core:' + androidXTestVersion testImplementation 'androidx.test.ext:junit:' + androidXTestVersion - testImplementation 'androidx.test.ext:truth:' + androidXTestVersion + testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion diff --git a/testutils/build.gradle b/testutils/build.gradle index bdc26d5c19..36465f5d5f 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -40,7 +40,7 @@ android { dependencies { api 'org.mockito:mockito-core:' + mockitoVersion api 'androidx.test.ext:junit:' + androidXTestVersion - api 'androidx.test.ext:truth:' + androidXTestVersion + api 'com.google.truth:truth:' + truthVersion implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion From 06586b75b00c9e43065fbd171465bd46d7aa01c9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 15:50:30 +0100 Subject: [PATCH 37/95] Fix bug which logs errors twice if stack traces are disabled. Disabling stack trackes currently logs messages twice, once with and once without stack trace. PiperOrigin-RevId: 244853127 --- .../java/com/google/android/exoplayer2/util/Log.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java index 2c3e4f1e7c..1eb0977847 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java @@ -88,8 +88,7 @@ public final class Log { public static void d(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { d(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel == LOG_LEVEL_ALL) { + } else if (logLevel == LOG_LEVEL_ALL) { android.util.Log.d(tag, message, throwable); } } @@ -105,8 +104,7 @@ public final class Log { public static void i(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { i(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel <= LOG_LEVEL_INFO) { + } else if (logLevel <= LOG_LEVEL_INFO) { android.util.Log.i(tag, message, throwable); } } @@ -122,8 +120,7 @@ public final class Log { public static void w(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { w(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel <= LOG_LEVEL_WARNING) { + } else if (logLevel <= LOG_LEVEL_WARNING) { android.util.Log.w(tag, message, throwable); } } @@ -139,8 +136,7 @@ public final class Log { public static void e(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { e(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel <= LOG_LEVEL_ERROR) { + } else if (logLevel <= LOG_LEVEL_ERROR) { android.util.Log.e(tag, message, throwable); } } From 03313fb5e29a92dfa4a3c6de5ca88ca8459a8204 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 23 Apr 2019 17:09:05 +0100 Subject: [PATCH 38/95] Add option to add entries in an ActionFile to DownloadIndex as completed PiperOrigin-RevId: 244864742 --- .../exoplayer2/demo/DemoApplication.java | 12 +++-- .../offline/ActionFileUpgradeUtil.java | 14 +++-- .../offline/ActionFileUpgradeUtilTest.java | 51 +++++++++++++++++-- 3 files changed, 65 insertions(+), 12 deletions(-) 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 2c9cd43d1e..6985d42b36 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 @@ -115,8 +115,10 @@ public class DemoApplication extends Application { private synchronized void initDownloadManager() { if (downloadManager == null) { DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider()); - upgradeActionFile(DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex); - upgradeActionFile(DOWNLOAD_ACTION_FILE, downloadIndex); + upgradeActionFile( + DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); + upgradeActionFile( + DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true); DownloaderConstructorHelper downloaderConstructorHelper = new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = @@ -127,13 +129,15 @@ public class DemoApplication extends Application { } } - private void upgradeActionFile(String fileName, DefaultDownloadIndex downloadIndex) { + private void upgradeActionFile( + String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) { try { ActionFileUpgradeUtil.upgradeAndDelete( new File(getDownloadDirectory(), fileName), /* downloadIdProvider= */ null, downloadIndex, - /* deleteOnFailure= */ true); + /* deleteOnFailure= */ true, + addNewDownloadsAsCompleted); } catch (IOException e) { Log.e(TAG, "Failed to upgrade action file: " + fileName, e); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index b601874f8d..975fc10b93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -52,6 +52,7 @@ public final class ActionFileUpgradeUtil { * each download will be its custom cache key if one is specified, or else its URL. * @param downloadIndex The index into which the requests will be merged. * @param deleteOnFailure Whether to delete the action file if the merge fails. + * @param addNewDownloadsAsCompleted Whether to add new downloads as completed. * @throws IOException If an error occurs loading or merging the requests. */ @SuppressWarnings("deprecation") @@ -59,7 +60,8 @@ public final class ActionFileUpgradeUtil { File actionFilePath, @Nullable DownloadIdProvider downloadIdProvider, DefaultDownloadIndex downloadIndex, - boolean deleteOnFailure) + boolean deleteOnFailure, + boolean addNewDownloadsAsCompleted) throws IOException { ActionFile actionFile = new ActionFile(actionFilePath); if (actionFile.exists()) { @@ -69,7 +71,7 @@ public final class ActionFileUpgradeUtil { if (downloadIdProvider != null) { request = request.copyWithId(downloadIdProvider.getId(request)); } - mergeRequest(request, downloadIndex); + mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted); } success = true; } finally { @@ -85,10 +87,14 @@ public final class ActionFileUpgradeUtil { * * @param request The request to be merged. * @param downloadIndex The index into which the request will be merged. + * @param addNewDownloadAsCompleted Whether to add new downloads as completed. * @throws IOException If an error occurs merging the request. */ /* package */ static void mergeRequest( - DownloadRequest request, DefaultDownloadIndex downloadIndex) throws IOException { + DownloadRequest request, + DefaultDownloadIndex downloadIndex, + boolean addNewDownloadAsCompleted) + throws IOException { Download download = downloadIndex.getDownload(request.id); if (download != null) { download = DownloadManager.mergeRequest(download, request, download.stopReason); @@ -97,7 +103,7 @@ public final class ActionFileUpgradeUtil { download = new Download( request, - STATE_QUEUED, + addNewDownloadAsCompleted ? Download.STATE_COMPLETED : STATE_QUEUED, /* startTimeMs= */ nowMs, /* updateTimeMs= */ nowMs, /* contentLength= */ C.LENGTH_UNSET, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java index 96b8ff21bc..dba7b74e9f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java @@ -88,7 +88,11 @@ public class ActionFileUpgradeUtilTest { new byte[] {5, 4, 3, 2, 1}); ActionFileUpgradeUtil.upgradeAndDelete( - tempFile, /* downloadIdProvider= */ null, downloadIndex, /* deleteOnFailure= */ true); + tempFile, + /* downloadIdProvider= */ null, + downloadIndex, + /* deleteOnFailure= */ true, + /* addNewDownloadsAsCompleted= */ false); assertDownloadIndexContainsRequest(expectedRequest1, Download.STATE_QUEUED); assertDownloadIndexContainsRequest(expectedRequest2, Download.STATE_QUEUED); @@ -108,7 +112,8 @@ public class ActionFileUpgradeUtilTest { /* customCacheKey= */ "key123", data); - ActionFileUpgradeUtil.mergeRequest(request, downloadIndex); + ActionFileUpgradeUtil.mergeRequest( + request, downloadIndex, /* addNewDownloadAsCompleted= */ false); assertDownloadIndexContainsRequest(request, Download.STATE_QUEUED); } @@ -135,8 +140,10 @@ public class ActionFileUpgradeUtilTest { asList(streamKey2), /* customCacheKey= */ "key123", new byte[] {5, 4, 3, 2, 1}); - ActionFileUpgradeUtil.mergeRequest(request1, downloadIndex); - ActionFileUpgradeUtil.mergeRequest(request2, downloadIndex); + ActionFileUpgradeUtil.mergeRequest( + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + ActionFileUpgradeUtil.mergeRequest( + request2, downloadIndex, /* addNewDownloadAsCompleted= */ false); Download download = downloadIndex.getDownload(request2.id); assertThat(download).isNotNull(); @@ -148,6 +155,42 @@ public class ActionFileUpgradeUtilTest { assertThat(download.state).isEqualTo(Download.STATE_QUEUED); } + @Test + public void mergeRequest_addNewDownloadAsCompleted() throws IOException { + StreamKey streamKey1 = + new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5); + StreamKey streamKey2 = + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2); + DownloadRequest request1 = + new DownloadRequest( + "id1", + TYPE_PROGRESSIVE, + Uri.parse("https://www.test.com/download1"), + asList(streamKey1), + /* customCacheKey= */ "key123", + new byte[] {1, 2, 3, 4}); + DownloadRequest request2 = + new DownloadRequest( + "id2", + TYPE_PROGRESSIVE, + Uri.parse("https://www.test.com/download2"), + asList(streamKey2), + /* customCacheKey= */ "key123", + new byte[] {5, 4, 3, 2, 1}); + ActionFileUpgradeUtil.mergeRequest( + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + + // Merging existing download, keeps it queued. + ActionFileUpgradeUtil.mergeRequest( + request1, downloadIndex, /* addNewDownloadAsCompleted= */ true); + assertThat(downloadIndex.getDownload(request1.id).state).isEqualTo(Download.STATE_QUEUED); + + // New download is merged as completed. + ActionFileUpgradeUtil.mergeRequest( + request2, downloadIndex, /* addNewDownloadAsCompleted= */ true); + assertThat(downloadIndex.getDownload(request2.id).state).isEqualTo(Download.STATE_COMPLETED); + } + private void assertDownloadIndexContainsRequest(DownloadRequest request, int state) throws IOException { Download download = downloadIndex.getDownload(request.id); From ba94f9dc0195c8698a39c04ac75d966b5fbc5b67 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Apr 2019 19:03:05 +0100 Subject: [PATCH 39/95] Migrate from isNotSameAs to isNotSameInstanceAs. The two behave identically, and isNotSameAs is being removed. More information: go/issameas-lsc Tested: TAP --sample for global presubmit queue http://test/OCL:244736857:BASE:244751659:1555988098671:1e0f72c5 PiperOrigin-RevId: 244886651 --- .../android/exoplayer2/text/ttml/TtmlRenderUtilTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java index 40fd6f288a..747cbd0c7b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java @@ -54,8 +54,8 @@ public final class TtmlRenderUtilTest { String[] styleIds = {"s0", "s1"}; TtmlStyle resolved = TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles); - assertThat(resolved).isNotSameAs(globalStyles.get("s0")); - assertThat(resolved).isNotSameAs(globalStyles.get("s1")); + assertThat(resolved).isNotSameInstanceAs(globalStyles.get("s0")); + assertThat(resolved).isNotSameInstanceAs(globalStyles.get("s1")); assertThat(resolved.getId()).isNull(); // inherited from s0 From d60b6d64abc6ae6a0900c1cdd1f98920f2a25cb6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Apr 2019 19:03:22 +0100 Subject: [PATCH 40/95] Migrate from containsAllOf to containsAtLeast. The two behave identically, and containsAllOf is being removed. More information: go/containsall-lsc Tested: TAP --sample for global presubmit queue http://test/OCL:244737393:BASE:244782138:1555991083653:3080d7c7 PiperOrigin-RevId: 244886736 --- .../java/com/google/android/exoplayer2/ExoPlayerTest.java | 2 +- .../android/exoplayer2/analytics/AnalyticsCollectorTest.java | 2 +- .../com/google/android/exoplayer2/drm/DrmInitDataTest.java | 2 +- .../exoplayer2/source/ConcatenatingMediaSourceTest.java | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 4172a61271..6d0627593f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -2092,7 +2092,7 @@ public final class ExoPlayerTest { testRunner.assertPlayedPeriodIndices(0, 1, 0, 1); assertThat(mediaSource.getCreatedMediaPeriods()) - .containsAllOf( + .containsAtLeast( new MediaPeriodId( timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0), new MediaPeriodId( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index d175a5eab3..c455e39de6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -325,7 +325,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); List loadingEvents = listener.getEvents(EVENT_LOADING_CHANGED); assertThat(loadingEvents).hasSize(4); - assertThat(loadingEvents).containsAllOf(period0, period0); + assertThat(loadingEvents).containsAtLeast(period0, period0); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index 36287d0579..e3d7cdbbf4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -124,7 +124,7 @@ public class DrmInitDataTest { @Test public void testGetByIndex() { DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); - assertThat(getAllSchemeData(testInitData)).containsAllOf(DATA_1, DATA_2); + assertThat(getAllSchemeData(testInitData)).containsAtLeast(DATA_1, DATA_2); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 17cdd9e7ae..8137289555 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -827,7 +827,7 @@ public final class ConcatenatingMediaSourceTest { Object childPeriodUid0 = childTimeline.getUidOfPeriod(/* periodIndex= */ 0); Object childPeriodUid1 = childTimeline.getUidOfPeriod(/* periodIndex= */ 1); assertThat(childSource.getCreatedMediaPeriods()) - .containsAllOf( + .containsAtLeast( new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 0), new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 2), new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 4), @@ -863,7 +863,7 @@ public final class ConcatenatingMediaSourceTest { testRunner.assertPrepareAndReleaseAllPeriods(); Object childPeriodUid = childTimeline.getUidOfPeriod(/* periodIndex= */ 0); assertThat(childSource.getCreatedMediaPeriods()) - .containsAllOf( + .containsAtLeast( new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 0), new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 1), new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 2), From 0ff47a8c0df183d8206d4f7bc98393a8f06a2aff Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 24 Apr 2019 11:08:00 +0100 Subject: [PATCH 41/95] Add DownloadService SET_REQUIREMENTS action PiperOrigin-RevId: 245014381 --- .../exoplayer2/offline/DownloadManager.java | 4 +- .../exoplayer2/offline/DownloadService.java | 103 ++++++++++++++---- .../exoplayer2/scheduler/Requirements.java | 34 +++++- 3 files changed, 113 insertions(+), 28 deletions(-) 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 d4df5cd18b..74332c08f3 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 @@ -268,9 +268,9 @@ public final class DownloadManager { } /** - * Sets the requirements needed to be met to start downloads. + * Sets the requirements that need to be met for downloads to progress. * - * @param requirements Need to be met to start downloads. + * @param requirements A {@link Requirements}. */ public void setRequirements(Requirements requirements) { if (requirements.equals(requirementsWatcher.getRequirements())) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index ea79204c46..ee00cf3d5f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -66,6 +66,17 @@ public abstract class DownloadService extends Service { public static final String ACTION_ADD_DOWNLOAD = "com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD"; + /** + * Removes a download. Extras: + * + *
      + *
    • {@link #KEY_CONTENT_ID} - The content id of a download to remove. + *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
    + */ + public static final String ACTION_REMOVE_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * @@ -91,10 +102,10 @@ public abstract class DownloadService extends Service { * Download#STOP_REASON_NONE}. Extras: * *
      - *
    • {@link #KEY_CONTENT_ID} - The content id of a single download to update with the manual - * stop reason. If omitted, all downloads will be updated. + *
    • {@link #KEY_CONTENT_ID} - The content id of a single download to update with the stop + * reason. If omitted, all downloads will be updated. *
    • {@link #KEY_STOP_REASON} - An application provided reason for stopping the download or - * downloads, or {@link Download#STOP_REASON_NONE} to clear the manual stop reason. + * downloads, or {@link Download#STOP_REASON_NONE} to clear the stop reason. *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
    */ @@ -102,15 +113,15 @@ public abstract class DownloadService extends Service { "com.google.android.exoplayer.downloadService.action.SET_STOP_REASON"; /** - * Removes a download. Extras: + * Sets the requirements that need to be met for downloads to progress. Extras: * *
      - *
    • {@link #KEY_CONTENT_ID} - The content id of a download to remove. + *
    • {@link #KEY_REQUIREMENTS} - A {@link Requirements}. *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
    */ - public static final String ACTION_REMOVE_DOWNLOAD = - "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + public static final String ACTION_SET_REQUIREMENTS = + "com.google.android.exoplayer.downloadService.action.SET_REQUIREMENTS"; /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD_DOWNLOAD} intents. */ public static final String KEY_DOWNLOAD_REQUEST = "download_request"; @@ -125,7 +136,10 @@ public abstract class DownloadService extends Service { * Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD_DOWNLOAD} * intents. */ - public static final String KEY_STOP_REASON = "manual_stop_reason"; + public static final String KEY_STOP_REASON = "stop_reason"; + + /** Key for the requirements in {@link #ACTION_SET_REQUIREMENTS} intents. */ + public static final String KEY_REQUIREMENTS = "requirements"; /** * Key for a boolean extra that can be set on any intent to indicate whether the service was @@ -236,7 +250,7 @@ public abstract class DownloadService extends Service { * @param clazz The concrete download service being targeted by the intent. * @param downloadRequest The request to be executed. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildAddDownloadIntent( Context context, @@ -255,7 +269,7 @@ public abstract class DownloadService extends Service { * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} * if the download should be started. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildAddDownloadIntent( Context context, @@ -275,7 +289,7 @@ public abstract class DownloadService extends Service { * @param clazz The concrete download service being targeted by the intent. * @param id The content id. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildRemoveDownloadIntent( Context context, Class clazz, String id, boolean foreground) { @@ -289,7 +303,7 @@ public abstract class DownloadService extends Service { * @param context A {@link Context}. * @param clazz The concrete download service being targeted by the intent. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildResumeDownloadsIntent( Context context, Class clazz, boolean foreground) { @@ -302,7 +316,7 @@ public abstract class DownloadService extends Service { * @param context A {@link Context}. * @param clazz The concrete download service being targeted by the intent. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildPauseDownloadsIntent( Context context, Class clazz, boolean foreground) { @@ -318,7 +332,7 @@ public abstract class DownloadService extends Service { * @param id The content id, or {@code null} to set the stop reason for all downloads. * @param stopReason An application defined stop reason. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildSetStopReasonIntent( Context context, @@ -331,6 +345,25 @@ public abstract class DownloadService extends Service { .putExtra(KEY_STOP_REASON, stopReason); } + /** + * Builds an {@link Intent} for setting the requirements that need to be met for downloads to + * progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param requirements A {@link Requirements}. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildSetRequirementsIntent( + Context context, + Class clazz, + Requirements requirements, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_REQUIREMENTS, foreground) + .putExtra(KEY_REQUIREMENTS, requirements); + } + /** * Starts the service if not started already and adds a new download. * @@ -428,6 +461,24 @@ public abstract class DownloadService extends Service { startService(context, intent, foreground); } + /** + * Starts the service if not started already and sets the requirements that need to be met for + * downloads to progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param requirements A {@link Requirements}. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendSetRequirements( + Context context, + Class clazz, + Requirements requirements, + boolean foreground) { + Intent intent = buildSetRequirementsIntent(context, clazz, requirements, foreground); + startService(context, intent, foreground); + } + /** * Starts a download service to resume any ongoing downloads. * @@ -479,10 +530,12 @@ public abstract class DownloadService extends Service { lastStartId = startId; taskRemoved = false; String intentAction = null; + String contentId = null; if (intent != null) { intentAction = intent.getAction(); startedInForeground |= intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); + contentId = intent.getStringExtra(KEY_CONTENT_ID); } // intentAction is null if the service is restarted or no action is specified. if (intentAction == null) { @@ -497,12 +550,19 @@ public abstract class DownloadService extends Service { case ACTION_ADD_DOWNLOAD: DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { - Log.e(TAG, "Ignored ADD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); + Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); downloadManager.addDownload(downloadRequest, stopReason); } break; + case ACTION_REMOVE_DOWNLOAD: + if (contentId == null) { + Log.e(TAG, "Ignored REMOVE_DOWNLOAD: Missing " + KEY_CONTENT_ID + " extra"); + } else { + downloadManager.removeDownload(contentId); + } + break; case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; @@ -511,19 +571,18 @@ public abstract class DownloadService extends Service { break; case ACTION_SET_STOP_REASON: if (!intent.hasExtra(KEY_STOP_REASON)) { - Log.e(TAG, "Ignored SET_MANUAL_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); + Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { - String contentId = intent.getStringExtra(KEY_CONTENT_ID); int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); downloadManager.setStopReason(contentId, stopReason); } break; - case ACTION_REMOVE_DOWNLOAD: - String contentId = intent.getStringExtra(KEY_CONTENT_ID); - if (contentId == null) { - Log.e(TAG, "Ignored REMOVE: Missing " + KEY_CONTENT_ID + " extra"); + case ACTION_SET_REQUIREMENTS: + Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + if (requirements == null) { + Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { - downloadManager.removeDownload(contentId); + downloadManager.setRequirements(requirements); } break; default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 28aa37ee2a..babc4e49fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -23,6 +23,8 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.os.BatteryManager; +import android.os.Parcel; +import android.os.Parcelable; import android.os.PowerManager; import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.Log; @@ -31,10 +33,8 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Defines a set of device state requirements. - */ -public final class Requirements { +/** Defines a set of device state requirements. */ +public final class Requirements implements Parcelable { /** * Requirement flags. Possible flag values are {@link #NETWORK}, {@link #NETWORK_UNMETERED}, @@ -205,4 +205,30 @@ public final class Requirements { public int hashCode() { return requirements; } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(requirements); + } + + public static final Parcelable.Creator CREATOR = + new Creator() { + + @Override + public Requirements createFromParcel(Parcel in) { + return new Requirements(in.readInt()); + } + + @Override + public Requirements[] newArray(int size) { + return new Requirements[size]; + } + }; } From 009d6296503965143959687a770528338a3b8885 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 25 Apr 2019 12:11:17 +0100 Subject: [PATCH 42/95] Check codec profiles/levels for VP9 Also add some unit tests for codecs strings parsing. PiperOrigin-RevId: 245210490 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 56 ++++++++++++- .../mediacodec/MediaCodecUtilTest.java | 80 +++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 1c3bdd95a5..4a50579cc4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -68,6 +68,10 @@ public final class MediaCodecUtil { private static final SparseIntArray AVC_LEVEL_NUMBER_TO_CONST; private static final String CODEC_ID_AVC1 = "avc1"; private static final String CODEC_ID_AVC2 = "avc2"; + // VP9 + private static final SparseIntArray VP9_PROFILE_NUMBER_TO_CONST; + private static final SparseIntArray VP9_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_VP09 = "vp09"; // HEVC. private static final Map HEVC_CODEC_STRING_TO_PROFILE_LEVEL; private static final String CODEC_ID_HEV1 = "hev1"; @@ -80,7 +84,7 @@ public final class MediaCodecUtil { // MP4A AAC. private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; private static final String CODEC_ID_MP4A = "mp4a"; - + // Lazily initialized. private static int maxH264DecodableFrameSize = -1; @@ -242,6 +246,8 @@ public final class MediaCodecUtil { case CODEC_ID_AVC1: case CODEC_ID_AVC2: return getAvcProfileAndLevel(codec, parts); + case CODEC_ID_VP09: + return getVp9ProfileAndLevel(codec, parts); case CODEC_ID_HEV1: case CODEC_ID_HVC1: return getHevcProfileAndLevel(codec, parts); @@ -640,6 +646,34 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } + private static Pair getVp9ProfileAndLevel(String codec, String[] parts) { + if (parts.length < 3) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + + int profile = VP9_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { + Log.w(TAG, "Unknown VP9 profile: " + profileInteger); + return null; + } + int level = VP9_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown VP9 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + /** * Conversion values taken from ISO 14496-10 Table A-1. * @@ -895,6 +929,26 @@ public final class MediaCodecUtil { AVC_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.AVCLevel51); AVC_LEVEL_NUMBER_TO_CONST.put(52, CodecProfileLevel.AVCLevel52); + VP9_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); + VP9_PROFILE_NUMBER_TO_CONST.put(0, CodecProfileLevel.VP9Profile0); + VP9_PROFILE_NUMBER_TO_CONST.put(1, CodecProfileLevel.VP9Profile1); + VP9_PROFILE_NUMBER_TO_CONST.put(2, CodecProfileLevel.VP9Profile2); + VP9_PROFILE_NUMBER_TO_CONST.put(3, CodecProfileLevel.VP9Profile3); + VP9_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + VP9_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.VP9Level1); + VP9_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.VP9Level11); + VP9_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.VP9Level2); + VP9_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.VP9Level21); + VP9_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.VP9Level3); + VP9_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.VP9Level31); + VP9_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.VP9Level4); + VP9_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.VP9Level41); + VP9_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.VP9Level5); + VP9_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.VP9Level51); + VP9_LEVEL_NUMBER_TO_CONST.put(60, CodecProfileLevel.VP9Level6); + VP9_LEVEL_NUMBER_TO_CONST.put(61, CodecProfileLevel.VP9Level61); + VP9_LEVEL_NUMBER_TO_CONST.put(62, CodecProfileLevel.VP9Level62); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL = new HashMap<>(); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L30", CodecProfileLevel.HEVCMainTierLevel1); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L60", CodecProfileLevel.HEVCMainTierLevel2); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java new file mode 100644 index 0000000000..a84c6f5d7b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java @@ -0,0 +1,80 @@ +/* + * 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.google.android.exoplayer2.mediacodec; + +import static com.google.common.truth.Truth.assertThat; + +import android.media.MediaCodecInfo; +import android.util.Pair; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link MediaCodecUtil}. */ +@RunWith(AndroidJUnit4.class) +public final class MediaCodecUtilTest { + + @Test + public void getCodecProfileAndLevel_handlesVp9Profile1CodecString() { + assertCodecProfileAndLevelForCodecsString( + "vp09.01.51", + MediaCodecInfo.CodecProfileLevel.VP9Profile1, + MediaCodecInfo.CodecProfileLevel.VP9Level51); + } + + @Test + public void getCodecProfileAndLevel_handlesVp9Profile2CodecString() { + assertCodecProfileAndLevelForCodecsString( + "vp09.02.10", + MediaCodecInfo.CodecProfileLevel.VP9Profile2, + MediaCodecInfo.CodecProfileLevel.VP9Level1); + } + + @Test + public void getCodecProfileAndLevel_handlesFullVp9CodecString() { + // Example from https://www.webmproject.org/vp9/mp4/#codecs-parameter-string. + assertCodecProfileAndLevelForCodecsString( + "vp09.02.10.10.01.09.16.09.01", + MediaCodecInfo.CodecProfileLevel.VP9Profile2, + MediaCodecInfo.CodecProfileLevel.VP9Level1); + } + + @Test + public void getCodecProfileAndLevel_handlesDolbyVisionCodecString() { + assertCodecProfileAndLevelForCodecsString( + "dvh1.05.05", + MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheStn, + MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60); + } + + @Test + public void getCodecProfileAndLevel_rejectsNullCodecString() { + assertThat(MediaCodecUtil.getCodecProfileAndLevel(/* codec= */ null)).isNull(); + } + + @Test + public void getCodecProfileAndLevel_rejectsEmptyCodecString() { + assertThat(MediaCodecUtil.getCodecProfileAndLevel("")).isNull(); + } + + private static void assertCodecProfileAndLevelForCodecsString( + String codecs, int profile, int level) { + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codecs); + assertThat(codecProfileAndLevel).isNotNull(); + assertThat(codecProfileAndLevel.first).isEqualTo(profile); + assertThat(codecProfileAndLevel.second).isEqualTo(level); + } +} From b4420f61d1c22b61d0c6d2fb5396b57b207e1919 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 25 Apr 2019 13:32:07 +0100 Subject: [PATCH 43/95] Update gradle plugin. This also removes the build warning about the experimental flag. PiperOrigin-RevId: 245218251 --- gradle.properties | 1 - gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 364a5d03c5..4b9bfa8fa2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,5 @@ ## Project-wide Gradle settings. android.useAndroidX=true android.enableJetifier=true -android.useDeprecatedNdk=true android.enableUnitTestBinaryResources=true buildDir=buildout diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7061ab9fe7..6d00e1ce97 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Feb 08 20:49:20 GMT 2019 +#Thu Apr 25 13:15:25 BST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip From 84e03aea7064f82e1220c094382a0410a3e3b348 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 25 Apr 2019 13:38:54 +0100 Subject: [PATCH 44/95] Update gradle plugin (part 2). PiperOrigin-RevId: 245218900 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f8326dd503..a0e8fcf20a 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.1' + classpath 'com.android.tools.build:gradle:3.4.0' classpath 'com.novoda:bintray-release:0.9' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } From 21b2a471bb1580ef5e0d1ae5584a25dc8ca9d683 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 25 Apr 2019 13:48:36 +0100 Subject: [PATCH 45/95] Toggle playback controls according to standard Android click handling. We currently toggle the view in onTouchEvent ACTION_DOWN which is non-standard and causes problems when used in a ViewGroup intercepting touch events. Switch to standard Android click handling instead which is also what most other player apps are doing. Issue:#5784 PiperOrigin-RevId: 245219728 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ui/PlayerView.java | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f4bbe39ae8..33bfa44ec7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ even if they are listed lower in the `MediaCodecList`. * Audio: fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). +* UI: Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). ### 2.10.0 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 93461c1b24..06f0927a99 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -303,6 +303,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; private int textureViewRotation; + private boolean isTouching; public PlayerView(Context context) { this(context, null); @@ -1039,11 +1040,21 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } @Override - public boolean onTouchEvent(MotionEvent ev) { - if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { - return false; + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + isTouching = true; + return true; + case MotionEvent.ACTION_UP: + if (isTouching) { + isTouching = false; + performClick(); + return true; + } + return false; + default: + return false; } - return performClick(); } @Override From 708cad6b28ed66c02826fb1d3ded8974591ecf7e Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 25 Apr 2019 16:52:35 +0100 Subject: [PATCH 46/95] Add DownloadHelper.createMediaSource utility method PiperOrigin-RevId: 245243488 --- .../exoplayer2/demo/DownloadTracker.java | 9 +- .../exoplayer2/demo/PlayerActivity.java | 29 ++-- library/core/proguard-rules.txt | 9 +- .../exoplayer2/offline/DownloadHelper.java | 126 +++++++++++------- 4 files changed, 98 insertions(+), 75 deletions(-) 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 f372a47df6..a913a9b891 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 @@ -30,15 +30,12 @@ import com.google.android.exoplayer2.offline.DownloadIndex; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** Tracks media that has been downloaded. */ @@ -86,11 +83,9 @@ public class DownloadTracker { } @SuppressWarnings("unchecked") - public List getOfflineStreamKeys(Uri uri) { + public DownloadRequest getDownloadRequest(Uri uri) { Download download = downloads.get(uri); - return download != null && download.state != Download.STATE_FAILED - ? download.request.streamKeys - : Collections.emptyList(); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; } public void toggleDownload( 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 acb24adebe..35307eb5d8 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 @@ -45,7 +45,8 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -75,7 +76,6 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ @@ -457,33 +457,26 @@ public class PlayerActivity extends AppCompatActivity } private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + DownloadRequest downloadRequest = + ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); + if (downloadRequest != null) { + return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); + } @ContentType int type = Util.inferContentType(uri, overrideExtension); - List offlineStreamKeys = getOfflineStreamKeys(uri); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: { + default: throw new IllegalStateException("Unsupported type: " + type); - } } } - 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/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 07ba438182..8c11810506 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -46,18 +46,21 @@ # Constructors accessed via reflection in DownloadHelper -dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index c9b0451f41..2742e85a5f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import androidx.annotation.Nullable; -import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -32,6 +31,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -106,30 +107,13 @@ public final class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } - @Nullable private static final Constructor DASH_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor HLS_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor SS_FACTORY_CONSTRUCTOR; - @Nullable private static final Method DASH_FACTORY_CREATE_METHOD; - @Nullable private static final Method HLS_FACTORY_CREATE_METHOD; - @Nullable private static final Method SS_FACTORY_CREATE_METHOD; - - static { - Pair<@NullableType Constructor, @NullableType Method> dashFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); - DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first; - DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> hlsFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); - HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first; - HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> ssFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); - SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first; - SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second; - } + private static final MediaSourceFactory DASH_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + private static final MediaSourceFactory SS_FACTORY = + getMediaSourceFactory( + "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + private static final MediaSourceFactory HLS_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); /** * Creates a {@link DownloadHelper} for progressive streams. @@ -202,8 +186,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_DASH, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD), + DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -252,8 +235,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_HLS, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD), + HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -302,11 +284,42 @@ public final class DownloadHelper { DownloadRequest.TYPE_SS, uri, /* cacheKey= */ null, - createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD), + SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** + * Utility method to create a MediaSource which only contains the tracks defined in {@code + * downloadRequest}. + * + * @param downloadRequest A {@link DownloadRequest}. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + MediaSourceFactory factory; + switch (downloadRequest.type) { + case DownloadRequest.TYPE_DASH: + factory = DASH_FACTORY; + break; + case DownloadRequest.TYPE_SS: + factory = SS_FACTORY; + break; + case DownloadRequest.TYPE_HLS: + factory = HLS_FACTORY; + break; + case DownloadRequest.TYPE_PROGRESSIVE: + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(downloadRequest.uri); + default: + throw new IllegalStateException("Unsupported type: " + downloadRequest.type); + } + return factory.createMediaSource( + downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); + } + private final String downloadType; private final Uri uri; @Nullable private final String cacheKey; @@ -728,35 +741,54 @@ public final class DownloadHelper { } } - private static Pair<@NullableType Constructor, @NullableType Method> - getMediaSourceFactoryMethods(String className) { + private static MediaSourceFactory getMediaSourceFactory(String className) { Constructor constructor = null; + Method setStreamKeysMethod = null; Method createMethod = null; try { // LINT.IfChange Class factoryClazz = Class.forName(className); - constructor = factoryClazz.getConstructor(DataSource.Factory.class); + constructor = factoryClazz.getConstructor(Factory.class); + setStreamKeysMethod = factoryClazz.getMethod("setStreamKeys", List.class); createMethod = factoryClazz.getMethod("createMediaSource", Uri.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - } catch (Exception e) { + } catch (ClassNotFoundException e) { // Expected if the app was built without the respective module. + } catch (NoSuchMethodException | SecurityException e) { + // Something is wrong with the library or the proguard configuration. + throw new IllegalStateException(e); } - return Pair.create(constructor, createMethod); + return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod); } - private static MediaSource createMediaSource( - Uri uri, - DataSource.Factory dataSourceFactory, - @Nullable Constructor factoryConstructor, - @Nullable Method createMediaSourceMethod) { - if (factoryConstructor == null || createMediaSourceMethod == null) { - throw new IllegalStateException("Module missing to create media source."); + private static final class MediaSourceFactory { + @Nullable private final Constructor constructor; + @Nullable private final Method setStreamKeysMethod; + @Nullable private final Method createMethod; + + public MediaSourceFactory( + @Nullable Constructor constructor, + @Nullable Method setStreamKeysMethod, + @Nullable Method createMethod) { + this.constructor = constructor; + this.setStreamKeysMethod = setStreamKeysMethod; + this.createMethod = createMethod; } - try { - Object factory = factoryConstructor.newInstance(dataSourceFactory); - return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri)); - } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate media source.", e); + + private MediaSource createMediaSource( + Uri uri, Factory dataSourceFactory, @Nullable List streamKeys) { + if (constructor == null || setStreamKeysMethod == null || createMethod == null) { + throw new IllegalStateException("Module missing to create media source."); + } + try { + Object factory = constructor.newInstance(dataSourceFactory); + if (streamKeys != null) { + setStreamKeysMethod.invoke(factory, streamKeys); + } + return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); + } } } From 3382c74488196f5b5977dfa545f142d6ac30f120 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Apr 2019 10:25:21 +0100 Subject: [PATCH 47/95] Allow content id to be set in DownloadHelper.getDownloadRequest PiperOrigin-RevId: 245388082 --- .../exoplayer2/offline/DownloadHelper.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 2742e85a5f..755f7e0343 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -591,16 +591,27 @@ public final class DownloadHelper { /** * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until - * after preparation completes. + * after preparation completes. The uri of the {@link DownloadRequest} will be used as content id. * * @param data Application provided data to store in {@link DownloadRequest#data}. * @return The built {@link DownloadRequest}. */ public DownloadRequest getDownloadRequest(@Nullable byte[] data) { - String downloadId = uri.toString(); + return getDownloadRequest(uri.toString(), data); + } + + /** + * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until + * after preparation completes. + * + * @param id The unique content id. + * @param data Application provided data to store in {@link DownloadRequest#data}. + * @return The built {@link DownloadRequest}. + */ + public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) { if (mediaSource == null) { return new DownloadRequest( - downloadId, downloadType, uri, /* streamKeys= */ Collections.emptyList(), cacheKey, data); + id, downloadType, uri, /* streamKeys= */ Collections.emptyList(), cacheKey, data); } assertPreparedWithMedia(); List streamKeys = new ArrayList<>(); @@ -614,7 +625,7 @@ public final class DownloadHelper { } streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); } - return new DownloadRequest(downloadId, downloadType, uri, streamKeys, cacheKey, data); + return new DownloadRequest(id, downloadType, uri, streamKeys, cacheKey, data); } // Initialization of array of Lists. From 01ad1c1a84ab8761736705a2c16940146daeb133 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Apr 2019 12:05:09 +0100 Subject: [PATCH 48/95] Add simpler DownloadManager constructor PiperOrigin-RevId: 245397736 --- .../exoplayer2/offline/DownloadManager.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 74332c08f3..bfcb5174cc 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 @@ -34,8 +34,14 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheEvictor; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -183,6 +189,24 @@ public final class DownloadManager { private volatile int maxParallelDownloads; private volatile int minRetryCount; + /** + * Constructs a {@link DownloadManager}. + * + * @param context Any context. + * @param databaseProvider Provides the SQLite database in which downloads are persisted. + * @param cache A cache to be used to store downloaded data. The cache should be configured with + * an {@link CacheEvictor} that will not evict downloaded content, for example {@link + * NoOpCacheEvictor}. + * @param upstreamFactory A {@link Factory} for creating {@link DataSource}s for downloading data. + */ + public DownloadManager( + Context context, DatabaseProvider databaseProvider, Cache cache, Factory upstreamFactory) { + this( + context, + new DefaultDownloadIndex(databaseProvider), + new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); + } + /** * Constructs a {@link DownloadManager}. * From 643e187d991ef7a0b688fcd7a4cdd8801b00032c Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 12:46:48 +0100 Subject: [PATCH 49/95] Add missing getters and clarify STATE_QUEUED documentation PiperOrigin-RevId: 245401274 --- .../android/exoplayer2/offline/Download.java | 12 +++- .../exoplayer2/offline/DownloadManager.java | 64 +++++++++++++++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index 9f6b473208..00d81b392c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -43,7 +43,17 @@ public final class Download { }) public @interface State {} // Important: These constants are persisted into DownloadIndex. Do not change them. - /** The download is waiting to be started. */ + /** + * The download is waiting to be started. A download may be queued because the {@link + * DownloadManager} + * + *
      + *
    • Is {@link DownloadManager#getDownloadsPaused() paused} + *
    • Has {@link DownloadManager#getRequirements() Requirements} that are not met + *
    • Has already started {@link DownloadManager#getMaxParallelDownloads() + * maxParallelDownloads} + *
    + */ public static final int STATE_QUEUED = 0; /** The download is stopped for a specified {@link #stopReason}. */ public static final int STATE_STOPPED = 1; 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 bfcb5174cc..0ca13e2385 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 @@ -130,7 +130,7 @@ public final class DownloadManager { // Messages posted to the background handler. private static final int MSG_INITIALIZE = 0; - private static final int MSG_SET_DOWNLOADS_RESUMED = 1; + private static final int MSG_SET_DOWNLOADS_PAUSED = 1; private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; private static final int MSG_SET_STOP_REASON = 3; private static final int MSG_ADD_DOWNLOAD = 4; @@ -178,11 +178,12 @@ public final class DownloadManager { private int activeDownloadCount; private boolean initialized; private boolean released; + private boolean downloadsPaused; private RequirementsWatcher requirementsWatcher; // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; - private boolean downloadsResumed; + private boolean downloadsPausedInternal; private int parallelDownloads; // TODO: Fix these to properly support changes at runtime. @@ -221,6 +222,8 @@ public final class DownloadManager { this.downloaderFactory = downloaderFactory; maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; + downloadsPaused = true; + downloadsPausedInternal = true; downloadInternals = new ArrayList<>(); downloads = new ArrayList<>(); @@ -306,6 +309,11 @@ public final class DownloadManager { onRequirementsStateChanged(requirementsWatcher, notMetRequirements); } + /** Returns the maximum number of parallel downloads. */ + public int getMaxParallelDownloads() { + return maxParallelDownloads; + } + /** * Sets the maximum number of parallel downloads. * @@ -316,6 +324,14 @@ public final class DownloadManager { this.maxParallelDownloads = maxParallelDownloads; } + /** + * Returns the minimum number of times that a download will be retried. A download will fail if + * the specified number of retries is exceeded without any progress being made. + */ + public int getMinRetryCount() { + return minRetryCount; + } + /** * Sets the minimum number of times that a download will be retried. A download will fail if the * specified number of retries is exceeded without any progress being made. @@ -341,19 +357,41 @@ public final class DownloadManager { return Collections.unmodifiableList(new ArrayList<>(downloads)); } - /** Resumes all downloads except those that have a non-zero {@link Download#stopReason}. */ + /** Returns whether downloads are currently paused. */ + public boolean getDownloadsPaused() { + return downloadsPaused; + } + + /** + * Resumes downloads. + * + *

    If the {@link #setRequirements(Requirements) Requirements} are met up to {@link + * #getMaxParallelDownloads() maxParallelDownloads} will be started, excluding those with non-zero + * {@link Download#stopReason stopReasons}. + */ public void resumeDownloads() { + if (!downloadsPaused) { + return; + } + downloadsPaused = false; pendingMessages++; internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_RESUMED, /* downloadsResumed */ 1, /* unused */ 0) + .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 0, /* unused */ 0) .sendToTarget(); } - /** Pauses all downloads. */ + /** + * Pauses downloads. Downloads that would otherwise be making progress transition to {@link + * Download#STATE_QUEUED}. + */ public void pauseDownloads() { + if (downloadsPaused) { + return; + } + downloadsPaused = true; pendingMessages++; internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_RESUMED, /* downloadsResumed */ 0, /* unused */ 0) + .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 1, /* unused */ 0) .sendToTarget(); } @@ -536,9 +574,9 @@ public final class DownloadManager { int notMetRequirements = message.arg1; initializeInternal(notMetRequirements); break; - case MSG_SET_DOWNLOADS_RESUMED: - boolean downloadsResumed = message.arg1 != 0; - setDownloadsResumed(downloadsResumed); + case MSG_SET_DOWNLOADS_PAUSED: + boolean downloadsPaused = message.arg1 != 0; + setDownloadsPausedInternal(downloadsPaused); break; case MSG_SET_NOT_MET_REQUIREMENTS: notMetRequirements = message.arg1; @@ -604,11 +642,11 @@ public final class DownloadManager { } } - private void setDownloadsResumed(boolean downloadsResumed) { - if (this.downloadsResumed == downloadsResumed) { + private void setDownloadsPausedInternal(boolean downloadsPaused) { + if (this.downloadsPausedInternal == downloadsPaused) { return; } - this.downloadsResumed = downloadsResumed; + this.downloadsPausedInternal = downloadsPaused; for (int i = 0; i < downloadInternals.size(); i++) { downloadInternals.get(i).updateStopState(); } @@ -820,7 +858,7 @@ public final class DownloadManager { } private boolean canStartDownloads() { - return downloadsResumed && notMetRequirements == 0; + return !downloadsPausedInternal && notMetRequirements == 0; } /* package */ static Download mergeRequest( From f37b28f12e1b50908dd1e8511f5e8544cb06873b Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 13:30:48 +0100 Subject: [PATCH 50/95] Post maxParallelDownload and minRetryCount changes PiperOrigin-RevId: 245405316 --- .../exoplayer2/offline/DownloadManager.java | 68 +++++++++++++++---- 1 file changed, 53 insertions(+), 15 deletions(-) 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 0ca13e2385..91a767cfab 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 @@ -133,11 +133,13 @@ public final class DownloadManager { private static final int MSG_SET_DOWNLOADS_PAUSED = 1; private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; private static final int MSG_SET_STOP_REASON = 3; - private static final int MSG_ADD_DOWNLOAD = 4; - private static final int MSG_REMOVE_DOWNLOAD = 5; - private static final int MSG_DOWNLOAD_THREAD_STOPPED = 6; - private static final int MSG_CONTENT_LENGTH_CHANGED = 7; - private static final int MSG_RELEASE = 8; + private static final int MSG_SET_MAX_PARALLEL_DOWNLOADS = 4; + private static final int MSG_SET_MIN_RETRY_COUNT = 5; + private static final int MSG_ADD_DOWNLOAD = 6; + private static final int MSG_REMOVE_DOWNLOAD = 7; + private static final int MSG_DOWNLOAD_THREAD_STOPPED = 8; + private static final int MSG_CONTENT_LENGTH_CHANGED = 9; + private static final int MSG_RELEASE = 10; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -179,17 +181,17 @@ public final class DownloadManager { private boolean initialized; private boolean released; private boolean downloadsPaused; + private int maxParallelDownloads; + private int minRetryCount; private RequirementsWatcher requirementsWatcher; // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; private boolean downloadsPausedInternal; + private int maxParallelDownloadsInternal; + private int minRetryCountInternal; private int parallelDownloads; - // TODO: Fix these to properly support changes at runtime. - private volatile int maxParallelDownloads; - private volatile int minRetryCount; - /** * Constructs a {@link DownloadManager}. * @@ -221,7 +223,9 @@ public final class DownloadManager { this.downloadIndex = downloadIndex; this.downloaderFactory = downloaderFactory; maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; + maxParallelDownloadsInternal = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; + minRetryCountInternal = DEFAULT_MIN_RETRY_COUNT; downloadsPaused = true; downloadsPausedInternal = true; @@ -319,9 +323,15 @@ public final class DownloadManager { * * @param maxParallelDownloads The maximum number of parallel downloads. */ - // TODO: Fix to properly support changes at runtime. public void setMaxParallelDownloads(int maxParallelDownloads) { + if (this.maxParallelDownloads == maxParallelDownloads) { + return; + } this.maxParallelDownloads = maxParallelDownloads; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_MAX_PARALLEL_DOWNLOADS, maxParallelDownloads, /* unused */ 0) + .sendToTarget(); } /** @@ -338,9 +348,15 @@ public final class DownloadManager { * * @param minRetryCount The minimum number of times that a download will be retried. */ - // TODO: Fix to properly support changes at runtime. public void setMinRetryCount(int minRetryCount) { + if (this.minRetryCount == minRetryCount) { + return; + } this.minRetryCount = minRetryCount; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_MIN_RETRY_COUNT, minRetryCount, /* unused */ 0) + .sendToTarget(); } /** Returns the used {@link DownloadIndex}. */ @@ -587,6 +603,14 @@ public final class DownloadManager { int stopReason = message.arg1; setStopReasonInternal(id, stopReason); break; + case MSG_SET_MAX_PARALLEL_DOWNLOADS: + int maxParallelDownloads = message.arg1; + setMaxParallelDownloadsInternal(maxParallelDownloads); + break; + case MSG_SET_MIN_RETRY_COUNT: + int minRetryCount = message.arg1; + setMinRetryCountInternal(minRetryCount); + break; case MSG_ADD_DOWNLOAD: DownloadRequest request = (DownloadRequest) message.obj; stopReason = message.arg1; @@ -688,6 +712,15 @@ public final class DownloadManager { } } + private void setMaxParallelDownloadsInternal(int maxParallelDownloads) { + maxParallelDownloadsInternal = maxParallelDownloads; + // TODO: Start or stop downloads if necessary. + } + + private void setMinRetryCountInternal(int minRetryCount) { + minRetryCountInternal = minRetryCount; + } + private void addDownloadInternal(DownloadRequest request, int stopReason) { DownloadInternal downloadInternal = getDownload(request.id); if (downloadInternal != null) { @@ -736,14 +769,14 @@ public final class DownloadManager { boolean tryToStartDownloads = false; if (!downloadThread.isRemove) { // If maxParallelDownloads was hit, there might be a download waiting for a slot. - tryToStartDownloads = parallelDownloads == maxParallelDownloads; + tryToStartDownloads = parallelDownloads == maxParallelDownloadsInternal; parallelDownloads--; } getDownload(downloadId) .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); if (tryToStartDownloads) { for (int i = 0; - parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); + parallelDownloads < maxParallelDownloadsInternal && i < downloadInternals.size(); i++) { downloadInternals.get(i).start(); } @@ -804,7 +837,7 @@ public final class DownloadManager { } boolean isRemove = downloadInternal.isInRemoveState(); if (!isRemove) { - if (parallelDownloads == maxParallelDownloads) { + if (parallelDownloads == maxParallelDownloadsInternal) { return START_THREAD_TOO_MANY_DOWNLOADS; } parallelDownloads++; @@ -813,7 +846,12 @@ public final class DownloadManager { DownloadProgress downloadProgress = downloadInternal.download.progress; DownloadThread downloadThread = new DownloadThread( - request, downloader, downloadProgress, isRemove, minRetryCount, internalHandler); + request, + downloader, + downloadProgress, + isRemove, + minRetryCountInternal, + internalHandler); downloadThreads.put(downloadId, downloadThread); downloadThread.start(); logd("Download is started", downloadInternal); From 2b0b6e1b2e52357d0c905901787d65f0302ab9ed Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 13:53:58 +0100 Subject: [PATCH 51/95] Move DownloadManager internal logic into isolated inner class There are no logic changes here. It's just moving code around and removing the "internal" part of names where no longer required. PiperOrigin-RevId: 245407238 --- .../exoplayer2/offline/DownloadManager.java | 767 +++++++++--------- 1 file changed, 388 insertions(+), 379 deletions(-) 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 91a767cfab..aa0cd12231 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 @@ -160,38 +160,21 @@ public final class DownloadManager { private final Context context; private final WritableDownloadIndex downloadIndex; - private final DownloaderFactory downloaderFactory; private final Handler mainHandler; - private final HandlerThread internalThread; - private final Handler internalHandler; + private final InternalHandler internalHandler; private final RequirementsWatcher.Listener requirementsListener; - private final Object releaseLock; - // Collections that are accessed on the main thread. private final CopyOnWriteArraySet listeners; private final ArrayList downloads; - // Collections that are accessed on the internal thread. - private final ArrayList downloadInternals; - private final HashMap downloadThreads; - - // Mutable fields that are accessed on the main thread. private int pendingMessages; private int activeDownloadCount; private boolean initialized; - private boolean released; private boolean downloadsPaused; private int maxParallelDownloads; private int minRetryCount; private RequirementsWatcher requirementsWatcher; - // Mutable fields that are accessed on the internal thread. - @Requirements.RequirementFlags private int notMetRequirements; - private boolean downloadsPausedInternal; - private int maxParallelDownloadsInternal; - private int minRetryCountInternal; - private int parallelDownloads; - /** * Constructs a {@link DownloadManager}. * @@ -221,31 +204,29 @@ public final class DownloadManager { Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) { this.context = context.getApplicationContext(); this.downloadIndex = downloadIndex; - this.downloaderFactory = downloaderFactory; maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; - maxParallelDownloadsInternal = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; - minRetryCountInternal = DEFAULT_MIN_RETRY_COUNT; downloadsPaused = true; - downloadsPausedInternal = true; - - downloadInternals = new ArrayList<>(); downloads = new ArrayList<>(); - downloadThreads = new HashMap<>(); listeners = new CopyOnWriteArraySet<>(); - releaseLock = new Object(); - requirementsListener = this::onRequirementsStateChanged; - - mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); - internalThread = new HandlerThread("DownloadManager file i/o"); - internalThread.start(); - internalHandler = new Handler(internalThread.getLooper(), this::handleInternalMessage); - requirementsWatcher = new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); int notMetRequirements = requirementsWatcher.start(); + mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); + HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); + internalThread.start(); + internalHandler = + new InternalHandler( + internalThread, + downloadIndex, + downloaderFactory, + mainHandler, + maxParallelDownloads, + minRetryCount, + downloadsPaused); + pendingMessages = 1; internalHandler .obtainMessage(MSG_INITIALIZE, notMetRequirements, /* unused */ 0) @@ -464,15 +445,15 @@ public final class DownloadManager { * download index. The manager must not be accessed after this method has been called. */ public void release() { - synchronized (releaseLock) { - if (released) { + synchronized (internalHandler) { + if (internalHandler.released) { return; } internalHandler.sendEmptyMessage(MSG_RELEASE); boolean wasInterrupted = false; - while (!released) { + while (!internalHandler.released) { try { - releaseLock.wait(); + internalHandler.wait(); } catch (InterruptedException e) { wasInterrupted = true; } @@ -581,324 +562,6 @@ public final class DownloadManager { return C.INDEX_UNSET; } - // Internal thread message handling. - - private boolean handleInternalMessage(Message message) { - boolean processedExternalMessage = true; - switch (message.what) { - case MSG_INITIALIZE: - int notMetRequirements = message.arg1; - initializeInternal(notMetRequirements); - break; - case MSG_SET_DOWNLOADS_PAUSED: - boolean downloadsPaused = message.arg1 != 0; - setDownloadsPausedInternal(downloadsPaused); - break; - case MSG_SET_NOT_MET_REQUIREMENTS: - notMetRequirements = message.arg1; - setNotMetRequirementsInternal(notMetRequirements); - break; - case MSG_SET_STOP_REASON: - String id = (String) message.obj; - int stopReason = message.arg1; - setStopReasonInternal(id, stopReason); - break; - case MSG_SET_MAX_PARALLEL_DOWNLOADS: - int maxParallelDownloads = message.arg1; - setMaxParallelDownloadsInternal(maxParallelDownloads); - break; - case MSG_SET_MIN_RETRY_COUNT: - int minRetryCount = message.arg1; - setMinRetryCountInternal(minRetryCount); - break; - case MSG_ADD_DOWNLOAD: - DownloadRequest request = (DownloadRequest) message.obj; - stopReason = message.arg1; - addDownloadInternal(request, stopReason); - break; - case MSG_REMOVE_DOWNLOAD: - id = (String) message.obj; - removeDownloadInternal(id); - break; - case MSG_DOWNLOAD_THREAD_STOPPED: - DownloadThread downloadThread = (DownloadThread) message.obj; - onDownloadThreadStoppedInternal(downloadThread); - processedExternalMessage = false; // This message is posted internally. - break; - case MSG_CONTENT_LENGTH_CHANGED: - downloadThread = (DownloadThread) message.obj; - onDownloadThreadContentLengthChangedInternal(downloadThread); - processedExternalMessage = false; // This message is posted internally. - break; - case MSG_RELEASE: - releaseInternal(); - return true; // Don't post back to mainHandler on release. - default: - throw new IllegalStateException(); - } - mainHandler - .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, downloadThreads.size()) - .sendToTarget(); - return true; - } - - private void initializeInternal(int notMetRequirements) { - this.notMetRequirements = notMetRequirements; - ArrayList loadedStates = new ArrayList<>(); - try (DownloadCursor cursor = - downloadIndex.getDownloads( - STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING)) { - while (cursor.moveToNext()) { - loadedStates.add(cursor.getDownload()); - } - logd("Downloads are loaded."); - } catch (Throwable e) { - Log.e(TAG, "Download state loading failed.", e); - loadedStates.clear(); - } - for (Download download : loadedStates) { - addDownloadForState(download); - } - logd("Downloads are created."); - mainHandler.obtainMessage(MSG_INITIALIZED, loadedStates).sendToTarget(); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).start(); - } - } - - private void setDownloadsPausedInternal(boolean downloadsPaused) { - if (this.downloadsPausedInternal == downloadsPaused) { - return; - } - this.downloadsPausedInternal = downloadsPaused; - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } - } - - private void setNotMetRequirementsInternal( - @Requirements.RequirementFlags int notMetRequirements) { - if (this.notMetRequirements == notMetRequirements) { - return; - } - this.notMetRequirements = notMetRequirements; - logdFlags("Not met requirements are changed", notMetRequirements); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } - } - - private void setStopReasonInternal(@Nullable String id, int stopReason) { - if (id != null) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - logd("download stop reason is set to : " + stopReason, downloadInternal); - downloadInternal.setStopReason(stopReason); - return; - } - } else { - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).setStopReason(stopReason); - } - } - try { - if (id != null) { - downloadIndex.setStopReason(id, stopReason); - } else { - downloadIndex.setStopReason(stopReason); - } - } catch (IOException e) { - Log.e(TAG, "setStopReason failed", e); - } - } - - private void setMaxParallelDownloadsInternal(int maxParallelDownloads) { - maxParallelDownloadsInternal = maxParallelDownloads; - // TODO: Start or stop downloads if necessary. - } - - private void setMinRetryCountInternal(int minRetryCount) { - minRetryCountInternal = minRetryCount; - } - - private void addDownloadInternal(DownloadRequest request, int stopReason) { - DownloadInternal downloadInternal = getDownload(request.id); - if (downloadInternal != null) { - downloadInternal.addRequest(request, stopReason); - logd("Request is added to existing download", downloadInternal); - } else { - Download download = loadDownload(request.id); - if (download == null) { - long nowMs = System.currentTimeMillis(); - download = - new Download( - request, - stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, - /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs, - /* contentLength= */ C.LENGTH_UNSET, - stopReason, - Download.FAILURE_REASON_NONE); - logd("Download state is created for " + request.id); - } else { - download = mergeRequest(download, request, stopReason); - logd("Download state is loaded for " + request.id); - } - addDownloadForState(download); - } - } - - private void removeDownloadInternal(String id) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - downloadInternal.remove(); - } else { - Download download = loadDownload(id); - if (download != null) { - addDownloadForState(copyWithState(download, STATE_REMOVING)); - } else { - logd("Can't remove download. No download with id: " + id); - } - } - } - - private void onDownloadThreadStoppedInternal(DownloadThread downloadThread) { - logd("Download is stopped", downloadThread.request); - String downloadId = downloadThread.request.id; - downloadThreads.remove(downloadId); - boolean tryToStartDownloads = false; - if (!downloadThread.isRemove) { - // If maxParallelDownloads was hit, there might be a download waiting for a slot. - tryToStartDownloads = parallelDownloads == maxParallelDownloadsInternal; - parallelDownloads--; - } - getDownload(downloadId) - .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); - if (tryToStartDownloads) { - for (int i = 0; - parallelDownloads < maxParallelDownloadsInternal && i < downloadInternals.size(); - i++) { - downloadInternals.get(i).start(); - } - } - } - - private void onDownloadThreadContentLengthChangedInternal(DownloadThread downloadThread) { - String downloadId = downloadThread.request.id; - getDownload(downloadId).setContentLength(downloadThread.contentLength); - } - - private void releaseInternal() { - for (DownloadThread downloadThread : downloadThreads.values()) { - downloadThread.cancel(/* released= */ true); - } - downloadThreads.clear(); - downloadInternals.clear(); - internalThread.quit(); - synchronized (releaseLock) { - released = true; - releaseLock.notifyAll(); - } - } - - private void onDownloadChangedInternal(DownloadInternal downloadInternal, Download download) { - logd("Download state is changed", downloadInternal); - try { - downloadIndex.putDownload(download); - } catch (IOException e) { - Log.e(TAG, "Failed to update index", e); - } - if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { - downloadInternals.remove(downloadInternal); - } - mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); - } - - private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Download download) { - logd("Download is removed", downloadInternal); - try { - downloadIndex.removeDownload(download.request.id); - } catch (IOException e) { - Log.e(TAG, "Failed to remove from index", e); - } - downloadInternals.remove(downloadInternal); - mainHandler.obtainMessage(MSG_DOWNLOAD_REMOVED, download).sendToTarget(); - } - - @StartThreadResults - private int startDownloadThread(DownloadInternal downloadInternal) { - DownloadRequest request = downloadInternal.download.request; - String downloadId = request.id; - if (downloadThreads.containsKey(downloadId)) { - if (stopDownloadThreadInternal(downloadId)) { - return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; - } - return START_THREAD_WAIT_REMOVAL_TO_FINISH; - } - boolean isRemove = downloadInternal.isInRemoveState(); - if (!isRemove) { - if (parallelDownloads == maxParallelDownloadsInternal) { - return START_THREAD_TOO_MANY_DOWNLOADS; - } - parallelDownloads++; - } - Downloader downloader = downloaderFactory.createDownloader(request); - DownloadProgress downloadProgress = downloadInternal.download.progress; - DownloadThread downloadThread = - new DownloadThread( - request, - downloader, - downloadProgress, - isRemove, - minRetryCountInternal, - internalHandler); - downloadThreads.put(downloadId, downloadThread); - downloadThread.start(); - logd("Download is started", downloadInternal); - return START_THREAD_SUCCEEDED; - } - - private boolean stopDownloadThreadInternal(String downloadId) { - DownloadThread downloadThread = downloadThreads.get(downloadId); - if (downloadThread != null && !downloadThread.isRemove) { - downloadThread.cancel(/* released= */ false); - logd("Download is cancelled", downloadThread.request); - return true; - } - return false; - } - - @Nullable - private DownloadInternal getDownload(String id) { - for (int i = 0; i < downloadInternals.size(); i++) { - DownloadInternal downloadInternal = downloadInternals.get(i); - if (downloadInternal.download.request.id.equals(id)) { - return downloadInternal; - } - } - return null; - } - - private Download loadDownload(String id) { - try { - return downloadIndex.getDownload(id); - } catch (IOException e) { - Log.e(TAG, "loadDownload failed", e); - } - return null; - } - - private void addDownloadForState(Download download) { - DownloadInternal downloadInternal = new DownloadInternal(this, download); - downloadInternals.add(downloadInternal); - logd("Download is added", downloadInternal); - downloadInternal.initialize(); - } - - private boolean canStartDownloads() { - return !downloadsPausedInternal && notMetRequirements == 0; - } - /* package */ static Download mergeRequest( Download download, DownloadRequest request, int stopReason) { @Download.State int state = download.state; @@ -955,9 +618,355 @@ public final class DownloadManager { } } + private static final class InternalHandler extends Handler { + + public boolean released; + + private final HandlerThread thread; + private final WritableDownloadIndex downloadIndex; + private final DownloaderFactory downloaderFactory; + private final Handler mainHandler; + private final ArrayList downloadInternals; + private final HashMap downloadThreads; + + // Mutable fields that are accessed on the internal thread. + @Requirements.RequirementFlags private int notMetRequirements; + private boolean downloadsPaused; + private int maxParallelDownloads; + private int minRetryCount; + private int parallelDownloads; + + public InternalHandler( + HandlerThread thread, + WritableDownloadIndex downloadIndex, + DownloaderFactory downloaderFactory, + Handler mainHandler, + int maxParallelDownloads, + int minRetryCount, + boolean downloadsPaused) { + super(thread.getLooper()); + this.thread = thread; + this.downloadIndex = downloadIndex; + this.downloaderFactory = downloaderFactory; + this.mainHandler = mainHandler; + this.maxParallelDownloads = maxParallelDownloads; + this.minRetryCount = minRetryCount; + this.downloadsPaused = downloadsPaused; + downloadInternals = new ArrayList<>(); + downloadThreads = new HashMap<>(); + } + + @Override + public void handleMessage(Message message) { + boolean processedExternalMessage = true; + switch (message.what) { + case MSG_INITIALIZE: + int notMetRequirements = message.arg1; + initialize(notMetRequirements); + break; + case MSG_SET_DOWNLOADS_PAUSED: + boolean downloadsPaused = message.arg1 != 0; + setDownloadsPaused(downloadsPaused); + break; + case MSG_SET_NOT_MET_REQUIREMENTS: + notMetRequirements = message.arg1; + setNotMetRequirements(notMetRequirements); + break; + case MSG_SET_STOP_REASON: + String id = (String) message.obj; + int stopReason = message.arg1; + setStopReason(id, stopReason); + break; + case MSG_SET_MAX_PARALLEL_DOWNLOADS: + int maxParallelDownloads = message.arg1; + setMaxParallelDownloads(maxParallelDownloads); + break; + case MSG_SET_MIN_RETRY_COUNT: + int minRetryCount = message.arg1; + setMinRetryCount(minRetryCount); + break; + case MSG_ADD_DOWNLOAD: + DownloadRequest request = (DownloadRequest) message.obj; + stopReason = message.arg1; + addDownload(request, stopReason); + break; + case MSG_REMOVE_DOWNLOAD: + id = (String) message.obj; + removeDownload(id); + break; + case MSG_DOWNLOAD_THREAD_STOPPED: + DownloadThread downloadThread = (DownloadThread) message.obj; + onDownloadThreadStopped(downloadThread); + processedExternalMessage = false; // This message is posted internally. + break; + case MSG_CONTENT_LENGTH_CHANGED: + downloadThread = (DownloadThread) message.obj; + onDownloadThreadContentLengthChanged(downloadThread); + processedExternalMessage = false; // This message is posted internally. + break; + case MSG_RELEASE: + release(); + return; // Don't post back to mainHandler on release. + default: + throw new IllegalStateException(); + } + mainHandler + .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, downloadThreads.size()) + .sendToTarget(); + } + + private void initialize(int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + ArrayList loadedStates = new ArrayList<>(); + try (DownloadCursor cursor = + downloadIndex.getDownloads( + STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING)) { + while (cursor.moveToNext()) { + loadedStates.add(cursor.getDownload()); + } + logd("Downloads are loaded."); + } catch (Throwable e) { + Log.e(TAG, "Download state loading failed.", e); + loadedStates.clear(); + } + for (Download download : loadedStates) { + addDownloadForState(download); + } + logd("Downloads are created."); + mainHandler.obtainMessage(MSG_INITIALIZED, loadedStates).sendToTarget(); + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).start(); + } + } + + private void setDownloadsPaused(boolean downloadsPaused) { + this.downloadsPaused = downloadsPaused; + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).updateStopState(); + } + } + + private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { + // TODO: Move this deduplication check to the main thread. + if (this.notMetRequirements == notMetRequirements) { + return; + } + this.notMetRequirements = notMetRequirements; + logdFlags("Not met requirements are changed", notMetRequirements); + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).updateStopState(); + } + } + + private void setStopReason(@Nullable String id, int stopReason) { + if (id != null) { + DownloadInternal downloadInternal = getDownload(id); + if (downloadInternal != null) { + logd("download stop reason is set to : " + stopReason, downloadInternal); + downloadInternal.setStopReason(stopReason); + return; + } + } else { + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).setStopReason(stopReason); + } + } + try { + if (id != null) { + downloadIndex.setStopReason(id, stopReason); + } else { + downloadIndex.setStopReason(stopReason); + } + } catch (IOException e) { + Log.e(TAG, "setStopReason failed", e); + } + } + + private void setMaxParallelDownloads(int maxParallelDownloads) { + this.maxParallelDownloads = maxParallelDownloads; + // TODO: Start or stop downloads if necessary. + } + + private void setMinRetryCount(int minRetryCount) { + this.minRetryCount = minRetryCount; + } + + private void addDownload(DownloadRequest request, int stopReason) { + DownloadInternal downloadInternal = getDownload(request.id); + if (downloadInternal != null) { + downloadInternal.addRequest(request, stopReason); + logd("Request is added to existing download", downloadInternal); + } else { + Download download = loadDownload(request.id); + if (download == null) { + long nowMs = System.currentTimeMillis(); + download = + new Download( + request, + stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, + /* startTimeMs= */ nowMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + Download.FAILURE_REASON_NONE); + logd("Download state is created for " + request.id); + } else { + download = mergeRequest(download, request, stopReason); + logd("Download state is loaded for " + request.id); + } + addDownloadForState(download); + } + } + + private void removeDownload(String id) { + DownloadInternal downloadInternal = getDownload(id); + if (downloadInternal != null) { + downloadInternal.remove(); + } else { + Download download = loadDownload(id); + if (download != null) { + addDownloadForState(copyWithState(download, STATE_REMOVING)); + } else { + logd("Can't remove download. No download with id: " + id); + } + } + } + + private void onDownloadThreadStopped(DownloadThread downloadThread) { + logd("Download is stopped", downloadThread.request); + String downloadId = downloadThread.request.id; + downloadThreads.remove(downloadId); + boolean tryToStartDownloads = false; + if (!downloadThread.isRemove) { + // If maxParallelDownloads was hit, there might be a download waiting for a slot. + tryToStartDownloads = parallelDownloads == maxParallelDownloads; + parallelDownloads--; + } + getDownload(downloadId) + .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); + if (tryToStartDownloads) { + for (int i = 0; + parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); + i++) { + downloadInternals.get(i).start(); + } + } + } + + private void onDownloadThreadContentLengthChanged(DownloadThread downloadThread) { + String downloadId = downloadThread.request.id; + getDownload(downloadId).setContentLength(downloadThread.contentLength); + } + + private void release() { + for (DownloadThread downloadThread : downloadThreads.values()) { + downloadThread.cancel(/* released= */ true); + } + downloadThreads.clear(); + downloadInternals.clear(); + thread.quit(); + synchronized (this) { + released = true; + notifyAll(); + } + } + + private void onDownloadChangedInternal(DownloadInternal downloadInternal, Download download) { + logd("Download state is changed", downloadInternal); + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index", e); + } + if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { + downloadInternals.remove(downloadInternal); + } + mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); + } + + private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Download download) { + logd("Download is removed", downloadInternal); + try { + downloadIndex.removeDownload(download.request.id); + } catch (IOException e) { + Log.e(TAG, "Failed to remove from index", e); + } + downloadInternals.remove(downloadInternal); + mainHandler.obtainMessage(MSG_DOWNLOAD_REMOVED, download).sendToTarget(); + } + + @StartThreadResults + private int startDownloadThread(DownloadInternal downloadInternal) { + DownloadRequest request = downloadInternal.download.request; + String downloadId = request.id; + if (downloadThreads.containsKey(downloadId)) { + if (stopDownloadThreadInternal(downloadId)) { + return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; + } + return START_THREAD_WAIT_REMOVAL_TO_FINISH; + } + boolean isRemove = downloadInternal.isInRemoveState(); + if (!isRemove) { + if (parallelDownloads == maxParallelDownloads) { + return START_THREAD_TOO_MANY_DOWNLOADS; + } + parallelDownloads++; + } + Downloader downloader = downloaderFactory.createDownloader(request); + DownloadProgress downloadProgress = downloadInternal.download.progress; + DownloadThread downloadThread = + new DownloadThread(request, downloader, downloadProgress, isRemove, minRetryCount, this); + downloadThreads.put(downloadId, downloadThread); + downloadThread.start(); + logd("Download is started", downloadInternal); + return START_THREAD_SUCCEEDED; + } + + private boolean stopDownloadThreadInternal(String downloadId) { + DownloadThread downloadThread = downloadThreads.get(downloadId); + if (downloadThread != null && !downloadThread.isRemove) { + downloadThread.cancel(/* released= */ false); + logd("Download is cancelled", downloadThread.request); + return true; + } + return false; + } + + @Nullable + private DownloadInternal getDownload(String id) { + for (int i = 0; i < downloadInternals.size(); i++) { + DownloadInternal downloadInternal = downloadInternals.get(i); + if (downloadInternal.download.request.id.equals(id)) { + return downloadInternal; + } + } + return null; + } + + private Download loadDownload(String id) { + try { + return downloadIndex.getDownload(id); + } catch (IOException e) { + Log.e(TAG, "loadDownload failed", e); + } + return null; + } + + private void addDownloadForState(Download download) { + DownloadInternal downloadInternal = new DownloadInternal(this, download); + downloadInternals.add(downloadInternal); + logd("Download is added", downloadInternal); + downloadInternal.initialize(); + } + + private boolean canStartDownloads() { + return !downloadsPaused && notMetRequirements == 0; + } + } + private static final class DownloadInternal { - private final DownloadManager downloadManager; + private final InternalHandler internalHandler; private Download download; @@ -967,8 +976,8 @@ public final class DownloadManager { private int stopReason; @MonotonicNonNull @Download.FailureReason private int failureReason; - private DownloadInternal(DownloadManager downloadManager, Download download) { - this.downloadManager = downloadManager; + private DownloadInternal(InternalHandler internalHandler, Download download) { + this.internalHandler = internalHandler; this.download = download; state = download.state; contentLength = download.contentLength; @@ -1016,7 +1025,7 @@ public final class DownloadManager { if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { startOrQueue(); } else if (isInRemoveState()) { - downloadManager.startDownloadThread(this); + internalHandler.startDownloadThread(this); } } @@ -1034,7 +1043,7 @@ public final class DownloadManager { return; } this.contentLength = contentLength; - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } private void updateStopState() { @@ -1045,12 +1054,12 @@ public final class DownloadManager { } } else { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - downloadManager.stopDownloadThreadInternal(download.request.id); + internalHandler.stopDownloadThreadInternal(download.request.id); setState(STATE_STOPPED); } } if (oldDownload == download) { - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } } @@ -1059,24 +1068,24 @@ public final class DownloadManager { // state immediately. state = initialState; if (isInRemoveState()) { - downloadManager.startDownloadThread(this); + internalHandler.startDownloadThread(this); } else if (canStart()) { startOrQueue(); } else { setState(STATE_STOPPED); } if (state == initialState) { - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } } private boolean canStart() { - return downloadManager.canStartDownloads() && stopReason == STOP_REASON_NONE; + return internalHandler.canStartDownloads() && stopReason == STOP_REASON_NONE; } private void startOrQueue() { Assertions.checkState(!isInRemoveState()); - @StartThreadResults int result = downloadManager.startDownloadThread(this); + @StartThreadResults int result = internalHandler.startDownloadThread(this); Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) { setState(STATE_DOWNLOADING); @@ -1088,7 +1097,7 @@ public final class DownloadManager { private void setState(@Download.State int newState) { if (state != newState) { state = newState; - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } } @@ -1097,9 +1106,9 @@ public final class DownloadManager { return; } if (isCanceled) { - downloadManager.startDownloadThread(this); + internalHandler.startDownloadThread(this); } else if (state == STATE_REMOVING) { - downloadManager.onDownloadRemovedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadRemovedInternal(this, getUpdatedDownload()); } else if (state == STATE_RESTARTING) { initialize(STATE_QUEUED); } else { // STATE_DOWNLOADING @@ -1122,7 +1131,7 @@ public final class DownloadManager { private final boolean isRemove; private final int minRetryCount; - private volatile Handler updateHandler; + private volatile InternalHandler internalHandler; private volatile boolean isCanceled; private Throwable finalError; @@ -1134,13 +1143,13 @@ public final class DownloadManager { DownloadProgress downloadProgress, boolean isRemove, int minRetryCount, - Handler updateHandler) { + InternalHandler internalHandler) { this.request = request; this.downloader = downloader; this.downloadProgress = downloadProgress; this.isRemove = isRemove; this.minRetryCount = minRetryCount; - this.updateHandler = updateHandler; + this.internalHandler = internalHandler; contentLength = C.LENGTH_UNSET; } @@ -1150,7 +1159,7 @@ public final class DownloadManager { // cancellation to complete depends on the implementation of the downloader being used. We // null the handler reference here so that it doesn't prevent garbage collection of the // download manager whilst cancellation is ongoing. - updateHandler = null; + internalHandler = null; } isCanceled = true; downloader.cancel(); @@ -1192,9 +1201,9 @@ public final class DownloadManager { } catch (Throwable e) { finalError = e; } - Handler updateHandler = this.updateHandler; - if (updateHandler != null) { - updateHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); + Handler internalHandler = this.internalHandler; + if (internalHandler != null) { + internalHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); } } @@ -1204,9 +1213,9 @@ public final class DownloadManager { downloadProgress.percentDownloaded = percentDownloaded; if (contentLength != this.contentLength) { this.contentLength = contentLength; - Handler updateHandler = this.updateHandler; - if (updateHandler != null) { - updateHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); + Handler internalHandler = this.internalHandler; + if (internalHandler != null) { + internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); } } } From 61f28f8107434143794b6c5757d7d8cc34a91248 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 14:35:51 +0100 Subject: [PATCH 52/95] Link blog post from release notes PiperOrigin-RevId: 245411528 --- RELEASENOTES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 33bfa44ec7..3eb4e400b1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,8 +12,10 @@ ### 2.10.0 ### * Core library: - * Improve decoder re-use between playbacks. TODO: Write and link a blog post - here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). + * Improve decoder re-use between playbacks + ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read + [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d) + for more details. * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`. * Fix issue where using `ProgressiveMediaSource.Factory` would mean that `DefaultExtractorsFactory` would be kept by proguard. Custom From 1bc279795b6a7d4e78ccab86ee0e08e540832588 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 26 Apr 2019 18:14:55 +0100 Subject: [PATCH 53/95] Log warnings when extension libraries can't be used Issue: #5788 PiperOrigin-RevId: 245440858 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 12 +++++++++++- .../android/exoplayer2/util/LibraryLoader.java | 8 +++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3eb4e400b1..dccd23781d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -126,6 +126,9 @@ * Add ability to include an extras `Bundle` when reporting a custom error. * LoadControl: Set minimum buffer for playbacks with video equal to maximum buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)). +* Log warnings when extension native libraries can't be used, to help with + diagnosing playback failures + ([#5788](https://github.com/google/ExoPlayer/issues/5788)). ### 2.9.6 ### diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index bc36fc4f3b..58109c1666 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -30,6 +31,8 @@ public final class FfmpegLibrary { ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg"); } + private static final String TAG = "FfmpegLibrary"; + private static final LibraryLoader LOADER = new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg"); @@ -69,7 +72,14 @@ public final class FfmpegLibrary { return false; } String codecName = getCodecName(mimeType, encoding); - return codecName != null && ffmpegHasDecoder(codecName); + if (codecName == null) { + return false; + } + if (!ffmpegHasDecoder(codecName)) { + Log.w(TAG, "No " + codecName + " decoder available. Check the FFmpeg build configuration."); + return false; + } + return true; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java index c12bae0a07..7ee88d8f0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java @@ -15,11 +15,15 @@ */ package com.google.android.exoplayer2.util; +import java.util.Arrays; + /** * Configurable loader for native libraries. */ public final class LibraryLoader { + private static final String TAG = "LibraryLoader"; + private String[] nativeLibraries; private boolean loadAttempted; private boolean isAvailable; @@ -54,7 +58,9 @@ public final class LibraryLoader { } isAvailable = true; } catch (UnsatisfiedLinkError exception) { - // Do nothing. + // Log a warning as an attempt to check for the library indicates that the app depends on an + // extension and generally would expect its native libraries to be available. + Log.w(TAG, "Failed to load " + Arrays.toString(nativeLibraries)); } return isAvailable; } From 3e519824ff909b0d5e0a31936ca2886c29f844c0 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Apr 2019 18:26:24 +0100 Subject: [PATCH 54/95] Downloading documentation PiperOrigin-RevId: 245443109 --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dccd23781d..0b59c1c6fc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,7 +32,8 @@ ([#5520](https://github.com/google/ExoPlayer/issues/5520)). * Offline: * Improve offline support. `DownloadManager` now tracks all offline content, - not just tasks in progress. TODO: Write and link a blog post here. + not just tasks in progress. Read [this page](https://exoplayer.dev/downloading-media.html) for + more details. * Caching: * Improve performance of `SimpleCache` ([#4253](https://github.com/google/ExoPlayer/issues/4253)). From 2f356badd231302ae1780f28285cb6741b2a49d7 Mon Sep 17 00:00:00 2001 From: Zsolt Matyas Date: Fri, 26 Apr 2019 14:37:26 -0700 Subject: [PATCH 55/95] Handling XDS and TEXT modes [Problem] There are 3 services / modes transported on line 21: - Captioning - TEXT (generally not program related) - XDS (eXtended Data Services) Bytes belonging to the unsupported modes are interleaved with the bytes of the captioning mode. See Chapter 7, Chapter 8.5 and Chapter 8.6 of the CEA608 Standard for more details. [Solution] Drop all bytes belonging to unsupported modes. [Test] - All streams containing only captioning services should not be influenced - Test all 4 CEA 608 channels with live over-the-air content and using all available TEXT and XDS streams. --- .../exoplayer2/text/cea/Cea608Decoder.java | 80 +++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 9316e4fb86..a30758c8e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -80,6 +80,17 @@ public final class Cea608Decoder extends CeaDecoder { * at which point the non-displayed memory becomes the displayed memory (and vice versa). */ private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20; + + private static final byte CTRL_BACKSPACE = 0x21; + + @SuppressWarnings("unused") + private static final byte CTRL_ALARM_OFF= 0x22; // not supported any more + + @SuppressWarnings("unused") + private static final byte CTRL_ALARM_ON= 0x23; // not supported any more + + private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; + /** * Command initiating roll-up style captioning, with the maximum of 2 rows displayed * simultaneously. @@ -95,11 +106,25 @@ public final class Cea608Decoder extends CeaDecoder { * simultaneously. */ private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27; + + @SuppressWarnings("unused") + private static final byte CTRL_FLASH_ON = 0x28; // not supported any more /** * Command initiating paint-on style captioning. Subsequent data should be addressed immediately * to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command. */ private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29; + /** + * TEXT commands are switching to TEXT mode from CAPTION mode. All consecutive incoming + * data must be filtered out until a command is received that switches back to CAPTION mode + */ + private static final byte CTRL_TEXT_RESTART = 0x2A; + private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B; + + private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C; + private static final byte CTRL_CARRIAGE_RETURN = 0x2D; + private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; + /** * Command indicating the end of a pop-on style caption. At this point the caption loaded in * non-displayed memory should be swapped with the one in displayed memory. If no @@ -108,13 +133,6 @@ public final class Cea608Decoder extends CeaDecoder { */ private static final byte CTRL_END_OF_CAPTION = 0x2F; - private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C; - private static final byte CTRL_CARRIAGE_RETURN = 0x2D; - private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; - private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; - - private static final byte CTRL_BACKSPACE = 0x21; - // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20). private static final int[] BASIC_CHARACTER_SET = new int[] { 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & ' @@ -237,6 +255,11 @@ public final class Cea608Decoder extends CeaDecoder { private byte repeatableControlCc2; private int currentChannel; + // The incoming characters may belong to 3 different services based on the last received control + // codes. The 3 services are Captioning, Text and XDS. In this decoder we only intend to process + // bytes belonging to the Captioning service. + private boolean isInCaptionMode = true; + public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); cueBuilders = new ArrayList<>(); @@ -288,6 +311,7 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc1 = 0; repeatableControlCc2 = 0; currentChannel = NTSC_CC_CHANNEL_1; + isInCaptionMode = true; } @Override @@ -306,6 +330,38 @@ public final class Cea608Decoder extends CeaDecoder { return new CeaSubtitle(cues); } + private boolean isCodeForUnsupportedMode(byte cc1, byte cc2) { + // Control codes from 0x01 to 0x0F indicate the beginning of XDS Data + if (0x01 <= cc1 && cc1 <= 0x0F) { + return true; + } + + // 2 commands switch to TEXT mode. + if (((cc1 & 0xF7) == 0x14) // first byte must be 0x14 or 0x1C based on channel + && (cc2 == CTRL_TEXT_RESTART || cc2 == CTRL_RESUME_TEXT_DISPLAY)) { + return true; + } + + return false; + } + + private static boolean isControlCodeSwitchingToCaptionMode(byte cc1, byte cc2) { + if ((cc1 & 0xF7) != 0x14) { // Matching commands must have the CC1 value: 0|0|0|1|CH|1|0|0 where CH is the channel bit + return false; + } + + switch (cc2) { + case CTRL_END_OF_CAPTION: + case CTRL_RESUME_CAPTION_LOADING: + case CTRL_RESUME_DIRECT_CAPTIONING: + case CTRL_ROLL_UP_CAPTIONS_2_ROWS: + case CTRL_ROLL_UP_CAPTIONS_3_ROWS: + case CTRL_ROLL_UP_CAPTIONS_4_ROWS: + return true; + } + return false; + } + @SuppressWarnings("ByteBufferBackingArray") @Override protected void decode(SubtitleInputBuffer inputBuffer) { @@ -363,6 +419,16 @@ public final class Cea608Decoder extends CeaDecoder { continue; } + if (isCodeForUnsupportedMode(ccData1, ccData2)) { + isInCaptionMode = false; + continue; + } else if (!isInCaptionMode) { + if (!isControlCodeSwitchingToCaptionMode(ccData1, ccData2)) { + continue; + } else { + isInCaptionMode = true; + } + } // Special North American character set. // ccData1 - 0|0|0|1|C|0|0|1 // ccData2 - 0|0|1|1|X|X|X|X From 3e14ce1094a5bf932fd33c225ed8b53b573d9db3 Mon Sep 17 00:00:00 2001 From: Zsolt Matyas Date: Mon, 29 Apr 2019 12:56:25 -0700 Subject: [PATCH 56/95] Code changes suggested by tonihei --- .../exoplayer2/text/cea/Cea608Decoder.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index a30758c8e0..332f844375 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -83,12 +83,6 @@ public final class Cea608Decoder extends CeaDecoder { private static final byte CTRL_BACKSPACE = 0x21; - @SuppressWarnings("unused") - private static final byte CTRL_ALARM_OFF= 0x22; // not supported any more - - @SuppressWarnings("unused") - private static final byte CTRL_ALARM_ON= 0x23; // not supported any more - private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; /** @@ -258,7 +252,7 @@ public final class Cea608Decoder extends CeaDecoder { // The incoming characters may belong to 3 different services based on the last received control // codes. The 3 services are Captioning, Text and XDS. In this decoder we only intend to process // bytes belonging to the Captioning service. - private boolean isInCaptionMode = true; + private boolean isInCaptionMode; public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); @@ -291,6 +285,7 @@ public final class Cea608Decoder extends CeaDecoder { setCaptionMode(CC_MODE_UNKNOWN); resetCueBuilders(); + isInCaptionMode = true; } @Override @@ -330,14 +325,14 @@ public final class Cea608Decoder extends CeaDecoder { return new CeaSubtitle(cues); } - private boolean isCodeForUnsupportedMode(byte cc1, byte cc2) { + private static boolean isCodeForUnsupportedMode(byte cc1, byte cc2) { // Control codes from 0x01 to 0x0F indicate the beginning of XDS Data if (0x01 <= cc1 && cc1 <= 0x0F) { return true; } // 2 commands switch to TEXT mode. - if (((cc1 & 0xF7) == 0x14) // first byte must be 0x14 or 0x1C based on channel + if ((isModeSwitchCommand(cc1)) && (cc2 == CTRL_TEXT_RESTART || cc2 == CTRL_RESUME_TEXT_DISPLAY)) { return true; } @@ -345,8 +340,13 @@ public final class Cea608Decoder extends CeaDecoder { return false; } + // first byte of these commands must be 0x14 or 0x1C based on channel + private static boolean isModeSwitchCommand(byte cc1) { + return (cc1 & 0xF7) == 0x14; + } + private static boolean isControlCodeSwitchingToCaptionMode(byte cc1, byte cc2) { - if ((cc1 & 0xF7) != 0x14) { // Matching commands must have the CC1 value: 0|0|0|1|CH|1|0|0 where CH is the channel bit + if (!isModeSwitchCommand(cc1)) { return false; } From 880f4031811460471791b2243d360bd8717333a7 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 18:54:07 +0100 Subject: [PATCH 57/95] Fix line break PiperOrigin-RevId: 245448908 --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0b59c1c6fc..0685184a9a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,8 +32,8 @@ ([#5520](https://github.com/google/ExoPlayer/issues/5520)). * Offline: * Improve offline support. `DownloadManager` now tracks all offline content, - not just tasks in progress. Read [this page](https://exoplayer.dev/downloading-media.html) for - more details. + not just tasks in progress. Read + [this page](https://exoplayer.dev/downloading-media.html) for more details. * Caching: * Improve performance of `SimpleCache` ([#4253](https://github.com/google/ExoPlayer/issues/4253)). From 1fb128df36f9c2a8ecbac5de319bd4cfa5b56f6a Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 29 Apr 2019 10:31:36 +0100 Subject: [PATCH 58/95] Move playback session manager to core library. This allows to use the session management capabilities for other analytics purposes. PiperOrigin-RevId: 245710588 --- .../DefaultPlaybackSessionManager.java | 359 +++++++ .../analytics/PlaybackSessionManager.java | 120 +++ .../DefaultPlaybackSessionManagerTest.java | 957 ++++++++++++++++++ 3 files changed, 1436 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java new file mode 100644 index 0000000000..b336d84dd2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -0,0 +1,359 @@ +/* + * 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.google.android.exoplayer2.analytics; + +import androidx.annotation.Nullable; +import android.util.Base64; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the + * timeline and also for each ad within the windows. + * + *

    Sessions are identified by Base64-encoded, URL-safe, random strings. + */ +public final class DefaultPlaybackSessionManager implements PlaybackSessionManager { + + private static final Random RANDOM = new Random(); + private static final int SESSION_ID_LENGTH = 12; + + private final Timeline.Window window; + private final Timeline.Period period; + private final HashMap sessions; + + @MonotonicNonNull private Listener listener; + private Timeline currentTimeline; + @Nullable private MediaPeriodId currentMediaPeriodId; + @Nullable private String activeSessionId; + + /** Creates session manager. */ + public DefaultPlaybackSessionManager() { + window = new Timeline.Window(); + period = new Timeline.Period(); + sessions = new HashMap<>(); + currentTimeline = Timeline.EMPTY; + } + + @Override + public void setListener(Listener listener) { + this.listener = listener; + } + + @Override + public synchronized String getSessionForMediaPeriodId( + Timeline timeline, MediaPeriodId mediaPeriodId) { + int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex; + return getOrAddSession(windowIndex, mediaPeriodId).sessionId; + } + + @Override + public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) { + SessionDescriptor sessionDescriptor = sessions.get(sessionId); + if (sessionDescriptor == null) { + return false; + } + sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId); + return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId); + } + + @Override + public synchronized void updateSessions(EventTime eventTime) { + boolean isObviouslyFinished = + eventTime.mediaPeriodId != null + && currentMediaPeriodId != null + && eventTime.mediaPeriodId.windowSequenceNumber + < currentMediaPeriodId.windowSequenceNumber; + if (!isObviouslyFinished) { + SessionDescriptor descriptor = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + if (!descriptor.isCreated) { + descriptor.isCreated = true; + Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId); + if (activeSessionId == null) { + updateActiveSession(eventTime, descriptor); + } + } + } + } + + @Override + public synchronized void handleTimelineUpdate(EventTime eventTime) { + Assertions.checkNotNull(listener); + Timeline previousTimeline = currentTimeline; + currentTimeline = eventTime.timeline; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) { + iterator.remove(); + if (session.isCreated) { + if (session.sessionId.equals(activeSessionId)) { + activeSessionId = null; + } + listener.onSessionFinished( + eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); + } + } + } + handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL); + } + + @Override + public synchronized void handlePositionDiscontinuity( + EventTime eventTime, @DiscontinuityReason int reason) { + Assertions.checkNotNull(listener); + boolean hasAutomaticTransition = + reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + || reason == Player.DISCONTINUITY_REASON_AD_INSERTION; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + if (session.isFinishedAtEventTime(eventTime)) { + iterator.remove(); + if (session.isCreated) { + boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId); + boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession; + if (isRemovingActiveSession) { + activeSessionId = null; + } + listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition); + } + } + } + SessionDescriptor activeSessionDescriptor = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + if (eventTime.mediaPeriodId != null + && eventTime.mediaPeriodId.isAd() + && (currentMediaPeriodId == null + || currentMediaPeriodId.windowSequenceNumber + != eventTime.mediaPeriodId.windowSequenceNumber + || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex + || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) { + // New ad playback started. Find corresponding content session and notify ad playback started. + MediaPeriodId contentMediaPeriodId = + new MediaPeriodId( + eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber); + SessionDescriptor contentSession = + getOrAddSession(eventTime.windowIndex, contentMediaPeriodId); + if (contentSession.isCreated && activeSessionDescriptor.isCreated) { + listener.onAdPlaybackStarted( + eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId); + } + } + updateActiveSession(eventTime, activeSessionDescriptor); + } + + private SessionDescriptor getOrAddSession( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is + // null, there may be multiple matching sessions with different window sequence numbers or + // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for + // windows with ads, the content session is preferred over ad sessions. + SessionDescriptor bestMatch = null; + long bestMatchWindowSequenceNumber = Long.MAX_VALUE; + for (SessionDescriptor sessionDescriptor : sessions.values()) { + sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId); + if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) { + long windowSequenceNumber = sessionDescriptor.windowSequenceNumber; + if (windowSequenceNumber == C.INDEX_UNSET + || windowSequenceNumber < bestMatchWindowSequenceNumber) { + bestMatch = sessionDescriptor; + bestMatchWindowSequenceNumber = windowSequenceNumber; + } else if (windowSequenceNumber == bestMatchWindowSequenceNumber + && Util.castNonNull(bestMatch).adMediaPeriodId != null + && sessionDescriptor.adMediaPeriodId != null) { + bestMatch = sessionDescriptor; + } + } + } + if (bestMatch == null) { + String sessionId = generateSessionId(); + bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId); + sessions.put(sessionId, bestMatch); + } + return bestMatch; + } + + @RequiresNonNull("listener") + private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) { + currentMediaPeriodId = eventTime.mediaPeriodId; + if (sessionDescriptor.isCreated && !sessionDescriptor.isActive) { + sessionDescriptor.isActive = true; + activeSessionId = sessionDescriptor.sessionId; + listener.onSessionActive(eventTime, sessionDescriptor.sessionId); + } + } + + private static String generateSessionId() { + byte[] randomBytes = new byte[SESSION_ID_LENGTH]; + RANDOM.nextBytes(randomBytes); + return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP); + } + + /** + * Descriptor for a session. + * + *

    The session may be described in one of three ways: + * + *

      + *
    • A window index with unset window sequence number and a null ad media period id + *
    • A content window with index and sequence number, but a null ad media period id. + *
    • An ad with all values set. + *
    + */ + private final class SessionDescriptor { + + private final String sessionId; + + private int windowIndex; + private long windowSequenceNumber; + private @MonotonicNonNull MediaPeriodId adMediaPeriodId; + + private boolean isCreated; + private boolean isActive; + + public SessionDescriptor( + String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + this.sessionId = sessionId; + this.windowIndex = windowIndex; + this.windowSequenceNumber = + mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber; + if (mediaPeriodId != null && mediaPeriodId.isAd()) { + this.adMediaPeriodId = mediaPeriodId; + } + } + + public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) { + windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex); + if (windowIndex == C.INDEX_UNSET) { + return false; + } + if (adMediaPeriodId != null) { + int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid); + if (newPeriodIndex == C.INDEX_UNSET) { + return false; + } + } + return true; + } + + public boolean belongsToSession( + int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { + if (eventMediaPeriodId == null) { + // Events without concrete media period id are for all sessions of the same window. + return eventWindowIndex == windowIndex; + } + if (adMediaPeriodId == null) { + // If this is a content session, only events for content with the same window sequence + // number belong to this session. + return !eventMediaPeriodId.isAd() + && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber; + } + // If this is an ad session, only events for this ad belong to the session. + return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber + && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex + && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup; + } + + public void maybeSetWindowSequenceNumber( + int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { + if (windowSequenceNumber == C.INDEX_UNSET + && eventWindowIndex == windowIndex + && eventMediaPeriodId != null + && !eventMediaPeriodId.isAd()) { + // Set window sequence number for this session as soon as we have one. + windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber; + } + } + + public boolean isFinishedAtEventTime(EventTime eventTime) { + if (windowSequenceNumber == C.INDEX_UNSET) { + // Sessions with unspecified window sequence number are kept until we know more. + return false; + } + if (eventTime.mediaPeriodId == null) { + // For event times without media period id (e.g. after seek to new window), we only keep + // sessions of this window. + return windowIndex != eventTime.windowIndex; + } + if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) { + // All past window sequence numbers are finished. + return true; + } + if (adMediaPeriodId == null) { + // Current or future content is not finished. + return false; + } + int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid); + int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid); + if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber + || eventPeriodIndex < adPeriodIndex) { + // Ads in future windows or periods are not finished. + return false; + } + if (eventPeriodIndex > adPeriodIndex) { + // Ads in past periods are finished. + return true; + } + if (eventTime.mediaPeriodId.isAd()) { + int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex; + int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup; + // Finished if event is for an ad after this one in the same period. + return eventAdGroup > adMediaPeriodId.adGroupIndex + || (eventAdGroup == adMediaPeriodId.adGroupIndex + && eventAdIndex > adMediaPeriodId.adIndexInAdGroup); + } else { + eventTime.timeline.getPeriod(adPeriodIndex, period); + long adGroupTimeMs = + adMediaPeriodId.adGroupIndex < period.getAdGroupCount() + ? C.usToMs(period.getAdGroupTimeUs(adMediaPeriodId.adGroupIndex)) + : 0; + // Finished if the event is for content after this ad. + return adGroupTimeMs <= eventTime.currentPlaybackPositionMs; + } + } + + private int resolveWindowIndexToNewTimeline( + Timeline oldTimeline, Timeline newTimeline, int windowIndex) { + if (windowIndex >= oldTimeline.getWindowCount()) { + return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET; + } + oldTimeline.getWindow(windowIndex, window); + for (int periodIndex = window.firstPeriodIndex; + periodIndex <= window.lastPeriodIndex; + periodIndex++) { + Object periodUid = oldTimeline.getUidOfPeriod(periodIndex); + int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid); + if (newPeriodIndex != C.INDEX_UNSET) { + return newTimeline.getPeriod(newPeriodIndex, period).windowIndex; + } + } + return C.INDEX_UNSET; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java new file mode 100644 index 0000000000..53d63e23fc --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java @@ -0,0 +1,120 @@ +/* + * 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.google.android.exoplayer2.analytics; + +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; + +/** + * Manager for active playback sessions. + * + *

    The manager keeps track of the association between window index and/or media period id to + * session identifier. + */ +public interface PlaybackSessionManager { + + /** A listener for session updates. */ + interface Listener { + + /** + * Called when a new session is created as a result of {@link #updateSessions(EventTime)}. + * + * @param eventTime The {@link EventTime} at which the session is created. + * @param sessionId The identifier of the new session. + */ + void onSessionCreated(EventTime eventTime, String sessionId); + + /** + * Called when a session becomes active, i.e. playing in the foreground. + * + * @param eventTime The {@link EventTime} at which the session becomes active. + * @param sessionId The identifier of the session. + */ + void onSessionActive(EventTime eventTime, String sessionId); + + /** + * Called when a session is interrupted by ad playback. + * + * @param eventTime The {@link EventTime} at which the ad playback starts. + * @param contentSessionId The session identifier of the content session. + * @param adSessionId The identifier of the ad session. + */ + void onAdPlaybackStarted(EventTime eventTime, String contentSessionId, String adSessionId); + + /** + * Called when a session is permanently finished. + * + * @param eventTime The {@link EventTime} at which the session finished. + * @param sessionId The identifier of the finished session. + * @param automaticTransitionToNextPlayback Whether the session finished because of an automatic + * transition to the next playback item. + */ + void onSessionFinished( + EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback); + } + + /** + * Sets the listener to be notified of session updates. Must be called before the session manager + * is used. + * + * @param listener The {@link Listener} to be notified of session updates. + */ + void setListener(Listener listener); + + /** + * Returns the session identifier for the given media period id. + * + *

    Note that this will reserve a new session identifier if it doesn't exist yet, but will not + * call any {@link Listener} callbacks. + * + * @param timeline The timeline, {@code mediaPeriodId} is part of. + * @param mediaPeriodId A {@link MediaPeriodId}. + */ + String getSessionForMediaPeriodId(Timeline timeline, MediaPeriodId mediaPeriodId); + + /** + * Returns whether an event time belong to a session. + * + * @param eventTime The {@link EventTime}. + * @param sessionId A session identifier. + * @return Whether the event belongs to the specified session. + */ + boolean belongsToSession(EventTime eventTime, String sessionId); + + /** + * Updates or creates sessions based on a player {@link EventTime}. + * + * @param eventTime The {@link EventTime}. + */ + void updateSessions(EventTime eventTime); + + /** + * Updates the session associations to a new timeline. + * + * @param eventTime The event time with the timeline change. + */ + void handleTimelineUpdate(EventTime eventTime); + + /** + * Handles a position discontinuity. + * + * @param eventTime The event time of the position discontinuity. + * @param reason The {@link DiscontinuityReason}. + */ + void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java new file mode 100644 index 0000000000..2993e960b4 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java @@ -0,0 +1,957 @@ +/* + * 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.google.android.exoplayer2.analytics; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit test for {@link DefaultPlaybackSessionManager}. */ +@RunWith(AndroidJUnit4.class) +public final class DefaultPlaybackSessionManagerTest { + + private DefaultPlaybackSessionManager sessionManager; + + @Mock private PlaybackSessionManager.Listener mockListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + sessionManager = new DefaultPlaybackSessionManager(); + sessionManager.setListener(mockListener); + } + + @Test + public void updateSessions_withoutMediaPeriodId_createsNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + EventTime eventTime = createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId */ null); + + sessionManager.updateSessions(eventTime); + + verify(mockListener).onSessionCreated(eq(eventTime), anyString()); + verify(mockListener).onSessionActive(eq(eventTime), anyString()); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void updateSessions_withMediaPeriodId_createsNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + EventTime eventTime = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId); + + sessionManager.updateSessions(eventTime); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture()); + verify(mockListener).onSessionActive(eventTime, sessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId)) + .isEqualTo(sessionId.getValue()); + } + + @Test + public void + updateSessions_ofSameWindow_withMediaPeriodId_afterWithoutMediaPeriodId_doesNotCreateNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + EventTime eventTime1 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId)) + .isEqualTo(sessionId.getValue()); + } + + @Test + public void updateSessions_ofSameWindow_withAd_afterWithoutMediaPeriodId_createsNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0); + EventTime eventTime1 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor contentSessionId = ArgumentCaptor.forClass(String.class); + ArgumentCaptor adSessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), contentSessionId.capture()); + verify(mockListener).onSessionCreated(eq(eventTime2), adSessionId.capture()); + verify(mockListener).onSessionActive(eventTime1, contentSessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(contentSessionId).isNotEqualTo(adSessionId); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId)) + .isEqualTo(adSessionId.getValue()); + } + + @Test + public void + updateSessions_ofSameWindow_withoutMediaPeriodId_afterMediaPeriodId_doesNotCreateNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId); + EventTime eventTime2 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId)) + .isEqualTo(sessionId.getValue()); + } + + @Test + public void updateSessions_ofSameWindow_withoutMediaPeriodId_afterAd_doesNotCreateNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId); + EventTime eventTime2 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId)) + .isEqualTo(sessionId.getValue()); + } + + @Test + public void updateSessions_withOtherMediaPeriodId_ofSameWindow_doesNotCreateNewSession() { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 0)); + MediaPeriodId mediaPeriodId1 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + MediaPeriodId mediaPeriodId2 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1)) + .isEqualTo(sessionId.getValue()); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2)) + .isEqualTo(sessionId.getValue()); + } + + @Test + public void updateSessions_withAd_ofSameWindow_createsNewSession() { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 0)); + MediaPeriodId mediaPeriodId1 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + MediaPeriodId mediaPeriodId2 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor contentSessionId = ArgumentCaptor.forClass(String.class); + ArgumentCaptor adSessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), contentSessionId.capture()); + verify(mockListener).onSessionActive(eventTime1, contentSessionId.getValue()); + verify(mockListener).onSessionCreated(eq(eventTime2), adSessionId.capture()); + verifyNoMoreInteractions(mockListener); + assertThat(contentSessionId).isNotEqualTo(adSessionId); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1)) + .isEqualTo(contentSessionId.getValue()); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2)) + .isEqualTo(adSessionId.getValue()); + } + + @Test + public void updateSessions_ofOtherWindow_createsNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + EventTime eventTime1 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + EventTime eventTime2 = + createEventTime(timeline, /* windowIndex= */ 1, /* mediaPeriodId= */ null); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class); + ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture()); + verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionId1).isNotEqualTo(sessionId2); + } + + @Test + public void updateSessions_withMediaPeriodId_ofOtherWindow_createsNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + MediaPeriodId mediaPeriodId1 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + MediaPeriodId mediaPeriodId2 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class); + ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture()); + verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionId1).isNotEqualTo(sessionId2); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1)) + .isEqualTo(sessionId1.getValue()); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2)) + .isEqualTo(sessionId2.getValue()); + } + + @Test + public void updateSessions_ofSameWindow_withNewWindowSequenceNumber_createsNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId1 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + MediaPeriodId mediaPeriodId2 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class); + ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue()); + verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionId1).isNotEqualTo(sessionId2); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1)) + .isEqualTo(sessionId1.getValue()); + assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2)) + .isEqualTo(sessionId2.getValue()); + } + + @Test + public void + updateSessions_withoutMediaPeriodId_andPreviouslyCreatedSessions_doesNotCreateNewSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId1 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + MediaPeriodId mediaPeriodId2 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1); + MediaPeriodId mediaPeriodIdWithAd = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2); + EventTime eventTime3 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodIdWithAd); + EventTime eventTime4 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + sessionManager.updateSessions(eventTime3); + sessionManager.updateSessions(eventTime4); + + verify(mockListener).onSessionCreated(eq(eventTime1), anyString()); + verify(mockListener).onSessionActive(eq(eventTime1), anyString()); + verify(mockListener).onSessionCreated(eq(eventTime2), anyString()); + verify(mockListener).onSessionCreated(eq(eventTime3), anyString()); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void getSessionForMediaPeriodId_returnsValue_butDoesNotCreateSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + String session = sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId); + + assertThat(session).isNotEmpty(); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void updateSessions_afterSessionForMediaPeriodId_withSameMediaPeriodId_returnsSameValue() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + EventTime eventTime = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId); + + String expectedSessionId = sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId); + sessionManager.updateSessions(eventTime); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture()); + verify(mockListener).onSessionActive(eventTime, sessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionId.getValue()).isEqualTo(expectedSessionId); + } + + @Test + public void updateSessions_withoutMediaPeriodId_afterSessionForMediaPeriodId_returnsSameValue() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + EventTime eventTime = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + + String expectedSessionId = sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId); + sessionManager.updateSessions(eventTime); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture()); + verify(mockListener).onSessionActive(eventTime, sessionId.getValue()); + verifyNoMoreInteractions(mockListener); + assertThat(sessionId.getValue()).isEqualTo(expectedSessionId); + } + + @Test + public void belongsToSession_withSameWindowIndex_returnsTrue() { + EventTime eventTime = + createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + EventTime eventTimeWithTimeline = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + EventTime eventTimeWithMediaPeriodId = + createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId); + sessionManager.updateSessions(eventTime); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture()); + assertThat(sessionManager.belongsToSession(eventTime, sessionId.getValue())).isTrue(); + assertThat(sessionManager.belongsToSession(eventTimeWithTimeline, sessionId.getValue())) + .isTrue(); + assertThat(sessionManager.belongsToSession(eventTimeWithMediaPeriodId, sessionId.getValue())) + .isTrue(); + } + + @Test + public void belongsToSession_withOtherWindowIndex_returnsFalse() { + EventTime eventTime = + createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + EventTime eventTimeOtherWindow = + createEventTime(Timeline.EMPTY, /* windowIndex= */ 1, /* mediaPeriodId= */ null); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1); + EventTime eventTimeWithOtherMediaPeriodId = + createEventTime(timeline, /* windowIndex= */ 1, mediaPeriodId); + sessionManager.updateSessions(eventTime); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture()); + assertThat(sessionManager.belongsToSession(eventTimeOtherWindow, sessionId.getValue())) + .isFalse(); + assertThat( + sessionManager.belongsToSession(eventTimeWithOtherMediaPeriodId, sessionId.getValue())) + .isFalse(); + } + + @Test + public void belongsToSession_withOtherWindowSequenceNumber_returnsFalse() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId1 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + MediaPeriodId mediaPeriodId2 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2); + sessionManager.updateSessions(eventTime1); + + ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture()); + assertThat(sessionManager.belongsToSession(eventTime2, sessionId.getValue())).isFalse(); + } + + @Test + public void belongsToSession_withAd_returnsFalse() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaPeriodId mediaPeriodId1 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + MediaPeriodId mediaPeriodId2 = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1); + EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1); + EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2); + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class); + ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture()); + verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture()); + assertThat(sessionManager.belongsToSession(eventTime2, sessionId1.getValue())).isFalse(); + assertThat(sessionManager.belongsToSession(eventTime1, sessionId2.getValue())).isFalse(); + assertThat(sessionManager.belongsToSession(eventTime2, sessionId2.getValue())).isTrue(); + } + + @Test + public void initialTimelineUpdate_finishesAllSessionsOutsideTimeline() { + EventTime eventTime1 = + createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + EventTime eventTime2 = + createEventTime(Timeline.EMPTY, /* windowIndex= */ 1, /* mediaPeriodId= */ null); + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + EventTime newTimelineEventTime = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + + sessionManager.handleTimelineUpdate(newTimelineEventTime); + + ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class); + ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class); + verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture()); + verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture()); + verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue()); + verify(mockListener) + .onSessionFinished( + newTimelineEventTime, + sessionId2.getValue(), + /* automaticTransitionToNextPlayback= */ false); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void dynamicTimelineUpdate_resolvesWindowIndices() { + Timeline initialTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 100), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 200), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 300)); + EventTime eventForInitialTimelineId100 = + createEventTime( + initialTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + initialTimeline.getUidOfPeriod(/* periodIndex= */ 1), + /* windowSequenceNumber= */ 0)); + EventTime eventForInitialTimelineId200 = + createEventTime( + initialTimeline, + /* windowIndex= */ 1, + new MediaPeriodId( + initialTimeline.getUidOfPeriod(/* periodIndex= */ 2), + /* windowSequenceNumber= */ 1)); + EventTime eventForInitialTimelineId300 = + createEventTime( + initialTimeline, + /* windowIndex= */ 2, + new MediaPeriodId( + initialTimeline.getUidOfPeriod(/* periodIndex= */ 3), + /* windowSequenceNumber= */ 2)); + sessionManager.handleTimelineUpdate(eventForInitialTimelineId100); + sessionManager.updateSessions(eventForInitialTimelineId100); + sessionManager.updateSessions(eventForInitialTimelineId200); + sessionManager.updateSessions(eventForInitialTimelineId300); + String sessionId100 = + sessionManager.getSessionForMediaPeriodId( + initialTimeline, eventForInitialTimelineId100.mediaPeriodId); + String sessionId200 = + sessionManager.getSessionForMediaPeriodId( + initialTimeline, eventForInitialTimelineId200.mediaPeriodId); + String sessionId300 = + sessionManager.getSessionForMediaPeriodId( + initialTimeline, eventForInitialTimelineId300.mediaPeriodId); + + Timeline timelineUpdate = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 300), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 100)); + EventTime eventForTimelineUpdateId100 = + createEventTime( + timelineUpdate, + /* windowIndex= */ 1, + new MediaPeriodId( + timelineUpdate.getUidOfPeriod(/* periodIndex= */ 1), + /* windowSequenceNumber= */ 0)); + EventTime eventForTimelineUpdateId300 = + createEventTime( + timelineUpdate, + /* windowIndex= */ 0, + new MediaPeriodId( + timelineUpdate.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 2)); + + sessionManager.handleTimelineUpdate(eventForTimelineUpdateId100); + String updatedSessionId100 = + sessionManager.getSessionForMediaPeriodId( + timelineUpdate, eventForTimelineUpdateId100.mediaPeriodId); + String updatedSessionId300 = + sessionManager.getSessionForMediaPeriodId( + timelineUpdate, eventForTimelineUpdateId300.mediaPeriodId); + + verify(mockListener).onSessionCreated(eventForInitialTimelineId100, sessionId100); + verify(mockListener).onSessionActive(eventForInitialTimelineId100, sessionId100); + verify(mockListener).onSessionCreated(eventForInitialTimelineId200, sessionId200); + verify(mockListener).onSessionCreated(eventForInitialTimelineId300, sessionId300); + verify(mockListener) + .onSessionFinished( + eventForTimelineUpdateId100, + sessionId200, + /* automaticTransitionToNextPlayback= */ false); + verifyNoMoreInteractions(mockListener); + assertThat(updatedSessionId100).isEqualTo(sessionId100); + assertThat(updatedSessionId300).isEqualTo(sessionId300); + } + + @Test + public void positionDiscontinuity_withinWindow_doesNotFinishSession() { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 100)); + EventTime eventTime1 = + createEventTime( + timeline, + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + EventTime eventTime2 = + createEventTime( + timeline, + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); + sessionManager.handleTimelineUpdate(eventTime1); + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + sessionManager.handlePositionDiscontinuity( + eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + + verify(mockListener).onSessionCreated(eq(eventTime1), anyString()); + verify(mockListener).onSessionActive(eq(eventTime1), anyString()); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void positionDiscontinuity_toNewWindow_withPeriodTransitionReason_finishesSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + EventTime eventTime1 = + createEventTime( + timeline, + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + EventTime eventTime2 = + createEventTime( + timeline, + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); + sessionManager.handleTimelineUpdate(eventTime1); + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + String sessionId1 = + sessionManager.getSessionForMediaPeriodId(timeline, eventTime1.mediaPeriodId); + String sessionId2 = + sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId); + + sessionManager.handlePositionDiscontinuity( + eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + + verify(mockListener).onSessionCreated(eventTime1, sessionId1); + verify(mockListener).onSessionActive(eventTime1, sessionId1); + verify(mockListener).onSessionCreated(eq(eventTime2), anyString()); + verify(mockListener) + .onSessionFinished(eventTime2, sessionId1, /* automaticTransitionToNextPlayback= */ true); + verify(mockListener).onSessionActive(eventTime2, sessionId2); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void positionDiscontinuity_toNewWindow_withSeekTransitionReason_finishesSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + EventTime eventTime1 = + createEventTime( + timeline, + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + EventTime eventTime2 = + createEventTime( + timeline, + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); + sessionManager.handleTimelineUpdate(eventTime1); + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + String sessionId1 = + sessionManager.getSessionForMediaPeriodId(timeline, eventTime1.mediaPeriodId); + String sessionId2 = + sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId); + + sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK); + + verify(mockListener).onSessionCreated(eventTime1, sessionId1); + verify(mockListener).onSessionActive(eventTime1, sessionId1); + verify(mockListener).onSessionCreated(eq(eventTime2), anyString()); + verify(mockListener) + .onSessionFinished(eventTime2, sessionId1, /* automaticTransitionToNextPlayback= */ false); + verify(mockListener).onSessionActive(eventTime2, sessionId2); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void positionDiscontinuity_toSameWindow_withoutMediaPeriodId_doesNotFinishSession() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + EventTime eventTime1 = + createEventTime( + timeline, + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + EventTime eventTime2 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + sessionManager.handleTimelineUpdate(eventTime1); + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + + sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK); + + verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); + } + + @Test + public void positionDiscontinuity_toNewWindow_finishesOnlyPastSessions() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 4); + EventTime eventTime1 = + createEventTime( + timeline, + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + EventTime eventTime2 = + createEventTime( + timeline, + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); + EventTime eventTime3 = + createEventTime( + timeline, + /* windowIndex= */ 2, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 2), /* windowSequenceNumber= */ 2)); + EventTime eventTime4 = + createEventTime( + timeline, + /* windowIndex= */ 3, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 3), /* windowSequenceNumber= */ 3)); + sessionManager.handleTimelineUpdate(eventTime1); + sessionManager.updateSessions(eventTime1); + sessionManager.updateSessions(eventTime2); + sessionManager.updateSessions(eventTime3); + sessionManager.updateSessions(eventTime4); + String sessionId1 = + sessionManager.getSessionForMediaPeriodId(timeline, eventTime1.mediaPeriodId); + String sessionId2 = + sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId); + + sessionManager.handlePositionDiscontinuity(eventTime3, Player.DISCONTINUITY_REASON_SEEK); + + verify(mockListener).onSessionCreated(eventTime1, sessionId1); + verify(mockListener).onSessionActive(eventTime1, sessionId1); + verify(mockListener).onSessionCreated(eventTime2, sessionId2); + verify(mockListener).onSessionCreated(eq(eventTime3), anyString()); + verify(mockListener).onSessionCreated(eq(eventTime4), anyString()); + verify(mockListener) + .onSessionFinished(eventTime3, sessionId1, /* automaticTransitionToNextPlayback= */ false); + verify(mockListener) + .onSessionFinished(eventTime3, sessionId2, /* automaticTransitionToNextPlayback= */ false); + verify(mockListener).onSessionActive(eq(eventTime3), anyString()); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void positionDiscontinuity_fromAdToContent_finishesAd() { + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs =*/ 10 * C.MICROS_PER_SECOND, + new AdPlaybackState(/* adGroupTimesUs= */ 0, 5 * C.MICROS_PER_SECOND) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); + EventTime adEventTime1 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime adEventTime2 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime contentEventTime = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0, + /* nextAdGroupIndex= */ 1)); + sessionManager.handleTimelineUpdate(adEventTime1); + sessionManager.updateSessions(adEventTime1); + sessionManager.updateSessions(adEventTime2); + String adSessionId1 = + sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime1.mediaPeriodId); + + sessionManager.handlePositionDiscontinuity( + contentEventTime, Player.DISCONTINUITY_REASON_AD_INSERTION); + + verify(mockListener).onSessionCreated(adEventTime1, adSessionId1); + verify(mockListener).onSessionActive(adEventTime1, adSessionId1); + verify(mockListener).onSessionCreated(eq(adEventTime2), anyString()); + verify(mockListener) + .onSessionFinished( + contentEventTime, adSessionId1, /* automaticTransitionToNextPlayback= */ true); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void positionDiscontinuity_fromContentToAd_doesNotFinishSessions() { + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs =*/ 10 * C.MICROS_PER_SECOND, + new AdPlaybackState( + /* adGroupTimesUs= */ 2 * C.MICROS_PER_SECOND, 5 * C.MICROS_PER_SECOND) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); + EventTime adEventTime1 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime adEventTime2 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime contentEventTime = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0, + /* nextAdGroupIndex= */ 0)); + sessionManager.handleTimelineUpdate(contentEventTime); + sessionManager.updateSessions(contentEventTime); + sessionManager.updateSessions(adEventTime1); + sessionManager.updateSessions(adEventTime2); + + sessionManager.handlePositionDiscontinuity( + adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + + verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); + } + + @Test + public void positionDiscontinuity_fromAdToAd_finishesPastAds_andNotifiesAdPlaybackStated() { + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs =*/ 10 * C.MICROS_PER_SECOND, + new AdPlaybackState(/* adGroupTimesUs= */ 0, 5 * C.MICROS_PER_SECOND) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); + EventTime adEventTime1 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime adEventTime2 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime contentEventTime = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0, + /* nextAdGroupIndex= */ 1)); + sessionManager.handleTimelineUpdate(contentEventTime); + sessionManager.updateSessions(contentEventTime); + sessionManager.updateSessions(adEventTime1); + sessionManager.updateSessions(adEventTime2); + String contentSessionId = + sessionManager.getSessionForMediaPeriodId(adTimeline, contentEventTime.mediaPeriodId); + String adSessionId1 = + sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime1.mediaPeriodId); + String adSessionId2 = + sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime2.mediaPeriodId); + + sessionManager.handlePositionDiscontinuity( + adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.handlePositionDiscontinuity(adEventTime2, Player.DISCONTINUITY_REASON_SEEK); + + verify(mockListener).onSessionCreated(eq(contentEventTime), anyString()); + verify(mockListener).onSessionActive(eq(contentEventTime), anyString()); + verify(mockListener).onSessionCreated(adEventTime1, adSessionId1); + verify(mockListener).onSessionCreated(adEventTime2, adSessionId2); + verify(mockListener).onAdPlaybackStarted(adEventTime1, contentSessionId, adSessionId1); + verify(mockListener).onSessionActive(adEventTime1, adSessionId1); + verify(mockListener) + .onSessionFinished( + adEventTime2, adSessionId1, /* automaticTransitionToNextPlayback= */ false); + verify(mockListener).onAdPlaybackStarted(adEventTime2, contentSessionId, adSessionId2); + verify(mockListener).onSessionActive(adEventTime2, adSessionId2); + verifyNoMoreInteractions(mockListener); + } + + private static EventTime createEventTime( + Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + return new EventTime( + /* realtimeMs = */ 0, + timeline, + windowIndex, + mediaPeriodId, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + } +} From 44ad47746a452ac24a37af37053afe6be7dd9d31 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 29 Apr 2019 15:56:41 +0100 Subject: [PATCH 59/95] Never set null as a session meta data object. Issue: #5810 PiperOrigin-RevId: 245745646 --- .../ext/mediasession/MediaSessionConnector.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 24cf4062f7..9c80fabc50 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -146,6 +146,9 @@ public final class MediaSessionConnector { private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; + private static final MediaMetadataCompat METADATA_EMPTY = + new MediaMetadataCompat.Builder().build(); + /** Receiver of media commands sent by a media controller. */ public interface CommandReceiver { /** @@ -639,8 +642,8 @@ public final class MediaSessionConnector { MediaMetadataCompat metadata = mediaMetadataProvider != null && player != null ? mediaMetadataProvider.getMetadata(player) - : null; - mediaSession.setMetadata(metadata); + : METADATA_EMPTY; + mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY); } /** @@ -888,7 +891,7 @@ public final class MediaSessionConnector { @Override public MediaMetadataCompat getMetadata(Player player) { if (player.getCurrentTimeline().isEmpty()) { - return null; + return METADATA_EMPTY; } MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); if (player.isPlayingAd()) { From 51914f8f82c8c17e16e4e59f9a64b9204d74ce0d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Apr 2019 10:56:23 +0100 Subject: [PATCH 60/95] Rename DownloadThread to Task This resolves some naming confusion that previously existed as a result of DownloadThread also being used for removals. Some related variables (e.g. activeDownloadCount) would refer to both download and removal tasks, whilst others (e.g. maxParallelDownloads) would refer only to downloads. This change renames those that refer to both to use "task" terminology. This change also includes minor test edits. PiperOrigin-RevId: 245913671 --- .../exoplayer2/offline/DownloadManager.java | 121 +++++++++--------- .../exoplayer2/offline/DownloadBuilder.java | 4 +- .../offline/DownloadManagerTest.java | 4 +- 3 files changed, 68 insertions(+), 61 deletions(-) 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 aa0cd12231..7ad22e000a 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 @@ -137,7 +137,7 @@ public final class DownloadManager { private static final int MSG_SET_MIN_RETRY_COUNT = 5; private static final int MSG_ADD_DOWNLOAD = 6; private static final int MSG_REMOVE_DOWNLOAD = 7; - private static final int MSG_DOWNLOAD_THREAD_STOPPED = 8; + private static final int MSG_TASK_STOPPED = 8; private static final int MSG_CONTENT_LENGTH_CHANGED = 9; private static final int MSG_RELEASE = 10; @@ -168,7 +168,7 @@ public final class DownloadManager { private final ArrayList downloads; private int pendingMessages; - private int activeDownloadCount; + private int activeTaskCount; private boolean initialized; private boolean downloadsPaused; private int maxParallelDownloads; @@ -244,7 +244,7 @@ public final class DownloadManager { * download requirements are not met). */ public boolean isIdle() { - return activeDownloadCount == 0 && pendingMessages == 0; + return activeTaskCount == 0 && pendingMessages == 0; } /** @@ -465,7 +465,7 @@ public final class DownloadManager { mainHandler.removeCallbacksAndMessages(/* token= */ null); // Reset state. pendingMessages = 0; - activeDownloadCount = 0; + activeTaskCount = 0; initialized = false; downloads.clear(); } @@ -503,8 +503,8 @@ public final class DownloadManager { break; case MSG_PROCESSED: int processedMessageCount = message.arg1; - int activeDownloadCount = message.arg2; - onMessageProcessed(processedMessageCount, activeDownloadCount); + int activeTaskCount = message.arg2; + onMessageProcessed(processedMessageCount, activeTaskCount); break; default: throw new IllegalStateException(); @@ -543,9 +543,9 @@ public final class DownloadManager { } } - private void onMessageProcessed(int processedMessageCount, int activeDownloadCount) { + private void onMessageProcessed(int processedMessageCount, int activeTaskCount) { this.pendingMessages -= processedMessageCount; - this.activeDownloadCount = activeDownloadCount; + this.activeTaskCount = activeTaskCount; if (isIdle()) { for (Listener listener : listeners) { listener.onIdle(this); @@ -627,7 +627,7 @@ public final class DownloadManager { private final DownloaderFactory downloaderFactory; private final Handler mainHandler; private final ArrayList downloadInternals; - private final HashMap downloadThreads; + private final HashMap activeTasks; // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; @@ -653,7 +653,7 @@ public final class DownloadManager { this.minRetryCount = minRetryCount; this.downloadsPaused = downloadsPaused; downloadInternals = new ArrayList<>(); - downloadThreads = new HashMap<>(); + activeTasks = new HashMap<>(); } @Override @@ -694,14 +694,14 @@ public final class DownloadManager { id = (String) message.obj; removeDownload(id); break; - case MSG_DOWNLOAD_THREAD_STOPPED: - DownloadThread downloadThread = (DownloadThread) message.obj; - onDownloadThreadStopped(downloadThread); + case MSG_TASK_STOPPED: + Task task = (Task) message.obj; + onTaskStopped(task); processedExternalMessage = false; // This message is posted internally. break; case MSG_CONTENT_LENGTH_CHANGED: - downloadThread = (DownloadThread) message.obj; - onDownloadThreadContentLengthChanged(downloadThread); + task = (Task) message.obj; + onContentLengthChanged(task); processedExternalMessage = false; // This message is posted internally. break; case MSG_RELEASE: @@ -711,7 +711,7 @@ public final class DownloadManager { throw new IllegalStateException(); } mainHandler - .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, downloadThreads.size()) + .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, activeTasks.size()) .sendToTarget(); } @@ -832,18 +832,17 @@ public final class DownloadManager { } } - private void onDownloadThreadStopped(DownloadThread downloadThread) { - logd("Download is stopped", downloadThread.request); - String downloadId = downloadThread.request.id; - downloadThreads.remove(downloadId); + private void onTaskStopped(Task task) { + logd("Task is stopped", task.request); + String downloadId = task.request.id; + activeTasks.remove(downloadId); boolean tryToStartDownloads = false; - if (!downloadThread.isRemove) { + if (!task.isRemove) { // If maxParallelDownloads was hit, there might be a download waiting for a slot. tryToStartDownloads = parallelDownloads == maxParallelDownloads; parallelDownloads--; } - getDownload(downloadId) - .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); + getDownload(downloadId).onTaskStopped(task.isCanceled, task.finalError); if (tryToStartDownloads) { for (int i = 0; parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); @@ -853,16 +852,16 @@ public final class DownloadManager { } } - private void onDownloadThreadContentLengthChanged(DownloadThread downloadThread) { - String downloadId = downloadThread.request.id; - getDownload(downloadId).setContentLength(downloadThread.contentLength); + private void onContentLengthChanged(Task task) { + String downloadId = task.request.id; + getDownload(downloadId).setContentLength(task.contentLength); } private void release() { - for (DownloadThread downloadThread : downloadThreads.values()) { - downloadThread.cancel(/* released= */ true); + for (Task task : activeTasks.values()) { + task.cancel(/* released= */ true); } - downloadThreads.clear(); + activeTasks.clear(); downloadInternals.clear(); thread.quit(); synchronized (this) { @@ -871,7 +870,7 @@ public final class DownloadManager { } } - private void onDownloadChangedInternal(DownloadInternal downloadInternal, Download download) { + private void onDownloadChanged(DownloadInternal downloadInternal, Download download) { logd("Download state is changed", downloadInternal); try { downloadIndex.putDownload(download); @@ -884,7 +883,7 @@ public final class DownloadManager { mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); } - private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Download download) { + private void onDownloadRemoved(DownloadInternal downloadInternal, Download download) { logd("Download is removed", downloadInternal); try { downloadIndex.removeDownload(download.request.id); @@ -896,11 +895,11 @@ public final class DownloadManager { } @StartThreadResults - private int startDownloadThread(DownloadInternal downloadInternal) { + private int startTask(DownloadInternal downloadInternal) { DownloadRequest request = downloadInternal.download.request; String downloadId = request.id; - if (downloadThreads.containsKey(downloadId)) { - if (stopDownloadThreadInternal(downloadId)) { + if (activeTasks.containsKey(downloadId)) { + if (stopDownloadTask(downloadId)) { return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; } return START_THREAD_WAIT_REMOVAL_TO_FINISH; @@ -914,19 +913,25 @@ public final class DownloadManager { } Downloader downloader = downloaderFactory.createDownloader(request); DownloadProgress downloadProgress = downloadInternal.download.progress; - DownloadThread downloadThread = - new DownloadThread(request, downloader, downloadProgress, isRemove, minRetryCount, this); - downloadThreads.put(downloadId, downloadThread); - downloadThread.start(); - logd("Download is started", downloadInternal); + Task task = + new Task( + request, + downloader, + downloadProgress, + isRemove, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(downloadId, task); + task.start(); + logd("Task is started", downloadInternal); return START_THREAD_SUCCEEDED; } - private boolean stopDownloadThreadInternal(String downloadId) { - DownloadThread downloadThread = downloadThreads.get(downloadId); - if (downloadThread != null && !downloadThread.isRemove) { - downloadThread.cancel(/* released= */ false); - logd("Download is cancelled", downloadThread.request); + private boolean stopDownloadTask(String downloadId) { + Task task = activeTasks.get(downloadId); + if (task != null && !task.isRemove) { + task.cancel(/* released= */ false); + logd("Task is cancelled", task.request); return true; } return false; @@ -1025,7 +1030,7 @@ public final class DownloadManager { if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { startOrQueue(); } else if (isInRemoveState()) { - internalHandler.startDownloadThread(this); + internalHandler.startTask(this); } } @@ -1043,7 +1048,7 @@ public final class DownloadManager { return; } this.contentLength = contentLength; - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } private void updateStopState() { @@ -1054,12 +1059,12 @@ public final class DownloadManager { } } else { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - internalHandler.stopDownloadThreadInternal(download.request.id); + internalHandler.stopDownloadTask(download.request.id); setState(STATE_STOPPED); } } if (oldDownload == download) { - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } } @@ -1068,14 +1073,14 @@ public final class DownloadManager { // state immediately. state = initialState; if (isInRemoveState()) { - internalHandler.startDownloadThread(this); + internalHandler.startTask(this); } else if (canStart()) { startOrQueue(); } else { setState(STATE_STOPPED); } if (state == initialState) { - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } } @@ -1085,7 +1090,7 @@ public final class DownloadManager { private void startOrQueue() { Assertions.checkState(!isInRemoveState()); - @StartThreadResults int result = internalHandler.startDownloadThread(this); + @StartThreadResults int result = internalHandler.startTask(this); Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) { setState(STATE_DOWNLOADING); @@ -1097,18 +1102,18 @@ public final class DownloadManager { private void setState(@Download.State int newState) { if (state != newState) { state = newState; - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } } - private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable error) { + private void onTaskStopped(boolean isCanceled, @Nullable Throwable error) { if (isIdle()) { return; } if (isCanceled) { - internalHandler.startDownloadThread(this); + internalHandler.startTask(this); } else if (state == STATE_REMOVING) { - internalHandler.onDownloadRemovedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadRemoved(this, getUpdatedDownload()); } else if (state == STATE_RESTARTING) { initialize(STATE_QUEUED); } else { // STATE_DOWNLOADING @@ -1123,7 +1128,7 @@ public final class DownloadManager { } } - private static class DownloadThread extends Thread implements Downloader.ProgressListener { + private static class Task extends Thread implements Downloader.ProgressListener { private final DownloadRequest request; private final Downloader downloader; @@ -1137,7 +1142,7 @@ public final class DownloadManager { private long contentLength; - private DownloadThread( + private Task( DownloadRequest request, Downloader downloader, DownloadProgress downloadProgress, @@ -1203,7 +1208,7 @@ public final class DownloadManager { } Handler internalHandler = this.internalHandler; if (internalHandler != null) { - internalHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); + internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java index f901b00f53..e07166a21c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java @@ -40,7 +40,7 @@ import java.util.List; @Nullable private String cacheKey; private byte[] customMetadata; - private int state; + @Download.State private int state; private long startTimeMs; private long updateTimeMs; private long contentLength; @@ -111,7 +111,7 @@ import java.util.List; return this; } - public DownloadBuilder setState(int state) { + public DownloadBuilder setState(@Download.State int state) { this.state = state; return this; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index b1d5e1fc29..f11bc5585b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -359,7 +359,7 @@ public class DownloadManagerTest { } @Test - public void stopAndResume() throws Throwable { + public void pauseAndResume() throws Throwable { DownloadRunner runner1 = new DownloadRunner(uri1); DownloadRunner runner2 = new DownloadRunner(uri2); DownloadRunner runner3 = new DownloadRunner(uri3); @@ -370,10 +370,12 @@ public class DownloadManagerTest { runOnMainThread(() -> downloadManager.pauseDownloads()); + // TODO: This should be assertQueued. Fix implementation and update test. runner1.getTask().assertStopped(); // remove requests aren't stopped. runner2.getDownloader(1).unblock().assertReleased(); + // TODO: This should be assertQueued. Fix implementation and update test. runner2.getTask().assertStopped(); // Although remove2 is finished, download2 doesn't start. runner2.getDownloader(2).assertDoesNotStart(); From 802ebc8db10336a9fc2a60b4128d3e522a549645 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Apr 2019 12:25:04 +0100 Subject: [PATCH 61/95] DownloadManager improvements - Do requirements TODO - Add useful helper method to retrieve not met requirements - Fix WritableDownloadIndex Javadoc PiperOrigin-RevId: 245922903 --- .../exoplayer2/offline/DownloadManager.java | 25 +++++++++++++----- .../offline/WritableDownloadIndex.java | 26 +++++++++---------- 2 files changed, 31 insertions(+), 20 deletions(-) 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 7ad22e000a..8502a56ea7 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 @@ -173,6 +173,7 @@ public final class DownloadManager { private boolean downloadsPaused; private int maxParallelDownloads; private int minRetryCount; + private int notMetRequirements; private RequirementsWatcher requirementsWatcher; /** @@ -212,7 +213,7 @@ public final class DownloadManager { requirementsListener = this::onRequirementsStateChanged; requirementsWatcher = new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); - int notMetRequirements = requirementsWatcher.start(); + notMetRequirements = requirementsWatcher.start(); mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); @@ -274,11 +275,21 @@ public final class DownloadManager { listeners.remove(listener); } - /** Returns the requirements needed to be met to start downloads. */ + /** Returns the requirements needed to be met to progress. */ public Requirements getRequirements() { return requirementsWatcher.getRequirements(); } + /** + * Returns the requirements needed for downloads to progress that are not currently met. + * + * @return The not met {@link Requirements.RequirementFlags}, or 0 if all requirements are met. + */ + @Requirements.RequirementFlags + public int getNotMetRequirements() { + return getRequirements().getNotMetRequirements(context); + } + /** * Sets the requirements that need to be met for downloads to progress. * @@ -413,7 +424,7 @@ public final class DownloadManager { * @param request The download request. */ public void addDownload(DownloadRequest request) { - addDownload(request, Download.STOP_REASON_NONE); + addDownload(request, STOP_REASON_NONE); } /** @@ -478,6 +489,10 @@ public final class DownloadManager { for (Listener listener : listeners) { listener.onRequirementsStateChanged(this, requirements, notMetRequirements); } + if (this.notMetRequirements == notMetRequirements) { + return; + } + this.notMetRequirements = notMetRequirements; pendingMessages++; internalHandler .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0) @@ -747,10 +762,6 @@ public final class DownloadManager { } private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { - // TODO: Move this deduplication check to the main thread. - if (this.notMetRequirements == notMetRequirements) { - return; - } this.notMetRequirements = notMetRequirements; logdFlags("Not met requirements are changed", notMetRequirements); for (int i = 0; i < downloadInternals.size(); i++) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index 2306363cf5..00b08dc76a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -17,43 +17,43 @@ package com.google.android.exoplayer2.offline; import java.io.IOException; -/** An writable index of {@link Download Downloads}. */ +/** A writable index of {@link Download Downloads}. */ public interface WritableDownloadIndex extends DownloadIndex { /** * Adds or replaces a {@link Download}. * * @param download The {@link Download} to be added. - * @throws throws IOException If an error occurs setting the state. + * @throws IOException If an error occurs setting the state. */ void putDownload(Download download) throws IOException; /** - * Removes the {@link Download} with the given {@code id}. + * Removes the download with the given ID. Does nothing if a download with the given ID does not + * exist. * - * @param id ID of a {@link Download}. - * @throws throws IOException If an error occurs removing the state. + * @param id The ID of the download to remove. + * @throws IOException If an error occurs removing the state. */ void removeDownload(String id) throws IOException; + /** * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). * * @param stopReason The stop reason. - * @throws throws IOException If an error occurs updating the state. + * @throws IOException If an error occurs updating the state. */ void setStopReason(int stopReason) throws IOException; /** - * Sets the stop reason of the download with the given {@code id} in a terminal state ({@link - * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). + * Sets the stop reason of the download with the given ID in a terminal state ({@link + * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). Does nothing if a download with the + * given ID does not exist, or if it's not in a terminal state. * - *

    If there's no {@link Download} with the given {@code id} or it isn't in a terminal state, - * then nothing happens. - * - * @param id ID of a {@link Download}. + * @param id The ID of the download to update. * @param stopReason The stop reason. - * @throws throws IOException If an error occurs updating the state. + * @throws IOException If an error occurs updating the state. */ void setStopReason(String id, int stopReason) throws IOException; } From f64011ae04632e23d6b5c4c20a8d90fad39e3bd6 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 30 Apr 2019 12:26:39 +0100 Subject: [PATCH 62/95] Prevent index out of bounds exceptions in some live HLS scenarios Can happen if the load position falls behind in every playlist and when we try to load the next segment, the adaptive selection logic decides to change variant. Issue:#5816 PiperOrigin-RevId: 245923006 --- RELEASENOTES.md | 2 ++ .../exoplayer2/source/hls/HlsChunkSource.java | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0685184a9a..6aea559602 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -112,6 +112,8 @@ ([#5441](https://github.com/google/ExoPlayer/issues/5441)). * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`. * Add metadata entry for HLS tracks to expose master playlist information. + * Prevent `IndexOutOfBoundsException` in some live HLS scenarios + ([#5816](https://github.com/google/ExoPlayer/issues/5816)). * Support for playing spherical videos on Daydream. * Cast extension: Work around Cast framework returning a limited-size queue items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)). diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 92756f19cf..261c9b531c 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -278,8 +278,7 @@ import java.util.Map; long chunkMediaSequence = getChunkMediaSequence( previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs); - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - if (previous != null && switchingTrack) { + if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) { // We try getting the next chunk without adapting in case that's the reason for falling // behind the live window. selectedTrackIndex = oldTrackIndex; @@ -289,10 +288,11 @@ import java.util.Map; startOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); chunkMediaSequence = previous.getNextChunkIndex(); - } else { - fatalError = new BehindLiveWindowException(); - return; - } + } + + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + fatalError = new BehindLiveWindowException(); + return; } int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence); From 0bb32a8f0917d589ecc5d90343986f57a7c70656 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 30 Apr 2019 12:51:34 +0100 Subject: [PATCH 63/95] Add IntDef for Player states. PiperOrigin-RevId: 245925254 --- .../castdemo/DefaultReceiverPlayerManager.java | 2 +- .../android/exoplayer2/demo/PlayerActivity.java | 2 +- .../android/exoplayer2/ext/cast/CastPlayer.java | 3 ++- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 2 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 2 +- .../android/exoplayer2/ext/ima/FakePlayer.java | 7 ++++--- .../ext/leanback/LeanbackPlayerAdapter.java | 8 ++++---- .../ext/mediasession/MediaSessionConnector.java | 2 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 2 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 2 +- .../android/exoplayer2/ExoPlayerImpl.java | 3 ++- .../google/android/exoplayer2/PlaybackInfo.java | 4 ++-- .../com/google/android/exoplayer2/Player.java | 17 +++++++++++++---- .../android/exoplayer2/SimpleExoPlayer.java | 1 + .../analytics/AnalyticsCollector.java | 2 +- .../exoplayer2/analytics/AnalyticsListener.java | 4 ++-- .../android/exoplayer2/util/EventLogger.java | 3 ++- .../android/exoplayer2/ExoPlayerTest.java | 10 +++++----- .../analytics/AnalyticsCollectorTest.java | 2 +- .../exoplayer2/ui/DebugTextViewHelper.java | 2 +- .../exoplayer2/ui/PlayerControlView.java | 2 +- .../ui/PlayerNotificationManager.java | 2 +- .../android/exoplayer2/ui/PlayerView.java | 2 +- .../android/exoplayer2/testutil/Action.java | 3 ++- .../exoplayer2/testutil/ExoHostedTest.java | 2 +- .../testutil/ExoPlayerTestRunner.java | 2 +- .../exoplayer2/testutil/StubExoPlayer.java | 1 + 27 files changed, 55 insertions(+), 39 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index 4b71b3a001..fcee88ec49 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -255,7 +255,7 @@ import java.util.ArrayList; // Player.EventListener implementation. @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { updateCurrentItemIndex(); } 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 35307eb5d8..8f46400670 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 @@ -627,7 +627,7 @@ public class PlayerActivity extends AppCompatActivity private class PlayerEventListener implements Player.EventListener { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playbackState == Player.STATE_ENDED) { showControls(); } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 14bb433d2b..390deac933 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -94,7 +94,7 @@ public final class CastPlayer extends BasePlayer { private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; - private int playbackState; + @Player.State private int playbackState; private int repeatMode; private int currentWindowIndex; private boolean playWhenReady; @@ -305,6 +305,7 @@ public final class CastPlayer extends BasePlayer { } @Override + @Player.State public int getPlaybackState() { return playbackState; } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 1cd9483178..12ef68ee3c 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -101,7 +101,7 @@ public class FlacPlaybackTest { } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playbackException != null)) { player.release(); diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 465ad51ac5..e860d07a14 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -963,7 +963,7 @@ public final class ImaAdsLoader } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (adsManager == null) { return; } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index d20ccbd728..a9d6a37fac 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -31,7 +31,7 @@ import java.util.ArrayList; private final Timeline timeline; private boolean prepared; - private int state; + @Player.State private int state; private boolean playWhenReady; private long position; private long contentPosition; @@ -96,8 +96,8 @@ import java.util.ArrayList; } } - /** Sets the state of this player with the given {@code STATE} constant. */ - public void setState(int state, boolean playWhenReady) { + /** Sets the {@link Player.State} of this player. */ + public void setState(@Player.State int state, boolean playWhenReady) { boolean notify = this.state != state || this.playWhenReady != playWhenReady; this.state = state; this.playWhenReady = playWhenReady; @@ -131,6 +131,7 @@ import java.util.ArrayList; } @Override + @Player.State public int getPlaybackState() { return state; } diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 3f4c5d6229..5705b73ab2 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -18,13 +18,13 @@ package com.google.android.exoplayer2.ext.leanback; import android.content.Context; import android.os.Handler; import androidx.annotation.Nullable; +import android.util.Pair; +import android.view.Surface; +import android.view.SurfaceHolder; import androidx.leanback.R; import androidx.leanback.media.PlaybackGlueHost; import androidx.leanback.media.PlayerAdapter; import androidx.leanback.media.SurfaceHolderGlueHost; -import android.util.Pair; -import android.view.Surface; -import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; @@ -271,7 +271,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab // Player.EventListener implementation. @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { notifyStateChanged(); } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 9c80fabc50..b922a2d0b8 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -994,7 +994,7 @@ public final class MediaSessionConnector { } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { invalidateMediaSessionPlaybackState(); } diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index dcd5f4957a..7c6835db0b 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -101,7 +101,7 @@ public class OpusPlaybackTest { } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playbackException != null)) { player.release(); diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 5120004889..5ebeca68d0 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -139,7 +139,7 @@ public class VpxPlaybackTest { } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playbackException != null)) { player.release(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 96b9072c5c..bea7af189a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override + @Player.State public int getPlaybackState() { return playbackInfo.playbackState; } @@ -658,7 +659,7 @@ import java.util.concurrent.CopyOnWriteArrayList; } private PlaybackInfo getResetPlaybackInfo( - boolean resetPosition, boolean resetState, int playbackState) { + boolean resetPosition, boolean resetState, @Player.State int playbackState) { if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index f45e61fb37..cf4643c5da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -52,7 +52,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; */ public final long contentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ - public final int playbackState; + @Player.State public final int playbackState; /** Whether the player is currently loading. */ public final boolean isLoading; /** The currently available track groups. */ @@ -128,7 +128,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; MediaPeriodId periodId, long startPositionUs, long contentPositionUs, - int playbackState, + @Player.State int playbackState, boolean isLoading, TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 8885be2e02..0e19212afa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -361,9 +361,9 @@ public interface Player { * #getPlaybackState()} changes. * * @param playWhenReady Whether playback will proceed when ready. - * @param playbackState One of the {@code STATE} constants. + * @param playbackState The new {@link State playback state}. */ - default void onPlayerStateChanged(boolean playWhenReady, int playbackState) {} + default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} /** * Called when the value of {@link #getRepeatMode()} changes. @@ -443,6 +443,14 @@ public interface Player { } } + /** + * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or + * {@link #STATE_ENDED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED}) + @interface State {} /** * The player does not have any media to play. */ @@ -581,10 +589,11 @@ public interface Player { void removeListener(EventListener listener); /** - * Returns the current state of the player. + * Returns the current {@link State playback state} of the player. * - * @return One of the {@code STATE} constants defined in this interface. + * @return The current {@link State playback state}. */ + @State int getPlaybackState(); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 910404a875..2ac71db44f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -873,6 +873,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override + @Player.State public int getPlaybackState() { verifyApplicationThread(); return player.getPlaybackState(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index c0d96e8e88..094024bc36 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -464,7 +464,7 @@ public class AnalyticsCollector } @Override - public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 7f74216cc8..9a0339f5d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -127,10 +127,10 @@ public interface AnalyticsListener { * * @param eventTime The event time. * @param playWhenReady Whether the playback will proceed when ready. - * @param playbackState One of the {@link Player}.STATE constants. + * @param playbackState The new {@link Player.State playback state}. */ default void onPlayerStateChanged( - EventTime eventTime, boolean playWhenReady, int playbackState) {} + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {} /** * Called when the timeline changed. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index bb3dc8b83a..7a2ea5daf2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -93,7 +93,8 @@ public class EventLogger implements AnalyticsListener { } @Override - public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int state) { + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int state) { logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 6d0627593f..639e80348b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -509,7 +509,7 @@ public final class ExoPlayerTest { private int currentPlaybackState = Player.STATE_IDLE; @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { currentPlaybackState = playbackState; } @@ -2113,7 +2113,7 @@ public final class ExoPlayerTest { final EventListener eventListener1 = new EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { eventListener1States.add(playbackState); if (playbackState == Player.STATE_READY) { playerReference.get().stop(/* reset= */ true); @@ -2123,7 +2123,7 @@ public final class ExoPlayerTest { final EventListener eventListener2 = new EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { eventListener2States.add(playbackState); } }; @@ -2170,7 +2170,7 @@ public final class ExoPlayerTest { } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { eventListenerPlayWhenReady.add(playWhenReady); eventListenerStates.add(playbackState); if (playbackState == Player.STATE_READY) { @@ -2219,7 +2219,7 @@ public final class ExoPlayerTest { EventListener eventListener = new EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playbackState == Player.STATE_READY && clockAtStartMs.get() == C.TIME_UNSET) { clockAtStartMs.set(clock.elapsedRealtime()); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index c455e39de6..2e9b539096 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -913,7 +913,7 @@ public final class AnalyticsCollectorTest { @Override public void onPlayerStateChanged( - EventTime eventTime, boolean playWhenReady, int playbackState) { + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { reportedEvents.add(new ReportedEvent(EVENT_PLAYER_STATE_CHANGED, eventTime)); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index da2081db31..0841296193 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -79,7 +79,7 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable { // Player.EventListener implementation. @Override - public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { updateAndPost(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index a5deb808c1..552774fe47 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -1130,7 +1130,7 @@ public class PlayerControlView extends FrameLayout { } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { updatePlayPauseButton(); updateProgress(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 1dbd696b12..aa9e4b1492 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -1276,7 +1276,7 @@ public class PlayerNotificationManager { private class PlayerListener implements Player.EventListener { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (wasPlayWhenReady != playWhenReady || lastPlaybackState != playbackState) { startOrUpdateNotification(); wasPlayWhenReady = playWhenReady; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 06f0927a99..dde35e6e99 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -1427,7 +1427,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Player.EventListener implementation @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { updateBuffering(); updateErrorMessage(); if (isPlayingAd() && controllerHideDuringAds) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index f1fdfc42aa..facbe8bbde 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -669,7 +669,8 @@ public abstract class Action { player.addListener( new Player.EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged( + boolean playWhenReady, @Player.State int playbackState) { if (targetPlaybackState == playbackState) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 74c0d4bb43..90f2294bfc 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -183,7 +183,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { @Override public final void onPlayerStateChanged( - EventTime eventTime, boolean playWhenReady, int playbackState) { + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]"); playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 517f1ce2e7..0d55dd8530 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -589,7 +589,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index a0d8c7f9d8..56de0a8b33 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -75,6 +75,7 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { } @Override + @Player.State public int getPlaybackState() { throw new UnsupportedOperationException(); } From c480818249b6c470990c4c1b7849969020d1f46f Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 30 Apr 2019 16:57:19 +0100 Subject: [PATCH 64/95] Fix some random AndroidStudio warnings. PiperOrigin-RevId: 245956915 --- .../exoplayer2/demo/DownloadTracker.java | 1 - .../mediasession/MediaSessionConnector.java | 3 +- .../mediasession/TimelineQueueNavigator.java | 2 +- .../java/com/google/android/exoplayer2/C.java | 1 - .../exoplayer2/ExoPlayerImplInternal.java | 1 - .../DefaultPlaybackSessionManager.java | 10 ++- .../exoplayer2/audio/DefaultAudioSink.java | 1 - .../exoplayer2/drm/DefaultDrmSession.java | 1 - .../android/exoplayer2/drm/DrmInitData.java | 1 - .../exoplayer2/drm/OfflineLicenseHelper.java | 4 +- .../exoplayer2/extractor/ogg/VorbisUtil.java | 66 +++++++++---------- .../exoplayer2/mediacodec/MediaCodecUtil.java | 1 - .../exoplayer2/metadata/MetadataRenderer.java | 1 - .../trackselection/MappingTrackSelector.java | 6 +- .../upstream/DefaultBandwidthMeter.java | 2 +- .../upstream/cache/CacheDataSink.java | 1 - .../upstream/cache/CachedContentIndex.java | 2 +- .../upstream/cache/SimpleCache.java | 1 - .../offline/ActionFileUpgradeUtilTest.java | 1 - .../offline/DownloadManagerTest.java | 13 ++-- .../AdaptiveTrackSelectionTest.java | 9 +-- .../upstream/cache/SimpleCacheTest.java | 10 +-- .../source/dash/DashMediaPeriod.java | 12 ++-- .../dash/manifest/DashManifestParser.java | 2 +- .../android/exoplayer2/ui/PlayerView.java | 10 ++- .../ui/spherical/SphericalSurfaceView.java | 5 +- .../exoplayer2/testutil/CacheAsserts.java | 5 +- .../testutil/MediaPeriodAsserts.java | 33 +++++----- 28 files changed, 82 insertions(+), 123 deletions(-) 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 a913a9b891..e1e866bbee 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 @@ -82,7 +82,6 @@ public class DownloadTracker { return download != null && download.state != Download.STATE_FAILED; } - @SuppressWarnings("unchecked") public DownloadRequest getDownloadRequest(Uri uri) { Download download = downloads.get(uri); return download != null && download.state != Download.STATE_FAILED ? download.request : null; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index b922a2d0b8..1330bd066e 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -952,8 +952,7 @@ public final class MediaSessionConnector { } if (description.getMediaId() != null) { builder.putString( - MediaMetadataCompat.METADATA_KEY_MEDIA_ID, - String.valueOf(description.getMediaId())); + MediaMetadataCompat.METADATA_KEY_MEDIA_ID, description.getMediaId()); } if (description.getMediaUri() != null) { builder.putString( diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 6e61ad2fe2..b89a6f4eab 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -80,7 +80,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu *

    Often artworks and icons need to be loaded asynchronously. In such a case, return a {@link * MediaDescriptionCompat} without the images, load your images asynchronously off the main thread * and then call {@link MediaSessionConnector#invalidateMediaSessionQueue()} to make the connector - * update the queue by calling {@link #getMediaDescription(Player, int)} again. + * update the queue by calling this method again. * * @param player The current player. * @param windowIndex The timeline window index for which to provide a description. 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 04a90b38d8..afe6a9879b 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 @@ -494,7 +494,6 @@ public final class C { /** Indicates that a buffer is (at least partially) encrypted. */ public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 /** Indicates that a buffer should be decoded but not rendered. */ - @SuppressWarnings("NumericOverflow") public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000 /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 37774bccb5..742f300df1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -302,7 +302,6 @@ import java.util.concurrent.atomic.AtomicBoolean; // Handler.Callback implementation. - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java index b336d84dd2..ba0f1b1663 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -253,13 +253,11 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag if (windowIndex == C.INDEX_UNSET) { return false; } - if (adMediaPeriodId != null) { - int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid); - if (newPeriodIndex == C.INDEX_UNSET) { - return false; - } + if (adMediaPeriodId == null) { + return true; } - return true; + int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid); + return newPeriodIndex != C.INDEX_UNSET; } public boolean belongsToSession( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index a90fc41df5..cf914567d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -1218,7 +1218,6 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.setVolume(volume); } - @SuppressWarnings("deprecation") private static void setVolumeInternalV3(AudioTrack audioTrack, float volume) { audioTrack.setStereoVolume(volume, volume); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 775d56f40b..c2faaba823 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -500,7 +500,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - @SuppressWarnings("unchecked") public void handleMessage(Message msg) { Object request = msg.obj; Object response; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 4fde9f05d3..89c5dd6650 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -428,7 +428,6 @@ public final class DrmInitData implements Comparator, Parcelable { dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); } - @SuppressWarnings("hiding") public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index ed77f41c83..55a7a901ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -92,7 +92,7 @@ public final class OfflineLicenseHelper { * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener) + * MediaDrmCallback, HashMap) */ public static OfflineLicenseHelper newWidevineInstance( String defaultLicenseUrl, @@ -115,7 +115,7 @@ public final class OfflineLicenseHelper { * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener) + * MediaDrmCallback, HashMap) */ public OfflineLicenseHelper( UUID uuid, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java index 1d7b50cd3c..eb4aee87a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java @@ -218,40 +218,38 @@ import java.util.Arrays; int mappingsCount = bitArray.readBits(6) + 1; for (int i = 0; i < mappingsCount; i++) { int mappingType = bitArray.readBits(16); - switch (mappingType) { - case 0: - int submaps; - if (bitArray.readBit()) { - submaps = bitArray.readBits(4) + 1; - } else { - submaps = 1; - } - int couplingSteps; - if (bitArray.readBit()) { - couplingSteps = bitArray.readBits(8) + 1; - for (int j = 0; j < couplingSteps; j++) { - bitArray.skipBits(iLog(channels - 1)); // magnitude - bitArray.skipBits(iLog(channels - 1)); // angle - } - } /*else { - couplingSteps = 0; - }*/ - if (bitArray.readBits(2) != 0x00) { - throw new ParserException("to reserved bits must be zero after mapping coupling steps"); - } - if (submaps > 1) { - for (int j = 0; j < channels; j++) { - bitArray.skipBits(4); // mappingMux - } - } - for (int j = 0; j < submaps; j++) { - bitArray.skipBits(8); // discard - bitArray.skipBits(8); // submapFloor - bitArray.skipBits(8); // submapResidue - } - break; - default: - Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); + if (mappingType != 0) { + Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); + continue; + } + int submaps; + if (bitArray.readBit()) { + submaps = bitArray.readBits(4) + 1; + } else { + submaps = 1; + } + int couplingSteps; + if (bitArray.readBit()) { + couplingSteps = bitArray.readBits(8) + 1; + for (int j = 0; j < couplingSteps; j++) { + bitArray.skipBits(iLog(channels - 1)); // magnitude + bitArray.skipBits(iLog(channels - 1)); // angle + } + } /*else { + couplingSteps = 0; + }*/ + if (bitArray.readBits(2) != 0x00) { + throw new ParserException("to reserved bits must be zero after mapping coupling steps"); + } + if (submaps > 1) { + for (int j = 0; j < channels; j++) { + bitArray.skipBits(4); // mappingMux + } + } + for (int j = 0; j < submaps; j++) { + bitArray.skipBits(8); // discard + bitArray.skipBits(8); // submapFloor + bitArray.skipBits(8); // submapResidue } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 4a50579cc4..f275dfe7d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -827,7 +827,6 @@ public final class MediaCodecUtil { } - @SuppressWarnings("deprecation") private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index d360224872..e34b4074fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -177,7 +177,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { pendingMetadataCount = 0; } - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { switch (msg.what) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 2738ee5926..dfb19e3bca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -156,10 +156,10 @@ public abstract class MappingTrackSelector extends TrackSelector { public @RendererSupport int getRendererSupport(int rendererIndex) { int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; - for (int i = 0; i < rendererFormatSupport.length; i++) { - for (int j = 0; j < rendererFormatSupport[i].length; j++) { + for (int[] trackGroupFormatSupport : rendererFormatSupport) { + for (int trackFormatSupport : trackGroupFormatSupport) { int trackRendererSupport; - switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { + switch (trackFormatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) { case RendererCapabilities.FORMAT_HANDLED: return RENDERER_SUPPORT_PLAYABLE_TRACKS; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 76515a98e6..b2333516a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -342,7 +342,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList totalElapsedTimeMs += sampleElapsedTimeMs; totalBytesTransferred += sampleBytesTransferred; if (sampleElapsedTimeMs > 0) { - float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs; + float bitsPerSecond = (sampleBytesTransferred * 8000f) / sampleElapsedTimeMs; slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond); if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 2caf4c92f8..3de52b560c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -199,7 +199,6 @@ public final class CacheDataSink implements DataSink { outputStreamBytesWritten = 0; } - @SuppressWarnings("ThrowFromFinallyBlock") private void closeCurrentOutputStream() throws IOException { if (outputStream == null) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 20a80a1a35..98ae6fa6f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -94,7 +94,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private Storage previousStorage; /** Returns whether the file is an index file. */ - public static final boolean isIndexFile(String fileName) { + public static boolean isIndexFile(String fileName) { // Atomic file backups add additional suffixes to the file name. return fileName.startsWith(FILE_NAME_ATOMIC); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index b31d3b66f3..c53c4337b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -780,7 +780,6 @@ public final class SimpleCache implements Cache { * * @param files The files belonging to the root directory. * @return The loaded UID, or {@link #UID_UNSET} if a UID has not yet been created. - * @throws IOException If there is an error loading or generating the UID. */ private static long loadUid(File[] files) { for (File file : files) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java index dba7b74e9f..b5dbe41521 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java @@ -198,7 +198,6 @@ public class ActionFileUpgradeUtilTest { assertThat(download.state).isEqualTo(state); } - @SuppressWarnings("unchecked") private static List asList(StreamKey... streamKeys) { return Arrays.asList(streamKeys); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index f11bc5585b..01c73caf46 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -763,15 +763,10 @@ public class DownloadManagerTest { private void block() throws InterruptedException { try { - while (true) { - try { - blocker.block(); - break; - } catch (InterruptedException e) { - interrupted = true; - throw e; - } - } + blocker.block(); + } catch (InterruptedException e) { + interrupted = true; + throw e; } finally { blocker.close(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index cb7ef40726..b077a92d99 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -266,14 +266,9 @@ public final class AdaptiveTrackSelectionTest { /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); ArgumentMatcher matcher = - new ArgumentMatcher() { - @Override - public boolean matches(Format[] argument) { - Format[] formats = (Format[]) argument; - return formats.length == 3 + formats -> + formats.length == 3 && Arrays.asList(formats).containsAll(Arrays.asList(format1, format2, format3)); - } - }; verify(estimator) .getBitrates( argThat(matcher), diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index da2815f09f..3d684aab82 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -401,11 +401,8 @@ public class SimpleCacheTest { private static void addCache(SimpleCache simpleCache, String key, int position, int length) throws IOException { File file = simpleCache.startFile(key, position, length); - FileOutputStream fos = new FileOutputStream(file); - try { + try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(generateData(key, position, length)); - } finally { - fos.close(); } simpleCache.commitFile(file, length); } @@ -413,11 +410,8 @@ public class SimpleCacheTest { private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException { assertThat(cacheSpan.isCached).isTrue(); byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length); - FileInputStream inputStream = new FileInputStream(cacheSpan.file); - try { + try (FileInputStream inputStream = new FileInputStream(cacheSpan.file)) { assertThat(toByteArray(inputStream)).isEqualTo(expected); - } finally { - inputStream.close(); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 431a0a4bd9..aa080bbdec 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -219,9 +219,8 @@ import java.util.regex.Pattern; int totalTracksInPreviousAdaptationSets = 0; int tracksInCurrentAdaptationSet = manifestAdaptationSets.get(adaptationSetIndices[0]).representations.size(); - for (int i = 0; i < trackIndices.length; i++) { - while (trackIndices[i] - >= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) { + for (int trackIndex : trackIndices) { + while (trackIndex >= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) { currentAdaptationSetIndex++; totalTracksInPreviousAdaptationSets += tracksInCurrentAdaptationSet; tracksInCurrentAdaptationSet = @@ -234,7 +233,7 @@ import java.util.regex.Pattern; new StreamKey( periodIndex, adaptationSetIndices[currentAdaptationSetIndex], - trackIndices[i] - totalTracksInPreviousAdaptationSets)); + trackIndex - totalTracksInPreviousAdaptationSets)); } } return streamKeys; @@ -515,10 +514,9 @@ import java.util.regex.Pattern; int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; adaptationSetIndices[0] = i; int outputIndex = 1; - for (int j = 0; j < extraAdaptationSetIds.length; j++) { + for (String adaptationSetId : extraAdaptationSetIds) { int extraIndex = - idToIndexMap.get( - Integer.parseInt(extraAdaptationSetIds[j]), /* valueIfKeyNotFound= */ -1); + idToIndexMap.get(Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1); if (extraIndex != -1) { adaptationSetUsedFlags[extraIndex] = true; adaptationSetIndices[outputIndex] = extraIndex; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index c4f61a73cd..0e3c6a8bda 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -100,7 +100,7 @@ public class DashManifestParser extends DefaultHandler long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); String typeString = xpp.getAttributeValue(null, "type"); - boolean dynamic = typeString != null && "dynamic".equals(typeString); + boolean dynamic = "dynamic".equals(typeString); long minUpdateTimeMs = dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET) : C.TIME_UNSET; long timeShiftBufferDepthMs = dynamic diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index dde35e6e99..c776898bc6 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ui; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -1076,8 +1075,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * Should be called when the player is visible to the user and if {@code surface_type} is {@code * spherical_view}. It is the counterpart to {@link #onPause()}. * - *

    This method should typically be called in {@link Activity#onStart()}, or {@link - * Activity#onResume()} for API versions <= 23. + *

    This method should typically be called in {@code Activity.onStart()}, or {@code + * Activity.onResume()} for API versions <= 23. */ public void onResume() { if (surfaceView instanceof SphericalSurfaceView) { @@ -1089,8 +1088,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * Should be called when the player is no longer visible to the user and if {@code surface_type} * is {@code spherical_view}. It is the counterpart to {@link #onResume()}. * - *

    This method should typically be called in {@link Activity#onStop()}, or {@link - * Activity#onPause()} for API versions <= 23. + *

    This method should typically be called in {@code Activity.onStop()}, or {@code + * Activity.onPause()} for API versions <= 23. */ public void onPause() { if (surfaceView instanceof SphericalSurfaceView) { @@ -1316,7 +1315,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); } - @SuppressWarnings("deprecation") private static void configureEditModeLogo(Resources resources, ImageView logo) { logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java index 1029a28323..f7b208d085 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java @@ -80,7 +80,6 @@ public final class SphericalSurfaceView extends GLSurfaceView { private final SensorManager sensorManager; private final @Nullable Sensor orientationSensor; private final OrientationListener orientationListener; - private final Renderer renderer; private final Handler mainHandler; private final TouchTracker touchTracker; private final SceneRenderer scene; @@ -114,7 +113,7 @@ public final class SphericalSurfaceView extends GLSurfaceView { this.orientationSensor = orientationSensor; scene = new SceneRenderer(); - renderer = new Renderer(scene); + Renderer renderer = new Renderer(scene); touchTracker = new TouchTracker(context, renderer, PX_PER_DEGREES); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); @@ -332,7 +331,7 @@ public final class SphericalSurfaceView extends GLSurfaceView { private float calculateFieldOfViewInYDirection(float aspect) { boolean landscapeMode = aspect > 1; if (landscapeMode) { - double halfFovX = FIELD_OF_VIEW_DEGREES / 2; + double halfFovX = FIELD_OF_VIEW_DEGREES / 2f; double tanY = Math.tan(Math.toRadians(halfFovX)) / aspect; double halfFovY = Math.toDegrees(Math.atan(tanY)); return (float) (halfFovY * 2); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 664532d3ff..9a17904379 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -114,14 +114,11 @@ public final class CacheAsserts { */ public static void assertReadData(DataSource dataSource, DataSpec dataSpec, byte[] expected) throws IOException { - DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); byte[] bytes = null; - try { + try (DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec)) { bytes = Util.toByteArray(inputStream); } catch (IOException e) { // Ignore - } finally { - inputStream.close(); } assertThat(bytes).isEqualTo(expected); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index 681f166837..5ee3e9561e 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -176,8 +176,8 @@ public final class MediaPeriodAsserts { for (int i = 0; i < trackGroup.length; i++) { allFormats.add(trackGroup.getFormat(i)); } - for (int i = 0; i < formats.length; i++) { - if (!allFormats.remove(formats[i])) { + for (Format format : formats) { + if (!allFormats.remove(format)) { return false; } } @@ -189,22 +189,21 @@ public final class MediaPeriodAsserts { DummyMainThread dummyMainThread = new DummyMainThread(); ConditionVariable preparedCondition = new ConditionVariable(); dummyMainThread.runOnMainThread( - () -> { - mediaPeriod.prepare( - new Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - trackGroupArray.set(mediaPeriod.getTrackGroups()); - preparedCondition.open(); - } + () -> + mediaPeriod.prepare( + new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + trackGroupArray.set(mediaPeriod.getTrackGroups()); + preparedCondition.open(); + } - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - // Ignore. - } - }, - /* positionUs= */ 0); - }); + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Ignore. + } + }, + /* positionUs= */ 0)); try { preparedCondition.block(); } catch (InterruptedException e) { From f8cd770d84dfcc03ac43251eb77cc1f3a8d5e83b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 May 2019 10:08:27 +0100 Subject: [PATCH 65/95] Migrate from is(Not)SameAs to is(Not)SameInstanceAs. They behave identically, and the old names are being removed. Open-source note: The new methods are available in Truth as of version 0.44. END_PUBLIC More information: go/issameas-lsc Tested: TAP --sample ran all affected tests and none failed http://test/OCL:246024032:BASE:246042619:1556672975894:513e7746 PiperOrigin-RevId: 246101315 --- .../source/ads/AdPlaybackStateTest.java | 2 +- .../exoplayer2/text/ttml/TtmlRenderUtilTest.java | 8 ++++---- .../trackselection/DefaultTrackSelectorTest.java | 4 ++-- .../trackselection/TrackSelectionUtilTest.java | 4 ++-- .../WindowedTrackBitrateEstimatorTest.java | 2 +- .../exoplayer2/upstream/BaseDataSourceTest.java | 16 ++++++++-------- .../hls/playlist/HlsMediaPlaylistParserTest.java | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index 80e7383d12..0cd27a90c0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -53,7 +53,7 @@ public final class AdPlaybackStateTest { assertThat(state.adGroups[0].uris[0]).isNull(); assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE); - assertThat(state.adGroups[0].uris[1]).isSameAs(TEST_URI); + assertThat(state.adGroups[0].uris[1]).isSameInstanceAs(TEST_URI); assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java index 747cbd0c7b..8785229b7c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java @@ -45,7 +45,7 @@ public final class TtmlRenderUtilTest { String[] styleIds = {"s0"}; assertThat(TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles)) - .isSameAs(globalStyles.get("s0")); + .isSameInstanceAs(globalStyles.get("s0")); } @Test @@ -74,7 +74,7 @@ public final class TtmlRenderUtilTest { style.setBackgroundColor(Color.YELLOW); TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - assertThat(resolved).isSameAs(style); + assertThat(resolved).isSameInstanceAs(style); // inline attribute not overridden assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW); @@ -90,7 +90,7 @@ public final class TtmlRenderUtilTest { style.setBackgroundColor(Color.YELLOW); TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - assertThat(resolved).isSameAs(style); + assertThat(resolved).isSameInstanceAs(style); // inline attribute not overridden assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW); @@ -101,7 +101,7 @@ public final class TtmlRenderUtilTest { @Test public void testResolveStyleOnlyInlineStyle() { TtmlStyle inlineStyle = new TtmlStyle(); - assertThat(TtmlRenderUtil.resolveStyle(inlineStyle, null, null)).isSameAs(inlineStyle); + assertThat(TtmlRenderUtil.resolveStyle(inlineStyle, null, null)).isSameInstanceAs(inlineStyle); } private static Map getGlobalStyles() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 83fe34db97..e5dd4ae636 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -1491,7 +1491,7 @@ public final class DefaultTrackSelectorTest { assertThat(selection.length()).isEqualTo(1); assertThat(selection.getIndexInTrackGroup(0)).isEqualTo(expectedTrack); assertThat(selection.getFormat(0)) - .isSameAs(expectedTrackGroup.getFormat(selection.getIndexInTrackGroup(0))); + .isSameInstanceAs(expectedTrackGroup.getFormat(selection.getIndexInTrackGroup(0))); } private static void assertNoSelection(TrackSelection selection) { @@ -1506,7 +1506,7 @@ public final class DefaultTrackSelectorTest { for (int i = 0; i < expectedTracks.length; i++) { assertThat(selection.getIndexInTrackGroup(i)).isEqualTo(expectedTracks[i]); assertThat(selection.getFormat(i)) - .isSameAs(expectedTrackGroup.getFormat(selection.getIndexInTrackGroup(i))); + .isSameInstanceAs(expectedTrackGroup.getFormat(selection.getIndexInTrackGroup(i))); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java index 9acef96c0b..963e90f139 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java @@ -202,7 +202,7 @@ public class TrackSelectionUtilTest { MAX_DURATION_US, bitratesArrayToUse); - assertThat(bitrates).isSameAs(bitratesArrayToUse); + assertThat(bitrates).isSameInstanceAs(bitratesArrayToUse); } @Test @@ -436,7 +436,7 @@ public class TrackSelectionUtilTest { MAX_DURATION_US, bitratesArrayToUse); - assertThat(bitrates).isSameAs(bitratesArrayToUse); + assertThat(bitrates).isSameInstanceAs(bitratesArrayToUse); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java index 887338078e..d40149baae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java @@ -145,7 +145,7 @@ public class WindowedTrackBitrateEstimatorTest { new MediaChunkIterator[] {iterator1, iterator2}, bitratesArrayToUse); - assertThat(bitrates).isSameAs(bitratesArrayToUse); + assertThat(bitrates).isSameInstanceAs(bitratesArrayToUse); } private static MediaChunk createMediaChunk(int formatBitrate, int actualBitrate) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java index 289de08c47..2426073d8a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java @@ -40,10 +40,10 @@ public class BaseDataSourceTest { testSource.read(/* buffer= */ null, /* offset= */ 0, /* readLength= */ 100); testSource.close(); - assertThat(transferListener.lastTransferInitializingSource).isSameAs(testSource); - assertThat(transferListener.lastTransferStartSource).isSameAs(testSource); - assertThat(transferListener.lastBytesTransferredSource).isSameAs(testSource); - assertThat(transferListener.lastTransferEndSource).isSameAs(testSource); + assertThat(transferListener.lastTransferInitializingSource).isSameInstanceAs(testSource); + assertThat(transferListener.lastTransferStartSource).isSameInstanceAs(testSource); + assertThat(transferListener.lastBytesTransferredSource).isSameInstanceAs(testSource); + assertThat(transferListener.lastTransferEndSource).isSameInstanceAs(testSource); assertThat(transferListener.lastTransferInitializingDataSpec).isEqualTo(dataSpec); assertThat(transferListener.lastTransferStartDataSpec).isEqualTo(dataSpec); @@ -69,10 +69,10 @@ public class BaseDataSourceTest { testSource.read(/* buffer= */ null, /* offset= */ 0, /* readLength= */ 100); testSource.close(); - assertThat(transferListener.lastTransferInitializingSource).isSameAs(testSource); - assertThat(transferListener.lastTransferStartSource).isSameAs(testSource); - assertThat(transferListener.lastBytesTransferredSource).isSameAs(testSource); - assertThat(transferListener.lastTransferEndSource).isSameAs(testSource); + assertThat(transferListener.lastTransferInitializingSource).isSameInstanceAs(testSource); + assertThat(transferListener.lastTransferStartSource).isSameInstanceAs(testSource); + assertThat(transferListener.lastBytesTransferredSource).isSameInstanceAs(testSource); + assertThat(transferListener.lastTransferEndSource).isSameInstanceAs(testSource); assertThat(transferListener.lastTransferInitializingDataSpec).isEqualTo(dataSpec); assertThat(transferListener.lastTransferStartDataSpec).isEqualTo(dataSpec); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 80cc6b23cd..3fd67b294a 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -362,7 +362,7 @@ public class HlsMediaPlaylistParserTest { List segments = playlist.segments; assertThat(segments.get(0).initializationSegment).isNull(); assertThat(segments.get(1).initializationSegment) - .isSameAs(segments.get(2).initializationSegment); + .isSameInstanceAs(segments.get(2).initializationSegment); assertThat(segments.get(1).initializationSegment.url).isEqualTo("init1.ts"); assertThat(segments.get(3).initializationSegment.url).isEqualTo("init2.ts"); } From eed5d957d87d44cb9c716f1a4c80f39ad2a6a442 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 May 2019 19:19:02 +0100 Subject: [PATCH 66/95] Rework DownloadManager to fix remaining TODOs - Removed DownloadInternal and its sometimes-out-of-sync duplicate state - Fixed downloads being in STOPPED rather than QUEUED state when the manager is paused - Fixed setMaxParallelDownloads to start/stop downloads if necessary when the value changes - Fixed isWaitingForRequirements PiperOrigin-RevId: 246164845 --- .../offline/ActionFileUpgradeUtil.java | 9 +- .../offline/DefaultDownloadIndex.java | 24 +- .../exoplayer2/offline/DownloadManager.java | 836 +++++++++--------- .../offline/WritableDownloadIndex.java | 7 + .../offline/ActionFileUpgradeUtilTest.java | 14 +- .../offline/DownloadManagerTest.java | 54 +- 6 files changed, 473 insertions(+), 471 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index 975fc10b93..baf47772ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -67,11 +67,12 @@ public final class ActionFileUpgradeUtil { if (actionFile.exists()) { boolean success = false; try { + long nowMs = System.currentTimeMillis(); for (DownloadRequest request : actionFile.load()) { if (downloadIdProvider != null) { request = request.copyWithId(downloadIdProvider.getId(request)); } - mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted); + mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted, nowMs); } success = true; } finally { @@ -93,13 +94,13 @@ public final class ActionFileUpgradeUtil { /* package */ static void mergeRequest( DownloadRequest request, DefaultDownloadIndex downloadIndex, - boolean addNewDownloadAsCompleted) + boolean addNewDownloadAsCompleted, + long nowMs) throws IOException { Download download = downloadIndex.getDownload(request.id); if (download != null) { - download = DownloadManager.mergeRequest(download, request, download.stopReason); + download = DownloadManager.mergeRequest(download, request, download.stopReason, nowMs); } else { - long nowMs = System.currentTimeMillis(); download = new Download( request, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 252c058b88..06f308d1e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -69,7 +69,9 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final int COLUMN_INDEX_BYTES_DOWNLOADED = 13; private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; - private static final String WHERE_STATE_TERMINAL = + private static final String WHERE_STATE_IS_DOWNLOADING = + COLUMN_STATE + " = " + Download.STATE_DOWNLOADING; + private static final String WHERE_STATE_IS_TERMINAL = getStateQuery(Download.STATE_COMPLETED, Download.STATE_FAILED); private static final String[] COLUMNS = @@ -218,6 +220,19 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } + @Override + public void setDownloadingStatesToQueued() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_QUEUED); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, WHERE_STATE_IS_DOWNLOADING, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + @Override public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); @@ -225,7 +240,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { ContentValues values = new ContentValues(); values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.update(tableName, values, WHERE_STATE_TERMINAL, /* whereArgs= */ null); + writableDatabase.update(tableName, values, WHERE_STATE_IS_TERMINAL, /* whereArgs= */ null); } catch (SQLException e) { throw new DatabaseIOException(e); } @@ -239,7 +254,10 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update( - tableName, values, WHERE_STATE_TERMINAL + " AND " + WHERE_ID_EQUALS, new String[] {id}); + tableName, + values, + WHERE_STATE_IS_TERMINAL + " AND " + WHERE_ID_EQUALS, + new String[] {id}); } catch (SQLException e) { throw new DatabaseIOException(e); } 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 8502a56ea7..b528d91759 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 @@ -31,7 +31,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.DatabaseProvider; @@ -46,14 +45,11 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Manages downloads. @@ -125,8 +121,7 @@ public final class DownloadManager { // Messages posted to the main handler. private static final int MSG_INITIALIZED = 0; private static final int MSG_PROCESSED = 1; - private static final int MSG_DOWNLOAD_CHANGED = 2; - private static final int MSG_DOWNLOAD_REMOVED = 3; + private static final int MSG_DOWNLOAD_UPDATE = 2; // Messages posted to the background handler. private static final int MSG_INITIALIZE = 0; @@ -141,31 +136,14 @@ public final class DownloadManager { private static final int MSG_CONTENT_LENGTH_CHANGED = 9; private static final int MSG_RELEASE = 10; - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - START_THREAD_SUCCEEDED, - START_THREAD_WAIT_REMOVAL_TO_FINISH, - START_THREAD_WAIT_DOWNLOAD_CANCELLATION, - START_THREAD_TOO_MANY_DOWNLOADS - }) - private @interface StartThreadResults {} - - private static final int START_THREAD_SUCCEEDED = 0; - private static final int START_THREAD_WAIT_REMOVAL_TO_FINISH = 1; - private static final int START_THREAD_WAIT_DOWNLOAD_CANCELLATION = 2; - private static final int START_THREAD_TOO_MANY_DOWNLOADS = 3; - private static final String TAG = "DownloadManager"; - private static final boolean DEBUG = false; private final Context context; private final WritableDownloadIndex downloadIndex; private final Handler mainHandler; private final InternalHandler internalHandler; private final RequirementsWatcher.Listener requirementsListener; - private final CopyOnWriteArraySet listeners; - private final ArrayList downloads; private int pendingMessages; private int activeTaskCount; @@ -174,6 +152,7 @@ public final class DownloadManager { private int maxParallelDownloads; private int minRetryCount; private int notMetRequirements; + private List downloads; private RequirementsWatcher requirementsWatcher; /** @@ -205,11 +184,13 @@ public final class DownloadManager { Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) { this.context = context.getApplicationContext(); this.downloadIndex = downloadIndex; + maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; downloadsPaused = true; - downloads = new ArrayList<>(); + downloads = Collections.emptyList(); listeners = new CopyOnWriteArraySet<>(); + requirementsListener = this::onRequirementsStateChanged; requirementsWatcher = new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); @@ -253,8 +234,14 @@ public final class DownloadManager { * reason that the {@link #getRequirements() Requirements} are not met. */ public boolean isWaitingForRequirements() { - // TODO: Fix this to return the right thing. - return !downloads.isEmpty(); + if (!downloadsPaused && notMetRequirements != 0) { + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == STATE_QUEUED) { + return true; + } + } + } + return false; } /** @@ -362,7 +349,7 @@ public final class DownloadManager { * #getDownloadIndex()} instead. */ public List getCurrentDownloads() { - return Collections.unmodifiableList(new ArrayList<>(downloads)); + return downloads; } /** Returns whether downloads are currently paused. */ @@ -475,10 +462,10 @@ public final class DownloadManager { } mainHandler.removeCallbacksAndMessages(/* token= */ null); // Reset state. + downloads = Collections.emptyList(); pendingMessages = 0; activeTaskCount = 0; initialized = false; - downloads.clear(); } } @@ -508,13 +495,9 @@ public final class DownloadManager { List downloads = (List) message.obj; onInitialized(downloads); break; - case MSG_DOWNLOAD_CHANGED: - Download state = (Download) message.obj; - onDownloadChanged(state); - break; - case MSG_DOWNLOAD_REMOVED: - state = (Download) message.obj; - onDownloadRemoved(state); + case MSG_DOWNLOAD_UPDATE: + DownloadUpdate update = (DownloadUpdate) message.obj; + onDownloadUpdate(update); break; case MSG_PROCESSED: int processedMessageCount = message.arg1; @@ -529,32 +512,23 @@ public final class DownloadManager { private void onInitialized(List downloads) { initialized = true; - this.downloads.addAll(downloads); + this.downloads = Collections.unmodifiableList(downloads); for (Listener listener : listeners) { listener.onInitialized(DownloadManager.this); } } - private void onDownloadChanged(Download download) { - int downloadIndex = getDownloadIndex(download.request.id); - if (download.isTerminalState()) { - if (downloadIndex != C.INDEX_UNSET) { - downloads.remove(downloadIndex); + private void onDownloadUpdate(DownloadUpdate update) { + downloads = Collections.unmodifiableList(update.downloads); + Download updatedDownload = update.download; + if (update.isRemove) { + for (Listener listener : listeners) { + listener.onDownloadRemoved(this, updatedDownload); } - } else if (downloadIndex != C.INDEX_UNSET) { - downloads.set(downloadIndex, download); } else { - downloads.add(download); - } - for (Listener listener : listeners) { - listener.onDownloadChanged(this, download); - } - } - - private void onDownloadRemoved(Download download) { - downloads.remove(getDownloadIndex(download.request.id)); - for (Listener listener : listeners) { - listener.onDownloadRemoved(this, download); + for (Listener listener : listeners) { + listener.onDownloadChanged(this, updatedDownload); + } } } @@ -568,18 +542,14 @@ public final class DownloadManager { } } - private int getDownloadIndex(String id) { - for (int i = 0; i < downloads.size(); i++) { - if (downloads.get(i).request.id.equals(id)) { - return i; - } - } - return C.INDEX_UNSET; - } - /* package */ static Download mergeRequest( - Download download, DownloadRequest request, int stopReason) { + Download download, DownloadRequest request, int stopReason, long nowMs) { @Download.State int state = download.state; + // Treat the merge as creating a new download if we're currently removing the existing one, or + // if the existing download is in a terminal state. Else treat the merge as updating the + // existing download. + long startTimeMs = + state == STATE_REMOVING || download.isTerminalState() ? nowMs : download.startTimeMs; if (state == STATE_REMOVING || state == STATE_RESTARTING) { state = STATE_RESTARTING; } else if (stopReason != STOP_REASON_NONE) { @@ -587,8 +557,6 @@ public final class DownloadManager { } else { state = STATE_QUEUED; } - long nowMs = System.currentTimeMillis(); - long startTimeMs = download.isTerminalState() ? nowMs : download.startTimeMs; return new Download( download.request.copyWithMergedRequest(request), state, @@ -599,40 +567,6 @@ public final class DownloadManager { FAILURE_REASON_NONE); } - private static Download copyWithState(Download download, @Download.State int state) { - return new Download( - download.request, - state, - download.startTimeMs, - /* updateTimeMs= */ System.currentTimeMillis(), - download.contentLength, - download.stopReason, - FAILURE_REASON_NONE, - download.progress); - } - - private static void logd(String message) { - if (DEBUG) { - Log.d(TAG, message); - } - } - - private static void logd(String message, DownloadInternal downloadInternal) { - logd(message, downloadInternal.download.request); - } - - private static void logd(String message, DownloadRequest request) { - if (DEBUG) { - logd(message + ": " + request); - } - } - - private static void logdFlags(String message, int flags) { - if (DEBUG) { - logd(message + ": " + Integer.toBinaryString(flags)); - } - } - private static final class InternalHandler extends Handler { public boolean released; @@ -641,15 +575,14 @@ public final class DownloadManager { private final WritableDownloadIndex downloadIndex; private final DownloaderFactory downloaderFactory; private final Handler mainHandler; - private final ArrayList downloadInternals; + private final ArrayList downloads; private final HashMap activeTasks; - // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; private boolean downloadsPaused; private int maxParallelDownloads; private int minRetryCount; - private int parallelDownloads; + private int activeDownloadTaskCount; public InternalHandler( HandlerThread thread, @@ -667,7 +600,7 @@ public final class DownloadManager { this.maxParallelDownloads = maxParallelDownloads; this.minRetryCount = minRetryCount; this.downloadsPaused = downloadsPaused; - downloadInternals = new ArrayList<>(); + downloads = new ArrayList<>(); activeTasks = new HashMap<>(); } @@ -732,70 +665,91 @@ public final class DownloadManager { private void initialize(int notMetRequirements) { this.notMetRequirements = notMetRequirements; - ArrayList loadedStates = new ArrayList<>(); - try (DownloadCursor cursor = - downloadIndex.getDownloads( - STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING)) { + DownloadCursor cursor = null; + try { + downloadIndex.setDownloadingStatesToQueued(); + cursor = + downloadIndex.getDownloads( + STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING); while (cursor.moveToNext()) { - loadedStates.add(cursor.getDownload()); + downloads.add(cursor.getDownload()); } - logd("Downloads are loaded."); - } catch (Throwable e) { - Log.e(TAG, "Download state loading failed.", e); - loadedStates.clear(); - } - for (Download download : loadedStates) { - addDownloadForState(download); - } - logd("Downloads are created."); - mainHandler.obtainMessage(MSG_INITIALIZED, loadedStates).sendToTarget(); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).start(); + } catch (IOException e) { + Log.e(TAG, "Failed to load index.", e); + downloads.clear(); + } finally { + Util.closeQuietly(cursor); } + // A copy must be used for the message to ensure that subsequent changes to the downloads list + // are not visible to the main thread when it processes the message. + ArrayList downloadsForMessage = new ArrayList<>(downloads); + mainHandler.obtainMessage(MSG_INITIALIZED, downloadsForMessage).sendToTarget(); + syncTasks(); } private void setDownloadsPaused(boolean downloadsPaused) { this.downloadsPaused = downloadsPaused; - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } + syncTasks(); } private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { this.notMetRequirements = notMetRequirements; - logdFlags("Not met requirements are changed", notMetRequirements); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } + syncTasks(); } private void setStopReason(@Nullable String id, int stopReason) { - if (id != null) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - logd("download stop reason is set to : " + stopReason, downloadInternal); - downloadInternal.setStopReason(stopReason); - return; + if (id == null) { + for (int i = 0; i < downloads.size(); i++) { + setStopReason(downloads.get(i), stopReason); + } + try { + // Set the stop reason for downloads in terminal states as well. + downloadIndex.setStopReason(stopReason); + } catch (IOException e) { + Log.e(TAG, "Failed to set manual stop reason", e); } } else { - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).setStopReason(stopReason); + Download download = getDownload(id, /* loadFromIndex= */ false); + if (download != null) { + setStopReason(download, stopReason); + } else { + try { + // Set the stop reason if the download is in a terminal state. + downloadIndex.setStopReason(id, stopReason); + } catch (IOException e) { + Log.e(TAG, "Failed to set manual stop reason: " + id, e); + } } } - try { - if (id != null) { - downloadIndex.setStopReason(id, stopReason); - } else { - downloadIndex.setStopReason(stopReason); + syncTasks(); + } + + private void setStopReason(Download download, int stopReason) { + if (stopReason == STOP_REASON_NONE) { + if (download.state == STATE_STOPPED) { + putDownloadWithState(download, STATE_QUEUED); } - } catch (IOException e) { - Log.e(TAG, "setStopReason failed", e); + } else if (stopReason != download.stopReason) { + @Download.State int state = download.state; + if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { + state = STATE_STOPPED; + } + putDownload( + new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + stopReason, + FAILURE_REASON_NONE, + download.progress)); } } private void setMaxParallelDownloads(int maxParallelDownloads) { this.maxParallelDownloads = maxParallelDownloads; - // TODO: Start or stop downloads if necessary. + syncTasks(); } private void setMinRetryCount(int minRetryCount) { @@ -803,77 +757,44 @@ public final class DownloadManager { } private void addDownload(DownloadRequest request, int stopReason) { - DownloadInternal downloadInternal = getDownload(request.id); - if (downloadInternal != null) { - downloadInternal.addRequest(request, stopReason); - logd("Request is added to existing download", downloadInternal); + Download download = getDownload(request.id, /* loadFromIndex= */ true); + long nowMs = System.currentTimeMillis(); + if (download != null) { + putDownload(mergeRequest(download, request, stopReason, nowMs)); } else { - Download download = loadDownload(request.id); - if (download == null) { - long nowMs = System.currentTimeMillis(); - download = - new Download( - request, - stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, - /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs, - /* contentLength= */ C.LENGTH_UNSET, - stopReason, - Download.FAILURE_REASON_NONE); - logd("Download state is created for " + request.id); - } else { - download = mergeRequest(download, request, stopReason); - logd("Download state is loaded for " + request.id); - } - addDownloadForState(download); + putDownload( + new Download( + request, + stopReason != STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, + /* startTimeMs= */ nowMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + FAILURE_REASON_NONE)); } + syncTasks(); } private void removeDownload(String id) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - downloadInternal.remove(); - } else { - Download download = loadDownload(id); - if (download != null) { - addDownloadForState(copyWithState(download, STATE_REMOVING)); - } else { - logd("Can't remove download. No download with id: " + id); - } + Download download = getDownload(id, /* loadFromIndex= */ true); + if (download == null) { + Log.e(TAG, "Failed to remove nonexistent download: " + id); + return; } - } - - private void onTaskStopped(Task task) { - logd("Task is stopped", task.request); - String downloadId = task.request.id; - activeTasks.remove(downloadId); - boolean tryToStartDownloads = false; - if (!task.isRemove) { - // If maxParallelDownloads was hit, there might be a download waiting for a slot. - tryToStartDownloads = parallelDownloads == maxParallelDownloads; - parallelDownloads--; - } - getDownload(downloadId).onTaskStopped(task.isCanceled, task.finalError); - if (tryToStartDownloads) { - for (int i = 0; - parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); - i++) { - downloadInternals.get(i).start(); - } - } - } - - private void onContentLengthChanged(Task task) { - String downloadId = task.request.id; - getDownload(downloadId).setContentLength(task.contentLength); + putDownloadWithState(download, STATE_REMOVING); + syncTasks(); } private void release() { for (Task task : activeTasks.values()) { task.cancel(/* released= */ true); } - activeTasks.clear(); - downloadInternals.clear(); + try { + downloadIndex.setDownloadingStatesToQueued(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + downloads.clear(); thread.quit(); synchronized (this) { released = true; @@ -881,261 +802,293 @@ public final class DownloadManager { } } - private void onDownloadChanged(DownloadInternal downloadInternal, Download download) { - logd("Download state is changed", downloadInternal); + // Start and cancel tasks based on the current download and manager states. + + private void syncTasks() { + int accumulatingDownloadTaskCount = 0; + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + Task activeTask = activeTasks.get(download.request.id); + switch (download.state) { + case STATE_STOPPED: + syncStoppedDownload(activeTask); + break; + case STATE_QUEUED: + activeTask = syncQueuedDownload(activeTask, download); + break; + case STATE_DOWNLOADING: + activeTask = Assertions.checkNotNull(activeTask); + syncDownloadingDownload(activeTask, download, accumulatingDownloadTaskCount); + break; + case STATE_REMOVING: + case STATE_RESTARTING: + syncRemovingDownload(activeTask, download); + break; + case STATE_COMPLETED: + case STATE_FAILED: + default: + throw new IllegalStateException(); + } + if (activeTask != null && !activeTask.isRemove) { + accumulatingDownloadTaskCount++; + } + } + } + + private void syncStoppedDownload(@Nullable Task activeTask) { + if (activeTask != null) { + // We have a task, which must be a download task. Cancel it. + Assertions.checkState(!activeTask.isRemove); + activeTask.cancel(/* released= */ false); + } + } + + private Task syncQueuedDownload(@Nullable Task activeTask, Download download) { + if (activeTask != null) { + // We have a task, which must be a download task. If the download state is queued we need to + // cancel it and start a new one, since a new request has been merged into the download. + Assertions.checkState(!activeTask.isRemove); + activeTask.cancel(/* released= */ false); + return activeTask; + } + + if (!canDownloadsRun() || activeDownloadTaskCount >= maxParallelDownloads) { + return null; + } + + // We can start a download task. + download = putDownloadWithState(download, STATE_DOWNLOADING); + Downloader downloader = downloaderFactory.createDownloader(download.request); + activeTask = + new Task( + download.request, + downloader, + download.progress, + /* isRemove= */ false, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(download.request.id, activeTask); + activeDownloadTaskCount++; + activeTask.start(); + return activeTask; + } + + private void syncDownloadingDownload( + Task activeTask, Download download, int accumulatingDownloadTaskCount) { + Assertions.checkState(!activeTask.isRemove); + if (!canDownloadsRun() || accumulatingDownloadTaskCount >= maxParallelDownloads) { + putDownloadWithState(download, STATE_QUEUED); + activeTask.cancel(/* released= */ false); + } + } + + private void syncRemovingDownload(@Nullable Task activeTask, Download download) { + if (activeTask != null) { + if (!activeTask.isRemove) { + // Cancel the downloading task. + activeTask.cancel(/* released= */ false); + } + // The activeTask is either a remove task, or a downloading task that we just cancelled. In + // the latter case we need to wait for the task to stop before we start a remove task. + return; + } + + // We can start a remove task. + Downloader downloader = downloaderFactory.createDownloader(download.request); + activeTask = + new Task( + download.request, + downloader, + download.progress, + /* isRemove= */ true, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(download.request.id, activeTask); + activeTask.start(); + } + + // Task event processing. + + private void onContentLengthChanged(Task task) { + String downloadId = task.request.id; + long contentLength = task.contentLength; + Download download = getDownload(downloadId, /* loadFromIndex= */ false); + if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) { + return; + } + putDownload( + new Download( + download.request, + download.state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + contentLength, + download.stopReason, + download.failureReason, + download.progress)); + } + + private void onTaskStopped(Task task) { + String downloadId = task.request.id; + activeTasks.remove(downloadId); + + boolean isRemove = task.isRemove; + if (!isRemove) { + activeDownloadTaskCount--; + } + + if (task.isCanceled) { + syncTasks(); + return; + } + + Throwable finalError = task.finalError; + if (finalError != null) { + Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError); + } + + Download download = + Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false)); + switch (download.state) { + case STATE_DOWNLOADING: + Assertions.checkState(!isRemove); + onDownloadTaskStopped(download, finalError); + break; + case STATE_REMOVING: + case STATE_RESTARTING: + Assertions.checkState(isRemove); + onRemoveTaskStopped(download); + break; + case STATE_QUEUED: + case STATE_STOPPED: + case STATE_COMPLETED: + case STATE_FAILED: + default: + throw new IllegalStateException(); + } + + syncTasks(); + } + + private void onDownloadTaskStopped(Download download, @Nullable Throwable finalError) { + download = + new Download( + download.request, + finalError == null ? STATE_COMPLETED : STATE_FAILED, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + download.stopReason, + finalError == null ? FAILURE_REASON_NONE : FAILURE_REASON_UNKNOWN, + download.progress); + // The download is now in a terminal state, so should not be in the downloads list. + downloads.remove(getDownloadIndex(download.request.id)); + // We still need to update the download index and main thread. try { downloadIndex.putDownload(download); } catch (IOException e) { - Log.e(TAG, "Failed to update index", e); + Log.e(TAG, "Failed to update index.", e); } - if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { - downloadInternals.remove(downloadInternal); - } - mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); } - private void onDownloadRemoved(DownloadInternal downloadInternal, Download download) { - logd("Download is removed", downloadInternal); - try { - downloadIndex.removeDownload(download.request.id); - } catch (IOException e) { - Log.e(TAG, "Failed to remove from index", e); - } - downloadInternals.remove(downloadInternal); - mainHandler.obtainMessage(MSG_DOWNLOAD_REMOVED, download).sendToTarget(); - } - - @StartThreadResults - private int startTask(DownloadInternal downloadInternal) { - DownloadRequest request = downloadInternal.download.request; - String downloadId = request.id; - if (activeTasks.containsKey(downloadId)) { - if (stopDownloadTask(downloadId)) { - return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; + private void onRemoveTaskStopped(Download download) { + if (download.state == STATE_RESTARTING) { + putDownloadWithState( + download, download.stopReason == STOP_REASON_NONE ? STATE_QUEUED : STATE_STOPPED); + syncTasks(); + } else { + int removeIndex = getDownloadIndex(download.request.id); + downloads.remove(removeIndex); + try { + downloadIndex.removeDownload(download.request.id); + } catch (IOException e) { + Log.e(TAG, "Failed to remove from database"); } - return START_THREAD_WAIT_REMOVAL_TO_FINISH; + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ true, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); } - boolean isRemove = downloadInternal.isInRemoveState(); - if (!isRemove) { - if (parallelDownloads == maxParallelDownloads) { - return START_THREAD_TOO_MANY_DOWNLOADS; - } - parallelDownloads++; - } - Downloader downloader = downloaderFactory.createDownloader(request); - DownloadProgress downloadProgress = downloadInternal.download.progress; - Task task = - new Task( - request, - downloader, - downloadProgress, - isRemove, - minRetryCount, - /* internalHandler= */ this); - activeTasks.put(downloadId, task); - task.start(); - logd("Task is started", downloadInternal); - return START_THREAD_SUCCEEDED; } - private boolean stopDownloadTask(String downloadId) { - Task task = activeTasks.get(downloadId); - if (task != null && !task.isRemove) { - task.cancel(/* released= */ false); - logd("Task is cancelled", task.request); - return true; - } - return false; - } + // Helper methods. - @Nullable - private DownloadInternal getDownload(String id) { - for (int i = 0; i < downloadInternals.size(); i++) { - DownloadInternal downloadInternal = downloadInternals.get(i); - if (downloadInternal.download.request.id.equals(id)) { - return downloadInternal; - } - } - return null; - } - - private Download loadDownload(String id) { - try { - return downloadIndex.getDownload(id); - } catch (IOException e) { - Log.e(TAG, "loadDownload failed", e); - } - return null; - } - - private void addDownloadForState(Download download) { - DownloadInternal downloadInternal = new DownloadInternal(this, download); - downloadInternals.add(downloadInternal); - logd("Download is added", downloadInternal); - downloadInternal.initialize(); - } - - private boolean canStartDownloads() { + private boolean canDownloadsRun() { return !downloadsPaused && notMetRequirements == 0; } - } - private static final class DownloadInternal { - - private final InternalHandler internalHandler; - - private Download download; - - // TODO: Get rid of these and use download directly. - @Download.State private int state; - private long contentLength; - private int stopReason; - @MonotonicNonNull @Download.FailureReason private int failureReason; - - private DownloadInternal(InternalHandler internalHandler, Download download) { - this.internalHandler = internalHandler; - this.download = download; - state = download.state; - contentLength = download.contentLength; - stopReason = download.stopReason; - failureReason = download.failureReason; - } - - private void initialize() { - initialize(download.state); - } - - public void addRequest(DownloadRequest newRequest, int stopReason) { - download = mergeRequest(download, newRequest, stopReason); - initialize(); - } - - public void remove() { - initialize(STATE_REMOVING); - } - - public Download getUpdatedDownload() { - download = + private Download putDownloadWithState(Download download, @Download.State int state) { + // Downloads in terminal states shouldn't be in the downloads list. This method cannot be used + // to set STATE_STOPPED either, because it doesn't have a stopReason argument. + Assertions.checkState( + state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED); + return putDownload( new Download( download.request, state, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), - contentLength, - stopReason, - state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, - download.progress); + download.contentLength, + /* stopReason= */ 0, + FAILURE_REASON_NONE, + download.progress)); + } + + private Download putDownload(Download download) { + // Downloads in terminal states shouldn't be in the downloads list. + Assertions.checkState(download.state != STATE_COMPLETED && download.state != STATE_FAILED); + int changedIndex = getDownloadIndex(download.request.id); + if (changedIndex == C.INDEX_UNSET) { + downloads.add(download); + Collections.sort(downloads, InternalHandler::compareStartTimes); + } else { + boolean needsSort = download.startTimeMs != downloads.get(changedIndex).startTimeMs; + downloads.set(changedIndex, download); + if (needsSort) { + Collections.sort(downloads, InternalHandler::compareStartTimes); + } + } + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); return download; } - public boolean isIdle() { - return state != STATE_DOWNLOADING && state != STATE_REMOVING && state != STATE_RESTARTING; - } - - @Override - public String toString() { - return download.request.id + ' ' + Download.getStateString(state); - } - - public void start() { - if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { - startOrQueue(); - } else if (isInRemoveState()) { - internalHandler.startTask(this); + @Nullable + private Download getDownload(String id, boolean loadFromIndex) { + int index = getDownloadIndex(id); + if (index != C.INDEX_UNSET) { + return downloads.get(index); } - } - - public void setStopReason(int stopReason) { - this.stopReason = stopReason; - updateStopState(); - } - - public boolean isInRemoveState() { - return state == STATE_REMOVING || state == STATE_RESTARTING; - } - - public void setContentLength(long contentLength) { - if (this.contentLength == contentLength) { - return; - } - this.contentLength = contentLength; - internalHandler.onDownloadChanged(this, getUpdatedDownload()); - } - - private void updateStopState() { - Download oldDownload = download; - if (canStart()) { - if (state == STATE_STOPPED) { - startOrQueue(); - } - } else { - if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - internalHandler.stopDownloadTask(download.request.id); - setState(STATE_STOPPED); + if (loadFromIndex) { + try { + return downloadIndex.getDownload(id); + } catch (IOException e) { + Log.e(TAG, "Failed to load download: " + id, e); } } - if (oldDownload == download) { - internalHandler.onDownloadChanged(this, getUpdatedDownload()); - } + return null; } - private void initialize(int initialState) { - // Don't notify listeners with initial state until we make sure we don't switch to another - // state immediately. - state = initialState; - if (isInRemoveState()) { - internalHandler.startTask(this); - } else if (canStart()) { - startOrQueue(); - } else { - setState(STATE_STOPPED); - } - if (state == initialState) { - internalHandler.onDownloadChanged(this, getUpdatedDownload()); - } - } - - private boolean canStart() { - return internalHandler.canStartDownloads() && stopReason == STOP_REASON_NONE; - } - - private void startOrQueue() { - Assertions.checkState(!isInRemoveState()); - @StartThreadResults int result = internalHandler.startTask(this); - Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); - if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) { - setState(STATE_DOWNLOADING); - } else { - setState(STATE_QUEUED); - } - } - - private void setState(@Download.State int newState) { - if (state != newState) { - state = newState; - internalHandler.onDownloadChanged(this, getUpdatedDownload()); - } - } - - private void onTaskStopped(boolean isCanceled, @Nullable Throwable error) { - if (isIdle()) { - return; - } - if (isCanceled) { - internalHandler.startTask(this); - } else if (state == STATE_REMOVING) { - internalHandler.onDownloadRemoved(this, getUpdatedDownload()); - } else if (state == STATE_RESTARTING) { - initialize(STATE_QUEUED); - } else { // STATE_DOWNLOADING - if (error != null) { - Log.e(TAG, "Download failed: " + download.request.id, error); - failureReason = FAILURE_REASON_UNKNOWN; - setState(STATE_FAILED); - } else { - setState(STATE_COMPLETED); + private int getDownloadIndex(String id) { + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + if (download.request.id.equals(id)) { + return i; } } + return C.INDEX_UNSET; + } + + private static int compareStartTimes(Download first, Download second) { + return Util.compareLong(first.startTimeMs, second.startTimeMs); } } @@ -1177,16 +1130,17 @@ public final class DownloadManager { // download manager whilst cancellation is ongoing. internalHandler = null; } - isCanceled = true; - downloader.cancel(); - interrupt(); + if (!isCanceled) { + isCanceled = true; + downloader.cancel(); + interrupt(); + } } // Methods running on download thread. @Override public void run() { - logd("Download started", request); try { if (isRemove) { downloader.remove(); @@ -1201,14 +1155,12 @@ public final class DownloadManager { if (!isCanceled) { long bytesDownloaded = downloadProgress.bytesDownloaded; if (bytesDownloaded != errorPosition) { - logd("Reset error count. bytesDownloaded = " + bytesDownloaded, request); errorPosition = bytesDownloaded; errorCount = 0; } if (++errorCount > minRetryCount) { throw e; } - logd("Download error. Retry " + errorCount, request); Thread.sleep(getRetryDelayMillis(errorCount)); } } @@ -1240,4 +1192,18 @@ public final class DownloadManager { return Math.min((errorCount - 1) * 1000, 5000); } } + + private static final class DownloadUpdate { + + private final Download download; + private final boolean isRemove; + + private final List downloads; + + public DownloadUpdate(Download download, boolean isRemove, List downloads) { + this.download = download; + this.isRemove = isRemove; + this.downloads = downloads; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index 00b08dc76a..ae634f8544 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -37,6 +37,13 @@ public interface WritableDownloadIndex extends DownloadIndex { */ void removeDownload(String id) throws IOException; + /** + * Sets all {@link Download#STATE_DOWNLOADING} states to {@link Download#STATE_QUEUED}. + * + * @throws IOException If an error occurs updating the state. + */ + void setDownloadingStatesToQueued() throws IOException; + /** * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java index b5dbe41521..17c1b57f37 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java @@ -38,6 +38,8 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class ActionFileUpgradeUtilTest { + private static final long NOW_MS = 1234; + private File tempFile; private ExoDatabaseProvider databaseProvider; private DefaultDownloadIndex downloadIndex; @@ -113,7 +115,7 @@ public class ActionFileUpgradeUtilTest { data); ActionFileUpgradeUtil.mergeRequest( - request, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); assertDownloadIndexContainsRequest(request, Download.STATE_QUEUED); } @@ -141,9 +143,9 @@ public class ActionFileUpgradeUtilTest { /* customCacheKey= */ "key123", new byte[] {5, 4, 3, 2, 1}); ActionFileUpgradeUtil.mergeRequest( - request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); ActionFileUpgradeUtil.mergeRequest( - request2, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request2, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); Download download = downloadIndex.getDownload(request2.id); assertThat(download).isNotNull(); @@ -178,16 +180,16 @@ public class ActionFileUpgradeUtilTest { /* customCacheKey= */ "key123", new byte[] {5, 4, 3, 2, 1}); ActionFileUpgradeUtil.mergeRequest( - request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); // Merging existing download, keeps it queued. ActionFileUpgradeUtil.mergeRequest( - request1, downloadIndex, /* addNewDownloadAsCompleted= */ true); + request1, downloadIndex, /* addNewDownloadAsCompleted= */ true, NOW_MS); assertThat(downloadIndex.getDownload(request1.id).state).isEqualTo(Download.STATE_QUEUED); // New download is merged as completed. ActionFileUpgradeUtil.mergeRequest( - request2, downloadIndex, /* addNewDownloadAsCompleted= */ true); + request2, downloadIndex, /* addNewDownloadAsCompleted= */ true, NOW_MS); assertThat(downloadIndex.getDownload(request2.id).state).isEqualTo(Download.STATE_COMPLETED); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 01c73caf46..a3df647efe 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -61,6 +61,8 @@ public class DownloadManagerTest { private static final int APP_STOP_REASON = 1; /** The minimum number of times a task must be retried before failing. */ private static final int MIN_RETRY_COUNT = 3; + /** Dummy value for the current time. */ + private static final long NOW_MS = 1234; private Uri uri1; private Uri uri2; @@ -132,6 +134,7 @@ public class DownloadManagerTest { task.assertCompleted(); runner.assertCreatedDownloaderCount(1); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -143,6 +146,7 @@ public class DownloadManagerTest { task.assertRemoved(); runner.assertCreatedDownloaderCount(2); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -158,6 +162,7 @@ public class DownloadManagerTest { downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); runner.getTask().assertFailed(); downloadManagerListener.blockUntilTasksComplete(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -174,6 +179,7 @@ public class DownloadManagerTest { downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); runner.getTask().assertCompleted(); downloadManagerListener.blockUntilTasksComplete(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -341,7 +347,7 @@ public class DownloadManagerTest { } @Test - public void getTasks_returnTasks() { + public void getCurrentDownloads_returnsCurrentDownloads() { TaskWrapper task1 = new DownloadRunner(uri1).postDownloadRequest().getTask(); TaskWrapper task2 = new DownloadRunner(uri2).postDownloadRequest().getTask(); TaskWrapper task3 = @@ -370,13 +376,11 @@ public class DownloadManagerTest { runOnMainThread(() -> downloadManager.pauseDownloads()); - // TODO: This should be assertQueued. Fix implementation and update test. - runner1.getTask().assertStopped(); + runner1.getTask().assertQueued(); // remove requests aren't stopped. runner2.getDownloader(1).unblock().assertReleased(); - // TODO: This should be assertQueued. Fix implementation and update test. - runner2.getTask().assertStopped(); + runner2.getTask().assertQueued(); // Although remove2 is finished, download2 doesn't start. runner2.getDownloader(2).assertDoesNotStart(); @@ -397,7 +401,7 @@ public class DownloadManagerTest { } @Test - public void manuallyStopAndResumeSingleDownload() throws Throwable { + public void setAndClearSingleDownloadStopReason() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest(); TaskWrapper task = runner.getTask(); @@ -415,7 +419,7 @@ public class DownloadManagerTest { } @Test - public void manuallyStoppedDownloadCanBeCancelled() throws Throwable { + public void setSingleDownloadStopReasonThenRemove_removesDownload() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest(); TaskWrapper task = runner.getTask(); @@ -433,7 +437,7 @@ public class DownloadManagerTest { } @Test - public void manuallyStoppedSingleDownload_doesNotAffectOthers() throws Throwable { + public void setSingleDownloadStopReason_doesNotAffectOtherDownloads() throws Throwable { DownloadRunner runner1 = new DownloadRunner(uri1); DownloadRunner runner2 = new DownloadRunner(uri2); DownloadRunner runner3 = new DownloadRunner(uri3); @@ -455,21 +459,22 @@ public class DownloadManagerTest { } @Test - public void mergeRequest_removingDownload_becomesRestarting() { + public void mergeRequest_removing_becomesRestarting() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest).setState(Download.STATE_REMOVING); Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); - Download expectedDownload = downloadBuilder.setState(Download.STATE_RESTARTING).build(); - assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); + Download expectedDownload = + downloadBuilder.setStartTimeMs(NOW_MS).setState(Download.STATE_RESTARTING).build(); + assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } @Test - public void mergeRequest_failedDownload_becomesQueued() { + public void mergeRequest_failed_becomesQueued() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) @@ -478,18 +483,19 @@ public class DownloadManagerTest { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); Download expectedDownload = downloadBuilder + .setStartTimeMs(NOW_MS) .setState(Download.STATE_QUEUED) .setFailureReason(Download.FAILURE_REASON_NONE) .build(); - assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); + assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } @Test - public void mergeRequest_stoppedDownload_staysStopped() { + public void mergeRequest_stopped_staysStopped() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) @@ -498,13 +504,13 @@ public class DownloadManagerTest { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); - assertEqualIgnoringTimeFields(mergedDownload, download); + assertEqualIgnoringUpdateTime(mergedDownload, download); } @Test - public void mergeRequest_stopReasonSetButNotStopped_becomesStopped() { + public void mergeRequest_completedWithStopReason_becomesStopped() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) @@ -513,10 +519,11 @@ public class DownloadManagerTest { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); - Download expectedDownload = downloadBuilder.setState(Download.STATE_STOPPED).build(); - assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); + Download expectedDownload = + downloadBuilder.setStartTimeMs(NOW_MS).setState(Download.STATE_STOPPED).build(); + assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } private void setUpDownloadManager(final int maxParallelDownloads) throws Exception { @@ -554,9 +561,10 @@ public class DownloadManagerTest { dummyMainThread.runTestOnMainThread(r); } - private static void assertEqualIgnoringTimeFields(Download download, Download that) { + private static void assertEqualIgnoringUpdateTime(Download download, Download that) { assertThat(download.request).isEqualTo(that.request); assertThat(download.state).isEqualTo(that.state); + assertThat(download.startTimeMs).isEqualTo(that.startTimeMs); assertThat(download.contentLength).isEqualTo(that.contentLength); assertThat(download.failureReason).isEqualTo(that.failureReason); assertThat(download.stopReason).isEqualTo(that.stopReason); From ea0efa74641fb688ece3e170a0ecfe42a3f3ffe5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 May 2019 20:06:11 +0100 Subject: [PATCH 67/95] Periodically persist progress to index whilst downloading PiperOrigin-RevId: 246173972 --- .../exoplayer2/offline/DownloadManager.java | 37 +++++++++++++++---- .../exoplayer2/offline/DownloadService.java | 4 +- 2 files changed, 32 insertions(+), 9 deletions(-) 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 b528d91759..3e0375718b 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 @@ -134,7 +134,8 @@ public final class DownloadManager { private static final int MSG_REMOVE_DOWNLOAD = 7; private static final int MSG_TASK_STOPPED = 8; private static final int MSG_CONTENT_LENGTH_CHANGED = 9; - private static final int MSG_RELEASE = 10; + private static final int MSG_UPDATE_PROGRESS = 10; + private static final int MSG_RELEASE = 11; private static final String TAG = "DownloadManager"; @@ -569,6 +570,8 @@ public final class DownloadManager { private static final class InternalHandler extends Handler { + private static final int UPDATE_PROGRESS_INTERVAL_MS = 5000; + public boolean released; private final HandlerThread thread; @@ -650,11 +653,13 @@ public final class DownloadManager { case MSG_CONTENT_LENGTH_CHANGED: task = (Task) message.obj; onContentLengthChanged(task); - processedExternalMessage = false; // This message is posted internally. - break; + return; // No need to post back to mainHandler. + case MSG_UPDATE_PROGRESS: + updateProgress(); + return; // No need to post back to mainHandler. case MSG_RELEASE: release(); - return; // Don't post back to mainHandler on release. + return; // No need to post back to mainHandler. default: throw new IllegalStateException(); } @@ -868,7 +873,9 @@ public final class DownloadManager { minRetryCount, /* internalHandler= */ this); activeTasks.put(download.request.id, activeTask); - activeDownloadTaskCount++; + if (activeDownloadTaskCount++ == 0) { + sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS); + } activeTask.start(); return activeTask; } @@ -933,8 +940,8 @@ public final class DownloadManager { activeTasks.remove(downloadId); boolean isRemove = task.isRemove; - if (!isRemove) { - activeDownloadTaskCount--; + if (!isRemove && --activeDownloadTaskCount == 0) { + removeMessages(MSG_UPDATE_PROGRESS); } if (task.isCanceled) { @@ -1013,6 +1020,22 @@ public final class DownloadManager { } } + // Progress updates. + + private void updateProgress() { + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + if (download.state == STATE_DOWNLOADING) { + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + } + } + sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS); + } + // Helper methods. private boolean canDownloadsRun() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index ee00cf3d5f..ce9087c6c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -683,7 +683,7 @@ public abstract class DownloadService extends Service { // Do nothing. } - private void notifyDownloadChange(Download download) { + private void notifyDownloadChanged(Download download) { onDownloadChanged(download); if (foregroundNotificationUpdater != null) { if (download.state == Download.STATE_DOWNLOADING @@ -834,7 +834,7 @@ public abstract class DownloadService extends Service { @Override public void onDownloadChanged(DownloadManager downloadManager, Download download) { if (downloadService != null) { - downloadService.notifyDownloadChange(download); + downloadService.notifyDownloadChanged(download); } } From de7c62a915076dd8363054acb0377c43601d794a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 May 2019 20:50:44 +0100 Subject: [PATCH 68/95] Remove unnecessary logging As justification for why we should not have this type of logging, it would scale up to about 13K LOC, 1800 Strings, and 36K (after pro-guarding - in the case of the demo app) if we did it through the whole code base*. It makes the code messier to read, and in most cases doesn't add significant value. Note: I left the Scheduler logging because it logs interactions with some awkward library components outside of ExoPlayer, so is perhaps a bit more justified. * This is a bit unfair since realistically we wouldn't ever add lots of logging into trivial classes. But I think it is fair to say that the deltas would be non-negligible. PiperOrigin-RevId: 246181421 --- .../jobdispatcher/JobDispatcherScheduler.java | 1 + .../android/exoplayer2/offline/Download.java | 22 --------------- .../exoplayer2/offline/DownloadService.java | 27 +++++-------------- .../scheduler/PlatformScheduler.java | 1 + .../exoplayer2/scheduler/Requirements.java | 12 --------- .../scheduler/RequirementsWatcher.java | 23 ---------------- .../exoplayer2/scheduler/Scheduler.java | 2 -- .../testutil/TestDownloadManagerListener.java | 22 +-------------- 8 files changed, 10 insertions(+), 100 deletions(-) diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java index 790f5ca4e5..d79dead0d7 100644 --- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util; */ public final class JobDispatcherScheduler implements Scheduler { + private static final boolean DEBUG = false; private static final String TAG = "JobDispatcherScheduler"; private static final String KEY_SERVICE_ACTION = "service_action"; private static final String KEY_SERVICE_PACKAGE = "service_package"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index 00d81b392c..97dff8394e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -81,28 +81,6 @@ public final class Download { /** The download isn't stopped. */ public static final int STOP_REASON_NONE = 0; - /** Returns the state string for the given state value. */ - public static String getStateString(@State int state) { - switch (state) { - case STATE_QUEUED: - return "QUEUED"; - case STATE_STOPPED: - return "STOPPED"; - case STATE_DOWNLOADING: - return "DOWNLOADING"; - case STATE_COMPLETED: - return "COMPLETED"; - case STATE_FAILED: - return "FAILED"; - case STATE_REMOVING: - return "REMOVING"; - case STATE_RESTARTING: - return "RESTARTING"; - default: - throw new IllegalStateException(); - } - } - /** The download request. */ public final DownloadRequest request; /** The state of the download. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index ce9087c6c8..fdd7163a2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -127,18 +127,18 @@ public abstract class DownloadService extends Service { public static final String KEY_DOWNLOAD_REQUEST = "download_request"; /** - * Key for the content id in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_REMOVE_DOWNLOAD} - * intents. + * Key for the {@link String} content id in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_REMOVE_DOWNLOAD} intents. */ public static final String KEY_CONTENT_ID = "content_id"; /** - * Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD_DOWNLOAD} - * intents. + * Key for the integer stop reason in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_ADD_DOWNLOAD} intents. */ public static final String KEY_STOP_REASON = "stop_reason"; - /** Key for the requirements in {@link #ACTION_SET_REQUIREMENTS} intents. */ + /** Key for the {@link Requirements} in {@link #ACTION_SET_REQUIREMENTS} intents. */ public static final String KEY_REQUIREMENTS = "requirements"; /** @@ -155,7 +155,6 @@ public abstract class DownloadService extends Service { public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL = 1000; private static final String TAG = "DownloadService"; - private static final boolean DEBUG = false; // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the // process is running). This allows DownloadService to restart when there's no scheduler. @@ -506,7 +505,6 @@ public abstract class DownloadService extends Service { @Override public void onCreate() { - logd("onCreate"); if (channelId != null) { NotificationUtil.createNotificationChannel( this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW); @@ -541,7 +539,6 @@ public abstract class DownloadService extends Service { if (intentAction == null) { intentAction = ACTION_INIT; } - logd("onStartCommand action: " + intentAction + " startId: " + startId); switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: @@ -573,7 +570,7 @@ public abstract class DownloadService extends Service { if (!intent.hasExtra(KEY_STOP_REASON)) { Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { - int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); + int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); downloadManager.setStopReason(contentId, stopReason); } break; @@ -598,13 +595,11 @@ public abstract class DownloadService extends Service { @Override public void onTaskRemoved(Intent rootIntent) { - logd("onTaskRemoved rootIntent: " + rootIntent); taskRemoved = true; } @Override public void onDestroy() { - logd("onDestroy"); isDestroyed = true; DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass()); boolean unschedule = !downloadManager.isWaitingForRequirements(); @@ -713,16 +708,8 @@ public abstract class DownloadService extends Service { } if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. stopSelf(); - logd("stopSelf()"); } else { - boolean stopSelfResult = stopSelfResult(lastStartId); - logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); - } - } - - private void logd(String message) { - if (DEBUG) { - Log.d(TAG, message); + stopSelfResult(lastStartId); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index fc8e8b61a5..8572c9c7ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.util.Util; @TargetApi(21) public final class PlatformScheduler implements Scheduler { + private static final boolean DEBUG = false; private static final String TAG = "PlatformScheduler"; private static final String KEY_SERVICE_ACTION = "service_action"; private static final String KEY_SERVICE_PACKAGE = "service_package"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index babc4e49fb..30cf452572 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -27,7 +27,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PowerManager; import androidx.annotation.IntDef; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -56,8 +55,6 @@ public final class Requirements implements Parcelable { /** Requirement that the device is charging. */ public static final int DEVICE_CHARGING = 1 << 3; - private static final String TAG = "Requirements"; - @RequirementFlags private final int requirements; /** @param requirements A combination of requirement flags. */ @@ -135,7 +132,6 @@ public final class Requirements implements Parcelable { if (networkInfo == null || !networkInfo.isConnected() || !isInternetConnectivityValidated(connectivityManager)) { - logd("No network info, connection or connectivity."); return requirements & (NETWORK | NETWORK_UNMETERED); } @@ -172,7 +168,6 @@ public final class Requirements implements Parcelable { } Network activeNetwork = connectivityManager.getActiveNetwork(); if (activeNetwork == null) { - logd("No active network."); return false; } NetworkCapabilities networkCapabilities = @@ -180,16 +175,9 @@ public final class Requirements implements Parcelable { boolean validated = networkCapabilities == null || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - logd("Network capability validated: " + validated); return !validated; } - private static void logd(String message) { - if (Scheduler.DEBUG) { - Log.d(TAG, message); - } - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index d2ad357ff6..f0d0f37cdf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -28,7 +28,6 @@ import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import androidx.annotation.RequiresApi; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; /** @@ -53,8 +52,6 @@ public final class RequirementsWatcher { @Requirements.RequirementFlags int notMetRequirements); } - private static final String TAG = "RequirementsWatcher"; - private final Context context; private final Listener listener; private final Requirements requirements; @@ -75,7 +72,6 @@ public final class RequirementsWatcher { this.listener = listener; this.requirements = requirements; handler = new Handler(Util.getLooper()); - logd(this + " created"); } /** @@ -110,7 +106,6 @@ public final class RequirementsWatcher { } receiver = new DeviceStatusChangeReceiver(); context.registerReceiver(receiver, filter, null, handler); - logd(this + " started"); return notMetRequirements; } @@ -121,7 +116,6 @@ public final class RequirementsWatcher { if (networkCallback != null) { unregisterNetworkCallback(); } - logd(this + " stopped"); } /** Returns watched {@link Requirements}. */ @@ -129,14 +123,6 @@ public final class RequirementsWatcher { return requirements; } - @Override - public String toString() { - if (!Scheduler.DEBUG) { - return super.toString(); - } - return "RequirementsWatcher{" + requirements + '}'; - } - @TargetApi(23) private void registerNetworkCallbackV23() { ConnectivityManager connectivityManager = @@ -163,22 +149,14 @@ public final class RequirementsWatcher { int notMetRequirements = requirements.getNotMetRequirements(context); if (this.notMetRequirements != notMetRequirements) { this.notMetRequirements = notMetRequirements; - logd("notMetRequirements has changed: " + notMetRequirements); listener.onRequirementsStateChanged(this, notMetRequirements); } } - private static void logd(String message) { - if (Scheduler.DEBUG) { - Log.d(TAG, message); - } - } - private class DeviceStatusChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!isInitialStickyBroadcast()) { - logd(RequirementsWatcher.this + " received " + intent.getAction()); checkRequirements(); } } @@ -200,7 +178,6 @@ public final class RequirementsWatcher { handler.post( () -> { if (networkCallback != null) { - logd(RequirementsWatcher.this + " NetworkCallback"); checkRequirements(); } }); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java index 1b225d9a4d..b5a6f40424 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java @@ -22,8 +22,6 @@ import android.content.Intent; /** Schedules a service to be started in the foreground when some {@link Requirements} are met. */ public interface Scheduler { - /* package */ boolean DEBUG = false; - /** * Schedules a service to be started in the foreground when some {@link Requirements} are met. * Anything that was previously scheduled will be canceled. diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java index 9d6223b8b1..4c334992b5 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -22,9 +22,7 @@ import android.os.ConditionVariable; import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.offline.DownloadManager; -import java.util.ArrayList; import java.util.HashMap; -import java.util.Locale; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -138,7 +136,6 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen } private void assertStateInternal(String taskId, int expectedState, int timeoutMs) { - ArrayList receivedStates = new ArrayList<>(); while (true) { Integer state = null; try { @@ -150,25 +147,8 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen if (expectedState == state) { return; } - receivedStates.add(state); } else { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < receivedStates.size(); i++) { - if (i > 0) { - sb.append(','); - } - int receivedState = receivedStates.get(i); - String receivedStateString = - receivedState == STATE_REMOVED ? "REMOVED" : Download.getStateString(receivedState); - sb.append(receivedStateString); - } - fail( - String.format( - Locale.US, - "for download (%s) expected:<%s> but was:<%s>", - taskId, - Download.getStateString(expectedState), - sb)); + fail("Didn't receive expected state: " + expectedState); } } } From 5992699d91406e8e1d289e565f95a15e3b40a11b Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 2 May 2019 10:37:31 +0100 Subject: [PATCH 69/95] Post-submit fixes for https://github.com/google/ExoPlayer/commit/eed5d957d87d44cb9c716f1a4c80f39ad2a6a442. One wrong return value, a useless assignment, unusual visibility of private class fields and some nullability issues. PiperOrigin-RevId: 246282995 --- .../exoplayer2/offline/DownloadManager.java | 34 ++++++++++++------- .../google/android/exoplayer2/util/Util.java | 4 +-- 2 files changed, 23 insertions(+), 15 deletions(-) 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 3e0375718b..e8b7eaf9b2 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 @@ -31,6 +31,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.DatabaseProvider; @@ -192,12 +193,9 @@ public final class DownloadManager { downloads = Collections.emptyList(); listeners = new CopyOnWriteArraySet<>(); - requirementsListener = this::onRequirementsStateChanged; - requirementsWatcher = - new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); - notMetRequirements = requirementsWatcher.start(); - - mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); + @SuppressWarnings("methodref.receiver.bound.invalid") + Handler mainHandler = Util.createHandler(this::handleMainMessage); + this.mainHandler = mainHandler; HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); internalThread.start(); internalHandler = @@ -210,6 +208,13 @@ public final class DownloadManager { minRetryCount, downloadsPaused); + @SuppressWarnings("methodref.receiver.bound.invalid") + RequirementsWatcher.Listener requirementsListener = this::onRequirementsStateChanged; + this.requirementsListener = requirementsListener; + requirementsWatcher = + new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); + notMetRequirements = requirementsWatcher.start(); + pendingMessages = 1; internalHandler .obtainMessage(MSG_INITIALIZE, notMetRequirements, /* unused */ 0) @@ -822,7 +827,7 @@ public final class DownloadManager { activeTask = syncQueuedDownload(activeTask, download); break; case STATE_DOWNLOADING: - activeTask = Assertions.checkNotNull(activeTask); + Assertions.checkNotNull(activeTask); syncDownloadingDownload(activeTask, download, accumulatingDownloadTaskCount); break; case STATE_REMOVING: @@ -848,6 +853,8 @@ public final class DownloadManager { } } + @Nullable + @CheckResult private Task syncQueuedDownload(@Nullable Task activeTask, Download download) { if (activeTask != null) { // We have a task, which must be a download task. If the download state is queued we need to @@ -919,7 +926,8 @@ public final class DownloadManager { private void onContentLengthChanged(Task task) { String downloadId = task.request.id; long contentLength = task.contentLength; - Download download = getDownload(downloadId, /* loadFromIndex= */ false); + Download download = + Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false)); if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) { return; } @@ -1125,7 +1133,7 @@ public final class DownloadManager { private volatile InternalHandler internalHandler; private volatile boolean isCanceled; - private Throwable finalError; + @Nullable private Throwable finalError; private long contentLength; @@ -1145,6 +1153,7 @@ public final class DownloadManager { contentLength = C.LENGTH_UNSET; } + @SuppressWarnings("nullness:assignment.type.incompatible") public void cancel(boolean released) { if (released) { // Download threads are GC roots for as long as they're running. The time taken for @@ -1218,10 +1227,9 @@ public final class DownloadManager { private static final class DownloadUpdate { - private final Download download; - private final boolean isRemove; - - private final List downloads; + public final Download download; + public final boolean isRemove; + public final List downloads; public DownloadUpdate(Download download, boolean isRemove, List downloads) { this.download = download; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index c05486bedf..97bcb68708 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -390,7 +390,7 @@ public final class Util { * * @param dataSource The {@link DataSource} to close. */ - public static void closeQuietly(DataSource dataSource) { + public static void closeQuietly(@Nullable DataSource dataSource) { try { if (dataSource != null) { dataSource.close(); @@ -406,7 +406,7 @@ public final class Util { * * @param closeable The {@link Closeable} to close. */ - public static void closeQuietly(Closeable closeable) { + public static void closeQuietly(@Nullable Closeable closeable) { try { if (closeable != null) { closeable.close(); From ad5948f3ed62941acf6a8e39ce43a4001c53a255 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 2 May 2019 11:41:06 +0100 Subject: [PATCH 70/95] Fix SmoothStreaming links NOTE: Streams are working on ExoPlayer but querying them from other platforms yields "bad request". The new links: + Match Microsoft's test server. + Allow querying from clients other than ExoPlayer, like curl. PiperOrigin-RevId: 246289755 --- demos/main/src/main/assets/media.exolist.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index c2acf3990b..bcb3ef4ad1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -330,11 +330,11 @@ "samples": [ { "name": "Super speed", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest" }, { "name": "Super speed (PlayReady)", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", "drm_scheme": "playready" } ] From 76184deb21af9ea058bcdf0cb35b28441e419c6e Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 May 2019 17:35:30 +0100 Subject: [PATCH 71/95] Minor download documentation tweaks PiperOrigin-RevId: 246333281 --- .../android/exoplayer2/offline/DefaultDownloaderFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java index ca20c769dc..d8126d4736 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java @@ -112,7 +112,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { .getConstructor(Uri.class, List.class, DownloaderConstructorHelper.class); } catch (NoSuchMethodException e) { // The downloader is present, but the expected constructor is missing. - throw new RuntimeException("DASH downloader constructor missing", e); + throw new RuntimeException("Downloader constructor missing", e); } } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) From 1292a35ec65f23a1ed9c453d25ef4e132390d9f3 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 May 2019 13:14:38 +0100 Subject: [PATCH 72/95] Add a couple of assertions to DownloadManager set methods PiperOrigin-RevId: 246491511 --- .../google/android/exoplayer2/offline/DownloadManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 e8b7eaf9b2..3bf03dd3e8 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 @@ -306,9 +306,10 @@ public final class DownloadManager { /** * Sets the maximum number of parallel downloads. * - * @param maxParallelDownloads The maximum number of parallel downloads. + * @param maxParallelDownloads The maximum number of parallel downloads. Must be greater than 0. */ public void setMaxParallelDownloads(int maxParallelDownloads) { + Assertions.checkArgument(maxParallelDownloads > 0); if (this.maxParallelDownloads == maxParallelDownloads) { return; } @@ -334,6 +335,7 @@ public final class DownloadManager { * @param minRetryCount The minimum number of times that a download will be retried. */ public void setMinRetryCount(int minRetryCount) { + Assertions.checkArgument(minRetryCount >= 0); if (this.minRetryCount == minRetryCount) { return; } From b53ac32b8c35cb62ede7df920d3d5b0015edad5e Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 5 May 2019 17:25:48 +0100 Subject: [PATCH 73/95] Prevent CachedContentIndex.idToKey from growing without bound PiperOrigin-RevId: 246727723 --- .../upstream/cache/CachedContentIndex.java | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 98ae6fa6f5..3749ecfc9a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -89,6 +89,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * efficiently when the index is next stored. */ private final SparseBooleanArray removedIds; + /** Tracks ids that are new since the index was last stored. */ + private final SparseBooleanArray newIds; private Storage storage; @Nullable private Storage previousStorage; @@ -150,6 +152,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); removedIds = new SparseBooleanArray(); + newIds = new SparseBooleanArray(); Storage databaseStorage = databaseProvider != null ? new DatabaseStorage(databaseProvider) : null; Storage legacyStorage = @@ -206,6 +209,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; idToKey.remove(removedIds.keyAt(i)); } removedIds.clear(); + newIds.clear(); } /** @@ -250,11 +254,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; CachedContent cachedContent = keyToContent.get(key); if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { keyToContent.remove(key); - storage.onRemove(cachedContent); - // Keep an entry in idToKey to stop the id from being reused until the index is next stored. - idToKey.put(cachedContent.id, /* value= */ null); - // Track that the entry should be removed from idToKey when the index is next stored. - removedIds.put(cachedContent.id, /* value= */ true); + int id = cachedContent.id; + boolean neverStored = newIds.get(id); + storage.onRemove(cachedContent, neverStored); + if (neverStored) { + // The id can be reused immediately. + idToKey.remove(id); + newIds.delete(id); + } else { + // Keep an entry in idToKey to stop the id from being reused until the index is next stored, + // and add an entry to removedIds to track that it should be removed when this does happen. + idToKey.put(id, /* value= */ null); + removedIds.put(id, /* value= */ true); + } } } @@ -297,8 +309,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private CachedContent addNew(String key) { int id = getNewId(idToKey); CachedContent cachedContent = new CachedContent(id, key); - keyToContent.put(cachedContent.key, cachedContent); - idToKey.put(cachedContent.id, cachedContent.key); + keyToContent.put(key, cachedContent); + idToKey.put(id, key); + newIds.put(id, true); storage.onUpdate(cachedContent); return cachedContent; } @@ -435,7 +448,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Ensures incremental changes to the index since the initial {@link #initialize(long)} or last * {@link #storeFully(HashMap)} are persisted. The storage will have been notified of all such - * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent)}. + * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent, boolean)}. * * @param content The key to content map to persist. * @throws IOException If an error occurs persisting the index. @@ -453,8 +466,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * Called when a {@link CachedContent} is removed. * * @param cachedContent The removed {@link CachedContent}. + * @param neverStored True if the {@link CachedContent} was added more recently than when the + * index was last stored. */ - void onRemove(CachedContent cachedContent); + void onRemove(CachedContent cachedContent, boolean neverStored); } /** {@link Storage} implementation that uses an {@link AtomicFile}. */ @@ -540,7 +555,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void onRemove(CachedContent cachedContent) { + public void onRemove(CachedContent cachedContent, boolean neverStored) { changed = true; } @@ -856,8 +871,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void onRemove(CachedContent cachedContent) { - pendingUpdates.put(cachedContent.id, null); + public void onRemove(CachedContent cachedContent, boolean neverStored) { + if (neverStored) { + pendingUpdates.delete(cachedContent.id); + } else { + pendingUpdates.put(cachedContent.id, null); + } } private Cursor getCursor() { From 1ef49ed205956122bff013c8359d1bce828125fc Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 5 May 2019 17:42:06 +0100 Subject: [PATCH 74/95] Fix broken Javadoc PiperOrigin-RevId: 246728418 --- .../java/com/google/android/exoplayer2/util/GlUtil.java | 4 ++-- .../android/exoplayer2/ui/spherical/CanvasRenderer.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index 915e855d23..7fc46dc363 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -51,7 +51,7 @@ public final class GlUtil { } /** - * Builds a GL shader program from vertex & fragment shader code. + * Builds a GL shader program from vertex and fragment shader code. * * @param vertexCode GLES20 vertex shader program as arrays of strings. Strings are joined by * adding a new line character in between each of them. @@ -64,7 +64,7 @@ public final class GlUtil { } /** - * Builds a GL shader program from vertex & fragment shader code. + * Builds a GL shader program from vertex and fragment shader code. * * @param vertexCode GLES20 vertex shader program. * @param fragmentCode GLES20 fragment shader program. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java index fdd59101e7..3d7e57bbd2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java @@ -72,7 +72,7 @@ public final class CanvasRenderer { "}" }; - // The quad has 2 triangles built from 4 total vertices. Each vertex has 3 position & 2 texture + // The quad has 2 triangles built from 4 total vertices. Each vertex has 3 position and 2 texture // coordinates. private static final int POSITION_COORDS_PER_VERTEX = 3; private static final int TEXTURE_COORDS_PER_VERTEX = 2; @@ -253,8 +253,8 @@ public final class CanvasRenderer { * Translates an orientation into pixel coordinates on the canvas. * *

    This is a minimal hit detection system that works for this quad because it has no model - * matrix. All the math is based on the fact that its size & distance are hard-coded into this - * class. For a more complex 3D mesh, a general bounding box & ray collision system would be + * matrix. All the math is based on the fact that its size and distance are hard-coded into this + * class. For a more complex 3D mesh, a general bounding box and ray collision system would be * required. * * @param yaw Yaw of the orientation in radians. @@ -287,7 +287,7 @@ public final class CanvasRenderer { return null; } // Convert from the polar coordinates of the controller to the rectangular coordinates of the - // View. Note the negative yaw & pitch used to generate Android-compliant x & y coordinates. + // View. Note the negative yaw and pitch used to generate Android-compliant x and y coordinates. float clickXPixel = (float) (widthPixel - clickXUnit * widthPixel / widthUnit); float clickYPixel = (float) (heightPixel - clickYUnit * heightPixel / heightUnit); return new PointF(clickXPixel, clickYPixel); From f4968e1a8e39563603a4b8528d8e826ee8c504ca Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 5 May 2019 17:58:33 +0100 Subject: [PATCH 75/95] Update translations PiperOrigin-RevId: 246729123 --- library/ui/src/main/res/values-af/strings.xml | 2 +- library/ui/src/main/res/values-cs/strings.xml | 2 +- library/ui/src/main/res/values-hi/strings.xml | 4 ++-- library/ui/src/main/res/values-hy/strings.xml | 2 +- library/ui/src/main/res/values-ja/strings.xml | 2 +- library/ui/src/main/res/values-ka/strings.xml | 2 +- library/ui/src/main/res/values-sw/strings.xml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 9e0fc245fc..8a983c543a 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -21,7 +21,7 @@ Verwyder tans aflaaie Video Oudio - SMS + Teks Geen Outo Onbekend diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 8c73c01d74..1568074f9f 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -21,7 +21,7 @@ Odstraňování staženého obsahu Videa Zvuk - SMS + Text Žádné Automaticky Neznámé diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index da606cd166..8ba92054ff 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -31,8 +31,8 @@ सराउंड साउंड 5.1 सराउंड साउंड 7.1 सराउंड साउंड - वैकल्पिक - अतिरिक्त + विकल्प + सप्लिमेंट्री कमेंट्री सबटाइटल %1$.2f एमबीपीएस diff --git a/library/ui/src/main/res/values-hy/strings.xml b/library/ui/src/main/res/values-hy/strings.xml index 11a9124f54..04a2aeb140 100644 --- a/library/ui/src/main/res/values-hy/strings.xml +++ b/library/ui/src/main/res/values-hy/strings.xml @@ -35,6 +35,6 @@ Լրացուցիչ Մեկնաբանություններ Ենթագրեր - %1$.2f մբ/վ + %1$.2f Մբիթ/վ %1$s, %2$s diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index aef5a12a96..b4158736a8 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -21,7 +21,7 @@ ダウンロードを削除しています 動画 音声 - SMS + 文字 なし 自動 不明 diff --git a/library/ui/src/main/res/values-ka/strings.xml b/library/ui/src/main/res/values-ka/strings.xml index f7b8272bcc..13ceaaf51f 100644 --- a/library/ui/src/main/res/values-ka/strings.xml +++ b/library/ui/src/main/res/values-ka/strings.xml @@ -21,7 +21,7 @@ მიმდინარეობს ჩამოტვირთვების ამოშლა ვიდეო აუდიო - SMS + ტექსტი არცერთი ავტომატური უცნობი diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index af58d417d6..1cdd325278 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -21,7 +21,7 @@ Inaondoa vipakuliwa Video Sauti - SMS + Maandishi Hamna Otomatiki Haijulikani From b626dd70c3445e921a63b5c3cf4797472378ac52 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 25 Apr 2019 16:52:35 +0100 Subject: [PATCH 76/95] Add DownloadHelper.createMediaSource utility method PiperOrigin-RevId: 245243488 --- .../exoplayer2/demo/DownloadTracker.java | 9 +- .../exoplayer2/demo/PlayerActivity.java | 29 ++-- library/core/proguard-rules.txt | 9 +- .../exoplayer2/offline/DownloadHelper.java | 126 +++++++++++------- 4 files changed, 98 insertions(+), 75 deletions(-) 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 f372a47df6..a913a9b891 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 @@ -30,15 +30,12 @@ import com.google.android.exoplayer2.offline.DownloadIndex; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** Tracks media that has been downloaded. */ @@ -86,11 +83,9 @@ public class DownloadTracker { } @SuppressWarnings("unchecked") - public List getOfflineStreamKeys(Uri uri) { + public DownloadRequest getDownloadRequest(Uri uri) { Download download = downloads.get(uri); - return download != null && download.state != Download.STATE_FAILED - ? download.request.streamKeys - : Collections.emptyList(); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; } public void toggleDownload( 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 acb24adebe..35307eb5d8 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 @@ -45,7 +45,8 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -75,7 +76,6 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ @@ -457,33 +457,26 @@ public class PlayerActivity extends AppCompatActivity } private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + DownloadRequest downloadRequest = + ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); + if (downloadRequest != null) { + return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); + } @ContentType int type = Util.inferContentType(uri, overrideExtension); - List offlineStreamKeys = getOfflineStreamKeys(uri); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: { + default: throw new IllegalStateException("Unsupported type: " + type); - } } } - 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/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 07ba438182..8c11810506 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -46,18 +46,21 @@ # Constructors accessed via reflection in DownloadHelper -dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 8a15c82c89..755f7e0343 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import androidx.annotation.Nullable; -import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -32,6 +31,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -106,30 +107,13 @@ public final class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } - @Nullable private static final Constructor DASH_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor HLS_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor SS_FACTORY_CONSTRUCTOR; - @Nullable private static final Method DASH_FACTORY_CREATE_METHOD; - @Nullable private static final Method HLS_FACTORY_CREATE_METHOD; - @Nullable private static final Method SS_FACTORY_CREATE_METHOD; - - static { - Pair<@NullableType Constructor, @NullableType Method> dashFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); - DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first; - DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> hlsFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); - HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first; - HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> ssFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); - SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first; - SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second; - } + private static final MediaSourceFactory DASH_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + private static final MediaSourceFactory SS_FACTORY = + getMediaSourceFactory( + "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + private static final MediaSourceFactory HLS_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); /** * Creates a {@link DownloadHelper} for progressive streams. @@ -202,8 +186,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_DASH, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD), + DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -252,8 +235,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_HLS, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD), + HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -302,11 +284,42 @@ public final class DownloadHelper { DownloadRequest.TYPE_SS, uri, /* cacheKey= */ null, - createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD), + SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** + * Utility method to create a MediaSource which only contains the tracks defined in {@code + * downloadRequest}. + * + * @param downloadRequest A {@link DownloadRequest}. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + MediaSourceFactory factory; + switch (downloadRequest.type) { + case DownloadRequest.TYPE_DASH: + factory = DASH_FACTORY; + break; + case DownloadRequest.TYPE_SS: + factory = SS_FACTORY; + break; + case DownloadRequest.TYPE_HLS: + factory = HLS_FACTORY; + break; + case DownloadRequest.TYPE_PROGRESSIVE: + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(downloadRequest.uri); + default: + throw new IllegalStateException("Unsupported type: " + downloadRequest.type); + } + return factory.createMediaSource( + downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); + } + private final String downloadType; private final Uri uri; @Nullable private final String cacheKey; @@ -739,35 +752,54 @@ public final class DownloadHelper { } } - private static Pair<@NullableType Constructor, @NullableType Method> - getMediaSourceFactoryMethods(String className) { + private static MediaSourceFactory getMediaSourceFactory(String className) { Constructor constructor = null; + Method setStreamKeysMethod = null; Method createMethod = null; try { // LINT.IfChange Class factoryClazz = Class.forName(className); - constructor = factoryClazz.getConstructor(DataSource.Factory.class); + constructor = factoryClazz.getConstructor(Factory.class); + setStreamKeysMethod = factoryClazz.getMethod("setStreamKeys", List.class); createMethod = factoryClazz.getMethod("createMediaSource", Uri.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - } catch (Exception e) { + } catch (ClassNotFoundException e) { // Expected if the app was built without the respective module. + } catch (NoSuchMethodException | SecurityException e) { + // Something is wrong with the library or the proguard configuration. + throw new IllegalStateException(e); } - return Pair.create(constructor, createMethod); + return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod); } - private static MediaSource createMediaSource( - Uri uri, - DataSource.Factory dataSourceFactory, - @Nullable Constructor factoryConstructor, - @Nullable Method createMediaSourceMethod) { - if (factoryConstructor == null || createMediaSourceMethod == null) { - throw new IllegalStateException("Module missing to create media source."); + private static final class MediaSourceFactory { + @Nullable private final Constructor constructor; + @Nullable private final Method setStreamKeysMethod; + @Nullable private final Method createMethod; + + public MediaSourceFactory( + @Nullable Constructor constructor, + @Nullable Method setStreamKeysMethod, + @Nullable Method createMethod) { + this.constructor = constructor; + this.setStreamKeysMethod = setStreamKeysMethod; + this.createMethod = createMethod; } - try { - Object factory = factoryConstructor.newInstance(dataSourceFactory); - return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri)); - } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate media source.", e); + + private MediaSource createMediaSource( + Uri uri, Factory dataSourceFactory, @Nullable List streamKeys) { + if (constructor == null || setStreamKeysMethod == null || createMethod == null) { + throw new IllegalStateException("Module missing to create media source."); + } + try { + Object factory = constructor.newInstance(dataSourceFactory); + if (streamKeys != null) { + setStreamKeysMethod.invoke(factory, streamKeys); + } + return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); + } } } From 0698bd1dbb5c4a8bf7f8253e5321dd56078226be Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 May 2019 18:05:26 +0100 Subject: [PATCH 77/95] Add option to clear all downloads. Adding an explicit option to clear all downloads prevents repeated database access in a loop when trying to delete all downloads. However, we still create an arbitrary number of parallel Task threads for this and seperate callbacks for each download. PiperOrigin-RevId: 247234181 --- RELEASENOTES.md | 4 ++ .../offline/DefaultDownloadIndex.java | 13 ++++ .../exoplayer2/offline/DownloadManager.java | 71 +++++++++++++++---- .../exoplayer2/offline/DownloadService.java | 39 ++++++++++ .../offline/WritableDownloadIndex.java | 7 ++ .../offline/DownloadManagerTest.java | 26 +++++++ 6 files changed, 146 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e69bcc917..310b947fdd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,9 @@ # Release notes # +### 2.10.1 ### + +* Offline: Add option to remove all downloads. + ### 2.10.0 ### * Core library: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 06f308d1e9..ef4bd00f20 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -233,6 +233,19 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } + @Override + public void setStatesToRemoving() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_REMOVING); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + @Override public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); 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 3bf03dd3e8..ec5ff81d97 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 @@ -133,10 +133,11 @@ public final class DownloadManager { private static final int MSG_SET_MIN_RETRY_COUNT = 5; private static final int MSG_ADD_DOWNLOAD = 6; private static final int MSG_REMOVE_DOWNLOAD = 7; - private static final int MSG_TASK_STOPPED = 8; - private static final int MSG_CONTENT_LENGTH_CHANGED = 9; - private static final int MSG_UPDATE_PROGRESS = 10; - private static final int MSG_RELEASE = 11; + private static final int MSG_REMOVE_ALL_DOWNLOADS = 8; + private static final int MSG_TASK_STOPPED = 9; + private static final int MSG_CONTENT_LENGTH_CHANGED = 10; + private static final int MSG_UPDATE_PROGRESS = 11; + private static final int MSG_RELEASE = 12; private static final String TAG = "DownloadManager"; @@ -446,6 +447,12 @@ public final class DownloadManager { internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget(); } + /** Cancels all pending downloads and removes all downloaded data. */ + public void removeAllDownloads() { + pendingMessages++; + internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget(); + } + /** * Stops the downloads and releases resources. Waits until the downloads are persisted to the * download index. The manager must not be accessed after this method has been called. @@ -652,6 +659,9 @@ public final class DownloadManager { id = (String) message.obj; removeDownload(id); break; + case MSG_REMOVE_ALL_DOWNLOADS: + removeAllDownloads(); + break; case MSG_TASK_STOPPED: Task task = (Task) message.obj; onTaskStopped(task); @@ -797,6 +807,36 @@ public final class DownloadManager { syncTasks(); } + private void removeAllDownloads() { + List terminalDownloads = new ArrayList<>(); + try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) { + while (cursor.moveToNext()) { + terminalDownloads.add(cursor.getDownload()); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load downloads."); + } + for (int i = 0; i < downloads.size(); i++) { + downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING)); + } + for (int i = 0; i < terminalDownloads.size(); i++) { + downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING)); + } + Collections.sort(downloads, InternalHandler::compareStartTimes); + try { + downloadIndex.setStatesToRemoving(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + ArrayList updateList = new ArrayList<>(downloads); + for (int i = 0; i < downloads.size(); i++) { + DownloadUpdate update = + new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + } + syncTasks(); + } + private void release() { for (Task task : activeTasks.values()) { task.cancel(/* released= */ true); @@ -1057,16 +1097,7 @@ public final class DownloadManager { // to set STATE_STOPPED either, because it doesn't have a stopReason argument. Assertions.checkState( state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED); - return putDownload( - new Download( - download.request, - state, - download.startTimeMs, - /* updateTimeMs= */ System.currentTimeMillis(), - download.contentLength, - /* stopReason= */ 0, - FAILURE_REASON_NONE, - download.progress)); + return putDownload(copyDownloadWithState(download, state)); } private Download putDownload(Download download) { @@ -1120,6 +1151,18 @@ public final class DownloadManager { return C.INDEX_UNSET; } + private static Download copyDownloadWithState(Download download, @Download.State int state) { + return new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + /* stopReason= */ 0, + FAILURE_REASON_NONE, + download.progress); + } + private static int compareStartTimes(Download first, Download second) { return Util.compareLong(first.startTimeMs, second.startTimeMs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index fdd7163a2c..3900dc8e93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -77,6 +77,16 @@ public abstract class DownloadService extends Service { public static final String ACTION_REMOVE_DOWNLOAD = "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + /** + * Removes all downloads. Extras: + * + *

      + *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
    + */ + public static final String ACTION_REMOVE_ALL_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS"; + /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * @@ -296,6 +306,19 @@ public abstract class DownloadService extends Service { .putExtra(KEY_CONTENT_ID, id); } + /** + * Builds an {@link Intent} for removing all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveAllDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground); + } + /** * Builds an {@link Intent} for resuming all downloads. * @@ -414,6 +437,19 @@ public abstract class DownloadService extends Service { startService(context, intent, foreground); } + /** + * Starts the service if not started already and removes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveAllDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + /** * Starts the service if not started already and resumes all downloads. * @@ -560,6 +596,9 @@ public abstract class DownloadService extends Service { downloadManager.removeDownload(contentId); } break; + case ACTION_REMOVE_ALL_DOWNLOADS: + downloadManager.removeAllDownloads(); + break; case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index ae634f8544..dc7085c85e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -44,6 +44,13 @@ public interface WritableDownloadIndex extends DownloadIndex { */ void setDownloadingStatesToQueued() throws IOException; + /** + * Sets all states to {@link Download#STATE_REMOVING}. + * + * @throws IOException If an error occurs updating the state. + */ + void setStatesToRemoving() throws IOException; + /** * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 2b9ef11235..de430d1416 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -243,6 +243,27 @@ public class DownloadManagerTest { downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test + public void removeAllDownloads_removesAllDownloads() throws Throwable { + // Finish one download and keep one running. + DownloadRunner runner1 = new DownloadRunner(uri1); + DownloadRunner runner2 = new DownloadRunner(uri2); + runner1.postDownloadRequest(); + runner1.getDownloader(0).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + runner2.postDownloadRequest(); + + runner1.postRemoveAllRequest(); + runner1.getDownloader(1).unblock(); + runner2.getDownloader(1).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + runner1.getTask().assertRemoved(); + runner2.getTask().assertRemoved(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); + assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0); + } + @Test public void differentDownloadRequestsMerged() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1); @@ -605,6 +626,11 @@ public class DownloadManagerTest { return this; } + private DownloadRunner postRemoveAllRequest() { + runOnMainThread(() -> downloadManager.removeAllDownloads()); + return this; + } + private DownloadRunner postDownloadRequest(StreamKey... keys) { DownloadRequest downloadRequest = new DownloadRequest( From 85a86e434a6aa4be083afe38130818865622d061 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 04:40:24 +0100 Subject: [PATCH 78/95] Increase gapless trim sample count PiperOrigin-RevId: 247348352 --- .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 0185a6d8af..6fb0ac6856 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -60,7 +60,7 @@ import java.util.List; * The threshold number of samples to trim from the start/end of an audio track when applying an * edit below which gapless info can be used (rather than removing samples from the sample table). */ - private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3; + private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4; /** The magic signature for an Opus Identification header, as defined in RFC-7845. */ private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead"); From ee5981c02dc1e6c465a463c2f8d826963619149b Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 May 2019 14:40:51 +0100 Subject: [PATCH 79/95] Ensure messages get deleted if they throw an exception. If a PlayerMessage throws an exception, it is currently not deleted from the list of pending messages. This may be problematic as the list of pending messages is kept when the player is retried without reset and the message is sent again in such a case. PiperOrigin-RevId: 247414494 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 37774bccb5..03c3482eac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1053,11 +1053,14 @@ import java.util.concurrent.atomic.AtomicBoolean; && nextInfo.resolvedPeriodIndex == currentPeriodIndex && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendMessageToTarget(nextInfo.message); - if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { - pendingMessages.remove(nextPendingMessageIndex); - } else { - nextPendingMessageIndex++; + try { + sendMessageToTarget(nextInfo.message); + } finally { + if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { + pendingMessages.remove(nextPendingMessageIndex); + } else { + nextPendingMessageIndex++; + } } nextInfo = nextPendingMessageIndex < pendingMessages.size() From 29add854af1b9ad2a645a229da8c601807731d52 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 15:10:41 +0100 Subject: [PATCH 80/95] Update player accessed on wrong thread URL PiperOrigin-RevId: 247418601 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 910404a875..697f35e417 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1231,7 +1231,7 @@ public class SimpleExoPlayer extends BasePlayer Log.w( TAG, "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/faqs.html#" + + "https://exoplayer.dev/troubleshooting.html#" + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean", hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; From ac07c56dab4b5d90f17731d3b5e878a9b154206a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 16:23:02 +0100 Subject: [PATCH 81/95] Fix NPE in HLS deriveAudioFormat. Issue:#5868 PiperOrigin-RevId: 247613811 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 310b947fdd..4f05b0a78d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### 2.10.1 ### +* Fix NPE when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). * Offline: Add option to remove all downloads. ### 2.10.0 ### diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index ef233bb566..2cfd14c79d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -802,7 +802,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; - roleFlags = mediaTagFormat.roleFlags; + roleFlags = variantFormat.roleFlags; language = variantFormat.language; label = variantFormat.label; } From 6ead14880bea2471add1fcea1f8fa026d06d7a61 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 17:13:36 +0100 Subject: [PATCH 82/95] Add setCodecOperatingRate workaround for 48KHz audio on ZTE Axon7 mini. Issue:#5821 PiperOrigin-RevId: 247621164 --- RELEASENOTES.md | 2 ++ .../exoplayer2/audio/MediaCodecAudioRenderer.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4f05b0a78d..4ee6c64444 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). * Offline: Add option to remove all downloads. +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing + 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). ### 2.10.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 07769e7d85..e75f7ffc7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -786,7 +786,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // Set codec configuration values. if (Util.SDK_INT >= 23) { mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); - if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET && !deviceDoesntSupportOperatingRate()) { mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); } } @@ -809,6 +809,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether the device's decoders are known to not support setting the codec operating + * rate. + * + *

    See GitHub issue #5821. + */ + private static boolean deviceDoesntSupportOperatingRate() { + return Util.SDK_INT == 23 + && ("ZTE B2017G".equals(Util.MODEL) || "AXON 7 mini".equals(Util.MODEL)); + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. From 1b9d018296ae5c2a6fa6bf23ed9b563d12e804c5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 18:12:05 +0100 Subject: [PATCH 83/95] Fix Javadoc links. PiperOrigin-RevId: 247630389 --- .../exoplayer2/analytics/AnalyticsListener.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 7f74216cc8..3400cf25b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -59,7 +59,7 @@ public interface AnalyticsListener { public final Timeline timeline; /** - * Window index in the {@code timeline} this event belongs to, or the prospective window index + * Window index in the {@link #timeline} this event belongs to, or the prospective window index * if the timeline is not yet known and empty. */ public final int windowIndex; @@ -76,7 +76,7 @@ public interface AnalyticsListener { public final long eventPlaybackPositionMs; /** - * Position in the current timeline window ({@code timeline.getCurrentWindowIndex()} or the + * Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the * currently playing ad at the time of the event, in milliseconds. */ public final long currentPlaybackPositionMs; @@ -91,15 +91,15 @@ public interface AnalyticsListener { * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at * the time of the event, in milliseconds. * @param timeline Timeline at the time of the event. - * @param windowIndex Window index in the {@code timeline} this event belongs to, or the + * @param windowIndex Window index in the {@link #timeline} this event belongs to, or the * prospective window index if the timeline is not yet known and empty. * @param mediaPeriodId Media period identifier for the media period this event belongs to, or * {@code null} if the event is not associated with a specific media period. * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time * of the event, in milliseconds. - * @param currentPlaybackPositionMs Position in the current timeline window ({@code - * timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event, - * in milliseconds. + * @param currentPlaybackPositionMs Position in the current timeline window ({@link + * Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in + * milliseconds. * @param totalBufferedDurationMs Total buffered duration from {@link * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes * pre-buffered data for subsequent ads and windows. From bef386bea8c9fe4ef3e765a46503a49a0401d1ae Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 11:56:35 +0100 Subject: [PATCH 84/95] Increase gradle heap size The update to Gradle 5.1.1 decreased the default heap size to 512MB and our build runs into Out-of-Memory errors. Setting the gradle flags to higher values instead. See https://developer.android.com/studio/releases/gradle-plugin#3-4-0 PiperOrigin-RevId: 247908526 --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index 4b9bfa8fa2..31ff0ad6b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,4 @@ android.useAndroidX=true android.enableJetifier=true android.enableUnitTestBinaryResources=true buildDir=buildout +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m From 48de1010a8ca84dcc89c0f6c139d644719acc6e0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 May 2019 16:05:34 +0100 Subject: [PATCH 85/95] Allow line terminators in ICY metadata Issue: #5876 PiperOrigin-RevId: 247935822 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/metadata/icy/IcyDecoder.java | 2 +- .../exoplayer2/metadata/icy/IcyDecoderTest.java | 11 +++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4ee6c64444..6a49f911d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,8 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Offline: Add option to remove all downloads. * Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index d04cd3a999..489719eda4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder { private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';"); + private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 9cbcea5814..97aac9995d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -70,6 +70,17 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isEqualTo("test_url"); } + @Test + public void decode_lineTerminatorInTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='test\r\ntitle';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEqualTo("test\r\ntitle"); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_notIcy() { IcyDecoder decoder = new IcyDecoder(); From 035686e58cd45916aac05e15e6c24f30350a77b6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 16:21:33 +0100 Subject: [PATCH 86/95] Fix Javadoc generation. Accessing task providers (like javaCompileProvider) at sync time is not possible. That's why the source sets of all generateJavadoc tasks is empty. The set of source directories can also be accessed directly through the static sourceSets field. Combining these allows to statically provide the relevant source files to the javadoc task without needing to access the run-time task provider. PiperOrigin-RevId: 247938176 --- javadoc_library.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/javadoc_library.gradle b/javadoc_library.gradle index a818ea390e..74fcc3dd6c 100644 --- a/javadoc_library.gradle +++ b/javadoc_library.gradle @@ -18,10 +18,13 @@ android.libraryVariants.all { variant -> if (!name.equals("release")) { return; // Skip non-release builds. } + def allSourceDirs = variant.sourceSets.inject ([]) { + acc, val -> acc << val.javaDirectories + } task("generateJavadoc", type: Javadoc) { description = "Generates Javadoc for the ${javadocTitle}." title = "ExoPlayer ${javadocTitle}" - source = variant.javaCompileProvider.get().source + source = allSourceDirs options { links "http://docs.oracle.com/javase/7/docs/api/" linksOffline "https://developer.android.com/reference", From cea3071b333618161d09931ee4dcab3d14fa3125 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 May 2019 12:33:19 +0100 Subject: [PATCH 87/95] Fix rendering DVB subtitle on API 28. Issue: #5862 PiperOrigin-RevId: 248112524 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/text/dvb/DvbParser.java | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6a49f911d7..55349ad42e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ * Offline: Add option to remove all downloads. * Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). +* Fix DVB subtitles for SDK 28 + ([#5862](https://github.com/google/ExoPlayer/issues/5862)). ### 2.10.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index eb956f06db..3f2fef454f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -21,7 +21,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; -import android.graphics.Region; import android.util.SparseArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; @@ -150,6 +149,8 @@ import java.util.List; List cues = new ArrayList<>(); SparseArray pageRegions = subtitleService.pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { + // Save clean clipping state. + canvas.save(); PageRegion pageRegion = pageRegions.valueAt(i); int regionId = pageRegions.keyAt(i); RegionComposition regionComposition = subtitleService.regions.get(regionId); @@ -163,9 +164,7 @@ import java.util.List; displayDefinition.horizontalPositionMaximum); int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, displayDefinition.verticalPositionMaximum); - canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, - Region.Op.REPLACE); - + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom); ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); if (clutDefinition == null) { clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); @@ -214,9 +213,11 @@ import java.util.List; (float) regionComposition.height / displayDefinition.height)); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // Restore clean clipping state. + canvas.restore(); } - return cues; + return Collections.unmodifiableList(cues); } // Static parsing. From 3ce0d89c56fa8d0a53b3ed82bd4dc67e58ef877a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 May 2019 13:42:16 +0100 Subject: [PATCH 88/95] Allow empty values in ICY metadata Issue: #5876 PiperOrigin-RevId: 248119726 --- RELEASENOTES.md | 2 +- .../android/exoplayer2/metadata/icy/IcyDecoder.java | 2 +- .../exoplayer2/metadata/icy/IcyDecoderTest.java | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55349ad42e..fa2baceac3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,7 +4,7 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). -* Fix handling of line terminators in SHOUTcast ICY metadata +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Offline: Add option to remove all downloads. * Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index 489719eda4..3d873926bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder { private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';", Pattern.DOTALL); + private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 97aac9995d..4602d172a6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -48,6 +48,17 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isNull(); } + @Test + public void decode_emptyTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEmpty(); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_semiColonInTitle() { IcyDecoder decoder = new IcyDecoder(); From 6e9df31e7d432d1c1c5196a35cfcd26d12bd8bef Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 14 May 2019 23:18:42 +0100 Subject: [PATCH 89/95] Add links to the developer guide in some READMEs PiperOrigin-RevId: 248221982 --- library/dash/README.md | 2 ++ library/hls/README.md | 2 ++ library/smoothstreaming/README.md | 2 ++ library/ui/README.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/library/dash/README.md b/library/dash/README.md index 7831033b99..1076716684 100644 --- a/library/dash/README.md +++ b/library/dash/README.md @@ -6,7 +6,9 @@ play DASH content, instantiate a `DashMediaSource` and pass it to ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/dash.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/hls/README.md b/library/hls/README.md index 1dd1b7a62e..3470c29e3c 100644 --- a/library/hls/README.md +++ b/library/hls/README.md @@ -5,7 +5,9 @@ instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/hls.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md index 4fa24543d6..d53471d17c 100644 --- a/library/smoothstreaming/README.md +++ b/library/smoothstreaming/README.md @@ -5,8 +5,10 @@ instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/smoothstreaming.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/ui/README.md b/library/ui/README.md index 341ea2fb16..16136b3d94 100644 --- a/library/ui/README.md +++ b/library/ui/README.md @@ -4,7 +4,9 @@ Provides UI components and resources for use with ExoPlayer. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/ui-components.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html From 7f89fa9a8ce63d56f840352c79e7360e445d5402 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 May 2019 17:50:50 +0100 Subject: [PATCH 90/95] Add simpler HttpDataSource constructors PiperOrigin-RevId: 248350557 --- .../ext/cronet/CronetDataSource.java | 24 +++++++++++++++---- .../ext/okhttp/OkHttpDataSource.java | 9 +++++++ .../upstream/DefaultHttpDataSource.java | 5 ++++ .../exoplayer2/testutil/FakeMediaChunk.java | 2 +- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index a9995af0e4..ca196b1d2f 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -113,7 +113,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final CronetEngine cronetEngine; private final Executor executor; - private final Predicate contentTypePredicate; + @Nullable private final Predicate contentTypePredicate; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -146,6 +146,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private volatile long currentConnectTimeoutMs; + /** + * @param cronetEngine A CronetEngine. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. + */ + public CronetDataSource(CronetEngine cronetEngine, Executor executor) { + this(cronetEngine, executor, /* contentTypePredicate= */ null); + } + /** * @param cronetEngine A CronetEngine. * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may @@ -158,7 +170,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * #open(DataSpec)}. */ public CronetDataSource( - CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) { + CronetEngine cronetEngine, + Executor executor, + @Nullable Predicate contentTypePredicate) { this( cronetEngine, executor, @@ -188,7 +202,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -225,7 +239,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -246,7 +260,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { /* package */ CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index a749495184..8eb8bba920 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -73,6 +73,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { private long bytesSkipped; private long bytesRead; + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. + * @param userAgent An optional User-Agent string. + */ + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { + this(callFactory, userAgent, /* contentTypePredicate= */ null); + } + /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 6aad517004..66036b7a84 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -89,6 +89,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private long bytesSkipped; private long bytesRead; + /** @param userAgent The User-Agent string that should be used. */ + public DefaultHttpDataSource(String userAgent) { + this(userAgent, /* contentTypePredicate= */ null); + } + /** * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java index 6669504c07..fd7be241df 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java @@ -27,7 +27,7 @@ import java.io.IOException; /** Fake {@link MediaChunk}. */ public final class FakeMediaChunk extends MediaChunk { - private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null); + private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT"); public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) { this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs); From 8efaf5fd7d5bdf1f55f35109a43380e8f7f6be0b Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 May 2019 19:20:27 +0100 Subject: [PATCH 91/95] don't call stop before preparing the player Issue: #5891 PiperOrigin-RevId: 248369509 --- .../mediasession/MediaSessionConnector.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 9c80fabc50..06f1cee001 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -834,10 +834,9 @@ public final class MediaSessionConnector { return player != null && mediaButtonEventHandler != null; } - private void stopPlayerForPrepare(boolean playWhenReady) { + private void setPlayWhenReady(boolean playWhenReady) { if (player != null) { - player.stop(); - player.setPlayWhenReady(playWhenReady); + controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); } } @@ -1052,14 +1051,14 @@ public final class MediaSessionConnector { } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); } } @@ -1182,7 +1181,7 @@ public final class MediaSessionConnector { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1190,7 +1189,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1198,7 +1197,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1206,7 +1205,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromUri(uri, extras); } } @@ -1214,7 +1213,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1222,7 +1221,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1230,7 +1229,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromUri(uri, extras); } } From 6e581f5270f5cfa9f09633ae83daefa62d83152d Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 May 2019 19:20:27 +0100 Subject: [PATCH 92/95] Revert "don't call stop before preparing the player" This reverts commit 8efaf5fd7d5bdf1f55f35109a43380e8f7f6be0b. --- .../mediasession/MediaSessionConnector.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 06f1cee001..9c80fabc50 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -834,9 +834,10 @@ public final class MediaSessionConnector { return player != null && mediaButtonEventHandler != null; } - private void setPlayWhenReady(boolean playWhenReady) { + private void stopPlayerForPrepare(boolean playWhenReady) { if (player != null) { - controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); + player.stop(); + player.setPlayWhenReady(playWhenReady); } } @@ -1051,14 +1052,14 @@ public final class MediaSessionConnector { } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } - setPlayWhenReady(/* playWhenReady= */ true); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - setPlayWhenReady(/* playWhenReady= */ false); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); } } @@ -1181,7 +1182,7 @@ public final class MediaSessionConnector { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1189,7 +1190,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1197,7 +1198,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1205,7 +1206,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepareFromUri(uri, extras); } } @@ -1213,7 +1214,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { - setPlayWhenReady(/* playWhenReady= */ true); + stopPlayerForPrepare(/* playWhenReady= */ true); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1221,7 +1222,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { - setPlayWhenReady(/* playWhenReady= */ true); + stopPlayerForPrepare(/* playWhenReady= */ true); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1229,7 +1230,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { - setPlayWhenReady(/* playWhenReady= */ true); + stopPlayerForPrepare(/* playWhenReady= */ true); playbackPreparer.onPrepareFromUri(uri, extras); } } From 9e4b89d1cb21a97c230321311cf0446540726249 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 16 May 2019 11:42:05 +0100 Subject: [PATCH 93/95] Ignore empty timelines in ImaAdsLoader. We previously only checked whether the reason for the timeline change is RESET which indicates an empty timeline. Change this to an explicit check for empty timelines to also ignore empty media or intermittent timeline changes to an empty timeline which are not marked as RESET. Issue:#5831 PiperOrigin-RevId: 248499118 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 465ad51ac5..f1316b2bfb 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -948,8 +948,8 @@ public final class ImaAdsLoader @Override public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { - if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { - // The player is being reset and this source will be released. + if (timeline.isEmpty()) { + // The player is being reset or contains no media. return; } Assertions.checkArgument(timeline.getPeriodCount() == 1); From 15b319cba24fcca91c18de6a111b0994651bbee1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 May 2019 12:30:13 +0100 Subject: [PATCH 94/95] Bump release to 2.10.1 and update release notes PiperOrigin-RevId: 248503235 --- RELEASENOTES.md | 8 ++++---- constants.gradle | 4 ++-- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fa2baceac3..9e7a992e11 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,15 +2,15 @@ ### 2.10.1 ### -* Fix NPE when using HLS chunkless preparation +* Offline: Add option to remove all downloads. +* HLS: Fix `NullPointerException` when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). * Fix handling of empty values and line terminators in SHOUTcast ICY metadata ([#5876](https://github.com/google/ExoPlayer/issues/5876)). -* Offline: Add option to remove all downloads. -* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing - 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). * Fix DVB subtitles for SDK 28 ([#5862](https://github.com/google/ExoPlayer/issues/5862)). +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing + 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). ### 2.10.0 ### diff --git a/constants.gradle b/constants.gradle index 5063c59141..b2ee322ee6 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.0' - releaseVersionCode = 2010000 + releaseVersion = '2.10.1' + releaseVersionCode = 2010001 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 72760db31b..a90435227b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.10.0"; + public static final String VERSION = "2.10.1"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2010000; + public static final int VERSION_INT = 2010001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 9ec330e7c771d33b8cb7ac043eb52aee4af4b316 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 May 2019 12:38:07 +0100 Subject: [PATCH 95/95] Fix platform scheduler javadoc PiperOrigin-RevId: 248503971 --- .../google/android/exoplayer2/scheduler/PlatformScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index 8572c9c7ca..e6679e1a5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.util.Util; * * * - * * }