Commit b3da82dc by eguven Committed by Andrew Lewis

Open source DownloadService, DownloadManager and related classes

Issue: #2643

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=184844484
parent 34050124
Showing with 2770 additions and 0 deletions
......@@ -89,6 +89,10 @@
* `EventLogger` moved from the demo app into the core library.
* Fix ANR issue on Huawei P8 Lite
([#3724](https://github.com/google/ExoPlayer/issues/3724)).
* Fix potential NPE when removing media sources from a
DynamicConcatenatingMediaSource
([#3796](https://github.com/google/ExoPlayer/issues/3796)).
* Open source DownloadService, DownloadManager and related classes.
### 2.6.1 ###
......
......@@ -35,6 +35,7 @@ include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
......@@ -54,6 +55,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
if (gradle.ext.has('exoplayerIncludeCronetExtension')
&& gradle.ext.exoplayerIncludeCronetExtension) {
......
# 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
/*
* 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'
<?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"/>
/*
* 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;
}
}
}
/*
* 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);
}
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
/*
* 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;
}
}
}
/*
* 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" : "")
+ '}';
}
}
/*
* 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);
}
}
}
/*
* 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();
}
/*
* 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;
}
}
}
/*
* 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);
}
}
/*
* 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();
}
}
/*
* 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);
}
});
}
}
/*
* 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);
}
}
}
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment