package com.jparkie.aizoban.controllers.downloads; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.IBinder; import android.os.PowerManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import com.jparkie.aizoban.BuildConfig; import com.jparkie.aizoban.R; import com.jparkie.aizoban.controllers.AizobanManager; import com.jparkie.aizoban.controllers.QueryManager; import com.jparkie.aizoban.controllers.databases.ApplicationSQLiteOpenHelper; import com.jparkie.aizoban.controllers.events.DownloadChapterQueryEvent; import com.jparkie.aizoban.controllers.factories.DefaultFactory; import com.jparkie.aizoban.models.Chapter; import com.jparkie.aizoban.models.downloads.DownloadChapter; import com.jparkie.aizoban.utils.DiskUtils; import com.jparkie.aizoban.utils.DownloadUtils; import com.jparkie.aizoban.utils.NavigationUtils; import com.jparkie.aizoban.utils.PreferenceUtils; import com.jparkie.aizoban.utils.wrappers.RequestWrapper; import com.jparkie.aizoban.views.activities.MainActivity; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import de.greenrobot.event.EventBus; import rx.Observable; import rx.Observer; import rx.Subscriber; import rx.Subscription; import rx.android.observables.AndroidObservable; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; public class DownloadService extends Service implements Observer<File> { public static final String TAG = DownloadService.class.getSimpleName(); public static final String INTENT_QUEUE_DOWNLOAD = TAG + ":" + "QueueDownloadIntent"; public static final String INTENT_CANCEL_DOWNLOAD = TAG + ":" + "CancelDownloadIntent"; public static final String INTENT_START_DOWNLOAD = TAG + ":" + "StartDownloadIntent"; public static final String INTENT_STOP_DOWNLOAD = TAG + ":" + "StopDownloadIntent"; public static final String INTENT_RESTART_DOWNLOAD = TAG + ":" + "RestartDownloadIntent"; private final static int DOWNLOAD_NOTIFICATION_ID = 1337; private static final int DOWNLOAD_CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors(); private static final int DOWNLOAD_MAXIMUM_POOL_SIZE = (DOWNLOAD_CORE_POOL_SIZE > 0) ? DOWNLOAD_CORE_POOL_SIZE : 2; private static final int KEEP_ALIVE_TIME = 1; private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; private NotificationCompat.Builder mDownloadNotificationBuilder; private PowerManager.WakeLock mWakeLock; private ThreadPoolExecutor mDownloadThreadPoolExecutor; private PublishSubject<DownloadChapter> mDownloadChapterPublishSubject; private ConcurrentHashMap<String, Subscription> mDownloadUrlToSubscriptionMap; private Subscription mDownloadChapterPublishSubjectSubscription; private Subscription mNetworkChangeBroadcastSubscription; private boolean mIsInitialized; private boolean mIsStopping; @Override public int onStartCommand(final Intent intent, int flags, int startId) { Observable.create(new Observable.OnSubscribe<Void>() { @Override public void call(Subscriber<? super Void> subscriber) { handleQueueDownloadIntent(intent); handleCancelDownloadIntent(intent); handleStartDownloadIntent(intent); handleStopDownloadIntent(intent); handleRestartDownloadIntent(intent); subscriber.onCompleted(); } }) .subscribeOn(Schedulers.newThread()) .subscribe(); return START_STICKY; } @Override public IBinder onBind(Intent intent) { return null; } @Override public boolean onUnbind(Intent intent) { return super.onUnbind(intent); } @Override public void onDestroy() { super.onDestroy(); destoryAllSubscriptions(); releaseWakeLock(); } // Observer<File>: @Override public void onCompleted() { if (QueryManager.queryShouldDownloadServiceStop().toBlocking().single()) { stopForeground(false); stopSelf(); } } @Override public void onError(Throwable e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } @Override public void onNext(File imageFile) { // Do Nothing. } private void initialize() { if (mIsInitialized) { return; } initializeWakeLock(); initializeThreadPoolExecutor(); initializeDownloadChapterPublishSubject(); initializeNetworkChangeBroadcastObservable(); initializeNotification(); mIsInitialized = true; } private void initializeWakeLock() { mWakeLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG + ":" + "WakeLock"); if (!mWakeLock.isHeld()) { mWakeLock.acquire(); } } private void releaseWakeLock() { if (mWakeLock != null) { if (mWakeLock.isHeld()) { mWakeLock.release(); } } } private void initializeThreadPoolExecutor() { mDownloadThreadPoolExecutor = new ThreadPoolExecutor( DOWNLOAD_MAXIMUM_POOL_SIZE, DOWNLOAD_MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, new LinkedBlockingDeque<Runnable>() ); } private void initializeDownloadChapterPublishSubject() { mDownloadUrlToSubscriptionMap = new ConcurrentHashMap<String, Subscription>(); mDownloadChapterPublishSubject = PublishSubject.create(); mDownloadChapterPublishSubjectSubscription = mDownloadChapterPublishSubject .filter(new Func1<DownloadChapter, Boolean>() { @Override public Boolean call(DownloadChapter downloadChapter) { return QueryManager.queryAllowDownloadServiceToPublishDownloadChapter(downloadChapter).toBlocking().single(); } }) .subscribeOn(Schedulers.io()) .subscribe(new Action1<DownloadChapter>() { @Override public void call(DownloadChapter allowedChapter) { startDownloadToSubscriptionMap(allowedChapter); } }); } private void initializeNetworkChangeBroadcastObservable() { IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); mNetworkChangeBroadcastSubscription = AndroidObservable .fromBroadcast(this, intentFilter) .subscribe(new Action1<Intent>() { @Override public void call(Intent intent) { if (isNetworkAvailableForDownloads()) { queueDownloadChapters(); startForeground(DOWNLOAD_NOTIFICATION_ID, mDownloadNotificationBuilder.build()); if (!mWakeLock.isHeld()) { mWakeLock.acquire(); } } else { stopForeground(false); if (mWakeLock.isHeld()) { mWakeLock.release(); } } } }); } private void startDownloadToSubscriptionMap(final DownloadChapter downloadChapter) { final String hashKey = DiskUtils.hashKeyForDisk(downloadChapter.getUrl()); Subscription newSubscription = AizobanManager.downloadChapterFromNetwork(downloadChapter) .finallyDo(new Action0() { @Override public void call() { Subscription finalSubscription = mDownloadUrlToSubscriptionMap.remove(hashKey); if (finalSubscription != null) { finalSubscription.unsubscribe(); finalSubscription = null; } if (isNetworkAvailableForDownloads()) { queueDownloadChapters(); } } }) .subscribeOn(Schedulers.from(mDownloadThreadPoolExecutor)) .subscribe(this); mDownloadUrlToSubscriptionMap.put(hashKey, newSubscription); } private void destoryAllSubscriptions() { if (mDownloadUrlToSubscriptionMap != null) { for (Subscription downloadSubscription : mDownloadUrlToSubscriptionMap.values()) { if (downloadSubscription != null) { downloadSubscription.unsubscribe(); downloadSubscription = null; } } } if (mDownloadChapterPublishSubjectSubscription != null) { mDownloadChapterPublishSubjectSubscription.unsubscribe(); mDownloadChapterPublishSubjectSubscription = null; } if (mNetworkChangeBroadcastSubscription != null) { mNetworkChangeBroadcastSubscription.unsubscribe(); mNetworkChangeBroadcastSubscription = null; } } private synchronized void handleQueueDownloadIntent(Intent queueDownloadIntent) { if (queueDownloadIntent != null) { if (queueDownloadIntent.hasExtra(INTENT_QUEUE_DOWNLOAD)) { ArrayList<Chapter> chaptersToDownload = queueDownloadIntent.getParcelableArrayListExtra(INTENT_QUEUE_DOWNLOAD); if (chaptersToDownload != null) { ApplicationSQLiteOpenHelper applicationSQLiteOpenHelper = ApplicationSQLiteOpenHelper.getInstance(); SQLiteDatabase applicationDatabase = applicationSQLiteOpenHelper.getWritableDatabase(); applicationDatabase.beginTransaction(); try { for (Chapter chapterToDownload : chaptersToDownload) { if (chapterToDownload != null) { DownloadChapter downloadChapter = DefaultFactory.DownloadChapter.constructDefault(); downloadChapter.setSource(chapterToDownload.getSource()); downloadChapter.setUrl(chapterToDownload.getUrl()); downloadChapter.setParentUrl(chapterToDownload.getParentUrl()); downloadChapter.setName(chapterToDownload.getName()); boolean isExternalStorage = PreferenceUtils.isExternalStorage(); if (isExternalStorage) { File externalDirectory = new File(PreferenceUtils.getDownloadDirectory()); File sourceDirectory = new File(externalDirectory, downloadChapter.getSource()); File urlHashDirectory = new File(sourceDirectory, DiskUtils.hashKeyForDisk(downloadChapter.getUrl())); downloadChapter.setDirectory(urlHashDirectory.getAbsolutePath()); } else { File internalDirectory = getApplicationContext().getFilesDir(); File sourceDirectory = new File(internalDirectory, downloadChapter.getSource()); File urlHashDirectory = new File(sourceDirectory, DiskUtils.hashKeyForDisk(downloadChapter.getUrl())); downloadChapter.setDirectory(urlHashDirectory.getAbsolutePath()); } if (isExternalStorage) { File externalDirectory = new File(PreferenceUtils.getDownloadDirectory()); File noMediaFile = new File(externalDirectory, ".nomedia"); if (!noMediaFile.exists()) { if (!externalDirectory.exists()) { externalDirectory.mkdirs(); } try { noMediaFile.createNewFile(); } catch (IOException e) { // Do Nothing. } } } downloadChapter.setFlag(DownloadUtils.FLAG_PENDING); QueryManager.putObjectToApplicationDatabase(downloadChapter); } } applicationDatabase.setTransactionSuccessful(); } finally { applicationDatabase.endTransaction(); } } EventBus.getDefault().post(new DownloadChapterQueryEvent()); queueDownloadIntent.removeExtra(INTENT_QUEUE_DOWNLOAD); if (mIsInitialized) { if (QueryManager.queryShouldDownloadServiceStop().toBlocking().single()) { stopForeground(false); stopSelf(); } } else { stopForeground(false); stopSelf(); } } } } private synchronized void handleCancelDownloadIntent(Intent cancelDownloadIntent) { if (cancelDownloadIntent != null) { if (cancelDownloadIntent.hasExtra(INTENT_CANCEL_DOWNLOAD)) { ArrayList<DownloadChapter> downloadChaptersToCancel = cancelDownloadIntent.getParcelableArrayListExtra(INTENT_CANCEL_DOWNLOAD); if (downloadChaptersToCancel != null) { ApplicationSQLiteOpenHelper applicationSQLiteOpenHelper = ApplicationSQLiteOpenHelper.getInstance(); SQLiteDatabase sqLiteDatabase = applicationSQLiteOpenHelper.getWritableDatabase(); sqLiteDatabase.beginTransaction(); try { for (DownloadChapter downloadChapter : downloadChaptersToCancel) { if (downloadChapter != null) { if (mIsInitialized) { String hashKey = DiskUtils.hashKeyForDisk(downloadChapter.getUrl()); if (mDownloadUrlToSubscriptionMap.containsKey(hashKey)) { Subscription currentSubscription = mDownloadUrlToSubscriptionMap.remove(hashKey); currentSubscription.unsubscribe(); currentSubscription = null; } } DiskUtils.deleteFiles(new File(downloadChapter.getDirectory())); QueryManager.deleteObjectToApplicationDatabase(downloadChapter); QueryManager.deleteDownloadPagesOfDownloadChapter(new RequestWrapper(downloadChapter.getSource(), downloadChapter.getUrl())) .toBlocking() .single(); } } sqLiteDatabase.setTransactionSuccessful(); } finally { sqLiteDatabase.endTransaction(); } } EventBus.getDefault().post(new DownloadChapterQueryEvent()); cancelDownloadIntent.removeExtra(INTENT_CANCEL_DOWNLOAD); if (mIsInitialized) { if (isNetworkAvailableForDownloads()) { queueDownloadChapters(); } if (QueryManager.queryShouldDownloadServiceStop().toBlocking().single()) { stopForeground(false); stopSelf(); } } else { stopForeground(false); stopSelf(); } } } } private synchronized void handleStartDownloadIntent(Intent startDownloadIntent) { if (startDownloadIntent != null) { if (startDownloadIntent.hasExtra(INTENT_START_DOWNLOAD)) { initialize(); EventBus.getDefault().post(new DownloadChapterQueryEvent()); startDownloadIntent.removeExtra(INTENT_START_DOWNLOAD); if (isNetworkAvailableForDownloads()) { queueDownloadChapters(); } if (QueryManager.queryShouldDownloadServiceStop().toBlocking().single()) { stopForeground(false); stopSelf(); } } } } private void handleStopDownloadIntent(Intent stopDownloadIntent) { if (stopDownloadIntent != null) { if (stopDownloadIntent.hasExtra(INTENT_STOP_DOWNLOAD)) { mIsStopping = true; destoryAllSubscriptions(); pauseDownloadChapters(); EventBus.getDefault().post(new DownloadChapterQueryEvent()); stopDownloadIntent.removeExtra(INTENT_STOP_DOWNLOAD); stopForeground(false); stopSelf(); } } } private synchronized void handleRestartDownloadIntent(Intent restartDownloadIntent) { if (restartDownloadIntent != null) { if (restartDownloadIntent.hasExtra(INTENT_RESTART_DOWNLOAD)) { pauseDownloadChapters(); restartDownloadIntent.removeExtra(INTENT_RESTART_DOWNLOAD); stopForeground(false); stopSelf(); } } } private synchronized void queueDownloadChapters() { if (mIsStopping) { return; } if (mDownloadChapterPublishSubject != null) { int dequeueLimit = DOWNLOAD_MAXIMUM_POOL_SIZE; Cursor runningCursor = QueryManager.queryRunningDownloadChapters() .toBlocking() .single(); if (runningCursor != null) { dequeueLimit -= runningCursor.getCount(); runningCursor.close(); runningCursor = null; } if (dequeueLimit > 0) { Cursor availableDownloadChapterCursor = QueryManager.queryAvailableDownloadChapters(dequeueLimit) .toBlocking() .single(); if (availableDownloadChapterCursor != null && availableDownloadChapterCursor.getCount() != 0) { List<DownloadChapter> downloadChapters = QueryManager.toList(availableDownloadChapterCursor, DownloadChapter.class); if (downloadChapters != null) { for (DownloadChapter currentDownloadChapter : downloadChapters) { currentDownloadChapter.setFlag(DownloadUtils.FLAG_RUNNING); QueryManager.putObjectToApplicationDatabase(currentDownloadChapter); } for (DownloadChapter streamDownloadChapter : downloadChapters) { mDownloadChapterPublishSubject.onNext(streamDownloadChapter); } EventBus.getDefault().post(new DownloadChapterQueryEvent()); } } } } } private synchronized void pauseDownloadChapters() { Cursor nonCompletedCursor = QueryManager.queryNonCompletedDownloadChapters() .toBlocking() .single(); if (nonCompletedCursor != null) { List<DownloadChapter> downloadChapters = QueryManager.toList(nonCompletedCursor, DownloadChapter.class); ApplicationSQLiteOpenHelper applicationSQLiteOpenHelper = ApplicationSQLiteOpenHelper.getInstance(); SQLiteDatabase sqLiteDatabase = applicationSQLiteOpenHelper.getWritableDatabase(); sqLiteDatabase.beginTransaction(); try { for (DownloadChapter downloadChapter : downloadChapters) { downloadChapter.setFlag(DownloadUtils.FLAG_PAUSED); QueryManager.putObjectToApplicationDatabase(downloadChapter); } sqLiteDatabase.setTransactionSuccessful(); } finally { sqLiteDatabase.endTransaction(); } } } private boolean isNetworkAvailableForDownloads() { boolean isWiFiOnly = PreferenceUtils.isWiFiOnly(); ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting(); if (isConnected) { if (isWiFiOnly) { if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) { return true; } } else { return true; } } return false; } private void initializeNotification() { Intent notificationIntent = new Intent(this, MainActivity.class); notificationIntent.putExtra(MainActivity.POSITION_ARGUMENT_KEY, NavigationUtils.POSITION_QUEUE); TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); stackBuilder.addParentStack(MainActivity.class); stackBuilder.addNextIntent(notificationIntent); PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); mDownloadNotificationBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_logo) .setContentTitle(getText(R.string.notification_download_title)) .setContentText(getText(R.string.notification_download_text)) .setProgress(0, 0, true) .setContentIntent(pendingIntent); startForeground(DOWNLOAD_NOTIFICATION_ID, mDownloadNotificationBuilder.build()); } }