package zlc.season.rxdownload2;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Semaphore;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.ObservableSource;
import io.reactivex.ObservableTransformer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import retrofit2.Retrofit;
import zlc.season.rxdownload2.entity.DownloadBean;
import zlc.season.rxdownload2.entity.DownloadEvent;
import zlc.season.rxdownload2.entity.DownloadFlag;
import zlc.season.rxdownload2.entity.DownloadRecord;
import zlc.season.rxdownload2.entity.DownloadStatus;
import zlc.season.rxdownload2.entity.MultiMission;
import zlc.season.rxdownload2.entity.SingleMission;
import zlc.season.rxdownload2.function.DownloadHelper;
import zlc.season.rxdownload2.function.DownloadService;
import zlc.season.rxdownload2.function.Utils;
import static zlc.season.rxdownload2.function.Utils.log;
/**
* Author: Season(ssseasonnn@gmail.com)
* Date: 2016/10/19
* Time: 10:46
* RxDownload
*/
public class RxDownload {
private static final Object object = new Object();
@SuppressLint("StaticFieldLeak")
private volatile static RxDownload instance;
private volatile static boolean bound = false;
static {
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
if (throwable instanceof InterruptedException) {
log("Thread interrupted");
} else if (throwable instanceof InterruptedIOException) {
log("Io interrupted");
} else if (throwable instanceof SocketException) {
log("Socket error");
}
}
});
}
private int maxDownloadNumber = 5;
private Context context;
private Semaphore semaphore;
private DownloadService downloadService;
private DownloadHelper downloadHelper;
private RxDownload(Context context) {
this.context = context.getApplicationContext();
downloadHelper = new DownloadHelper(context);
semaphore = new Semaphore(1);
}
/**
* Return RxDownload Instance
*
* @param context context
* @return RxDownload
*/
public static RxDownload getInstance(Context context) {
if (instance == null) {
synchronized (RxDownload.class) {
if (instance == null) {
instance = new RxDownload(context);
}
}
}
return instance;
}
/**
* get Files by url. May be NULL if this url record not exists.
* File[] {DownloadFile, TempFile, LastModifyFile}
*
* @param url url
* @return Files
*/
@Nullable
public File[] getRealFiles(String url) {
return downloadHelper.getFiles(url);
}
/**
* get Files by saveName and savePath.
*
* @param saveName saveName
* @param savePath savePath
* @return Files
*/
public File[] getRealFiles(String saveName, String savePath) {
return Utils.getFiles(saveName, savePath);
}
/**
* set default save path.
*
* @param savePath default save path.
* @return instance.
*/
public RxDownload defaultSavePath(String savePath) {
downloadHelper.setDefaultSavePath(savePath);
return this;
}
/**
* If you have own Retrofit client, set it.
*
* @param retrofit retrofit client
* @return instance.
*/
public RxDownload retrofit(Retrofit retrofit) {
downloadHelper.setRetrofit(retrofit);
return this;
}
/**
* set max thread to download file.
*
* @param max max threads
* @return instance
*/
public RxDownload maxThread(int max) {
downloadHelper.setMaxThreads(max);
return this;
}
/**
* set max retry count when download failed
*
* @param max max retry count
* @return instance
*/
public RxDownload maxRetryCount(int max) {
downloadHelper.setMaxRetryCount(max);
return this;
}
/**
* set max download number when service download
*
* @param max max download number
* @return instance
*/
public RxDownload maxDownloadNumber(int max) {
this.maxDownloadNumber = max;
return this;
}
/**
* Receive the url download event.
* <p>
* Will receive the following event:
* {@link DownloadFlag#NORMAL}、{@link DownloadFlag#WAITING}、
* {@link DownloadFlag#STARTED}、{@link DownloadFlag#PAUSED}、
* {@link DownloadFlag#COMPLETED}、{@link DownloadFlag#FAILED};
* <p>
* Every event has {@link DownloadStatus}, you can get it and display it on the interface.
*
* @param url url
* @return DownloadEvent
*/
public Observable<DownloadEvent> receiveDownloadStatus(final String url) {
return createGeneralObservable(null)
.flatMap(new Function<Object, ObservableSource<DownloadEvent>>() {
@Override
public ObservableSource<DownloadEvent> apply(Object o) throws Exception {
return downloadService.receiveDownloadEvent(url).toObservable();
}
})
.observeOn(AndroidSchedulers.mainThread());
}
/**
* Read all the download record from the database.
*
* @return Observable<List<DownloadRecord>>
*/
public Observable<List<DownloadRecord>> getTotalDownloadRecords() {
return downloadHelper.readAllRecords();
}
/**
* Read single download record with url.
* If record contain, return correct record, else return empty record.
*
* @param url download url
* @return Observable<DownloadStatus>
*/
public Observable<DownloadRecord> getDownloadRecord(String url) {
return downloadHelper.readRecord(url);
}
/**
* Pause download.
* <p>
* Pause a download.
*
* @param url url
*/
public Observable<?> pauseServiceDownload(final String url) {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() {
downloadService.pauseDownload(url);
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Delete download.
* <p>
* Delete a download.
*
* @param url url
* @param deleteFile whether delete file
*/
public Observable<?> deleteServiceDownload(final String url, final boolean deleteFile) {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() {
downloadService.deleteDownload(url, deleteFile);
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Start all mission. Not include multi mission.
*
* @return Observable
*/
public Observable<?> startAll() {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() throws InterruptedException {
downloadService.startAll();
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Pause all mission. Not include multi mission.
*
* @return Observable
*/
public Observable<?> pauseAll() {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() {
downloadService.pauseAll();
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Start all mission which associate with missionId
*
* @param missionId missionId
* @return Observable
*/
public Observable<?> startAll(final String missionId) {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() throws InterruptedException {
downloadService.startAll(missionId);
}
}).observeOn(AndroidSchedulers.mainThread());
}
public Observable<?> pauseAll(final String missionId) {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() {
downloadService.pauseAll(missionId);
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Delete all mission which associate with missionId.
*
* @param missionId missionId
* @param deleteFile deleteFile ?
* @return Observable
*/
public Observable<?> deleteAll(final String missionId, final boolean deleteFile) {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() {
downloadService.deleteAll(missionId, deleteFile);
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Normal download.
* <p>
* Will save the download records in the database.
* <p>
* Un subscribe will pause download.
*
* @param url Url
* @return Observable<DownloadStatus>
*/
public Observable<DownloadStatus> download(String url) {
return download(url, null);
}
/**
* Normal download with assigned Name.
*
* @param url url
* @param saveName SaveName
* @return Observable<DownloadStatus>
*/
public Observable<DownloadStatus> download(String url, String saveName) {
return download(url, saveName, null);
}
/**
* Normal download with assigned name and path.
*
* @param url url
* @param saveName SaveName
* @param savePath SavePath
* @return Observable<DownloadStatus>
*/
public Observable<DownloadStatus> download(String url, String saveName, String savePath) {
return download(new DownloadBean.Builder(url).setSaveName(saveName)
.setSavePath(savePath).build());
}
/**
* Normal download.
* <p>
* You can construct a DownloadBean to save extra data to the database.
*
* @param downloadBean download bean.
* @return Observable<DownloadStatus>
*/
public Observable<DownloadStatus> download(DownloadBean downloadBean) {
return downloadHelper.downloadDispatcher(downloadBean);
}
/**
* Normal download for Transformer.
*
* @param url url
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, DownloadStatus> transform(String url) {
return transform(url, null);
}
/**
* Normal download for Transformer.
*
* @param url url
* @param saveName saveName
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, DownloadStatus> transform(
String url, String saveName) {
return transform(url, saveName, null);
}
/**
* Normal download for Transformer.
*
* @param url url
* @param saveName saveName
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, DownloadStatus> transform(
String url, String saveName, String savePath) {
return transform(new DownloadBean.Builder(url)
.setSaveName(saveName).setSavePath(savePath).build());
}
/**
* Normal download version of the Transformer.
*
* @param downloadBean download bean
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, DownloadStatus> transform(
final DownloadBean downloadBean) {
return new ObservableTransformer<Upstream, DownloadStatus>() {
@Override
public ObservableSource<DownloadStatus> apply(Observable<Upstream> upstream) {
return upstream.flatMap(new Function<Upstream, ObservableSource<DownloadStatus>>() {
@Override
public ObservableSource<DownloadStatus> apply(Upstream upstream) throws Exception {
return download(downloadBean);
}
});
}
};
}
/**
* Using Service to download single url.
* <p>
* Will save the download records in the database.
* <p>
* Un subscribe will not pause download.
* <p>
* If you want receive download status, see {@link #receiveDownloadStatus(String)}
* <p>
* If you want pause download, see {@link #pauseServiceDownload(String)}
* <p>
* If you want get record from database, see {@link #getDownloadRecord(String)}
*
* @param url url
* @return Observable<DownloadStatus>
*/
public Observable<?> serviceDownload(String url) {
return serviceDownload(url, "");
}
/**
* Using Service to download.
*
* @param url url
* @param saveName saveName
* @return Observable<DownloadStatus>
*/
public Observable<?> serviceDownload(String url, String saveName) {
return serviceDownload(url, saveName, null);
}
/**
* Using Service to download.
*
* @param url url
* @param saveName saveName
* @param savePath savePath
* @return Observable<DownloadStatus>
*/
public Observable<?> serviceDownload(String url, String saveName, String savePath) {
return serviceDownload(new DownloadBean.Builder(url)
.setSaveName(saveName).setSavePath(savePath).build());
}
/**
* Using Service to download.
*
* @param bean download bean
* @return Observable<DownloadStatus>
*/
public Observable<?> serviceDownload(final DownloadBean bean) {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() throws InterruptedException {
downloadService.addDownloadMission(new SingleMission(RxDownload.this, bean));
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Service download version of the Transformer.
*
* @param url url
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, Object> transformService(String url) {
return transformService(url, null);
}
/**
* Service download version of the Transformer.
*
* @param url url
* @param saveName saveName
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, Object> transformService(String url,
String saveName) {
return transformService(url, saveName, null);
}
/**
* Service download version of the Transformer.
*
* @param url url
* @param saveName saveName
* @param savePath savePath
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, Object> transformService(
String url, String saveName, String savePath) {
return transformService(new DownloadBean.Builder(url)
.setSaveName(saveName).setSavePath(savePath).build());
}
/**
* Service download version of the Transformer.
*
* @param bean download bean
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, Object> transformService(final DownloadBean bean) {
return new ObservableTransformer<Upstream, Object>() {
@Override
public ObservableSource<Object> apply(Observable<Upstream> upstream) {
return upstream.flatMap(new Function<Upstream, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Upstream upstream) throws Exception {
return serviceDownload(bean);
}
});
}
};
}
/**
* Using Service to download multi urls.
*
* @param missionId missionId
* @param urls urls
* @return Observable<DownloadStatus>
*/
public Observable<?> serviceMultiDownload(String missionId, String... urls) {
return serviceMultiDownload(missionId, Arrays.asList(urls));
}
/**
* Using Service to download multi urls.
*
* @param missionId missionId
* @param urls List urls
* @return Observable<DownloadStatus>
*/
public Observable<?> serviceMultiDownload(String missionId, List<String> urls) {
List<DownloadBean> list = new ArrayList<>();
for (String each : urls) {
list.add(new DownloadBean.Builder(each).build());
}
return serviceMultiDownload(list, missionId);
}
/**
* Using Service to download multi urls.
*
* @param beans download beans
* @param missionId missionId
* @return Observable<DownloadStatus>
*/
public Observable<?> serviceMultiDownload(final List<DownloadBean> beans, final String missionId) {
return createGeneralObservable(new GeneralObservableCallback() {
@Override
public void call() throws InterruptedException {
downloadService.addDownloadMission(new MultiMission(RxDownload.this, missionId, beans));
}
}).observeOn(AndroidSchedulers.mainThread());
}
/**
* Service multi download version of the Transformer.
*
* @param missionId missionId
* @param urls multi download urls
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, Object> transformMulti(
String missionId, String... urls) {
return transformMulti(missionId, Arrays.asList(urls));
}
/**
* Service multi download version of the Transformer.
*
* @param missionId missionId
* @param urls multi download urls
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, Object> transformMulti(
String missionId, List<String> urls) {
List<DownloadBean> list = new ArrayList<>();
for (String each : urls) {
list.add(new DownloadBean.Builder(each).build());
}
return transformMulti(list, missionId);
}
/**
* Service multi download version of the Transformer.
*
* @param beans multi download bean
* @param missionId missionId
* @param <Upstream> Upstream
* @return Transformer
*/
public <Upstream> ObservableTransformer<Upstream, Object> transformMulti(
final List<DownloadBean> beans, final String missionId) {
return new ObservableTransformer<Upstream, Object>() {
@Override
public ObservableSource<Object> apply(Observable<Upstream> upstream) {
return upstream.flatMap(new Function<Upstream, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Upstream upstream) throws Exception {
return serviceMultiDownload(beans, missionId);
}
});
}
};
}
/**
* return general observable
*
* @param callback Called when observable created.
* @return Observable
*/
private Observable<?> createGeneralObservable(final GeneralObservableCallback callback) {
return Observable.create(new ObservableOnSubscribe<Object>() {
@Override
public void subscribe(final ObservableEmitter<Object> emitter) throws Exception {
if (!bound) {
semaphore.acquire();
if (!bound) {
startBindServiceAndDo(new ServiceConnectedCallback() {
@Override
public void call() {
doCall(callback, emitter);
semaphore.release();
}
});
} else {
doCall(callback, emitter);
semaphore.release();
}
} else {
doCall(callback, emitter);
}
}
}).subscribeOn(Schedulers.io());
}
private void doCall(GeneralObservableCallback callback, ObservableEmitter<Object> emitter) {
if (callback != null) {
try {
callback.call();
} catch (Exception e) {
emitter.onError(e);
}
}
emitter.onNext(object);
emitter.onComplete();
}
/**
* start and bind service.
*
* @param callback Called when service connected.
*/
private void startBindServiceAndDo(final ServiceConnectedCallback callback) {
Intent intent = new Intent(context, DownloadService.class);
intent.putExtra(DownloadService.INTENT_KEY, maxDownloadNumber);
context.startService(intent);
context.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
DownloadService.DownloadBinder downloadBinder
= (DownloadService.DownloadBinder) binder;
downloadService = downloadBinder.getService();
context.unbindService(this);
bound = true;
callback.call();
}
@Override
public void onServiceDisconnected(ComponentName name) {
//注意!!这个方法只会在系统杀掉Service时才会调用!!
bound = false;
}
}, Context.BIND_AUTO_CREATE);
}
private interface GeneralObservableCallback {
void call() throws Exception;
}
private interface ServiceConnectedCallback {
void call();
}
}