package yuku.alkitab.base.sv;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.util.Pair;
import okhttp3.Call;
import okhttp3.ResponseBody;
import yuku.alkitab.base.App;
import yuku.alkitab.debug.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.LinkedHashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class DownloadService extends Service {
public static final String TAG = DownloadService.class.getSimpleName();
private static final int MSG_progress = 1;
private static final int MSG_stateChanged = 2;
private static final int MSG_stopSelf = 3;
LinkedHashMap<String, DownloadEntry> db = new LinkedHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(3);
/** waiting or running */
AtomicInteger nwaiting = new AtomicInteger(0);
DownloadListener listener;
static class ListenerHandler extends Handler {
private WeakReference<DownloadService> sv;
public ListenerHandler(DownloadService sv) {
this.sv = new WeakReference<>(sv);
}
@Override public void handleMessage(Message msg) {
DownloadService sv = this.sv.get();
if (sv == null) return;
switch (msg.what) {
case MSG_stopSelf:
sv.stopSelf();
break;
case MSG_progress:
if (sv.listener != null) {
@SuppressWarnings("unchecked") Pair<DownloadEntry, State> obj = (Pair<DownloadEntry, State>) msg.obj;
sv.listener.onProgress(obj.first, obj.second);
}
break;
case MSG_stateChanged:
if (sv.listener != null) {
@SuppressWarnings("unchecked") Pair<DownloadEntry, State> obj = (Pair<DownloadEntry, State>) msg.obj;
sv.listener.onStateChanged(obj.first, obj.second);
}
break;
}
}
}
private Handler handler = new ListenerHandler(this);
public interface DownloadListener {
void onStateChanged(DownloadEntry entry, State originalState);
void onProgress(DownloadEntry entry, State originalState);
}
public enum State {
created,
downloading,
finished,
failed,
;
}
public static class DownloadEntry {
public String key;
public State state;
public String url;
public File completeFile;
public File tempFile;
public long progress;
public long length;
public String errorMsg;
}
@Override public IBinder onBind(Intent intent) {
return new DownloadBinder();
}
@Override public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
public void setDownloadListener(DownloadListener listener) {
this.listener = listener;
}
public DownloadEntry getEntry(String key) {
return db.get(key);
}
public boolean removeEntry(String key) {
DownloadEntry entry = db.get(key);
if (entry == null) {
return false;
}
if (entry.state == State.downloading) {
return false;
}
db.remove(key);
// TODO cancel download, delete tmp file, etc.
return true;
}
public boolean startDownload(String key, String url, String completeFile) {
if (db.get(key) != null) {
return false;
}
DownloadEntry e = new DownloadEntry();
e.key = key;
e.state = State.created;
e.url = url;
e.completeFile = new File(completeFile);
e.tempFile = new File(completeFile + ".part." + System.nanoTime() + ".tmp");
e.length = -1;
e.progress = 0;
enqueueAndStart(e);
return true;
}
private void enqueueAndStart(DownloadEntry e) {
db.put(e.key, e);
dispatchStateChanged(e);
dispatchProgress(e);
incrementWaiting();
startService(new Intent(App.context, DownloadService.class)); // start self so it's not killed
executor.submit(new DownloadTask(e));
}
class DownloadTask implements Callable<DownloadEntry> {
DownloadEntry entry;
public DownloadTask(DownloadEntry e) {
this.entry = e;
}
void changeState(State newState) {
entry.state = newState;
dispatchStateChanged(entry);
}
@Override public DownloadEntry call() throws Exception {
try {
FileOutputStream tempOut = new FileOutputStream(entry.tempFile);
// download
final Call call = App.downloadCall(entry.url);
final ResponseBody body = call.execute().body();
try {
changeState(State.downloading);
entry.progress = 0;
dispatchProgress(entry);
final InputStream is = body.byteStream();
byte[] buf = new byte[16384];
while (true) {
int read = is.read(buf);
if (read < 0) break;
tempOut.write(buf, 0, read);
entry.progress += read;
dispatchProgress(entry);
Log.d(TAG, "Entry " + entry.key + " progress " + entry.progress + "/" + entry.length);
}
tempOut.close();
is.close();
} finally {
body.close();
}
// move
entry.completeFile.delete();
boolean renameOk = entry.tempFile.renameTo(entry.completeFile);
if (!renameOk) {
Log.w(TAG, "Failed to rename file from " + entry.tempFile + " to " + entry.completeFile);
entry.errorMsg = getString(R.string.dl_failed_to_rename_temporary_file);
changeState(State.failed);
return entry;
}
// finished successfully
changeState(State.finished);
} catch (Exception e) {
Log.w(TAG, "Failed download because of exception", e);
entry.tempFile.delete();
entry.errorMsg = e.getClass().getSimpleName() + ' ' + e.getMessage();
changeState(State.failed);
} finally {
decrementWaitingAndCheck();
}
return entry;
}
}
public class DownloadBinder extends Binder {
public DownloadService getService() {
return DownloadService.this;
}
}
private void incrementWaiting() {
nwaiting.incrementAndGet();
Log.d(TAG, "(inc) now nwaiting is " + nwaiting);
}
public synchronized void decrementWaitingAndCheck() {
int newValue = nwaiting.decrementAndGet();
if (newValue == 0) {
Message.obtain(handler, MSG_stopSelf).sendToTarget();
}
Log.d(TAG, "(dec) now nwaiting is " + nwaiting);
}
public void dispatchProgress(DownloadEntry entry) {
Message.obtain(handler, MSG_progress, Pair.create(entry, entry.state)).sendToTarget();
}
public void dispatchStateChanged(DownloadEntry entry) {
Log.d(TAG, "dispatch state", new Throwable().fillInStackTrace());
Message.obtain(handler, MSG_stateChanged, Pair.create(entry, entry.state)).sendToTarget();
}
}