/*
* Copyright (C) 2014 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.exoplayer.upstream;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
/**
* Manages the background loading of {@link Loadable}s.
*/
public final class Loader {
/**
* Thrown when an unexpected exception is encountered during loading.
*/
public static final class UnexpectedLoaderException extends IOException {
public UnexpectedLoaderException(Exception cause) {
super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
}
}
/**
* Interface definition of an object that can be loaded using a {@link Loader}.
*/
public interface Loadable {
/**
* Cancels the load.
*/
void cancelLoad();
/**
* Whether the load has been canceled.
*
* @return True if the load has been canceled. False otherwise.
*/
boolean isLoadCanceled();
/**
* Performs the load, returning on completion or cancelation.
*
* @throws IOException
* @throws InterruptedException
*/
void load() throws IOException, InterruptedException;
}
/**
* Interface definition for a callback to be notified of {@link Loader} events.
*/
public interface Listener {
/**
* Invoked when loading has been canceled.
*/
void onCanceled();
/**
* Invoked when the data source has been fully loaded.
*/
void onLoaded();
/**
* Invoked when the data source is stopped due to an error.
*/
void onError(IOException exception);
}
private static final int MSG_END_OF_SOURCE = 0;
private static final int MSG_ERROR = 1;
private final ExecutorService downloadExecutorService;
private final Listener listener;
private LoadTask currentTask;
private boolean loading;
/**
* @param threadName A name for the loader's thread.
* @param listener A listener to invoke when state changes occur.
*/
public Loader(String threadName, Listener listener) {
this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
this.listener = listener;
}
/**
* Start loading a {@link Loadable}.
* <p>
* A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method
* must not be called when another load is in progress.
*
* @param loadable The {@link Loadable} to load.
*/
public void startLoading(Loadable loadable) {
Assertions.checkState(!loading);
loading = true;
currentTask = new LoadTask(loadable);
downloadExecutorService.submit(currentTask);
}
/**
* Whether the {@link Loader} is currently loading a {@link Loadable}.
*
* @return Whether the {@link Loader} is currently loading a {@link Loadable}.
*/
public boolean isLoading() {
return loading;
}
/**
* Cancels the current load.
* <p>
* This method should only be called when a load is in progress.
*/
public void cancelLoading() {
Assertions.checkState(loading);
currentTask.quit();
}
/**
* Releases the {@link Loader}.
* <p>
* This method should be called when the {@link Loader} is no longer required.
*/
public void release() {
if (loading) {
cancelLoading();
}
downloadExecutorService.shutdown();
}
@SuppressLint("HandlerLeak")
private final class LoadTask extends Handler implements Runnable {
private static final String TAG = "LoadTask";
private final Loadable loadable;
private volatile Thread executorThread;
public LoadTask(Loadable loadable) {
this.loadable = loadable;
}
public void quit() {
loadable.cancelLoad();
if (executorThread != null) {
executorThread.interrupt();
}
}
@Override
public void run() {
try {
executorThread = Thread.currentThread();
if (!loadable.isLoadCanceled()) {
loadable.load();
}
sendEmptyMessage(MSG_END_OF_SOURCE);
} catch (IOException e) {
obtainMessage(MSG_ERROR, e).sendToTarget();
} catch (InterruptedException e) {
// The load was canceled.
Assertions.checkState(loadable.isLoadCanceled());
sendEmptyMessage(MSG_END_OF_SOURCE);
} catch (Exception e) {
// This should never happen, but handle it anyway.
Log.e(TAG, "Unexpected error loading stream", e);
obtainMessage(MSG_ERROR, new UnexpectedLoaderException(e)).sendToTarget();
}
}
@Override
public void handleMessage(Message msg) {
onFinished();
if (loadable.isLoadCanceled()) {
listener.onCanceled();
return;
}
switch (msg.what) {
case MSG_END_OF_SOURCE:
listener.onLoaded();
break;
case MSG_ERROR:
listener.onError((IOException) msg.obj);
break;
}
}
private void onFinished() {
loading = false;
currentTask = null;
}
}
}