Open source DownloadService, DownloadManager and related classes
Issue: #2643 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184844484
This commit is contained in:
parent
340501246b
commit
b3da82dc1c
@ -89,6 +89,10 @@
|
||||
* `EventLogger` moved from the demo app into the core library.
|
||||
* Fix ANR issue on Huawei P8 Lite
|
||||
([#3724](https://github.com/google/ExoPlayer/issues/3724)).
|
||||
* Fix potential NPE when removing media sources from a
|
||||
DynamicConcatenatingMediaSource
|
||||
([#3796](https://github.com/google/ExoPlayer/issues/3796)).
|
||||
* Open source DownloadService, DownloadManager and related classes.
|
||||
|
||||
### 2.6.1 ###
|
||||
|
||||
|
@ -35,6 +35,7 @@ include modulePrefix + 'extension-opus'
|
||||
include modulePrefix + 'extension-vp9'
|
||||
include modulePrefix + 'extension-rtmp'
|
||||
include modulePrefix + 'extension-leanback'
|
||||
include modulePrefix + 'extension-jobdispatcher'
|
||||
|
||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
||||
@ -54,6 +55,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi
|
||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
|
||||
|
||||
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
||||
&& gradle.ext.exoplayerIncludeCronetExtension) {
|
||||
|
23
extensions/jobdispatcher/README.md
Normal file
23
extensions/jobdispatcher/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# ExoPlayer Firebase JobDispatcher extension #
|
||||
|
||||
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
|
||||
|
||||
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-jobdispatcher:rX.X.X'
|
||||
```
|
||||
|
||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
43
extensions/jobdispatcher/build.gradle
Normal file
43
extensions/jobdispatcher/build.gradle
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.firebase:firebase-jobdispatcher:0.8.5'
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Firebase JobDispatcher extension'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
ext {
|
||||
releaseArtifact = 'extension-jobdispatcher'
|
||||
releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
18
extensions/jobdispatcher/src/main/AndroidManifest.xml
Normal file
18
extensions/jobdispatcher/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.jobdispatcher"/>
|
@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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.ext.jobdispatcher;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import com.firebase.jobdispatcher.Constraint;
|
||||
import com.firebase.jobdispatcher.FirebaseJobDispatcher;
|
||||
import com.firebase.jobdispatcher.GooglePlayDriver;
|
||||
import com.firebase.jobdispatcher.Job;
|
||||
import com.firebase.jobdispatcher.Job.Builder;
|
||||
import com.firebase.jobdispatcher.JobParameters;
|
||||
import com.firebase.jobdispatcher.JobService;
|
||||
import com.firebase.jobdispatcher.Lifetime;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.util.scheduler.Requirements;
|
||||
import com.google.android.exoplayer2.util.scheduler.Scheduler;
|
||||
|
||||
/**
|
||||
* A {@link Scheduler} which uses {@link com.firebase.jobdispatcher.FirebaseJobDispatcher} to
|
||||
* schedule a {@link Service} to be started when its requirements are met. The started service must
|
||||
* call {@link Service#startForeground(int, Notification)} to make itself a foreground service upon
|
||||
* being started, as documented by {@link Service#startForegroundService(Intent)}.
|
||||
*
|
||||
* <p>To use {@link JobDispatcherScheduler} application needs to have RECEIVE_BOOT_COMPLETED
|
||||
* permission and you need to define JobDispatcherSchedulerService in your manifest:
|
||||
*
|
||||
* <pre>{@literal
|
||||
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
*
|
||||
* <service
|
||||
* android:name="com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService"
|
||||
* android:exported="false">
|
||||
* <intent-filter>
|
||||
* <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
|
||||
* </intent-filter>
|
||||
* </service>
|
||||
* }</pre>
|
||||
*
|
||||
* The service to be scheduled must be defined in the manifest with an intent-filter:
|
||||
*
|
||||
* <pre>{@literal
|
||||
* <service android:name="MyJobService"
|
||||
* android:exported="false">
|
||||
* <intent-filter>
|
||||
* <action android:name="MyJobService.action"/>
|
||||
* <category android:name="android.intent.category.DEFAULT"/>
|
||||
* </intent-filter>
|
||||
* </service>
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses
|
||||
* should be guarded with a call to {@code
|
||||
* GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
|
||||
*
|
||||
* @see <a
|
||||
* href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
|
||||
*/
|
||||
public final class JobDispatcherScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = "JobDispatcherScheduler";
|
||||
private static final String SERVICE_ACTION = "SERVICE_ACTION";
|
||||
private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE";
|
||||
private static final String REQUIREMENTS = "REQUIREMENTS";
|
||||
|
||||
private final String jobTag;
|
||||
private final Job job;
|
||||
private final FirebaseJobDispatcher jobDispatcher;
|
||||
|
||||
/**
|
||||
* @param context Used to create a {@link FirebaseJobDispatcher} service.
|
||||
* @param requirements The requirements to execute the job.
|
||||
* @param jobTag Unique tag for the job. Using the same tag as a previous job can cause that job
|
||||
* to be replaced or canceled.
|
||||
* @param serviceAction The action which the service will be started with.
|
||||
* @param servicePackage The package of the service which contains the logic of the job.
|
||||
*/
|
||||
public JobDispatcherScheduler(
|
||||
Context context,
|
||||
Requirements requirements,
|
||||
String jobTag,
|
||||
String serviceAction,
|
||||
String servicePackage) {
|
||||
this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
|
||||
this.jobTag = jobTag;
|
||||
this.job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean schedule() {
|
||||
int result = jobDispatcher.schedule(job);
|
||||
logd("Scheduling JobDispatcher job: " + jobTag + " result: " + result);
|
||||
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
int result = jobDispatcher.cancel(jobTag);
|
||||
logd("Canceling JobDispatcher job: " + jobTag + " result: " + result);
|
||||
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
private static Job buildJob(
|
||||
FirebaseJobDispatcher dispatcher,
|
||||
Requirements requirements,
|
||||
String tag,
|
||||
String serviceAction,
|
||||
String servicePackage) {
|
||||
Builder builder =
|
||||
dispatcher
|
||||
.newJobBuilder()
|
||||
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
|
||||
.setTag(tag);
|
||||
|
||||
switch (requirements.getRequiredNetworkType()) {
|
||||
case Requirements.NETWORK_TYPE_NONE:
|
||||
// do nothing.
|
||||
break;
|
||||
case Requirements.NETWORK_TYPE_ANY:
|
||||
builder.addConstraint(Constraint.ON_ANY_NETWORK);
|
||||
break;
|
||||
case Requirements.NETWORK_TYPE_UNMETERED:
|
||||
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
if (requirements.isIdleRequired()) {
|
||||
builder.addConstraint(Constraint.DEVICE_IDLE);
|
||||
}
|
||||
if (requirements.isChargingRequired()) {
|
||||
builder.addConstraint(Constraint.DEVICE_CHARGING);
|
||||
}
|
||||
builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
|
||||
|
||||
// Extras, work duration.
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(SERVICE_ACTION, serviceAction);
|
||||
extras.putString(SERVICE_PACKAGE, servicePackage);
|
||||
extras.putInt(REQUIREMENTS, requirements.getRequirementsData());
|
||||
|
||||
builder.setExtras(extras);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static void logd(String message) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
/** A {@link JobService} to start a service if the requirements are met. */
|
||||
public static final class JobDispatcherSchedulerService extends JobService {
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
logd("JobDispatcherSchedulerService is started");
|
||||
Bundle extras = params.getExtras();
|
||||
Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS));
|
||||
if (requirements.checkRequirements(this)) {
|
||||
logd("requirements are met");
|
||||
String serviceAction = extras.getString(SERVICE_ACTION);
|
||||
String servicePackage = extras.getString(SERVICE_PACKAGE);
|
||||
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
|
||||
logd("starting service action: " + serviceAction + " package: " + servicePackage);
|
||||
if (Util.SDK_INT >= 26) {
|
||||
startForegroundService(intent);
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
} else {
|
||||
logd("requirements are not met");
|
||||
jobFinished(params, /* needsReschedule */ true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStopJob(JobParameters params) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,707 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.os.ConditionVariable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State;
|
||||
import com.google.android.exoplayer2.testutil.MockitoUtil;
|
||||
import com.google.android.exoplayer2.upstream.DummyDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
/** Tests {@link DownloadManager}. */
|
||||
public class DownloadManagerTest extends InstrumentationTestCase {
|
||||
|
||||
/* Used to check if condition becomes true in this time interval. */
|
||||
private static final int ASSERT_TRUE_TIMEOUT = 10000;
|
||||
/* Used to check if condition stays false for this time interval. */
|
||||
private static final int ASSERT_FALSE_TIME = 1000;
|
||||
/* Maximum retry delay in DownloadManager. */
|
||||
private static final int MAX_RETRY_DELAY = 5000;
|
||||
|
||||
private static final int MIN_RETRY_COUNT = 3;
|
||||
|
||||
private DownloadManager downloadManager;
|
||||
private File actionFile;
|
||||
private TestDownloadListener testDownloadListener;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
MockitoUtil.setUpMockito(this);
|
||||
|
||||
actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
testDownloadListener = new TestDownloadListener();
|
||||
setUpDownloadManager(100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
releaseDownloadManager();
|
||||
actionFile.delete();
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception {
|
||||
if (downloadManager != null) {
|
||||
releaseDownloadManager();
|
||||
}
|
||||
try {
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
new DownloaderConstructorHelper(
|
||||
Mockito.mock(Cache.class), DummyDataSource.FACTORY),
|
||||
maxActiveDownloadTasks,
|
||||
MIN_RETRY_COUNT,
|
||||
actionFile.getAbsolutePath());
|
||||
downloadManager.addListener(testDownloadListener);
|
||||
downloadManager.startDownloads();
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
throw new Exception(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseDownloadManager() throws Exception {
|
||||
try {
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager.release();
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
throw new Exception(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public void testDownloadActionRuns() throws Throwable {
|
||||
doTestActionRuns(createDownloadAction("media 1"));
|
||||
}
|
||||
|
||||
public void testRemoveActionRuns() throws Throwable {
|
||||
doTestActionRuns(createRemoveAction("media 1"));
|
||||
}
|
||||
|
||||
public void testDownloadRetriesThenFails() throws Throwable {
|
||||
FakeDownloadAction downloadAction = createDownloadAction("media 1");
|
||||
downloadAction.post();
|
||||
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader();
|
||||
fakeDownloader.enableDownloadIOException = true;
|
||||
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
|
||||
fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock();
|
||||
}
|
||||
downloadAction.assertError();
|
||||
testDownloadListener.clearDownloadError();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testDownloadNoRetryWhenCancelled() throws Throwable {
|
||||
FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts();
|
||||
downloadAction.getFakeDownloader().enableDownloadIOException = true;
|
||||
downloadAction.post().assertStarted();
|
||||
|
||||
FakeDownloadAction removeAction = createRemoveAction("media 1").post();
|
||||
|
||||
downloadAction.unblock().assertCancelled();
|
||||
removeAction.unblock();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testDownloadRetriesThenContinues() throws Throwable {
|
||||
FakeDownloadAction downloadAction = createDownloadAction("media 1");
|
||||
downloadAction.post();
|
||||
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader();
|
||||
fakeDownloader.enableDownloadIOException = true;
|
||||
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
|
||||
fakeDownloader.assertStarted(MAX_RETRY_DELAY);
|
||||
if (i == MIN_RETRY_COUNT) {
|
||||
fakeDownloader.enableDownloadIOException = false;
|
||||
}
|
||||
fakeDownloader.unblock();
|
||||
}
|
||||
downloadAction.assertEnded();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
|
||||
public void testDownloadRetryCountResetsOnProgress() throws Throwable {
|
||||
FakeDownloadAction downloadAction = createDownloadAction("media 1");
|
||||
downloadAction.post();
|
||||
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader();
|
||||
fakeDownloader.enableDownloadIOException = true;
|
||||
fakeDownloader.downloadedBytes = 0;
|
||||
for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) {
|
||||
fakeDownloader.assertStarted(MAX_RETRY_DELAY);
|
||||
fakeDownloader.downloadedBytes++;
|
||||
if (i == MIN_RETRY_COUNT + 10) {
|
||||
fakeDownloader.enableDownloadIOException = false;
|
||||
}
|
||||
fakeDownloader.unblock();
|
||||
}
|
||||
downloadAction.assertEnded();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable {
|
||||
doTestActionsRunInParallel(createDownloadAction("media 1"),
|
||||
createDownloadAction("media 2"));
|
||||
}
|
||||
|
||||
public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable {
|
||||
doTestActionsRunInParallel(createDownloadAction("media 1"),
|
||||
createRemoveAction("media 2"));
|
||||
}
|
||||
|
||||
public void testSameMediaDownloadActionsStartInParallel() throws Throwable {
|
||||
doTestActionsRunInParallel(createDownloadAction("media 1"),
|
||||
createDownloadAction("media 1"));
|
||||
}
|
||||
|
||||
public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable {
|
||||
doTestActionsRunSequentially(createDownloadAction("media 1"),
|
||||
createRemoveAction("media 1"));
|
||||
}
|
||||
|
||||
public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable {
|
||||
doTestActionsRunSequentially(createRemoveAction("media 1"),
|
||||
createDownloadAction("media 1"));
|
||||
}
|
||||
|
||||
public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable {
|
||||
doTestActionsRunSequentially(createRemoveAction("media 1"),
|
||||
createRemoveAction("media 1"));
|
||||
}
|
||||
|
||||
public void testSameMediaMultipleActions() throws Throwable {
|
||||
FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts();
|
||||
FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts();
|
||||
FakeDownloadAction removeAction1 = createRemoveAction("media 1");
|
||||
FakeDownloadAction downloadAction3 = createDownloadAction("media 1");
|
||||
FakeDownloadAction removeAction2 = createRemoveAction("media 1");
|
||||
|
||||
// Two download actions run in parallel.
|
||||
downloadAction1.post().assertStarted();
|
||||
downloadAction2.post().assertStarted();
|
||||
// removeAction1 is added. It interrupts the two download actions' threads but they are
|
||||
// configured to ignore it so removeAction1 doesn't start.
|
||||
removeAction1.post().assertDoesNotStart();
|
||||
|
||||
// downloadAction2 finishes but it isn't enough to start removeAction1.
|
||||
downloadAction2.unblock().assertCancelled();
|
||||
removeAction1.assertDoesNotStart();
|
||||
// downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish.
|
||||
downloadAction3.post().assertDoesNotStart();
|
||||
|
||||
// When downloadAction1 finishes, removeAction1 starts.
|
||||
downloadAction1.unblock().assertCancelled();
|
||||
removeAction1.assertStarted();
|
||||
// downloadAction3 still waits removeAction1
|
||||
downloadAction3.assertDoesNotStart();
|
||||
|
||||
// removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2
|
||||
// starts immediately.
|
||||
removeAction2.post();
|
||||
removeAction1.assertCancelled();
|
||||
downloadAction3.assertCancelled();
|
||||
removeAction2.assertStarted().unblock().assertEnded();
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable {
|
||||
FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts();
|
||||
FakeDownloadAction removeAction2 = createRemoveAction("media 1");
|
||||
FakeDownloadAction removeAction3 = createRemoveAction("media 1");
|
||||
|
||||
removeAction1.post().assertStarted();
|
||||
removeAction2.post().assertDoesNotStart();
|
||||
removeAction3.post().assertDoesNotStart();
|
||||
|
||||
removeAction2.assertCancelled();
|
||||
|
||||
removeAction1.unblock().assertCancelled();
|
||||
removeAction3.assertStarted().unblock().assertEnded();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testGetTasks() throws Throwable {
|
||||
FakeDownloadAction removeAction = createRemoveAction("media 1");
|
||||
FakeDownloadAction downloadAction1 = createDownloadAction("media 1");
|
||||
FakeDownloadAction downloadAction2 = createDownloadAction("media 1");
|
||||
|
||||
removeAction.post().assertStarted();
|
||||
downloadAction1.post().assertDoesNotStart();
|
||||
downloadAction2.post().assertDoesNotStart();
|
||||
|
||||
DownloadState[] states = downloadManager.getDownloadStates();
|
||||
assertThat(states).hasLength(3);
|
||||
assertThat(states[0].downloadAction).isEqualTo(removeAction);
|
||||
assertThat(states[1].downloadAction).isEqualTo(downloadAction1);
|
||||
assertThat(states[2].downloadAction).isEqualTo(downloadAction2);
|
||||
}
|
||||
|
||||
public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable {
|
||||
FakeDownloadAction removeAction = createRemoveAction("media 1");
|
||||
FakeDownloadAction downloadAction1 = createDownloadAction("media 1");
|
||||
FakeDownloadAction downloadAction2 = createDownloadAction("media 1");
|
||||
|
||||
removeAction.post().assertStarted();
|
||||
downloadAction1.post().assertDoesNotStart();
|
||||
downloadAction2.post().assertDoesNotStart();
|
||||
|
||||
removeAction.unblock().assertEnded();
|
||||
downloadAction1.assertStarted();
|
||||
downloadAction2.assertStarted();
|
||||
downloadAction1.unblock().assertEnded();
|
||||
downloadAction2.unblock().assertEnded();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable {
|
||||
FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts();
|
||||
FakeDownloadAction downloadAction1 = createDownloadAction("media 1");
|
||||
FakeDownloadAction downloadAction2 = createDownloadAction("media 2");
|
||||
|
||||
removeAction.post().assertStarted();
|
||||
downloadAction1.post().assertDoesNotStart();
|
||||
downloadAction2.post().assertDoesNotStart();
|
||||
|
||||
removeAction.unblock().assertEnded();
|
||||
downloadAction1.assertStarted();
|
||||
downloadAction2.assertStarted();
|
||||
downloadAction1.unblock().assertEnded();
|
||||
downloadAction2.unblock().assertEnded();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable {
|
||||
FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts();
|
||||
FakeDownloadAction removeAction1 = createRemoveAction("media 1");
|
||||
FakeDownloadAction removeAction2 = createRemoveAction("media 2");
|
||||
|
||||
downloadAction.post().assertStarted();
|
||||
removeAction1.post().assertDoesNotStart();
|
||||
removeAction2.post().assertStarted();
|
||||
|
||||
downloadAction.unblock().assertCancelled();
|
||||
removeAction2.unblock().assertEnded();
|
||||
|
||||
removeAction1.assertStarted();
|
||||
removeAction1.unblock().assertEnded();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testStopAndResume() throws Throwable {
|
||||
FakeDownloadAction download1Action = createDownloadAction("media 1");
|
||||
FakeDownloadAction remove2Action = createRemoveAction("media 2");
|
||||
FakeDownloadAction download2Action = createDownloadAction("media 2");
|
||||
FakeDownloadAction remove1Action = createRemoveAction("media 1");
|
||||
FakeDownloadAction download3Action = createDownloadAction("media 3");
|
||||
|
||||
download1Action.post().assertStarted();
|
||||
remove2Action.post().assertStarted();
|
||||
download2Action.post().assertDoesNotStart();
|
||||
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager.stopDownloads();
|
||||
}
|
||||
});
|
||||
|
||||
download1Action.assertStopped();
|
||||
|
||||
// remove actions aren't stopped.
|
||||
remove2Action.unblock().assertEnded();
|
||||
// Although remove2Action is finished, download2Action doesn't start.
|
||||
download2Action.assertDoesNotStart();
|
||||
|
||||
// When a new remove action is added, it cancels stopped download actions with the same media.
|
||||
remove1Action.post();
|
||||
download1Action.assertCancelled();
|
||||
remove1Action.assertStarted().unblock().assertEnded();
|
||||
|
||||
// New download actions can be added but they don't start.
|
||||
download3Action.post().assertDoesNotStart();
|
||||
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager.startDownloads();
|
||||
}
|
||||
});
|
||||
|
||||
download2Action.assertStarted().unblock().assertEnded();
|
||||
download3Action.assertStarted().unblock().assertEnded();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
public void testResumeBeforeTotallyStopped() throws Throwable {
|
||||
setUpDownloadManager(2);
|
||||
FakeDownloadAction download1Action = createDownloadAction("media 1").ignoreInterrupts();
|
||||
FakeDownloadAction download2Action = createDownloadAction("media 2");
|
||||
FakeDownloadAction download3Action = createDownloadAction("media 3");
|
||||
|
||||
download1Action.post().assertStarted();
|
||||
download2Action.post().assertStarted();
|
||||
// download3Action doesn't start as DM was configured to run two downloads in parallel.
|
||||
download3Action.post().assertDoesNotStart();
|
||||
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager.stopDownloads();
|
||||
}
|
||||
});
|
||||
|
||||
// download1Action doesn't stop yet as it ignores interrupts.
|
||||
download2Action.assertStopped();
|
||||
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager.startDownloads();
|
||||
}
|
||||
});
|
||||
|
||||
// download2Action starts immediately.
|
||||
download2Action.assertStarted();
|
||||
|
||||
// download3Action doesn't start as download1Action still holds its slot.
|
||||
download3Action.assertDoesNotStart();
|
||||
|
||||
// when unblocked download1Action stops and starts immediately.
|
||||
download1Action.unblock().assertStopped().assertStarted();
|
||||
|
||||
download1Action.unblock();
|
||||
download2Action.unblock();
|
||||
download3Action.unblock();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
private void doTestActionRuns(FakeDownloadAction action) throws Throwable {
|
||||
action.post().assertStarted().unblock().assertEnded();
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
private void doTestActionsRunSequentially(FakeDownloadAction action1,
|
||||
FakeDownloadAction action2) throws Throwable {
|
||||
action1.ignoreInterrupts().post().assertStarted();
|
||||
action2.post().assertDoesNotStart();
|
||||
|
||||
action1.unblock();
|
||||
action2.assertStarted();
|
||||
|
||||
action2.unblock().assertEnded();
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
private void doTestActionsRunInParallel(FakeDownloadAction action1,
|
||||
FakeDownloadAction action2) throws Throwable {
|
||||
action1.post().assertStarted();
|
||||
action2.post().assertStarted();
|
||||
action1.unblock().assertEnded();
|
||||
action2.unblock().assertEnded();
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
private FakeDownloadAction createDownloadAction(String mediaId) {
|
||||
return new FakeDownloadAction(mediaId, false);
|
||||
}
|
||||
|
||||
private FakeDownloadAction createRemoveAction(String mediaId) {
|
||||
return new FakeDownloadAction(mediaId, true);
|
||||
}
|
||||
|
||||
private static final class TestDownloadListener implements DownloadListener {
|
||||
|
||||
private ConditionVariable downloadFinishedCondition;
|
||||
private Throwable downloadError;
|
||||
|
||||
private TestDownloadListener() {
|
||||
downloadFinishedCondition = new ConditionVariable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) {
|
||||
if (downloadState.state == DownloadState.STATE_ERROR && downloadError == null) {
|
||||
downloadError = downloadState.error;
|
||||
}
|
||||
((FakeDownloadAction) downloadState.downloadAction).onStateChange(downloadState.state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIdle(DownloadManager downloadManager) {
|
||||
downloadFinishedCondition.open();
|
||||
}
|
||||
|
||||
private void clearDownloadError() {
|
||||
this.downloadError = null;
|
||||
}
|
||||
|
||||
private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {
|
||||
assertThat(downloadFinishedCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue();
|
||||
downloadFinishedCondition.close();
|
||||
if (downloadError != null) {
|
||||
throw new Exception(downloadError);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class FakeDownloadAction extends DownloadAction {
|
||||
|
||||
private final String mediaId;
|
||||
private final boolean removeAction;
|
||||
private final FakeDownloader downloader;
|
||||
private final BlockingQueue<Integer> states;
|
||||
|
||||
private FakeDownloadAction(String mediaId, boolean removeAction) {
|
||||
super(mediaId);
|
||||
this.mediaId = mediaId;
|
||||
this.removeAction = removeAction;
|
||||
this.downloader = new FakeDownloader(removeAction);
|
||||
this.states = new ArrayBlockingQueue<>(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getType() {
|
||||
return "FakeDownloadAction";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeToStream(DataOutputStream output) throws IOException {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemoveAction() {
|
||||
return removeAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSameMedia(DownloadAction other) {
|
||||
return other instanceof FakeDownloadAction
|
||||
&& mediaId.equals(((FakeDownloadAction) other).mediaId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
private FakeDownloader getFakeDownloader() {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
private FakeDownloadAction post() throws Throwable {
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager.handleAction(FakeDownloadAction.this);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
private FakeDownloadAction assertDoesNotStart() {
|
||||
assertThat(downloader.started.block(ASSERT_FALSE_TIME)).isFalse();
|
||||
return this;
|
||||
}
|
||||
|
||||
private FakeDownloadAction assertStarted() {
|
||||
downloader.assertStarted(ASSERT_TRUE_TIMEOUT);
|
||||
return assertState(DownloadState.STATE_STARTED);
|
||||
}
|
||||
|
||||
private FakeDownloadAction assertEnded() {
|
||||
return assertState(DownloadState.STATE_ENDED);
|
||||
}
|
||||
|
||||
private FakeDownloadAction assertError() {
|
||||
return assertState(DownloadState.STATE_ERROR);
|
||||
}
|
||||
|
||||
private FakeDownloadAction assertCancelled() {
|
||||
return assertState(DownloadState.STATE_CANCELED);
|
||||
}
|
||||
|
||||
private FakeDownloadAction assertStopped() {
|
||||
assertState(DownloadState.STATE_STOPPING);
|
||||
return assertState(DownloadState.STATE_WAITING);
|
||||
}
|
||||
|
||||
private FakeDownloadAction assertState(@State int expectedState) {
|
||||
ArrayList<Integer> receivedStates = new ArrayList<>();
|
||||
while (true) {
|
||||
Integer state = null;
|
||||
try {
|
||||
state = states.poll(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
if (state != null) {
|
||||
if (expectedState == state) {
|
||||
return this;
|
||||
}
|
||||
receivedStates.add(state);
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < receivedStates.size(); i++) {
|
||||
if (i > 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append(DownloadState.getStateString(receivedStates.get(i)));
|
||||
}
|
||||
fail(
|
||||
String.format(
|
||||
Locale.US,
|
||||
"expected:<%s> but was:<%s>",
|
||||
DownloadState.getStateString(expectedState),
|
||||
sb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FakeDownloadAction unblock() {
|
||||
downloader.unblock();
|
||||
return this;
|
||||
}
|
||||
|
||||
private FakeDownloadAction ignoreInterrupts() {
|
||||
downloader.ignoreInterrupts = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void onStateChange(int state) {
|
||||
states.add(state);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeDownloader implements Downloader {
|
||||
private final ConditionVariable started;
|
||||
private final com.google.android.exoplayer2.util.ConditionVariable blocker;
|
||||
private final boolean removeAction;
|
||||
private boolean ignoreInterrupts;
|
||||
private volatile boolean enableDownloadIOException;
|
||||
private volatile int downloadedBytes = C.LENGTH_UNSET;
|
||||
|
||||
private FakeDownloader(boolean removeAction) {
|
||||
this.removeAction = removeAction;
|
||||
this.started = new ConditionVariable();
|
||||
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws InterruptedException, IOException {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(@Nullable ProgressListener listener)
|
||||
throws InterruptedException, IOException {
|
||||
assertThat(removeAction).isFalse();
|
||||
started.open();
|
||||
block();
|
||||
if (enableDownloadIOException) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() throws InterruptedException {
|
||||
assertThat(removeAction).isTrue();
|
||||
started.open();
|
||||
block();
|
||||
}
|
||||
|
||||
private void block() throws InterruptedException {
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
blocker.block();
|
||||
break;
|
||||
} catch (InterruptedException e) {
|
||||
if (!ignoreInterrupts) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
blocker.close();
|
||||
}
|
||||
}
|
||||
|
||||
private FakeDownloader assertStarted(int timeout) {
|
||||
assertThat(started.block(timeout)).isTrue();
|
||||
started.close();
|
||||
return this;
|
||||
}
|
||||
|
||||
private FakeDownloader unblock() {
|
||||
blocker.open();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDownloadedBytes() {
|
||||
return downloadedBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getDownloadPercentage() {
|
||||
return Float.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
|
||||
import com.google.android.exoplayer2.util.AtomicFile;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Stores and loads {@link DownloadAction}s to/from a file.
|
||||
*/
|
||||
public final class ActionFile {
|
||||
|
||||
private final AtomicFile atomicFile;
|
||||
private final File actionFile;
|
||||
|
||||
/**
|
||||
* @param actionFile File to be used to store and load {@link DownloadAction}s.
|
||||
*/
|
||||
public ActionFile(File actionFile) {
|
||||
this.actionFile = actionFile;
|
||||
atomicFile = new AtomicFile(actionFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads {@link DownloadAction}s from file.
|
||||
*
|
||||
* @param deserializers {@link Deserializer}s to deserialize DownloadActions.
|
||||
* @return Loaded DownloadActions. If the action file doesn't exists returns an empty array.
|
||||
* @throws IOException If there is an error during loading.
|
||||
*/
|
||||
public DownloadAction[] load(Deserializer... deserializers) throws IOException {
|
||||
if (!actionFile.exists()) {
|
||||
return new DownloadAction[0];
|
||||
}
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = atomicFile.openRead();
|
||||
DataInputStream dataInputStream = new DataInputStream(inputStream);
|
||||
int version = dataInputStream.readInt();
|
||||
if (version > DownloadAction.MASTER_VERSION) {
|
||||
throw new IOException("Not supported action file version: " + version);
|
||||
}
|
||||
int actionCount = dataInputStream.readInt();
|
||||
DownloadAction[] actions = new DownloadAction[actionCount];
|
||||
for (int i = 0; i < actionCount; i++) {
|
||||
actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream, version);
|
||||
}
|
||||
return actions;
|
||||
} finally {
|
||||
Util.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores {@link DownloadAction}s to file.
|
||||
*
|
||||
* @param downloadActions DownloadActions to store to file.
|
||||
* @throws IOException If there is an error during storing.
|
||||
*/
|
||||
public void store(DownloadAction... downloadActions) throws IOException {
|
||||
DataOutputStream output = null;
|
||||
try {
|
||||
output = new DataOutputStream(atomicFile.startWrite());
|
||||
output.writeInt(DownloadAction.MASTER_VERSION);
|
||||
output.writeInt(downloadActions.length);
|
||||
for (DownloadAction action : downloadActions) {
|
||||
DownloadAction.serializeToStream(action, output);
|
||||
}
|
||||
atomicFile.endWrite(output);
|
||||
// Avoid calling close twice.
|
||||
output = null;
|
||||
} finally {
|
||||
Util.closeQuietly(output);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/** Contains the necessary parameters for a download or remove action. */
|
||||
public abstract class DownloadAction {
|
||||
|
||||
/**
|
||||
* Master version for all {@link DownloadAction} serialization/deserialization implementations. On
|
||||
* each change on any {@link DownloadAction} serialization format this version needs to be
|
||||
* increased.
|
||||
*/
|
||||
public static final int MASTER_VERSION = 0;
|
||||
|
||||
/** Used to deserialize {@link DownloadAction}s. */
|
||||
public interface Deserializer {
|
||||
|
||||
/** Returns the type string of the {@link DownloadAction}. This string should be unique. */
|
||||
String getType();
|
||||
|
||||
/**
|
||||
* Deserializes a {@link DownloadAction} from the {@code input}.
|
||||
*
|
||||
* @param version Version of the data.
|
||||
* @param input DataInputStream to read data from.
|
||||
* @see DownloadAction#writeToStream(DataOutputStream)
|
||||
* @see DownloadAction#MASTER_VERSION
|
||||
*/
|
||||
DownloadAction readFromStream(int version, DataInputStream input) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes one {@code action} which was serialized by {@link
|
||||
* #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the
|
||||
* {@link Deserializer}s which supports the type of the action.
|
||||
*
|
||||
* <p>The caller is responsible for closing the given {@link InputStream}.
|
||||
*
|
||||
* @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}.
|
||||
* @param input Input stream to read serialized data.
|
||||
* @return The deserialized {@link DownloadAction}.
|
||||
* @throws IOException If there is an IO error from {@code input} or the action type isn't
|
||||
* supported by any of the {@code deserializers}.
|
||||
*/
|
||||
public static DownloadAction deserializeFromStream(
|
||||
Deserializer[] deserializers, InputStream input) throws IOException {
|
||||
return deserializeFromStream(deserializers, input, MASTER_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes one {@code action} which was serialized by {@link
|
||||
* #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the
|
||||
* {@link Deserializer}s which supports the type of the action.
|
||||
*
|
||||
* <p>The caller is responsible for closing the given {@link InputStream}.
|
||||
*
|
||||
* @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}.
|
||||
* @param input Input stream to read serialized data.
|
||||
* @param version Master version of the serialization. See {@link DownloadAction#MASTER_VERSION}.
|
||||
* @return The deserialized {@link DownloadAction}.
|
||||
* @throws IOException If there is an IO error from {@code input}.
|
||||
* @throws DownloadException If the action type isn't supported by any of the {@code
|
||||
* deserializers}.
|
||||
*/
|
||||
public static DownloadAction deserializeFromStream(
|
||||
Deserializer[] deserializers, InputStream input, int version) throws IOException {
|
||||
// Don't close the stream as it closes the underlying stream too.
|
||||
DataInputStream dataInputStream = new DataInputStream(input);
|
||||
String type = dataInputStream.readUTF();
|
||||
for (Deserializer deserializer : deserializers) {
|
||||
if (type.equals(deserializer.getType())) {
|
||||
return deserializer.readFromStream(version, dataInputStream);
|
||||
}
|
||||
}
|
||||
throw new DownloadException("No Deserializer can be found to parse the data.");
|
||||
}
|
||||
|
||||
/** Serializes {@code action} type and data into the {@code output}. */
|
||||
public static void serializeToStream(DownloadAction action, OutputStream output)
|
||||
throws IOException {
|
||||
// Don't close the stream as it closes the underlying stream too.
|
||||
DataOutputStream dataOutputStream = new DataOutputStream(output);
|
||||
dataOutputStream.writeUTF(action.getType());
|
||||
action.writeToStream(dataOutputStream);
|
||||
dataOutputStream.flush();
|
||||
}
|
||||
|
||||
private final String data;
|
||||
|
||||
/** @param data Optional custom data for this action. If null, an empty string is used. */
|
||||
protected DownloadAction(String data) {
|
||||
this.data = data != null ? data : "";
|
||||
}
|
||||
|
||||
/** Serializes itself into a byte array. */
|
||||
public final byte[] toByteArray() {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
try {
|
||||
serializeToStream(this, output);
|
||||
} catch (IOException e) {
|
||||
// ByteArrayOutputStream shouldn't throw IOException.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
/** Returns custom data for this action. */
|
||||
public final String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Returns whether this is a remove action or a download action. */
|
||||
public abstract boolean isRemoveAction();
|
||||
|
||||
/** Returns the type string of the {@link DownloadAction}. This string should be unique. */
|
||||
protected abstract String getType();
|
||||
|
||||
/** Serializes itself into the {@code output}. */
|
||||
protected abstract void writeToStream(DataOutputStream output) throws IOException;
|
||||
|
||||
/** Returns whether this is action is for the same media as the {@code other}. */
|
||||
protected abstract boolean isSameMedia(DownloadAction other);
|
||||
|
||||
/** Creates a {@link Downloader} with the given parameters. */
|
||||
protected abstract Downloader createDownloader(
|
||||
DownloaderConstructorHelper downloaderConstructorHelper);
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DownloadAction that = (DownloadAction) o;
|
||||
return data.equals(that.data) && isRemoveAction() == that.isRemoveAction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = data.hashCode();
|
||||
result = 31 * result + (isRemoveAction() ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,717 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_CANCELED;
|
||||
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_CANCELING;
|
||||
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ENDED;
|
||||
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ERROR;
|
||||
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STARTED;
|
||||
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STOPPING;
|
||||
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_WAITING;
|
||||
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* Manages multiple stream download and remove requests.
|
||||
*
|
||||
* <p>By default downloads are stopped. Call {@link #startDownloads()} to start downloads.
|
||||
*
|
||||
* <p>WARNING: Methods of this class must be called only on the main thread of the application.
|
||||
*/
|
||||
public final class DownloadManager {
|
||||
|
||||
/**
|
||||
* Listener for download events. Listener methods are called on the main thread of the
|
||||
* application.
|
||||
*/
|
||||
public interface DownloadListener {
|
||||
/**
|
||||
* Called on download state change.
|
||||
*
|
||||
* @param downloadManager The reporting instance.
|
||||
* @param downloadState The download task.
|
||||
*/
|
||||
void onStateChange(DownloadManager downloadManager, DownloadState downloadState);
|
||||
|
||||
/**
|
||||
* Called when there is no active task left.
|
||||
*
|
||||
* @param downloadManager The reporting instance.
|
||||
*/
|
||||
void onIdle(DownloadManager downloadManager);
|
||||
}
|
||||
|
||||
private static final String TAG = "DownloadManager";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private final DownloaderConstructorHelper downloaderConstructorHelper;
|
||||
private final int maxActiveDownloadTasks;
|
||||
private final int minRetryCount;
|
||||
private final ActionFile actionFile;
|
||||
private final DownloadAction.Deserializer[] deserializers;
|
||||
private final ArrayList<DownloadTask> tasks;
|
||||
private final ArrayList<DownloadTask> activeDownloadTasks;
|
||||
private final Handler handler;
|
||||
private final HandlerThread fileIOThread;
|
||||
private final Handler fileIOHandler;
|
||||
private final CopyOnWriteArraySet<DownloadListener> listeners;
|
||||
|
||||
private int nextTaskId;
|
||||
private boolean actionFileLoadCompleted;
|
||||
private boolean released;
|
||||
private boolean downloadsStopped;
|
||||
|
||||
/**
|
||||
* Constructs a {@link DownloadManager}.
|
||||
*
|
||||
* @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s
|
||||
* for downloading data.
|
||||
* @param maxActiveDownloadTasks Max number of download tasks to be started in parallel.
|
||||
* @param minRetryCount The minimum number of times the downloads must be retried before failing.
|
||||
* @param actionSaveFile File to save active actions.
|
||||
* @param deserializers Used to deserialize {@link DownloadAction}s.
|
||||
*/
|
||||
public DownloadManager(
|
||||
DownloaderConstructorHelper constructorHelper,
|
||||
int maxActiveDownloadTasks,
|
||||
int minRetryCount,
|
||||
String actionSaveFile,
|
||||
Deserializer... deserializers) {
|
||||
this.downloaderConstructorHelper = constructorHelper;
|
||||
this.maxActiveDownloadTasks = maxActiveDownloadTasks;
|
||||
this.minRetryCount = minRetryCount;
|
||||
this.actionFile = new ActionFile(new File(actionSaveFile));
|
||||
this.deserializers = deserializers;
|
||||
this.downloadsStopped = true;
|
||||
|
||||
tasks = new ArrayList<>();
|
||||
activeDownloadTasks = new ArrayList<>();
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
fileIOThread = new HandlerThread("DownloadManager file i/o");
|
||||
fileIOThread.start();
|
||||
fileIOHandler = new Handler(fileIOThread.getLooper());
|
||||
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
loadActions();
|
||||
logd("DownloadManager is created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all of the tasks and releases resources. If the action file isn't up to date,
|
||||
* waits for the changes to be written.
|
||||
*/
|
||||
public void release() {
|
||||
released = true;
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
tasks.get(i).stop();
|
||||
}
|
||||
final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
|
||||
fileIOHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fileIOFinishedCondition.open();
|
||||
}
|
||||
});
|
||||
fileIOFinishedCondition.block();
|
||||
fileIOThread.quit();
|
||||
logd("DownloadManager is released");
|
||||
}
|
||||
|
||||
/** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */
|
||||
public void stopDownloads() {
|
||||
if (!downloadsStopped) {
|
||||
downloadsStopped = true;
|
||||
for (int i = 0; i < activeDownloadTasks.size(); i++) {
|
||||
activeDownloadTasks.get(i).stop();
|
||||
}
|
||||
logd("Downloads are stopping");
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the download tasks. */
|
||||
public void startDownloads() {
|
||||
if (downloadsStopped) {
|
||||
downloadsStopped = false;
|
||||
maybeStartTasks();
|
||||
logd("Downloads are started");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link DownloadListener}.
|
||||
*
|
||||
* @param listener The listener to be added.
|
||||
*/
|
||||
public void addListener(DownloadListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link DownloadListener}.
|
||||
*
|
||||
* @param listener The listener to be removed.
|
||||
*/
|
||||
public void removeListener(DownloadListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes one {@link DownloadAction} from {@code actionData} and calls {@link
|
||||
* #handleAction(DownloadAction)}.
|
||||
*
|
||||
* @param actionData Serialized {@link DownloadAction} data.
|
||||
* @return The task id.
|
||||
* @throws IOException If an error occurs during handling action.
|
||||
*/
|
||||
public int handleAction(byte[] actionData) throws IOException {
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(actionData);
|
||||
DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input);
|
||||
return handleAction(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given {@link DownloadAction}. A task is created and added to the task queue. If
|
||||
* it's a remove action then this method cancels any download tasks which works on the same media
|
||||
* immediately.
|
||||
*
|
||||
* @param downloadAction Action to be executed.
|
||||
* @return The task id.
|
||||
*/
|
||||
public int handleAction(DownloadAction downloadAction) {
|
||||
DownloadTask downloadTask = createDownloadTask(downloadAction);
|
||||
saveActions();
|
||||
if (downloadsStopped && !downloadAction.isRemoveAction()) {
|
||||
logd("Can't start the task as downloads are stopped", downloadTask);
|
||||
} else {
|
||||
maybeStartTasks();
|
||||
}
|
||||
return downloadTask.id;
|
||||
}
|
||||
|
||||
private DownloadTask createDownloadTask(DownloadAction downloadAction) {
|
||||
DownloadTask downloadTask = new DownloadTask(nextTaskId++, this, downloadAction, minRetryCount);
|
||||
tasks.add(downloadTask);
|
||||
logd("Task is added", downloadTask);
|
||||
notifyListenersTaskStateChange(downloadTask);
|
||||
return downloadTask;
|
||||
}
|
||||
|
||||
/** Returns number of tasks. */
|
||||
public int getTaskCount() {
|
||||
return tasks.size();
|
||||
}
|
||||
|
||||
/** Returns a {@link DownloadTask} for a task. */
|
||||
public DownloadState getDownloadState(int taskId) {
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
DownloadTask task = tasks.get(i);
|
||||
if (task.id == taskId) {
|
||||
return task.getDownloadState();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns {@link DownloadState}s for all tasks. */
|
||||
public DownloadState[] getDownloadStates() {
|
||||
return getDownloadStates(tasks);
|
||||
}
|
||||
|
||||
/** Returns an array of {@link DownloadState}s for active download tasks. */
|
||||
public DownloadState[] getActiveDownloadStates() {
|
||||
return getDownloadStates(activeDownloadTasks);
|
||||
}
|
||||
|
||||
/** Returns whether there are no active tasks. */
|
||||
public boolean isIdle() {
|
||||
if (!actionFileLoadCompleted) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
if (tasks.get(i).isRunning()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through the task queue and starts any task if all of the following are true:
|
||||
*
|
||||
* <ul>
|
||||
* <li>It hasn't started yet.
|
||||
* <li>There are no preceding conflicting tasks.
|
||||
* <li>If it's a download task then there are no preceding download tasks on hold and the
|
||||
* maximum number of active downloads hasn't been reached.
|
||||
* </ul>
|
||||
*
|
||||
* If the task is a remove action then preceding conflicting tasks are canceled.
|
||||
*/
|
||||
private void maybeStartTasks() {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean skipDownloadActions = downloadsStopped
|
||||
|| activeDownloadTasks.size() == maxActiveDownloadTasks;
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
DownloadTask downloadTask = tasks.get(i);
|
||||
if (!downloadTask.canStart()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DownloadAction downloadAction = downloadTask.downloadAction;
|
||||
boolean removeAction = downloadAction.isRemoveAction();
|
||||
if (!removeAction && skipDownloadActions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean canStartTask = true;
|
||||
for (int j = 0; j < i; j++) {
|
||||
DownloadTask task = tasks.get(j);
|
||||
if (task.downloadAction.isSameMedia(downloadAction)) {
|
||||
if (removeAction) {
|
||||
canStartTask = false;
|
||||
logd(downloadTask + " clashes with " + task);
|
||||
task.cancel();
|
||||
// Continue loop to cancel any other preceding clashing tasks.
|
||||
} else if (task.downloadAction.isRemoveAction()) {
|
||||
canStartTask = false;
|
||||
skipDownloadActions = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canStartTask) {
|
||||
downloadTask.start();
|
||||
if (!removeAction) {
|
||||
activeDownloadTasks.add(downloadTask);
|
||||
skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyListenersIdle() {
|
||||
if (!isIdle()) {
|
||||
return;
|
||||
}
|
||||
logd("Notify idle state");
|
||||
for (DownloadListener listener : listeners) {
|
||||
listener.onIdle(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void onTaskStateChange(DownloadTask downloadTask) {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
logd("Task state is changed", downloadTask);
|
||||
boolean stopped = !downloadTask.isRunning();
|
||||
if (stopped) {
|
||||
activeDownloadTasks.remove(downloadTask);
|
||||
}
|
||||
notifyListenersTaskStateChange(downloadTask);
|
||||
if (downloadTask.isFinished()) {
|
||||
tasks.remove(downloadTask);
|
||||
saveActions();
|
||||
}
|
||||
if (stopped) {
|
||||
maybeStartTasks();
|
||||
maybeNotifyListenersIdle();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListenersTaskStateChange(DownloadTask downloadTask) {
|
||||
DownloadState downloadState = downloadTask.getDownloadState();
|
||||
for (DownloadListener listener : listeners) {
|
||||
listener.onStateChange(this, downloadState);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadActions() {
|
||||
fileIOHandler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DownloadAction[] loadedActions;
|
||||
try {
|
||||
loadedActions = actionFile.load(DownloadManager.this.deserializers);
|
||||
logd("Action file is loaded.");
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "Action file loading failed.", e);
|
||||
loadedActions = new DownloadAction[0];
|
||||
}
|
||||
final DownloadAction[] actions = loadedActions;
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
for (DownloadAction action : actions) {
|
||||
createDownloadTask(action);
|
||||
}
|
||||
logd("Tasks are created.");
|
||||
maybeStartTasks();
|
||||
} finally {
|
||||
actionFileLoadCompleted = true;
|
||||
maybeNotifyListenersIdle();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void saveActions() {
|
||||
if (!actionFileLoadCompleted || released) {
|
||||
return;
|
||||
}
|
||||
final DownloadAction[] actions = new DownloadAction[tasks.size()];
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
actions[i] = tasks.get(i).downloadAction;
|
||||
}
|
||||
fileIOHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
actionFile.store(actions);
|
||||
logd("Actions persisted.");
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Persisting actions failed.", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void logd(String message) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void logd(String message, DownloadTask task) {
|
||||
logd(message + ": " + task);
|
||||
}
|
||||
|
||||
private static DownloadState[] getDownloadStates(ArrayList<DownloadTask> tasks) {
|
||||
DownloadState[] states = new DownloadState[tasks.size()];
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
DownloadTask task = tasks.get(i);
|
||||
states[i] = task.getDownloadState();
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
/** Represents state of a download task. */
|
||||
public static final class DownloadState {
|
||||
|
||||
/**
|
||||
* Task states.
|
||||
*
|
||||
* <p>Transition map (vertical states are source states):
|
||||
* <pre>
|
||||
* +-------+-------+-----+---------+--------+--------+-----+
|
||||
* |waiting|started|ended|canceling|canceled|stopping|error|
|
||||
* +---------+-------+-------+-----+---------+--------+--------+-----+
|
||||
* |waiting | | X | | X | | | |
|
||||
* |started | | | X | X | | X | X |
|
||||
* |canceling| | | | | X | | |
|
||||
* |stopping | X | | | | | | |
|
||||
* +---------+-------+-------+-----+---------+--------+--------+-----+
|
||||
* </pre>
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({STATE_WAITING, STATE_STARTED, STATE_ENDED, STATE_CANCELING, STATE_CANCELED,
|
||||
STATE_STOPPING, STATE_ERROR})
|
||||
public @interface State {}
|
||||
/** The task is waiting to be started. */
|
||||
public static final int STATE_WAITING = 0;
|
||||
/** The task is currently started. */
|
||||
public static final int STATE_STARTED = 1;
|
||||
/** The task completed. */
|
||||
public static final int STATE_ENDED = 2;
|
||||
/** The task is about to be canceled. */
|
||||
public static final int STATE_CANCELING = 3;
|
||||
/** The task was canceled. */
|
||||
public static final int STATE_CANCELED = 4;
|
||||
/** The task is about to be stopped. */
|
||||
public static final int STATE_STOPPING = 5;
|
||||
/** The task failed. */
|
||||
public static final int STATE_ERROR = 6;
|
||||
|
||||
/** Returns the state string for the given state value. */
|
||||
public static String getStateString(@State int state) {
|
||||
switch (state) {
|
||||
case STATE_WAITING:
|
||||
return "WAITING";
|
||||
case STATE_STARTED:
|
||||
return "STARTED";
|
||||
case STATE_ENDED:
|
||||
return "ENDED";
|
||||
case STATE_CANCELING:
|
||||
return "CANCELING";
|
||||
case STATE_CANCELED:
|
||||
return "CANCELED";
|
||||
case STATE_STOPPING:
|
||||
return "STOPPING";
|
||||
case STATE_ERROR:
|
||||
return "ERROR";
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
/** Unique id of the task. */
|
||||
public final int taskId;
|
||||
/** The {@link DownloadAction} which is being executed. */
|
||||
public final DownloadAction downloadAction;
|
||||
/** The state of the task. See {@link State}. */
|
||||
public final @State int state;
|
||||
/**
|
||||
* The download percentage, or {@link Float#NaN} if it can't be calculated or the task is for
|
||||
* removing.
|
||||
*/
|
||||
public final float downloadPercentage;
|
||||
/**
|
||||
* The downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been calculated yet or the task
|
||||
* is for removing.
|
||||
*/
|
||||
public final long downloadedBytes;
|
||||
/** If {@link #state} is {@link #STATE_ERROR} then this is the cause, otherwise null. */
|
||||
public final Throwable error;
|
||||
|
||||
private DownloadState(
|
||||
int taskId,
|
||||
DownloadAction downloadAction,
|
||||
int state,
|
||||
float downloadPercentage,
|
||||
long downloadedBytes,
|
||||
Throwable error) {
|
||||
this.taskId = taskId;
|
||||
this.downloadAction = downloadAction;
|
||||
this.state = state;
|
||||
this.downloadPercentage = downloadPercentage;
|
||||
this.downloadedBytes = downloadedBytes;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
/** Returns whether the task is finished. */
|
||||
public boolean isFinished() {
|
||||
return state == STATE_ERROR || state == STATE_ENDED || state == STATE_CANCELED;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DownloadTask implements Runnable {
|
||||
|
||||
private final int id;
|
||||
private final DownloadManager downloadManager;
|
||||
private final DownloadAction downloadAction;
|
||||
private final int minRetryCount;
|
||||
private volatile @State int currentState;
|
||||
private volatile Downloader downloader;
|
||||
private Thread thread;
|
||||
private Throwable error;
|
||||
|
||||
private DownloadTask(
|
||||
int id, DownloadManager downloadManager, DownloadAction downloadAction, int minRetryCount) {
|
||||
this.id = id;
|
||||
this.downloadManager = downloadManager;
|
||||
this.downloadAction = downloadAction;
|
||||
this.currentState = STATE_WAITING;
|
||||
this.minRetryCount = minRetryCount;
|
||||
}
|
||||
|
||||
public DownloadState getDownloadState() {
|
||||
return new DownloadState(
|
||||
id, downloadAction, currentState, getDownloadPercentage(), getDownloadedBytes(), error);
|
||||
}
|
||||
|
||||
/** Returns the {@link DownloadAction}. */
|
||||
public DownloadAction getDownloadAction() {
|
||||
return downloadAction;
|
||||
}
|
||||
|
||||
/** Returns the state of the task. */
|
||||
public @State int getState() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
/** Returns whether the task is finished. */
|
||||
public boolean isFinished() {
|
||||
return currentState == STATE_ERROR || currentState == STATE_ENDED
|
||||
|| currentState == STATE_CANCELED;
|
||||
}
|
||||
|
||||
/** Returns whether the task is running. */
|
||||
public boolean isRunning() {
|
||||
return currentState == STATE_STARTED
|
||||
|| currentState == STATE_STOPPING
|
||||
|| currentState == STATE_CANCELING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This
|
||||
* value can be an estimation.
|
||||
*/
|
||||
public float getDownloadPercentage() {
|
||||
return downloader != null ? downloader.getDownloadPercentage() : Float.NaN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been
|
||||
* calculated yet.
|
||||
*/
|
||||
public long getDownloadedBytes() {
|
||||
return downloader != null ? downloader.getDownloadedBytes() : C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!DEBUG) {
|
||||
return super.toString();
|
||||
}
|
||||
return downloadAction.getType()
|
||||
+ ' '
|
||||
+ (downloadAction.isRemoveAction() ? "remove" : "download")
|
||||
+ ' '
|
||||
+ downloadAction.getData()
|
||||
+ ' '
|
||||
+ DownloadState.getStateString(currentState);
|
||||
}
|
||||
|
||||
private void start() {
|
||||
if (changeStateAndNotify(STATE_WAITING, STATE_STARTED)) {
|
||||
thread = new Thread(this);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canStart() {
|
||||
return currentState == STATE_WAITING;
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
if (changeStateAndNotify(STATE_WAITING, STATE_CANCELING)) {
|
||||
downloadManager.handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
changeStateAndNotify(STATE_CANCELING, STATE_CANCELED);
|
||||
}
|
||||
});
|
||||
} else if (changeStateAndNotify(STATE_STARTED, STATE_CANCELING)) {
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
if (changeStateAndNotify(STATE_STARTED, STATE_STOPPING)) {
|
||||
downloadManager.logd("Stopping", this);
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean changeStateAndNotify(@State int oldState, @State int newState) {
|
||||
return changeStateAndNotify(oldState, newState, null);
|
||||
}
|
||||
|
||||
private boolean changeStateAndNotify(@State int oldState, @State int newState,
|
||||
Throwable error) {
|
||||
if (currentState != oldState) {
|
||||
return false;
|
||||
}
|
||||
currentState = newState;
|
||||
this.error = error;
|
||||
downloadManager.onTaskStateChange(DownloadTask.this);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Methods running on download thread. */
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
downloadManager.logd("Task is started", DownloadTask.this);
|
||||
Throwable error = null;
|
||||
try {
|
||||
downloader = downloadAction.createDownloader(downloadManager.downloaderConstructorHelper);
|
||||
if (downloadAction.isRemoveAction()) {
|
||||
downloader.remove();
|
||||
} else {
|
||||
int errorCount = 0;
|
||||
long errorPosition = C.LENGTH_UNSET;
|
||||
while (true) {
|
||||
try {
|
||||
downloader.download(null);
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
long downloadedBytes = downloader.getDownloadedBytes();
|
||||
if (downloadedBytes != errorPosition) {
|
||||
downloadManager.logd(
|
||||
"Reset error count. downloadedBytes = " + downloadedBytes, this);
|
||||
errorPosition = downloadedBytes;
|
||||
errorCount = 0;
|
||||
}
|
||||
if (currentState != STATE_STARTED || ++errorCount > minRetryCount) {
|
||||
throw e;
|
||||
}
|
||||
downloadManager.logd("Download error. Retry " + errorCount, this);
|
||||
Thread.sleep(getRetryDelayMillis(errorCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e){
|
||||
error = e;
|
||||
}
|
||||
final Throwable finalError = error;
|
||||
downloadManager.handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (changeStateAndNotify(STATE_STARTED,
|
||||
finalError != null ? STATE_ERROR : STATE_ENDED, finalError)
|
||||
|| changeStateAndNotify(STATE_CANCELING, STATE_CANCELED)
|
||||
|| changeStateAndNotify(STATE_STOPPING, STATE_WAITING)) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int getRetryDelayMillis(int errorCount) {
|
||||
return Math.min((errorCount - 1) * 1000, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,396 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Notification.Builder;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.util.scheduler.Requirements;
|
||||
import com.google.android.exoplayer2.util.scheduler.RequirementsWatcher;
|
||||
import com.google.android.exoplayer2.util.scheduler.Scheduler;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link Service} that downloads streams in the background.
|
||||
*
|
||||
* <p>To start the service, create an instance of one of the subclasses of {@link DownloadAction}
|
||||
* and call {@link #addDownloadAction(Context, Class, DownloadAction)} with it.
|
||||
*/
|
||||
public abstract class DownloadService extends Service implements DownloadManager.DownloadListener {
|
||||
|
||||
/** Use this action to initialize {@link DownloadManager}. */
|
||||
public static final String ACTION_INIT =
|
||||
"com.google.android.exoplayer.downloadService.action.INIT";
|
||||
|
||||
/** Use this action to add a {@link DownloadAction} to {@link DownloadManager} action queue. */
|
||||
public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD";
|
||||
|
||||
/** Use this action to make {@link DownloadManager} stop download tasks. */
|
||||
private static final String ACTION_STOP =
|
||||
"com.google.android.exoplayer.downloadService.action.STOP";
|
||||
|
||||
/** Use this action to make {@link DownloadManager} start download tasks. */
|
||||
private static final String ACTION_START =
|
||||
"com.google.android.exoplayer.downloadService.action.START";
|
||||
|
||||
/** A {@link DownloadAction} to be executed. */
|
||||
public static final String DOWNLOAD_ACTION = "DownloadAction";
|
||||
|
||||
/** Default progress update interval in milliseconds. */
|
||||
public static final long DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS = 1000;
|
||||
|
||||
private static final String TAG = "DownloadService";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
// Keep requirementsWatcher and scheduler alive beyond DownloadService life span (until the app is
|
||||
// killed) because it may take long time for Scheduler to start the service.
|
||||
private static RequirementsWatcher requirementsWatcher;
|
||||
private static Scheduler scheduler;
|
||||
|
||||
private final int notificationIdOffset;
|
||||
private final long progressUpdateIntervalMillis;
|
||||
|
||||
private DownloadManager downloadManager;
|
||||
private ProgressUpdater progressUpdater;
|
||||
private int lastStartId;
|
||||
|
||||
/** @param notificationIdOffset Value to offset notification ids. Must be greater than 0. */
|
||||
protected DownloadService(int notificationIdOffset) {
|
||||
this(notificationIdOffset, DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param notificationIdOffset Value to offset notification ids. Must be greater than 0.
|
||||
* @param progressUpdateIntervalMillis {@link #onProgressUpdate(DownloadState[])} is called using
|
||||
* this interval. If it's {@link C#TIME_UNSET}, then {@link
|
||||
* #onProgressUpdate(DownloadState[])} isn't called.
|
||||
*/
|
||||
protected DownloadService(int notificationIdOffset, long progressUpdateIntervalMillis) {
|
||||
this.notificationIdOffset = notificationIdOffset;
|
||||
this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link Intent} to be used to start this service and adds the {@link DownloadAction}
|
||||
* to the {@link DownloadManager}.
|
||||
*
|
||||
* @param context A {@link Context} of the application calling this service.
|
||||
* @param clazz Class object of DownloadService or subclass.
|
||||
* @param downloadAction A {@link DownloadAction} to be executed.
|
||||
* @return Created Intent.
|
||||
*/
|
||||
public static Intent createAddDownloadActionIntent(
|
||||
Context context, Class<? extends DownloadService> clazz, DownloadAction downloadAction) {
|
||||
return new Intent(context, clazz)
|
||||
.setAction(ACTION_ADD)
|
||||
.putExtra(DOWNLOAD_ACTION, downloadAction.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link DownloadAction} to the {@link DownloadManager}. This will start the download
|
||||
* service if it was not running.
|
||||
*
|
||||
* @param context A {@link Context} of the application calling this service.
|
||||
* @param clazz Class object of DownloadService or subclass.
|
||||
* @param downloadAction A {@link DownloadAction} to be executed.
|
||||
* @see #createAddDownloadActionIntent(Context, Class, DownloadAction)
|
||||
*/
|
||||
public static void addDownloadAction(
|
||||
Context context, Class<? extends DownloadService> clazz, DownloadAction downloadAction) {
|
||||
context.startService(createAddDownloadActionIntent(context, clazz, downloadAction));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
logd("onCreate");
|
||||
downloadManager = getDownloadManager();
|
||||
downloadManager.addListener(this);
|
||||
|
||||
if (requirementsWatcher == null) {
|
||||
Requirements requirements = getRequirements();
|
||||
if (requirements != null) {
|
||||
scheduler = getScheduler();
|
||||
RequirementsListener listener =
|
||||
new RequirementsListener(getApplicationContext(), getClass(), scheduler);
|
||||
requirementsWatcher =
|
||||
new RequirementsWatcher(getApplicationContext(), listener, requirements);
|
||||
requirementsWatcher.start();
|
||||
}
|
||||
}
|
||||
|
||||
progressUpdater = new ProgressUpdater(this, progressUpdateIntervalMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
logd("onDestroy");
|
||||
progressUpdater.stop();
|
||||
downloadManager.removeListener(this);
|
||||
if (downloadManager.getTaskCount() == 0) {
|
||||
if (requirementsWatcher != null) {
|
||||
requirementsWatcher.stop();
|
||||
requirementsWatcher = null;
|
||||
}
|
||||
if (scheduler != null) {
|
||||
scheduler.cancel();
|
||||
scheduler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
this.lastStartId = startId;
|
||||
String intentAction = intent != null ? intent.getAction() : null;
|
||||
if (intentAction == null) {
|
||||
intentAction = ACTION_INIT;
|
||||
}
|
||||
logd("onStartCommand action: " + intentAction + " startId: " + startId);
|
||||
switch (intentAction) {
|
||||
case ACTION_INIT:
|
||||
// Do nothing. DownloadManager and RequirementsWatcher is initialized. If there are download
|
||||
// or remove tasks loaded from file, they will start if the requirements are met.
|
||||
break;
|
||||
case ACTION_ADD:
|
||||
byte[] actionData = intent.getByteArrayExtra(DOWNLOAD_ACTION);
|
||||
if (actionData == null) {
|
||||
onCommandError(intent, new IllegalArgumentException("DownloadAction is missing."));
|
||||
} else {
|
||||
try {
|
||||
onNewTask(intent, downloadManager.handleAction(actionData));
|
||||
} catch (IOException e) {
|
||||
onCommandError(intent, e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ACTION_STOP:
|
||||
downloadManager.stopDownloads();
|
||||
break;
|
||||
case ACTION_START:
|
||||
downloadManager.startDownloads();
|
||||
break;
|
||||
default:
|
||||
onCommandError(intent, new IllegalArgumentException("Unknown action: " + intentAction));
|
||||
break;
|
||||
}
|
||||
if (downloadManager.isIdle()) {
|
||||
onIdle(null);
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the
|
||||
* life cycle of the service.
|
||||
*/
|
||||
protected abstract DownloadManager getDownloadManager();
|
||||
|
||||
/**
|
||||
* Returns a {@link Scheduler} which contains a job to initialize {@link DownloadService} when the
|
||||
* requirements are met, or null. If not null, scheduler is used to start downloads even when the
|
||||
* app isn't running.
|
||||
*/
|
||||
protected abstract @Nullable Scheduler getScheduler();
|
||||
|
||||
/** Returns requirements for downloads to take place, or null. */
|
||||
protected abstract @Nullable Requirements getRequirements();
|
||||
|
||||
/** Called on error in start command. */
|
||||
protected void onCommandError(Intent intent, Exception error) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/** Called when a new task is added to the {@link DownloadManager}. */
|
||||
protected void onNewTask(Intent intent, int taskId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/** Returns a notification channelId. See {@link NotificationChannel}. */
|
||||
protected abstract String getNotificationChannelId();
|
||||
|
||||
/**
|
||||
* Helper method which calls {@link #startForeground(int, Notification)} with {@code
|
||||
* notificationIdOffset} and {@code foregroundNotification}.
|
||||
*/
|
||||
public void startForeground(Notification foregroundNotification) {
|
||||
// logd("start foreground");
|
||||
startForeground(notificationIdOffset, foregroundNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/replaces or cancels the notification for the given id.
|
||||
*
|
||||
* @param id A unique id for the notification. This value is offset by {@code
|
||||
* notificationIdOffset}.
|
||||
* @param notification If not null, it's showed, replacing any previous notification. Otherwise
|
||||
* any previous notification is canceled.
|
||||
*/
|
||||
public void setNotification(int id, @Nullable Notification notification) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notification != null) {
|
||||
notificationManager.notify(notificationIdOffset + 1 + id, notification);
|
||||
} else {
|
||||
notificationManager.cancel(notificationIdOffset + 1 + id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to get notified.
|
||||
*
|
||||
* <p>{@inheritDoc}
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) {
|
||||
if (downloadState.state == DownloadState.STATE_STARTED) {
|
||||
progressUpdater.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to get notified.
|
||||
*
|
||||
* <p>{@inheritDoc}
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onIdle(DownloadManager downloadManager) {
|
||||
// Make sure startForeground is called before stopping.
|
||||
if (Util.SDK_INT >= 26) {
|
||||
Builder notificationBuilder = new Builder(this, getNotificationChannelId());
|
||||
Notification foregroundNotification = notificationBuilder.build();
|
||||
startForeground(foregroundNotification);
|
||||
}
|
||||
boolean stopSelfResult = stopSelfResult(lastStartId);
|
||||
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
|
||||
}
|
||||
|
||||
/** Override this method to get notified on every second while there are active downloads. */
|
||||
protected void onProgressUpdate(DownloadState[] activeDownloadTasks) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void logd(String message) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ProgressUpdater implements Runnable {
|
||||
|
||||
private final DownloadService downloadService;
|
||||
private final long progressUpdateIntervalMillis;
|
||||
private final Handler handler;
|
||||
private boolean stopped;
|
||||
|
||||
public ProgressUpdater(DownloadService downloadService, long progressUpdateIntervalMillis) {
|
||||
this.downloadService = downloadService;
|
||||
this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
|
||||
this.handler = new Handler(Looper.getMainLooper());
|
||||
stopped = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
DownloadState[] activeDownloadTasks =
|
||||
downloadService.downloadManager.getActiveDownloadStates();
|
||||
if (activeDownloadTasks.length > 0) {
|
||||
downloadService.onProgressUpdate(activeDownloadTasks);
|
||||
if (progressUpdateIntervalMillis != C.TIME_UNSET) {
|
||||
handler.postDelayed(this, progressUpdateIntervalMillis);
|
||||
}
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
stopped = true;
|
||||
handler.removeCallbacks(this);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (stopped) {
|
||||
stopped = false;
|
||||
if (progressUpdateIntervalMillis != C.TIME_UNSET) {
|
||||
handler.post(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class RequirementsListener implements RequirementsWatcher.Listener {
|
||||
|
||||
private final Context context;
|
||||
private final Class<? extends DownloadService> serviceClass;
|
||||
private final Scheduler scheduler;
|
||||
|
||||
private RequirementsListener(
|
||||
Context context, Class<? extends DownloadService> serviceClass, Scheduler scheduler) {
|
||||
this.context = context;
|
||||
this.serviceClass = serviceClass;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requirementsMet(RequirementsWatcher requirementsWatcher) {
|
||||
startServiceWithAction(DownloadService.ACTION_START);
|
||||
if (scheduler != null) {
|
||||
scheduler.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requirementsNotMet(RequirementsWatcher requirementsWatcher) {
|
||||
startServiceWithAction(DownloadService.ACTION_STOP);
|
||||
if (scheduler != null) {
|
||||
if (!scheduler.schedule()) {
|
||||
Log.e(TAG, "Scheduling downloads failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startServiceWithAction(String action) {
|
||||
Intent intent = new Intent(context, serviceClass).setAction(action);
|
||||
if (Util.SDK_INT >= 26) {
|
||||
context.startForegroundService(intent);
|
||||
} else {
|
||||
context.startService(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/** An action to download or remove downloaded progressive streams. */
|
||||
public final class ProgressiveDownloadAction extends DownloadAction {
|
||||
|
||||
public static final Deserializer DESERIALIZER = new Deserializer() {
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressiveDownloadAction readFromStream(int version, DataInputStream input)
|
||||
throws IOException {
|
||||
return new ProgressiveDownloadAction(input.readUTF(),
|
||||
input.readBoolean() ? input.readUTF() : null, input.readBoolean(), input.readUTF());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static final String TYPE = "ProgressiveDownloadAction";
|
||||
|
||||
private final String uri;
|
||||
private final @Nullable String customCacheKey;
|
||||
private final boolean removeAction;
|
||||
|
||||
/**
|
||||
* @param uri Uri of the data to be downloaded.
|
||||
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
|
||||
* indexing. May be null.
|
||||
* @param removeAction Whether the data should be downloaded or removed.
|
||||
* @param data Optional custom data for this action. If null, an empty string is used.
|
||||
*/
|
||||
public ProgressiveDownloadAction(String uri, @Nullable String customCacheKey,
|
||||
boolean removeAction, String data) {
|
||||
super(data);
|
||||
this.uri = Assertions.checkNotNull(uri);
|
||||
this.customCacheKey = customCacheKey;
|
||||
this.removeAction = removeAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemoveAction() {
|
||||
return removeAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProgressiveDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
|
||||
return new ProgressiveDownloader(uri, customCacheKey, constructorHelper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeToStream(DataOutputStream output) throws IOException {
|
||||
output.writeUTF(uri);
|
||||
boolean customCacheKeyAvailable = customCacheKey != null;
|
||||
output.writeBoolean(customCacheKeyAvailable);
|
||||
if (customCacheKeyAvailable) {
|
||||
output.writeUTF(customCacheKey);
|
||||
}
|
||||
output.writeBoolean(isRemoveAction());
|
||||
output.writeUTF(getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSameMedia(DownloadAction other) {
|
||||
if (!(other instanceof ProgressiveDownloadAction)) {
|
||||
return false;
|
||||
}
|
||||
ProgressiveDownloadAction action = (ProgressiveDownloadAction) other;
|
||||
return customCacheKey != null ? customCacheKey.equals(action.customCacheKey)
|
||||
: uri.equals(action.uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(o)) {
|
||||
return false;
|
||||
}
|
||||
ProgressiveDownloadAction that = (ProgressiveDownloadAction) o;
|
||||
return uri.equals(that.uri) && Util.areEqual(customCacheKey, that.customCacheKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = super.hashCode();
|
||||
result = 31 * result + uri.hashCode();
|
||||
result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* {@link DownloadAction} for {@link SegmentDownloader}s.
|
||||
*
|
||||
* @param <K> The type of the representation key object.
|
||||
*/
|
||||
public abstract class SegmentDownloadAction<K> extends DownloadAction {
|
||||
|
||||
/**
|
||||
* Base class for {@link SegmentDownloadAction} {@link Deserializer}s.
|
||||
*
|
||||
* @param <K> The type of the representation key object.
|
||||
*/
|
||||
protected abstract static class SegmentDownloadActionDeserializer<K> implements Deserializer {
|
||||
|
||||
@Override
|
||||
public DownloadAction readFromStream(int version, DataInputStream input) throws IOException {
|
||||
Uri manifestUri = Uri.parse(input.readUTF());
|
||||
String data = input.readUTF();
|
||||
int keyCount = input.readInt();
|
||||
boolean removeAction = keyCount == -1;
|
||||
K[] keys;
|
||||
if (removeAction) {
|
||||
keys = null;
|
||||
} else {
|
||||
keys = createKeyArray(keyCount);
|
||||
for (int i = 0; i < keyCount; i++) {
|
||||
keys[i] = readKey(input);
|
||||
}
|
||||
}
|
||||
return createDownloadAction(manifestUri, removeAction, data, keys);
|
||||
}
|
||||
|
||||
/** Deserializes a key from the {@code input}. */
|
||||
protected abstract K readKey(DataInputStream input) throws IOException;
|
||||
|
||||
/** Returns a key array. */
|
||||
protected abstract K[] createKeyArray(int keyCount);
|
||||
|
||||
/** Returns a {@link DownloadAction}. */
|
||||
protected abstract DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
|
||||
String data, K[] keys);
|
||||
|
||||
}
|
||||
|
||||
protected final Uri manifestUri;
|
||||
protected final K[] keys;
|
||||
private final boolean removeAction;
|
||||
|
||||
/**
|
||||
* @param manifestUri The {@link Uri} of the manifest to be downloaded.
|
||||
* @param removeAction Whether the data will be removed. If {@code false} it will be downloaded.
|
||||
* @param data Optional custom data for this action. If null, an empty string is used.
|
||||
* @param keys Keys of representations to be downloaded. If empty or null, all representations are
|
||||
* downloaded. If {@code removeAction} is true, this is ignored.
|
||||
*/
|
||||
protected SegmentDownloadAction(Uri manifestUri, boolean removeAction, String data, K[] keys) {
|
||||
super(data);
|
||||
Assertions.checkArgument(!removeAction || keys == null || keys.length == 0);
|
||||
this.manifestUri = manifestUri;
|
||||
this.keys = keys;
|
||||
this.removeAction = removeAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isRemoveAction() {
|
||||
return removeAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void writeToStream(DataOutputStream output) throws IOException {
|
||||
output.writeUTF(manifestUri.toString());
|
||||
output.writeUTF(getData());
|
||||
if (isRemoveAction()) {
|
||||
output.writeInt(-1);
|
||||
} else {
|
||||
output.writeInt(keys.length);
|
||||
for (K key : keys) {
|
||||
writeKey(output, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Serializes the {@code key} into the {@code output}. */
|
||||
protected abstract void writeKey(DataOutputStream output, K key) throws IOException;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isSameMedia(DownloadAction other) {
|
||||
return other instanceof SegmentDownloadAction
|
||||
&& manifestUri.equals(((SegmentDownloadAction<?>) other).manifestUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(o)) {
|
||||
return false;
|
||||
}
|
||||
SegmentDownloadAction<?> that = (SegmentDownloadAction<?>) o;
|
||||
return manifestUri.equals(that.manifestUri)
|
||||
&& (keys == null || keys.length == 0
|
||||
? (that.keys == null || that.keys.length == 0)
|
||||
: (that.keys != null
|
||||
&& that.keys.length == keys.length
|
||||
&& Arrays.asList(keys).containsAll(Arrays.asList(that.keys))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = super.hashCode();
|
||||
result = 31 * result + manifestUri.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(keys);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.util.scheduler;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.app.job.JobInfo;
|
||||
import android.app.job.JobParameters;
|
||||
import android.app.job.JobScheduler;
|
||||
import android.app.job.JobService;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.PersistableBundle;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* A {@link Scheduler} which uses {@link android.app.job.JobScheduler} to schedule a {@link Service}
|
||||
* to be started when its requirements are met. The started service must call {@link
|
||||
* Service#startForeground(int, Notification)} to make itself a foreground service upon being
|
||||
* started, as documented by {@link Service#startForegroundService(Intent)}.
|
||||
*
|
||||
* <p>To use {@link PlatformScheduler} application needs to have RECEIVE_BOOT_COMPLETED permission
|
||||
* and you need to define PlatformSchedulerService in your manifest:
|
||||
*
|
||||
* <pre>{@literal
|
||||
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
*
|
||||
* <service android:name="com.google.android.exoplayer2.util.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
* android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
* android:exported="true"/>
|
||||
* }</pre>
|
||||
*
|
||||
* The service to be scheduled must be defined in the manifest with an intent-filter:
|
||||
*
|
||||
* <pre>{@literal
|
||||
* <service android:name="MyJobService"
|
||||
* android:exported="false">
|
||||
* <intent-filter>
|
||||
* <action android:name="MyJobService.action"/>
|
||||
* <category android:name="android.intent.category.DEFAULT"/>
|
||||
* </intent-filter>
|
||||
* </service>
|
||||
* }</pre>
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public final class PlatformScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = "PlatformScheduler";
|
||||
private static final String SERVICE_ACTION = "SERVICE_ACTION";
|
||||
private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE";
|
||||
private static final String REQUIREMENTS = "REQUIREMENTS";
|
||||
|
||||
private final int jobId;
|
||||
private final JobInfo jobInfo;
|
||||
private final JobScheduler jobScheduler;
|
||||
|
||||
/**
|
||||
* @param context Used to access to {@link JobScheduler} service.
|
||||
* @param requirements The requirements to execute the job.
|
||||
* @param jobId Unique identifier for the job. Using the same id as a previous job can cause that
|
||||
* job to be replaced or canceled.
|
||||
* @param serviceAction The action which the service will be started with.
|
||||
* @param servicePackage The package of the service which contains the logic of the job.
|
||||
*/
|
||||
public PlatformScheduler(
|
||||
Context context,
|
||||
Requirements requirements,
|
||||
int jobId,
|
||||
String serviceAction,
|
||||
String servicePackage) {
|
||||
this.jobId = jobId;
|
||||
this.jobInfo = buildJobInfo(context, requirements, jobId, serviceAction, servicePackage);
|
||||
this.jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean schedule() {
|
||||
int result = jobScheduler.schedule(jobInfo);
|
||||
logd("Scheduling JobScheduler job: " + jobId + " result: " + result);
|
||||
return result == JobScheduler.RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
logd("Canceling JobScheduler job: " + jobId);
|
||||
jobScheduler.cancel(jobId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static JobInfo buildJobInfo(
|
||||
Context context,
|
||||
Requirements requirements,
|
||||
int jobId,
|
||||
String serviceAction,
|
||||
String servicePackage) {
|
||||
JobInfo.Builder builder =
|
||||
new JobInfo.Builder(jobId, new ComponentName(context, PlatformSchedulerService.class));
|
||||
|
||||
int networkType;
|
||||
switch (requirements.getRequiredNetworkType()) {
|
||||
case Requirements.NETWORK_TYPE_NONE:
|
||||
networkType = JobInfo.NETWORK_TYPE_NONE;
|
||||
break;
|
||||
case Requirements.NETWORK_TYPE_ANY:
|
||||
networkType = JobInfo.NETWORK_TYPE_ANY;
|
||||
break;
|
||||
case Requirements.NETWORK_TYPE_UNMETERED:
|
||||
networkType = JobInfo.NETWORK_TYPE_UNMETERED;
|
||||
break;
|
||||
case Requirements.NETWORK_TYPE_NOT_ROAMING:
|
||||
if (Util.SDK_INT >= 24) {
|
||||
networkType = JobInfo.NETWORK_TYPE_NOT_ROAMING;
|
||||
} else {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
break;
|
||||
case Requirements.NETWORK_TYPE_METERED:
|
||||
if (Util.SDK_INT >= 26) {
|
||||
networkType = JobInfo.NETWORK_TYPE_METERED;
|
||||
} else {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
builder.setRequiredNetworkType(networkType);
|
||||
builder.setRequiresDeviceIdle(requirements.isIdleRequired());
|
||||
builder.setRequiresCharging(requirements.isChargingRequired());
|
||||
builder.setPersisted(true);
|
||||
|
||||
// Extras, work duration.
|
||||
PersistableBundle extras = new PersistableBundle();
|
||||
extras.putString(SERVICE_ACTION, serviceAction);
|
||||
extras.putString(SERVICE_PACKAGE, servicePackage);
|
||||
extras.putInt(REQUIREMENTS, requirements.getRequirementsData());
|
||||
|
||||
builder.setExtras(extras);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static void logd(String message) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
/** A {@link JobService} to start a service if the requirements are met. */
|
||||
public static final class PlatformSchedulerService extends JobService {
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
logd("PlatformSchedulerService is started");
|
||||
PersistableBundle extras = params.getExtras();
|
||||
Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS));
|
||||
if (requirements.checkRequirements(this)) {
|
||||
logd("requirements are met");
|
||||
String serviceAction = extras.getString(SERVICE_ACTION);
|
||||
String servicePackage = extras.getString(SERVICE_PACKAGE);
|
||||
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
|
||||
logd("starting service action: " + serviceAction + " package: " + servicePackage);
|
||||
if (Util.SDK_INT >= 26) {
|
||||
startForegroundService(intent);
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
} else {
|
||||
logd("requirements are not met");
|
||||
jobFinished(params, /* needsReschedule */ true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStopJob(JobParameters params) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.util.scheduler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.PowerManager;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Defines a set of device state requirements.
|
||||
*
|
||||
* <p>To use network type requirement, application needs to have ACCESS_NETWORK_STATE permission.
|
||||
*/
|
||||
public final class Requirements {
|
||||
|
||||
/** Network types. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
NETWORK_TYPE_NONE,
|
||||
NETWORK_TYPE_ANY,
|
||||
NETWORK_TYPE_UNMETERED,
|
||||
NETWORK_TYPE_NOT_ROAMING,
|
||||
NETWORK_TYPE_METERED,
|
||||
})
|
||||
public @interface NetworkType {}
|
||||
/** This job doesn't require network connectivity. */
|
||||
public static final int NETWORK_TYPE_NONE = 0;
|
||||
/** This job requires network connectivity. */
|
||||
public static final int NETWORK_TYPE_ANY = 1;
|
||||
/** This job requires network connectivity that is unmetered. */
|
||||
public static final int NETWORK_TYPE_UNMETERED = 2;
|
||||
/** This job requires network connectivity that is not roaming. */
|
||||
public static final int NETWORK_TYPE_NOT_ROAMING = 3;
|
||||
/** This job requires metered connectivity such as most cellular data networks. */
|
||||
public static final int NETWORK_TYPE_METERED = 4;
|
||||
/** This job requires the device to be idle. */
|
||||
private static final int DEVICE_IDLE = 8;
|
||||
/** This job requires the device to be charging. */
|
||||
private static final int DEVICE_CHARGING = 16;
|
||||
|
||||
private static final int NETWORK_TYPE_MASK = 7;
|
||||
|
||||
private static final String TAG = "Requirements";
|
||||
|
||||
private static final String[] NETWORK_TYPE_STRINGS;
|
||||
|
||||
static {
|
||||
if (Scheduler.DEBUG) {
|
||||
NETWORK_TYPE_STRINGS =
|
||||
new String[] {
|
||||
"NETWORK_TYPE_NONE",
|
||||
"NETWORK_TYPE_ANY",
|
||||
"NETWORK_TYPE_UNMETERED",
|
||||
"NETWORK_TYPE_NOT_ROAMING",
|
||||
"NETWORK_TYPE_METERED"
|
||||
};
|
||||
} else {
|
||||
NETWORK_TYPE_STRINGS = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final int requirements;
|
||||
|
||||
/**
|
||||
* @param networkType Required network type.
|
||||
* @param charging Whether the device should be charging.
|
||||
* @param idle Whether the device should be idle.
|
||||
*/
|
||||
public Requirements(@NetworkType int networkType, boolean charging, boolean idle) {
|
||||
this(networkType | (charging ? DEVICE_CHARGING : 0) | (idle ? DEVICE_IDLE : 0));
|
||||
}
|
||||
|
||||
/** @param requirementsData The value returned by {@link #getRequirementsData()}. */
|
||||
public Requirements(int requirementsData) {
|
||||
this.requirements = requirementsData;
|
||||
}
|
||||
|
||||
/** Returns required network type. */
|
||||
public int getRequiredNetworkType() {
|
||||
return requirements & NETWORK_TYPE_MASK;
|
||||
}
|
||||
|
||||
/** Returns whether the device should be charging. */
|
||||
public boolean isChargingRequired() {
|
||||
return (requirements & DEVICE_CHARGING) != 0;
|
||||
}
|
||||
|
||||
/** Returns whether the device should be idle. */
|
||||
public boolean isIdleRequired() {
|
||||
return (requirements & DEVICE_IDLE) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the requirements are met.
|
||||
*
|
||||
* @param context Any context.
|
||||
*/
|
||||
public boolean checkRequirements(Context context) {
|
||||
return checkNetworkRequirements(context)
|
||||
&& checkChargingRequirement(context)
|
||||
&& checkIdleRequirement(context);
|
||||
}
|
||||
|
||||
/** Returns the encoded requirements data which can be used with {@link #Requirements(int)}. */
|
||||
public int getRequirementsData() {
|
||||
return requirements;
|
||||
}
|
||||
|
||||
private boolean checkNetworkRequirements(Context context) {
|
||||
int networkRequirement = getRequiredNetworkType();
|
||||
if (networkRequirement == NETWORK_TYPE_NONE) {
|
||||
return true;
|
||||
}
|
||||
ConnectivityManager connectivityManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
|
||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
||||
logd("No network info or no connection.");
|
||||
return false;
|
||||
} else if (Util.SDK_INT >= 23) {
|
||||
// TODO Check internet connectivity using http://clients3.google.com/generate_204 on API
|
||||
// levels prior to 23.
|
||||
Network activeNetwork = connectivityManager.getActiveNetwork();
|
||||
if (activeNetwork == null) {
|
||||
logd("No active network.");
|
||||
return false;
|
||||
}
|
||||
NetworkCapabilities networkCapabilities =
|
||||
connectivityManager.getNetworkCapabilities(activeNetwork);
|
||||
if (networkCapabilities == null
|
||||
|| !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
|
||||
logd("Net capability isn't validated.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered();
|
||||
switch (networkRequirement) {
|
||||
case NETWORK_TYPE_ANY:
|
||||
return true;
|
||||
case NETWORK_TYPE_UNMETERED:
|
||||
if (activeNetworkMetered) {
|
||||
logd("Network is metered.");
|
||||
}
|
||||
return !activeNetworkMetered;
|
||||
case NETWORK_TYPE_NOT_ROAMING:
|
||||
boolean roaming = networkInfo.isRoaming();
|
||||
if (roaming) {
|
||||
logd("Roaming.");
|
||||
}
|
||||
return !roaming;
|
||||
case NETWORK_TYPE_METERED:
|
||||
if (!activeNetworkMetered) {
|
||||
logd("Network isn't metered.");
|
||||
}
|
||||
return activeNetworkMetered;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkChargingRequirement(Context context) {
|
||||
if (!isChargingRequired()) {
|
||||
return true;
|
||||
}
|
||||
Intent batteryStatus =
|
||||
context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
if (batteryStatus == null) {
|
||||
return false;
|
||||
}
|
||||
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
return status == BatteryManager.BATTERY_STATUS_CHARGING
|
||||
|| status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
}
|
||||
|
||||
private boolean checkIdleRequirement(Context context) {
|
||||
if (!isIdleRequired()) {
|
||||
return true;
|
||||
}
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return Util.SDK_INT >= 23
|
||||
? !powerManager.isDeviceIdleMode()
|
||||
: Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn();
|
||||
}
|
||||
|
||||
private static void logd(String message) {
|
||||
if (Scheduler.DEBUG) {
|
||||
Log.d(TAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!Scheduler.DEBUG) {
|
||||
return super.toString();
|
||||
}
|
||||
return "requirements{"
|
||||
+ NETWORK_TYPE_STRINGS[getRequiredNetworkType()]
|
||||
+ (isChargingRequired() ? ",charging" : "")
|
||||
+ (isIdleRequired() ? ",idle" : "")
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.util.scheduler;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* Watches whether the {@link Requirements} are met and notifies the {@link Listener} on changes.
|
||||
*/
|
||||
public final class RequirementsWatcher {
|
||||
|
||||
/**
|
||||
* Notified when RequirementsWatcher instance first created and on changes whether the {@link
|
||||
* Requirements} are met.
|
||||
*/
|
||||
public interface Listener {
|
||||
|
||||
/**
|
||||
* Called when the requirements are met.
|
||||
*
|
||||
* @param requirementsWatcher Calling instance.
|
||||
*/
|
||||
void requirementsMet(RequirementsWatcher requirementsWatcher);
|
||||
|
||||
/**
|
||||
* Called when the requirements are not met.
|
||||
*
|
||||
* @param requirementsWatcher Calling instance.
|
||||
*/
|
||||
void requirementsNotMet(RequirementsWatcher requirementsWatcher);
|
||||
}
|
||||
|
||||
private static final String TAG = "RequirementsWatcher";
|
||||
|
||||
private final Context context;
|
||||
private final Listener listener;
|
||||
private final Requirements requirements;
|
||||
private DeviceStatusChangeReceiver receiver;
|
||||
|
||||
private boolean requirementsWereMet;
|
||||
private CapabilityValidatedCallback networkCallback;
|
||||
|
||||
/**
|
||||
* @param context Used to register for broadcasts.
|
||||
* @param listener Notified whether the {@link Requirements} are met.
|
||||
* @param requirements The requirements to watch.
|
||||
*/
|
||||
public RequirementsWatcher(Context context, Listener listener, Requirements requirements) {
|
||||
this.requirements = requirements;
|
||||
this.listener = listener;
|
||||
this.context = context;
|
||||
logd(this + " created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts watching for changes. Must be called from a thread that has an associated {@link
|
||||
* Looper}. Listener methods are called on the caller thread.
|
||||
*/
|
||||
public void start() {
|
||||
Assertions.checkNotNull(Looper.myLooper());
|
||||
|
||||
checkRequirements(true);
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) {
|
||||
if (Util.SDK_INT >= 23) {
|
||||
registerNetworkCallbackV23();
|
||||
} else {
|
||||
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
}
|
||||
}
|
||||
if (requirements.isChargingRequired()) {
|
||||
filter.addAction(Intent.ACTION_POWER_CONNECTED);
|
||||
filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
|
||||
}
|
||||
if (requirements.isIdleRequired()) {
|
||||
if (Util.SDK_INT >= 23) {
|
||||
filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
|
||||
} else {
|
||||
filter.addAction(Intent.ACTION_SCREEN_ON);
|
||||
filter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
}
|
||||
}
|
||||
receiver = new DeviceStatusChangeReceiver();
|
||||
context.registerReceiver(receiver, filter, null, new Handler());
|
||||
logd(this + " started");
|
||||
}
|
||||
|
||||
/** Stops watching for changes. */
|
||||
public void stop() {
|
||||
context.unregisterReceiver(receiver);
|
||||
receiver = null;
|
||||
if (networkCallback != null) {
|
||||
unregisterNetworkCallback();
|
||||
}
|
||||
logd(this + " stopped");
|
||||
}
|
||||
|
||||
/** Returns watched {@link Requirements}. */
|
||||
public Requirements getRequirements() {
|
||||
return requirements;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!Scheduler.DEBUG) {
|
||||
return super.toString();
|
||||
}
|
||||
return "RequirementsWatcher{" + requirements + '}';
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
private void registerNetworkCallbackV23() {
|
||||
ConnectivityManager connectivityManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkRequest request =
|
||||
new NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build();
|
||||
networkCallback = new CapabilityValidatedCallback();
|
||||
connectivityManager.registerNetworkCallback(request, networkCallback);
|
||||
}
|
||||
|
||||
private void unregisterNetworkCallback() {
|
||||
if (Util.SDK_INT >= 21) {
|
||||
ConnectivityManager connectivityManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback);
|
||||
networkCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRequirements(boolean force) {
|
||||
boolean requirementsAreMet = requirements.checkRequirements(context);
|
||||
if (!force) {
|
||||
if (requirementsAreMet == requirementsWereMet) {
|
||||
logd("requirementsAreMet is still " + requirementsAreMet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
requirementsWereMet = requirementsAreMet;
|
||||
if (requirementsAreMet) {
|
||||
logd("start job");
|
||||
listener.requirementsMet(this);
|
||||
} else {
|
||||
logd("stop job");
|
||||
listener.requirementsNotMet(this);
|
||||
}
|
||||
}
|
||||
|
||||
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(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 21)
|
||||
private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback {
|
||||
@Override
|
||||
public void onAvailable(Network network) {
|
||||
super.onAvailable(network);
|
||||
logd(RequirementsWatcher.this + " NetworkCallback.onAvailable");
|
||||
checkRequirements(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLost(Network network) {
|
||||
super.onLost(network);
|
||||
logd(RequirementsWatcher.this + " NetworkCallback.onLost");
|
||||
checkRequirements(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.util.scheduler;
|
||||
|
||||
/**
|
||||
* Implementer of this interface schedules one implementation specific job to be run when some
|
||||
* requirements are met even if the app isn't running.
|
||||
*/
|
||||
public interface Scheduler {
|
||||
|
||||
/*package*/ boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* Schedules the job to be run when the requirements are met.
|
||||
*
|
||||
* @return Whether the job scheduled successfully.
|
||||
*/
|
||||
boolean schedule();
|
||||
|
||||
/**
|
||||
* Cancels any previous schedule.
|
||||
*
|
||||
* @return Whether the job cancelled successfully.
|
||||
*/
|
||||
boolean cancel();
|
||||
}
|
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProgressiveDownloadAction}.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
|
||||
public class ActionFileTest {
|
||||
|
||||
private File tempFile;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadNoDataThrowsIOException() throws Exception {
|
||||
try {
|
||||
loadActions(new Object[] {});
|
||||
Assert.fail();
|
||||
} catch (IOException e) {
|
||||
// Expected exception.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadIncompleteHeaderThrowsIOException() throws Exception {
|
||||
try {
|
||||
loadActions(new Object[] {DownloadAction.MASTER_VERSION});
|
||||
Assert.fail();
|
||||
} catch (IOException e) {
|
||||
// Expected exception.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadCompleteHeaderZeroAction() throws Exception {
|
||||
DownloadAction[] actions =
|
||||
loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/0});
|
||||
assertThat(actions).isNotNull();
|
||||
assertThat(actions).hasLength(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadAction() throws Exception {
|
||||
DownloadAction[] actions = loadActions(
|
||||
new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, /*action 1*/"type2", 321},
|
||||
new FakeDeserializer("type2"));
|
||||
assertThat(actions).isNotNull();
|
||||
assertThat(actions).hasLength(1);
|
||||
assertAction(actions[0], "type2", DownloadAction.MASTER_VERSION, 321);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadActions() throws Exception {
|
||||
DownloadAction[] actions = loadActions(
|
||||
new Object[] {DownloadAction.MASTER_VERSION, /*action count*/2, /*action 1*/"type1", 123,
|
||||
/*action 2*/"type2", 321}, // Action 2
|
||||
new FakeDeserializer("type1"), new FakeDeserializer("type2"));
|
||||
assertThat(actions).isNotNull();
|
||||
assertThat(actions).hasLength(2);
|
||||
assertAction(actions[0], "type1", DownloadAction.MASTER_VERSION, 123);
|
||||
assertAction(actions[1], "type2", DownloadAction.MASTER_VERSION, 321);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadNotSupportedVersion() throws Exception {
|
||||
try {
|
||||
loadActions(new Object[] {DownloadAction.MASTER_VERSION + 1, /*action count*/1,
|
||||
/*action 1*/"type2", 321}, new FakeDeserializer("type2"));
|
||||
Assert.fail();
|
||||
} catch (IOException e) {
|
||||
// Expected exception.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadNotSupportedType() throws Exception {
|
||||
try {
|
||||
loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1,
|
||||
/*action 1*/"type2", 321}, new FakeDeserializer("type1"));
|
||||
Assert.fail();
|
||||
} catch (DownloadException e) {
|
||||
// Expected exception.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreAndLoadNoActions() throws Exception {
|
||||
doTestSerializationRoundTrip(new DownloadAction[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreAndLoadActions() throws Exception {
|
||||
doTestSerializationRoundTrip(new DownloadAction[] {
|
||||
new FakeDownloadAction("type1", DownloadAction.MASTER_VERSION, 123),
|
||||
new FakeDownloadAction("type2", DownloadAction.MASTER_VERSION, 321),
|
||||
}, new FakeDeserializer("type1"), new FakeDeserializer("type2"));
|
||||
}
|
||||
|
||||
private void doTestSerializationRoundTrip(DownloadAction[] actions,
|
||||
Deserializer... deserializers) throws IOException {
|
||||
ActionFile actionFile = new ActionFile(tempFile);
|
||||
actionFile.store(actions);
|
||||
assertThat(actionFile.load(deserializers)).isEqualTo(actions);
|
||||
}
|
||||
|
||||
private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers)
|
||||
throws IOException {
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
|
||||
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
|
||||
try {
|
||||
for (Object value : values) {
|
||||
if (value instanceof Integer) {
|
||||
dataOutputStream.writeInt((Integer) value); // Action count
|
||||
} else if (value instanceof String) {
|
||||
dataOutputStream.writeUTF((String) value); // Action count
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
dataOutputStream.close();
|
||||
}
|
||||
return new ActionFile(tempFile).load(deserializers);
|
||||
}
|
||||
|
||||
private static void assertAction(DownloadAction action, String type, int version, int data) {
|
||||
assertThat(action).isInstanceOf(FakeDownloadAction.class);
|
||||
assertThat(action.getType()).isEqualTo(type);
|
||||
assertThat(((FakeDownloadAction) action).version).isEqualTo(version);
|
||||
assertThat(((FakeDownloadAction) action).data).isEqualTo(data);
|
||||
}
|
||||
|
||||
private static class FakeDeserializer implements Deserializer {
|
||||
final String type;
|
||||
|
||||
FakeDeserializer(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadAction readFromStream(int version, DataInputStream input) throws IOException {
|
||||
return new FakeDownloadAction(type, version, input.readInt());
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeDownloadAction extends DownloadAction {
|
||||
final String type;
|
||||
final int version;
|
||||
final int data;
|
||||
|
||||
private FakeDownloadAction(String type, int version, int data) {
|
||||
super(null);
|
||||
this.type = type;
|
||||
this.version = version;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeToStream(DataOutputStream output) throws IOException {
|
||||
output.writeInt(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemoveAction() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSameMedia(DownloadAction other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// auto generated code
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
FakeDownloadAction that = (FakeDownloadAction) o;
|
||||
return version == that.version && data == that.data && type.equals(that.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = type.hashCode();
|
||||
result = 31 * result + version;
|
||||
result = 31 * result + data;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DummyDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProgressiveDownloadAction}.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
|
||||
public class ProgressiveDownloadActionTest {
|
||||
|
||||
@Test
|
||||
public void testDownloadActionIsNotRemoveAction() throws Exception {
|
||||
ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null);
|
||||
assertThat(action.isRemoveAction()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveActionIsRemoveAction() throws Exception {
|
||||
ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
assertThat(action2.isRemoveAction()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateDownloader() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null);
|
||||
DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(
|
||||
Mockito.mock(Cache.class), DummyDataSource.FACTORY);
|
||||
assertThat(action.createDownloader(constructorHelper)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false, null);
|
||||
assertThat(action1.isSameMedia(action2)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true, null);
|
||||
ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false, null);
|
||||
assertThat(action3.isSameMedia(action4)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true, null);
|
||||
ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false, null);
|
||||
assertThat(action5.isSameMedia(action6)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception {
|
||||
ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true, null);
|
||||
ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false, null);
|
||||
assertThat(action7.isSameMedia(action8)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEquals() throws Exception {
|
||||
ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
assertThat(action1.equals(action1)).isTrue();
|
||||
|
||||
ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
assertThat(action2.equals(action3)).isTrue();
|
||||
|
||||
ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false, null);
|
||||
assertThat(action4.equals(action5)).isFalse();
|
||||
|
||||
ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true, null);
|
||||
assertThat(action6.equals(action7)).isFalse();
|
||||
|
||||
ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true, null);
|
||||
ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true, null);
|
||||
assertThat(action8.equals(action9)).isFalse();
|
||||
|
||||
ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true, null);
|
||||
ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true, null);
|
||||
assertThat(action10.equals(action11)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializerGetType() throws Exception {
|
||||
ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null);
|
||||
assertThat(action.getType()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializerWriteRead() throws Exception {
|
||||
doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false, null));
|
||||
doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true, null));
|
||||
}
|
||||
|
||||
private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action1)
|
||||
throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DataOutputStream output = new DataOutputStream(out);
|
||||
action1.writeToStream(output);
|
||||
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
DataInputStream input = new DataInputStream(in);
|
||||
DownloadAction action2 =
|
||||
ProgressiveDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input);
|
||||
|
||||
assertThat(action2).isEqualTo(action1);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.dash.offline;
|
||||
|
||||
import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD;
|
||||
import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI;
|
||||
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
|
||||
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.ConditionVariable;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.test.UiThreadTest;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.MockitoUtil;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Tests {@link DownloadManager}.
|
||||
*/
|
||||
public class DownloadManagerDashTest extends InstrumentationTestCase {
|
||||
|
||||
private static final int ASSERT_TRUE_TIMEOUT = 1000;
|
||||
|
||||
private SimpleCache cache;
|
||||
private File tempFolder;
|
||||
private FakeDataSet fakeDataSet;
|
||||
private DownloadManager downloadManager;
|
||||
private RepresentationKey fakeRepresentationKey1;
|
||||
private RepresentationKey fakeRepresentationKey2;
|
||||
private TestDownloadListener downloadListener;
|
||||
private File actionFile;
|
||||
|
||||
@UiThreadTest
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
Context context = getInstrumentation().getContext();
|
||||
tempFolder = Util.createTempDirectory(context, "ExoPlayerTest");
|
||||
File cacheFolder = new File(tempFolder, "cache");
|
||||
cacheFolder.mkdir();
|
||||
cache = new SimpleCache(cacheFolder, new NoOpCacheEvictor());
|
||||
MockitoUtil.setUpMockito(this);
|
||||
fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.setRandomData("audio_init_data", 10)
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
|
||||
fakeRepresentationKey1 = new RepresentationKey(0, 0, 0);
|
||||
fakeRepresentationKey2 = new RepresentationKey(0, 1, 0);
|
||||
actionFile = new File(tempFolder, "actionFile");
|
||||
createDownloadManager();
|
||||
}
|
||||
|
||||
@UiThreadTest
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
downloadManager.release();
|
||||
Util.recursiveDelete(tempFolder);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
// Disabled due to flakiness.
|
||||
public void disabledTestSaveAndLoadActionFile() throws Throwable {
|
||||
// Configure fakeDataSet to block until interrupted when TEST_MPD is read.
|
||||
fakeDataSet.newData(TEST_MPD_URI)
|
||||
.appendReadAction(new Runnable() {
|
||||
@SuppressWarnings("InfiniteLoopStatement")
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// Wait until interrupted.
|
||||
while (true) {
|
||||
Thread.sleep(100000);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
})
|
||||
.appendReadData(TEST_MPD)
|
||||
.endData();
|
||||
|
||||
// Run DM accessing code on UI/main thread as it should be. Also not to block handling of loaded
|
||||
// actions.
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Setup an Action and immediately release the DM.
|
||||
handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2);
|
||||
downloadManager.release();
|
||||
|
||||
assertThat(actionFile.exists()).isTrue();
|
||||
assertThat(actionFile.length()).isGreaterThan(0L);
|
||||
|
||||
assertCacheEmpty(cache);
|
||||
|
||||
// Revert fakeDataSet to normal.
|
||||
fakeDataSet.setData(TEST_MPD_URI, TEST_MPD);
|
||||
|
||||
createDownloadManager();
|
||||
}
|
||||
});
|
||||
|
||||
// Block on the test thread.
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
public void testHandleDownloadAction() throws Throwable {
|
||||
handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2);
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
public void testHandleMultipleDownloadAction() throws Throwable {
|
||||
handleDownloadAction(fakeRepresentationKey1);
|
||||
handleDownloadAction(fakeRepresentationKey2);
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
public void testHandleInterferingDownloadAction() throws Throwable {
|
||||
fakeDataSet
|
||||
.newData("audio_segment_2")
|
||||
.appendReadAction(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handleDownloadAction(fakeRepresentationKey2);
|
||||
}
|
||||
})
|
||||
.appendReadData(TestUtil.buildTestData(5))
|
||||
.endData();
|
||||
|
||||
handleDownloadAction(fakeRepresentationKey1);
|
||||
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
public void testHandleRemoveAction() throws Throwable {
|
||||
handleDownloadAction(fakeRepresentationKey1);
|
||||
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
handleRemoveAction();
|
||||
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
// Disabled due to flakiness.
|
||||
public void disabledTestHandleRemoveActionBeforeDownloadFinish() throws Throwable {
|
||||
handleDownloadAction(fakeRepresentationKey1);
|
||||
handleRemoveAction();
|
||||
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
public void testHandleInterferingRemoveAction() throws Throwable {
|
||||
final ConditionVariable downloadInProgressCondition = new ConditionVariable();
|
||||
fakeDataSet.newData("audio_segment_2")
|
||||
.appendReadAction(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
downloadInProgressCondition.open();
|
||||
}
|
||||
})
|
||||
.appendReadData(TestUtil.buildTestData(5))
|
||||
.endData();
|
||||
|
||||
handleDownloadAction(fakeRepresentationKey1);
|
||||
|
||||
assertThat(downloadInProgressCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue();
|
||||
|
||||
handleRemoveAction();
|
||||
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {
|
||||
downloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
private void handleDownloadAction(RepresentationKey... keys) {
|
||||
downloadManager.handleAction(new DashDownloadAction(TEST_MPD_URI, false, null, keys));
|
||||
}
|
||||
|
||||
private void handleRemoveAction() {
|
||||
downloadManager.handleAction(new DashDownloadAction(TEST_MPD_URI, true, null));
|
||||
}
|
||||
|
||||
private void createDownloadManager() {
|
||||
Factory fakeDataSourceFactory = new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet);
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
new DownloaderConstructorHelper(cache, fakeDataSourceFactory),
|
||||
1,
|
||||
3,
|
||||
actionFile.getAbsolutePath(),
|
||||
DashDownloadAction.DESERIALIZER);
|
||||
|
||||
downloadListener = new TestDownloadListener(downloadManager, this);
|
||||
downloadManager.addListener(downloadListener);
|
||||
downloadManager.startDownloads();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.dash.offline;
|
||||
|
||||
import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD;
|
||||
import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI;
|
||||
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
|
||||
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.util.scheduler.Requirements;
|
||||
import com.google.android.exoplayer2.util.scheduler.Scheduler;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DownloadService}.
|
||||
*/
|
||||
public class DownloadServiceDashTest extends InstrumentationTestCase {
|
||||
|
||||
private SimpleCache cache;
|
||||
private File tempFolder;
|
||||
private FakeDataSet fakeDataSet;
|
||||
private RepresentationKey fakeRepresentationKey1;
|
||||
private RepresentationKey fakeRepresentationKey2;
|
||||
private Context context;
|
||||
private DownloadService dashDownloadService;
|
||||
private ConditionVariable pauseDownloadCondition;
|
||||
private TestDownloadListener testDownloadListener;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
|
||||
|
||||
Runnable pauseAction = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (pauseDownloadCondition != null) {
|
||||
try {
|
||||
pauseDownloadCondition.block();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
fakeDataSet = new FakeDataSet()
|
||||
.setData(TEST_MPD_URI, TEST_MPD)
|
||||
.newData("audio_init_data")
|
||||
.appendReadAction(pauseAction)
|
||||
.appendReadData(TestUtil.buildTestData(10))
|
||||
.endData()
|
||||
.setRandomData("audio_segment_1", 4)
|
||||
.setRandomData("audio_segment_2", 5)
|
||||
.setRandomData("audio_segment_3", 6)
|
||||
.setRandomData("text_segment_1", 1)
|
||||
.setRandomData("text_segment_2", 2)
|
||||
.setRandomData("text_segment_3", 3);
|
||||
DataSource.Factory fakeDataSourceFactory = new FakeDataSource.Factory(null)
|
||||
.setFakeDataSet(fakeDataSet);
|
||||
fakeRepresentationKey1 = new RepresentationKey(0, 0, 0);
|
||||
fakeRepresentationKey2 = new RepresentationKey(0, 1, 0);
|
||||
|
||||
context = getInstrumentation().getContext();
|
||||
|
||||
File actionFile = Util.createTempFile(context, "ExoPlayerTest");
|
||||
actionFile.delete();
|
||||
final DownloadManager dashDownloadManager =
|
||||
new DownloadManager(
|
||||
new DownloaderConstructorHelper(cache, fakeDataSourceFactory),
|
||||
1,
|
||||
3,
|
||||
actionFile.getAbsolutePath(),
|
||||
DashDownloadAction.DESERIALIZER);
|
||||
testDownloadListener = new TestDownloadListener(dashDownloadManager, this);
|
||||
dashDownloadManager.addListener(testDownloadListener);
|
||||
dashDownloadManager.startDownloads();
|
||||
|
||||
try {
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dashDownloadService =
|
||||
new DownloadService(101010) {
|
||||
|
||||
@Override
|
||||
protected DownloadManager getDownloadManager() {
|
||||
return dashDownloadManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getNotificationChannelId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Scheduler getScheduler() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Requirements getRequirements() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
dashDownloadService.onCreate();
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
throw new Exception(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
try {
|
||||
runTestOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dashDownloadService.onDestroy();
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
throw new Exception(throwable);
|
||||
}
|
||||
Util.recursiveDelete(tempFolder);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
public void testMultipleDownloadAction() throws Throwable {
|
||||
downloadKeys(fakeRepresentationKey1);
|
||||
downloadKeys(fakeRepresentationKey2);
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
public void testRemoveAction() throws Throwable {
|
||||
downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2);
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
removeAll();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
public void testRemoveBeforeDownloadComplete() throws Throwable {
|
||||
pauseDownloadCondition = new ConditionVariable();
|
||||
downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2);
|
||||
|
||||
removeAll();
|
||||
|
||||
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
assertCacheEmpty(cache);
|
||||
}
|
||||
|
||||
private void removeAll() throws Throwable {
|
||||
callDownloadServiceOnStart(new DashDownloadAction(TEST_MPD_URI, true, null));
|
||||
}
|
||||
|
||||
private void downloadKeys(RepresentationKey... keys) throws Throwable {
|
||||
callDownloadServiceOnStart(new DashDownloadAction(TEST_MPD_URI, false, null, keys));
|
||||
}
|
||||
|
||||
private void callDownloadServiceOnStart(final DashDownloadAction action) throws Throwable {
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Intent startIntent =
|
||||
DownloadService.createAddDownloadActionIntent(
|
||||
context, DownloadService.class, action);
|
||||
dashDownloadService.onStartCommand(startIntent, 0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.dash.offline;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
|
||||
|
||||
/** A {@link DownloadListener} for testing. */
|
||||
/*package*/ final class TestDownloadListener implements DownloadListener {
|
||||
|
||||
private static final int TIMEOUT = 1000;
|
||||
|
||||
private final DownloadManager downloadManager;
|
||||
private final InstrumentationTestCase testCase;
|
||||
private final android.os.ConditionVariable downloadFinishedCondition;
|
||||
private Throwable downloadError;
|
||||
|
||||
public TestDownloadListener(DownloadManager downloadManager, InstrumentationTestCase testCase) {
|
||||
this.downloadManager = downloadManager;
|
||||
this.testCase = testCase;
|
||||
this.downloadFinishedCondition = new android.os.ConditionVariable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) {
|
||||
if (downloadState.state == DownloadState.STATE_ERROR && downloadError == null) {
|
||||
downloadError = downloadState.error;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIdle(DownloadManager downloadManager) {
|
||||
downloadFinishedCondition.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks until all remove and download tasks are complete and throws an exception if there was an
|
||||
* error.
|
||||
*/
|
||||
public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {
|
||||
testCase.runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (downloadManager.isIdle()) {
|
||||
downloadFinishedCondition.open();
|
||||
} else {
|
||||
downloadFinishedCondition.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
assertThat(downloadFinishedCondition.block(TIMEOUT)).isTrue();
|
||||
if (downloadError != null) {
|
||||
throw new Exception(downloadError);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.dash.offline;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/** An action to download or remove downloaded DASH streams. */
|
||||
public final class DashDownloadAction extends SegmentDownloadAction<RepresentationKey> {
|
||||
|
||||
public static final Deserializer DESERIALIZER =
|
||||
new SegmentDownloadActionDeserializer<RepresentationKey>() {
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RepresentationKey readKey(DataInputStream input) throws IOException {
|
||||
return new RepresentationKey(input.readInt(), input.readInt(), input.readInt());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RepresentationKey[] createKeyArray(int keyCount) {
|
||||
return new RepresentationKey[keyCount];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
|
||||
String data, RepresentationKey[] keys) {
|
||||
return new DashDownloadAction(manifestUri, removeAction, data, keys);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static final String TYPE = "DashDownloadAction";
|
||||
|
||||
/** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */
|
||||
public DashDownloadAction(Uri manifestUri, boolean removeAction, String data,
|
||||
RepresentationKey... keys) {
|
||||
super(manifestUri, removeAction, data, keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
|
||||
DashDownloader downloader = new DashDownloader(manifestUri, constructorHelper);
|
||||
if (!isRemoveAction()) {
|
||||
downloader.selectRepresentations(keys);
|
||||
}
|
||||
return downloader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeKey(DataOutputStream output, RepresentationKey key) throws IOException {
|
||||
output.writeInt(key.periodIndex);
|
||||
output.writeInt(key.adaptationSetIndex);
|
||||
output.writeInt(key.representationIndex);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.hls.offline;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/** An action to download or remove downloaded HLS streams. */
|
||||
public final class HlsDownloadAction extends SegmentDownloadAction<String> {
|
||||
|
||||
public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer<String>() {
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String readKey(DataInputStream input) throws IOException {
|
||||
return input.readUTF();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] createKeyArray(int keyCount) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
|
||||
String data, String[] keys) {
|
||||
return new HlsDownloadAction(manifestUri, removeAction, data, keys);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static final String TYPE = "HlsDownloadAction";
|
||||
|
||||
/** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */
|
||||
public HlsDownloadAction(Uri manifestUri, boolean removeAction, String data, String... keys) {
|
||||
super(manifestUri, removeAction, data, keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
|
||||
HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper);
|
||||
if (!isRemoveAction()) {
|
||||
downloader.selectRepresentations(keys);
|
||||
}
|
||||
return downloader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeKey(DataOutputStream output, String key) throws IOException {
|
||||
output.writeUTF(key);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.smoothstreaming.offline;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/** An action to download or remove downloaded SmoothStreaming streams. */
|
||||
public final class SsDownloadAction extends SegmentDownloadAction<TrackKey> {
|
||||
|
||||
public static final Deserializer DESERIALIZER =
|
||||
new SegmentDownloadActionDeserializer<TrackKey>() {
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackKey readKey(DataInputStream input) throws IOException {
|
||||
return new TrackKey(input.readInt(), input.readInt());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackKey[] createKeyArray(int keyCount) {
|
||||
return new TrackKey[keyCount];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
|
||||
String data, TrackKey[] keys) {
|
||||
return new SsDownloadAction(manifestUri, removeAction, data, keys);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static final String TYPE = "SsDownloadAction";
|
||||
|
||||
/** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */
|
||||
public SsDownloadAction(Uri manifestUri, boolean removeAction, String data, TrackKey... keys) {
|
||||
super(manifestUri, removeAction, data, keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
|
||||
SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper);
|
||||
if (!isRemoveAction()) {
|
||||
downloader.selectRepresentations(keys);
|
||||
}
|
||||
return downloader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeKey(DataOutputStream output, TrackKey key) throws IOException {
|
||||
output.writeInt(key.streamElementIndex);
|
||||
output.writeInt(key.trackIndex);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Notification.BigTextStyle;
|
||||
import android.app.Notification.Builder;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** Helper class to create notifications for downloads using {@link DownloadManager}. */
|
||||
public final class DownloadNotificationUtil {
|
||||
|
||||
private DownloadNotificationUtil() {}
|
||||
|
||||
/**
|
||||
* Returns a notification for the given {@link DownloadState}, or null if no notification should
|
||||
* be displayed.
|
||||
*
|
||||
* @param downloadState State of the download.
|
||||
* @param context Used to access resources.
|
||||
* @param smallIcon A small icon for the notifications.
|
||||
* @param channelId The id of the notification channel to use. Only required for API level 26 and
|
||||
* above.
|
||||
* @param errorMessageProvider An optional {@link ErrorMessageProvider} for translating download
|
||||
* errors into readable error messages.
|
||||
* @return A notification for the given {@link DownloadState}, or null if no notification should
|
||||
* be displayed.
|
||||
*/
|
||||
public static @Nullable Notification createNotification(
|
||||
DownloadState downloadState,
|
||||
Context context,
|
||||
int smallIcon,
|
||||
String channelId,
|
||||
@Nullable ErrorMessageProvider<Throwable> errorMessageProvider) {
|
||||
DownloadAction downloadAction = downloadState.downloadAction;
|
||||
if (downloadAction.isRemoveAction() || downloadState.state == DownloadState.STATE_CANCELED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Builder notificationBuilder = new Builder(context);
|
||||
if (Util.SDK_INT >= 26) {
|
||||
notificationBuilder.setChannelId(channelId);
|
||||
}
|
||||
notificationBuilder.setSmallIcon(smallIcon);
|
||||
|
||||
int titleStringId = getTitleStringId(downloadState);
|
||||
notificationBuilder.setContentTitle(context.getResources().getString(titleStringId));
|
||||
|
||||
if (downloadState.isRunning()) {
|
||||
notificationBuilder.setOngoing(true);
|
||||
float percentage = downloadState.downloadPercentage;
|
||||
boolean indeterminate = Float.isNaN(percentage);
|
||||
notificationBuilder.setProgress(100, indeterminate ? 0 : (int) percentage, indeterminate);
|
||||
}
|
||||
|
||||
String message;
|
||||
if (downloadState.error != null && errorMessageProvider != null) {
|
||||
message = errorMessageProvider.getErrorMessage(downloadState.error).second;
|
||||
} else {
|
||||
message = downloadAction.getData();
|
||||
}
|
||||
|
||||
if (Util.SDK_INT >= 16) {
|
||||
notificationBuilder.setStyle(new BigTextStyle().bigText(message));
|
||||
} else {
|
||||
notificationBuilder.setContentText(message);
|
||||
}
|
||||
return notificationBuilder.getNotification();
|
||||
}
|
||||
|
||||
private static int getTitleStringId(DownloadState downloadState) {
|
||||
int titleStringId;
|
||||
switch (downloadState.state) {
|
||||
case DownloadState.STATE_WAITING:
|
||||
titleStringId = R.string.exo_download_queued;
|
||||
break;
|
||||
case DownloadState.STATE_STARTED:
|
||||
case DownloadState.STATE_STOPPING:
|
||||
case DownloadState.STATE_CANCELING:
|
||||
titleStringId = R.string.exo_downloading;
|
||||
break;
|
||||
case DownloadState.STATE_ENDED:
|
||||
titleStringId = R.string.exo_download_completed;
|
||||
break;
|
||||
case DownloadState.STATE_ERROR:
|
||||
titleStringId = R.string.exo_download_failed;
|
||||
break;
|
||||
case DownloadState.STATE_CANCELED:
|
||||
default:
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return titleStringId;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user