package me.devsaki.hentoid.services; import android.app.IntentService; import android.content.Intent; import org.greenrobot.eventbus.EventBus; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.database.HentoidDB; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.events.DownloadEvent; import me.devsaki.hentoid.parsers.ASMHentaiParser; import me.devsaki.hentoid.parsers.HentaiCafeParser; import me.devsaki.hentoid.parsers.HitomiParser; import me.devsaki.hentoid.parsers.NhentaiParser; import me.devsaki.hentoid.parsers.TsuminoParser; import me.devsaki.hentoid.util.FileHelper; import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.LogHelper; import me.devsaki.hentoid.util.NetworkStatus; /** * Download Manager implemented as a service * <p/> * TODO: Implement download job tracking: https://github.com/AVnetWS/Hentoid/issues/110 * 1 image = 1 task, n images = 1 chapter = 1 job = 1 bundled task. */ public class DownloadService extends IntentService { private static final String TAG = LogHelper.makeLogTag(DownloadService.class); public static boolean paused; private Content currentContent; private HentoidDB db; private NotificationPresenter notificationPresenter; public DownloadService() { super(DownloadService.class.getName()); } @Override public void onCreate() { super.onCreate(); db = HentoidDB.getInstance(this); notificationPresenter = new NotificationPresenter(); EventBus.getDefault().register(notificationPresenter); LogHelper.d(TAG, "Download service created"); } @Override public void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(notificationPresenter); notificationPresenter = null; LogHelper.d(TAG, "Download service destroyed"); } @Override protected void onHandleIntent(Intent intent) { if (!NetworkStatus.isOnline(this)) { LogHelper.w(TAG, "No connection!"); return; } currentContent = db.selectContentByStatus(StatusContent.DOWNLOADING); if (currentContent != null && currentContent.getStatus() != StatusContent.DOWNLOADED) { initDownload(); File dir = FileHelper.getContentDownloadDir(this, currentContent); LogHelper.d(TAG, "Content Download Dir; " + dir); LogHelper.d(TAG, "Directory created: " + FileHelper.createDirectory(dir)); ImageDownloadBatch downloadBatch = new ImageDownloadBatch(); addTask(dir, downloadBatch); queryForAdditionalDownloads(); } } private void addTask(File dir, ImageDownloadBatch downloadBatch) { // Add download tasks downloadBatch.newTask(dir, "thumb", currentContent.getCoverImageUrl()); List<ImageFile> imageFiles = currentContent.getImageFiles(); for (int i = 0; i < imageFiles.size() && !paused; i++) { ImageFile imageFile = imageFiles.get(i); downloadBatch.newTask(dir, imageFile.getName(), imageFile.getUrl()); downloadBatch.waitForOneCompletedTask(); double percent = (i + 1) * 100.0 / imageFiles.size(); updateActivity(percent); } if (paused) { LogHelper.d(TAG, "Pause requested"); interruptDownload(); downloadBatch.cancelAllTasks(); if (currentContent.getStatus() == StatusContent.CANCELED) { notificationPresenter.downloadInterrupted(currentContent); } return; } // Assign status tag to ImageFile(s) short errorCount = downloadBatch.getErrorCount(); for (ImageFile imageFile : currentContent.getImageFiles()) { if (errorCount > 0) { imageFile.setStatus(StatusContent.ERROR); errorCount--; } else { imageFile.setStatus(StatusContent.DOWNLOADED); } db.updateImageFileStatus(imageFile); } // Assign status tag to Content, add timestamp, and save to db if (downloadBatch.hasError()) { currentContent.setStatus(StatusContent.ERROR); } else { currentContent.setStatus(StatusContent.DOWNLOADED); } currentContent.setDownloadDate(new Date().getTime()); db.updateContentStatus(currentContent); postDownloadCompleted(dir); } private void initDownload() { notificationPresenter.downloadStarted(currentContent); if (paused) { interruptDownload(); return; } try { parseImageFiles(); } catch (Exception e) { currentContent.setStatus(StatusContent.UNHANDLED_ERROR); currentContent.setStatus(StatusContent.PAUSED); db.updateContentStatus(currentContent); updateActivity(-1); LogHelper.e(TAG, e, "Exception while parsing image files"); return; } if (paused) { interruptDownload(); return; } LogHelper.d(TAG, "Content download started: " + currentContent.getTitle()); // Tracking Event (Download Added) HentoidApp.getInstance().trackEvent(TAG, "Download", "Download Content: Start"); } private void postDownloadCompleted(File dir) { // Save JSON file try { JsonHelper.saveJson(currentContent, dir); } catch (IOException e) { LogHelper.e(TAG, e, "Error saving JSON: " + currentContent.getTitle()); } HentoidApp.downloadComplete(); updateActivity(-1); LogHelper.d(TAG, "Content download finished: " + currentContent.getTitle()); // Tracking Event (Download Completed) HentoidApp.getInstance().trackEvent(TAG, "Download", "Download Content: Complete"); } private void queryForAdditionalDownloads() { // Search for queued content download tasks and fire intent currentContent = db.selectContentByStatus(StatusContent.DOWNLOADING); if (currentContent != null) { Intent intentService = new Intent(Intent.ACTION_SYNC, null, this, DownloadService.class); intentService.putExtra("content_id", currentContent.getId()); startService(intentService); } } private void interruptDownload() { paused = false; currentContent = db.selectContentById(currentContent.getId()); notificationPresenter.downloadInterrupted(currentContent); } private void updateActivity(double percent) { EventBus.getDefault().post(new DownloadEvent(percent)); } // TODO: Implement null handling as fail/retry state private void parseImageFiles() { List<String> aUrls = new ArrayList<>(); switch (currentContent.getSite()) { case ASMHENTAI: aUrls = ASMHentaiParser.parseImageList(currentContent); break; case HENTAICAFE: aUrls = HentaiCafeParser.parseImageList(currentContent); break; case HITOMI: aUrls = HitomiParser.parseImageList(currentContent); break; case NHENTAI: aUrls = NhentaiParser.parseImageList(currentContent); break; case TSUMINO: aUrls = TsuminoParser.parseImageList(currentContent); break; default: break; } int i = 1; List<ImageFile> imageFileList = new ArrayList<>(); for (String str : aUrls) { String name = String.format(Locale.US, "%03d", i); imageFileList.add(new ImageFile() .setUrl(str) .setOrder(i++) .setStatus(StatusContent.SAVED) .setName(name)); } currentContent.setImageFiles(imageFileList); db.insertImageFiles(currentContent); } }