mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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
26 changed files with 4590 additions and 0 deletions
|
|
@ -89,6 +89,10 @@
|
||||||
* `EventLogger` moved from the demo app into the core library.
|
* `EventLogger` moved from the demo app into the core library.
|
||||||
* Fix ANR issue on Huawei P8 Lite
|
* Fix ANR issue on Huawei P8 Lite
|
||||||
([#3724](https://github.com/google/ExoPlayer/issues/3724)).
|
([#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 ###
|
### 2.6.1 ###
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ include modulePrefix + 'extension-opus'
|
||||||
include modulePrefix + 'extension-vp9'
|
include modulePrefix + 'extension-vp9'
|
||||||
include modulePrefix + 'extension-rtmp'
|
include modulePrefix + 'extension-rtmp'
|
||||||
include modulePrefix + 'extension-leanback'
|
include modulePrefix + 'extension-leanback'
|
||||||
|
include modulePrefix + 'extension-jobdispatcher'
|
||||||
|
|
||||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||||
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
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-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
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')
|
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
||||||
&& gradle.ext.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…
Reference in a new issue