com.nttec.everychan.ui.downloading.DownloadingService.java Source code

Java tutorial

Introduction

Here is the source code for com.nttec.everychan.ui.downloading.DownloadingService.java

Source

/*
 * Everychan Android (Meta Imageboard Client)
 * Copyright (C) 2014-2016  miku-nyan <https://github.com/miku-nyan>
 *     
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.nttec.everychan.ui.downloading;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import com.nttec.everychan.R;
import com.nttec.everychan.api.ChanModule;
import com.nttec.everychan.api.interfaces.CancellableTask;
import com.nttec.everychan.api.interfaces.ProgressListener;
import com.nttec.everychan.api.models.AttachmentModel;
import com.nttec.everychan.api.models.BadgeIconModel;
import com.nttec.everychan.api.models.BoardModel;
import com.nttec.everychan.api.models.PostModel;
import com.nttec.everychan.api.models.UrlPageModel;
import com.nttec.everychan.api.util.ChanModels;
import com.nttec.everychan.api.util.PageLoaderFromChan;
import com.nttec.everychan.cache.BitmapCache;
import com.nttec.everychan.cache.FileCache;
import com.nttec.everychan.cache.SerializablePage;
import com.nttec.everychan.common.Async;
import com.nttec.everychan.common.IOUtils;
import com.nttec.everychan.common.Logger;
import com.nttec.everychan.common.MainApplication;
import com.nttec.everychan.containers.WriteableContainer;
import com.nttec.everychan.http.interactive.InteractiveException;
import com.nttec.everychan.lib.base64.Base64;
import com.nttec.everychan.lib.base64.Base64OutputStream;
import com.nttec.everychan.ui.Attachments;
import com.nttec.everychan.ui.settings.ApplicationSettings;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.res.ResourcesCompat;

public class DownloadingService extends Service {
    private static final String TAG = "DownloadingService";

    public static final String EXTRA_DOWNLOADING_ITEM = "DownloadingItem";
    public static final String EXTRA_DOWNLOADING_REPORT = "DownloadingReport";

    public static final int REPORT_NONE = 0;
    public static final int REPORT_OK = 1;
    public static final int REPORT_ERROR = 2;

    public static final String BROADCAST_UPDATED = "com.nttec.everychan.BROADCAST_ACTION_DOWNLOADING_UPDATED";

    public static final String SHARED_PREFERENCES_NAME = "downloading_last_error_report";
    public static final String PREF_ERROR_REPORT = "LAST_ERROR_REPORT";
    public static final String PREF_ERROR_ITEMS = "LAST_ERROR_ITEMS";

    /**   ?  ? ? (?)  ? ?   */
    public static final String MAIN_OBJECT_FILE = "data/serialized.bin";
    /** ?  ?  favicon ?? HTML ?   */
    public static final String FAVICON_FILE = "favicon.png";
    /**  ?? -   ? ? (%s ?? ? ?) */
    public static final String THUMBNAIL_FILE_FORMAT = "thumbnails/%s.png";
    /**  ?? - (/.?)   ? ? (%s ?? ? ) */
    public static final String ICON_FILE_FORMAT = "icons/%s.png";
    /**   (  ? ?) ?  - */
    public static final String ORIGINALS_FOLDER = "originals";

    public static final int MODE_ONLY_CACHE = 1;
    public static final int MODE_DOWNLOAD_THUMBS = 2;
    public static final int MODE_DOWNLOAD_ALL = 3;

    public static final int DOWNLOADING_NOTIFICATION_ID = 20;
    public static final int ERROR_REPORT_NOTIFICATION_ID = 30;

    private volatile boolean nowTaskRunning = false;

    private NotificationCompat.Builder progressNotifBuilder;

    private Queue<DownloadingQueueItem> downloadingQueue;
    private DownloadingTask currentTask;
    private DownloadingServiceBinder binder;
    private NotificationManager notificationManager;
    private ApplicationSettings settings;
    private FileCache fileCache;
    private DownloadingLocker downloadingLocker;
    private BitmapCache bitmapCache;

    private boolean isForeground = false;

    private static DownloadingTask sCurrentTask;
    private static Queue<DownloadingQueueItem> sQueue;

    public static boolean isInQueue(DownloadingQueueItem item) {
        DownloadingTask currentTask = sCurrentTask;
        if (currentTask != null && currentTask.getCurrentItem() != null
                && currentTask.getCurrentItem().equals(item)) {
            return true;
        }
        return sQueue != null && sQueue.contains(item);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        downloadingQueue = new LinkedBlockingQueue<DownloadingQueueItem>();
        sQueue = downloadingQueue;
        binder = new DownloadingServiceBinder(this);
        notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        settings = MainApplication.getInstance().settings;
        fileCache = MainApplication.getInstance().fileCache;
        downloadingLocker = MainApplication.getInstance().downloadingLocker;
        bitmapCache = MainApplication.getInstance().bitmapCache;
        Logger.d(TAG, "created downloading service");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        sCurrentTask = null;
        Logger.d(TAG, "destroyed downloading service");
    }

    private void notifyForeground(int id, Notification notification) {
        if (!isForeground) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ECLAIR) {
                try {
                    getClass().getMethod("setForeground", new Class[] { boolean.class }).invoke(this, Boolean.TRUE);
                } catch (Exception e) {
                    Logger.e(TAG, "cannot invoke setForeground(true)", e);
                }
                notificationManager.notify(id, notification);
            } else {
                ForegroundCompat.startForeground(this, id, notification);
            }
            isForeground = true;
        } else {
            notificationManager.notify(id, notification);
        }
    }

    private void cancelForeground(int id) {
        if (isForeground) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ECLAIR) {
                notificationManager.cancel(id);
                try {
                    getClass().getMethod("setForeground", new Class[] { boolean.class }).invoke(this,
                            Boolean.FALSE);
                } catch (Exception e) {
                    Logger.e(TAG, "cannot invoke setForeground(false)", e);
                }
            } else {
                ForegroundCompat.stopForeground(this);
            }
            isForeground = false;
        } else {
            notificationManager.cancel(id);
        }
    }

    @TargetApi(Build.VERSION_CODES.ECLAIR)
    private static class ForegroundCompat {
        static void startForeground(Service service, int id, Notification notification) {
            service.startForeground(id, notification);
        }

        static void stopForeground(Service service) {
            service.stopForeground(true);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    @SuppressLint("InlinedApi")
    public int onStartCommand(Intent intent, int flags, int startId) {
        onStart(intent, startId);
        return Service.START_REDELIVER_INTENT;
    }

    @Override
    public void onStart(Intent intent, int startId) {
        if (intent != null) {
            DownloadingQueueItem item = (DownloadingQueueItem) intent.getSerializableExtra(EXTRA_DOWNLOADING_ITEM);
            if (item != null)
                downloadingQueue.add(item);
        }
        if (currentTask == null || !nowTaskRunning) {
            Logger.d(TAG, "starting downloading task");
            nowTaskRunning = true;
            currentTask = new DownloadingTask(startId);
            sCurrentTask = currentTask;
            Async.runAsync(currentTask);
        } else {
            Logger.d(TAG, "item added to download queue");
            if (progressNotifBuilder != null) {
                progressNotifBuilder
                        .setContentTitle(getString(R.string.downloading_title, downloadingQueue.size() + 1));
                notifyForeground(DOWNLOADING_NOTIFICATION_ID, progressNotifBuilder.build());
            }
            sendBroadcast(new Intent(BROADCAST_UPDATED));
            currentTask.setStartId(startId);
        }
    }

    public class DownloadingTask extends CancellableTask.BaseCancellableTask implements Runnable {
        private int startId;
        private long maxProgressValue = 100;
        private int curProgress = -1;
        private String currentItemName;
        private DownloadingQueueItem currentItem;
        private StringBuilder errorReport;
        private ArrayList<DownloadingQueueItem> errorItems;

        public DownloadingTask(int startId) {
            setStartId(startId);
        }

        public void setStartId(int startId) {
            this.startId = startId;
        }

        public int getCurrentProgress() {
            return curProgress;
        }

        public String getCurrentItemName() {
            return currentItemName;
        }

        public DownloadingQueueItem getCurrentItem() {
            return currentItem;
        }

        @Override
        public void run() {
            errorReport = new StringBuilder();
            errorItems = new ArrayList<>();
            Intent intentToProgressDialog = new Intent(DownloadingService.this, DownloadingProgressActivity.class);
            PendingIntent pIntentToProgressDialog = PendingIntent.getActivity(DownloadingService.this, 0,
                    intentToProgressDialog, PendingIntent.FLAG_CANCEL_CURRENT);
            progressNotifBuilder = new NotificationCompat.Builder(DownloadingService.this)
                    .setSmallIcon(android.R.drawable.stat_sys_download)
                    .setTicker(getString(R.string.downloading_start_ticker))
                    .setContentIntent(pIntentToProgressDialog).setOngoing(true)
                    .setCategory(NotificationCompat.CATEGORY_PROGRESS).setProgress(100, 0, true);

            while (!isCancelled() && !downloadingQueue.isEmpty()) {
                DownloadingQueueItem item = downloadingQueue.poll();
                currentItem = item;
                progressNotifBuilder.setContentTitle(downloadingQueue.size() > 0
                        ? getString(R.string.downloading_title, downloadingQueue.size() + 1)
                        : getString(R.string.downloading_title_simple));

                if (item.type == DownloadingQueueItem.TYPE_ATTACHMENT) {
                    final String filename = Attachments.getAttachmentLocalFileName(item.attachment,
                            item.boardModel);
                    if (filename == null)
                        continue;
                    String elementName = getString(R.string.downloading_element_format, item.chanName,
                            Attachments.getAttachmentLocalShortName(item.attachment, item.boardModel));
                    currentItemName = elementName;

                    curProgress = -1;
                    progressNotifBuilder.setContentText(filename).setProgress(100, 0, true);
                    notifyForeground(DOWNLOADING_NOTIFICATION_ID, progressNotifBuilder.build());
                    sendBroadcast(new Intent(BROADCAST_UPDATED));

                    ProgressListener listener = new ProgressListener() {
                        @Override
                        public void setProgress(long value) {
                            int newProgress = (int) (100 * (double) value / maxProgressValue);
                            if (newProgress == curProgress)
                                return;
                            curProgress = newProgress;
                            progressNotifBuilder.setProgress(100, newProgress, false);
                            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                                progressNotifBuilder.setContentText("(" + newProgress + "%) " + filename);
                            }
                            notifyForeground(DOWNLOADING_NOTIFICATION_ID, progressNotifBuilder.build());
                            sendBroadcast(new Intent(BROADCAST_UPDATED));
                        }

                        @Override
                        public void setMaxValue(long value) {
                            if (value > 0)
                                maxProgressValue = value;
                        }

                        @Override
                        public void setIndeterminate() {
                            if (curProgress == -1)
                                return;
                            progressNotifBuilder.setProgress(100, 0, true);
                            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                                progressNotifBuilder.setContentText(filename);
                            }
                            notifyForeground(DOWNLOADING_NOTIFICATION_ID, progressNotifBuilder.build());
                            sendBroadcast(new Intent(BROADCAST_UPDATED));
                            curProgress = -1;
                        }
                    };

                    File directory = new File(settings.getDownloadDirectory(), item.chanName);
                    if (item.subdirectory != null && item.subdirectory.length() > 0)
                        directory = new File(directory, item.subdirectory);
                    if (!directory.mkdirs() && !directory.isDirectory()) {
                        addError(item, elementName, getString(R.string.downloading_error_mkdir));
                        continue;
                    }
                    File target = new File(directory, filename);
                    if (target.exists()) {
                        addError(item, elementName, getString(R.string.downloading_error_file_exists));
                        continue;
                    }
                    File fromCache = fileCache
                            .get(FileCache.PREFIX_ORIGINALS + ChanModels.hashAttachmentModel(item.attachment)
                                    + Attachments.getAttachmentExtention(item.attachment));
                    if (fromCache != null) {
                        String fromCacheFilename = fromCache.getAbsolutePath();
                        while (downloadingLocker.isLocked(fromCacheFilename))
                            downloadingLocker.waitUnlock(fromCacheFilename);
                        if (isCancelled())
                            continue;
                        boolean success = false;
                        InputStream is = null;
                        OutputStream os = null;
                        try {
                            if (listener != null)
                                listener.setMaxValue(fromCache.length());
                            is = IOUtils.modifyInputStream(new FileInputStream(fromCache), listener, this);
                            os = new FileOutputStream(target);
                            IOUtils.copyStream(is, os);
                            success = true;
                        } catch (Exception e) {
                            if (!isCancelled()) {
                                addError(item, elementName, getString(IOUtils.isENOSPC(e) ? R.string.error_no_space
                                        : R.string.downloading_error_copy));
                            }
                        } finally {
                            IOUtils.closeQuietly(is);
                            IOUtils.closeQuietly(os);
                            if (!success)
                                target.delete();
                            else
                                notifyMediaScanner(target);
                        }
                    } else {
                        String targetFilename = target.getAbsolutePath();
                        while (!downloadingLocker.lock(targetFilename))
                            downloadingLocker.waitUnlock(targetFilename);
                        if (isCancelled()) {
                            downloadingLocker.unlock(targetFilename);
                            continue;
                        }
                        boolean success = false;
                        FileOutputStream out = null;
                        try {
                            out = new FileOutputStream(target);
                            MainApplication.getInstance().getChanModule(item.chanName)
                                    .downloadFile(item.attachment.path, out, listener, this);
                            success = true;
                        } catch (Exception e) {
                            Logger.e(TAG, e);
                            if (!isCancelled())
                                addError(item, elementName,
                                        e instanceof InteractiveException
                                                ? getString(R.string.downloading_error_interactive_format,
                                                        ((InteractiveException) e).getServiceName())
                                                : getMessageOrENOSPC(e));
                        } finally {
                            IOUtils.closeQuietly(out);
                            if (!success)
                                target.delete();
                            else
                                notifyMediaScanner(target);
                            downloadingLocker.unlock(targetFilename);
                        }
                    }

                } else if (item.type == DownloadingQueueItem.TYPE_THREAD) {
                    String filename = item.boardModel.boardName + "-" + item.threadUrlPage.threadNumber
                            + settings.getDownloadThreadFormat();
                    String htmlname = item.chanName + "_" + item.boardModel.boardName + "_"
                            + item.threadUrlPage.threadNumber + ".html";
                    String elementName = getString(R.string.downloading_element_format, item.chanName,
                            getString(R.string.downloading_thread_format, item.boardModel.boardName,
                                    item.threadUrlPage.threadNumber));
                    currentItemName = elementName;

                    curProgress = -1;
                    progressNotifBuilder.setContentText(elementName).setProgress(100, 0, true);
                    notifyForeground(DOWNLOADING_NOTIFICATION_ID, progressNotifBuilder.build());
                    sendBroadcast(new Intent(BROADCAST_UPDATED));

                    File directory = new File(settings.getDownloadDirectory(), item.chanName);
                    if (!directory.mkdirs() && !directory.isDirectory()) {
                        addError(item, elementName, getString(R.string.downloading_error_mkdir));
                        continue;
                    }

                    WriteableContainer zip = null;
                    File zipFile = new File(directory, filename);
                    try {
                        try {
                            zip = WriteableContainer.obtain(zipFile);
                        } catch (Exception e) {
                            throw new Exception(getString(IOUtils.isENOSPC(e) ? R.string.error_no_space
                                    : R.string.downloading_error_mkfile));
                        }
                        final SerializablePage page = getSerializablePage(item);
                        if (isCancelled())
                            throw new Exception();

                        HtmlBuilder htmlBuilder = null;
                        try {
                            htmlBuilder = new HtmlBuilder(zip.openStream(htmlname), new HtmlBuilder.RefsGetter() {
                                final ChanModule chan = MainApplication.getInstance()
                                        .getChanModule(page.boardModel.chan);

                                @Override
                                public String getFavicon() {
                                    return HtmlBuilder.DATA_DIR + "/" + FAVICON_FILE;
                                }

                                @Override
                                public String getThumbnail(AttachmentModel attachment) {
                                    if (attachment.isSpoiler)
                                        return getFavicon(); //TODO   ?
                                    return attachment.thumbnail == null ? null
                                            : String.format(Locale.US, THUMBNAIL_FILE_FORMAT,
                                                    ChanModels.hashAttachmentModel(attachment));
                                }

                                @Override
                                public String getOriginal(AttachmentModel attachment) {
                                    String chanRef = chan.fixRelativeUrl(
                                            attachment.path != null ? attachment.path : attachment.thumbnail);
                                    if (attachment.type != AttachmentModel.TYPE_OTHER_NOTFILE) {
                                        String filename = Attachments.getAttachmentLocalFileName(attachment,
                                                page.boardModel);
                                        if (filename != null && filename.length() != 0) {
                                            //TODO ?,   ??    ? ,   ?,  ??
                                            return ORIGINALS_FOLDER + "/" + filename;
                                        } else {
                                            return chanRef;
                                        }
                                    } else
                                        return chanRef;
                                }

                                @Override
                                public String getIcon(BadgeIconModel icon) {
                                    return String.format(Locale.US, ICON_FILE_FORMAT,
                                            ChanModels.hashBadgeIconModel(icon, chan.getChanName()));
                                }
                            });
                            htmlBuilder.write(page);
                        } catch (Exception e) {
                            Logger.e(TAG, e);
                            throw new Exception(getString(IOUtils.isENOSPC(e) ? R.string.error_no_space
                                    : R.string.downloading_error_save_html));
                        } finally {
                            IOUtils.closeQuietly(htmlBuilder);
                        }

                        String pageTitle = HtmlBuilder.buildTitle(page);

                        try {
                            MainApplication.getInstance().serializer.savePage(zip.openStream(MAIN_OBJECT_FILE),
                                    pageTitle, page.pageModel, page);
                        } catch (Exception e) {
                            Logger.e(TAG, e);
                            throw new Exception(getString(IOUtils.isENOSPC(e) ? R.string.error_no_space
                                    : R.string.downloading_error_serialize));
                        }

                        for (String asset : HtmlBuilder.ASSETS) {
                            if (zip.hasFile(asset))
                                continue;
                            InputStream in = null;
                            OutputStream out = null;
                            try {
                                in = getAssets().open(asset);
                                out = zip.openStream(HtmlBuilder.DATA_DIR + "/" + asset);
                                IOUtils.copyStream(in, out);
                            } catch (Exception e) {
                                Logger.e(TAG, e);
                                if (!isCancelled()) {
                                    if (IOUtils.isENOSPC(e)) {
                                        throw new Exception(getString(R.string.error_no_space));
                                    } else {
                                        addError(item, asset, getString(R.string.downloading_error_copy));
                                    }
                                }
                            } finally {
                                IOUtils.closeQuietly(in);
                                IOUtils.closeQuietly(out);
                            }
                        }

                        OutputStream faviconStream = null;
                        try {
                            faviconStream = zip.openStream(HtmlBuilder.DATA_DIR + "/" + FAVICON_FILE);
                            Drawable favicon = new LayerDrawable(new Drawable[] {
                                    MainApplication.getInstance().getChanModule(item.chanName).getChanFavicon(),
                                    ResourcesCompat.getDrawable(getResources(), R.drawable.favicon_overlay_local,
                                            null) });
                            Bitmap bmp = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
                            favicon.setBounds(0, 0, 32, 32);
                            favicon.draw(new Canvas(bmp));
                            bmp.compress(Bitmap.CompressFormat.PNG, 100, faviconStream);
                        } catch (Exception e) {
                            Logger.e(TAG, e);
                            if (!isCancelled()) {
                                if (IOUtils.isENOSPC(e)) {
                                    throw new Exception(getString(R.string.error_no_space));
                                } else {
                                    addError(item, FAVICON_FILE, getString(R.string.downloading_error_copy));
                                }
                            }
                        } finally {
                            IOUtils.closeQuietly(faviconStream);
                        }

                        try {
                            zip.transfer(null, this);
                        } catch (Exception e) {
                            Logger.e(TAG, e);
                            throw new Exception(getString(IOUtils.isENOSPC(e) ? R.string.error_no_space
                                    : R.string.downloading_error_copy));
                        }

                        List<AttachmentModel> attachments = new ArrayList<AttachmentModel>();
                        List<BadgeIconModel> icons = new ArrayList<BadgeIconModel>();
                        Set<String> iconsHashes = new HashSet<String>();
                        int threadsCount = page.threads == null ? 0 : page.threads.length;
                        for (int i = -1; i < threadsCount; ++i) {
                            PostModel[] posts = i == -1 ? page.posts : page.threads[i].posts;
                            if (posts == null)
                                continue;
                            for (PostModel postModel : page.posts) {
                                if (postModel.attachments != null) {
                                    for (AttachmentModel attachment : postModel.attachments) {
                                        attachments.add(attachment);
                                    }
                                }
                                if (postModel.icons != null) {
                                    for (BadgeIconModel icon : postModel.icons) {
                                        String iconHash = ChanModels.hashBadgeIconModel(icon, item.chanName);
                                        if (iconsHashes.contains(iconHash))
                                            continue;
                                        icons.add(icon);
                                        iconsHashes.add(iconHash);
                                    }
                                }
                            }
                        }

                        for (int i = 0; i < icons.size(); ++i) {
                            if (isCancelled())
                                throw new Exception();
                            BadgeIconModel icon = icons.get(i);
                            if (icon.source == null || icon.source.length() == 0)
                                continue;
                            String hash = ChanModels.hashBadgeIconModel(icon, item.chanName);
                            String curElementName = icon.source.substring(icon.source.lastIndexOf('/') + 1);
                            if (!zip.hasFile(String.format(Locale.US, ICON_FILE_FORMAT, hash))) {
                                Bitmap bmp = bitmapCache.getFromCache(hash);
                                if (bmp == null && item.downloadingThreadMode == MODE_ONLY_CACHE)
                                    continue;
                                if (bmp == null)
                                    bmp = bitmapCache.download(hash, icon.source,
                                            getResources().getDimensionPixelSize(R.dimen.post_badge_size),
                                            MainApplication.getInstance().getChanModule(item.chanName), this);
                                if (isCancelled())
                                    throw new Exception();
                                if (bmp != null) {
                                    OutputStream out = null;
                                    try {
                                        out = zip.openStream(String.format(Locale.US, ICON_FILE_FORMAT, hash));
                                        bmp.compress(Bitmap.CompressFormat.PNG, 100, out);
                                    } catch (Exception e) {
                                        Logger.e(TAG, e);
                                        if (!isCancelled()) {
                                            if (IOUtils.isENOSPC(e)) {
                                                throw new Exception(getString(R.string.error_no_space));
                                            } else {
                                                addError(item, curElementName,
                                                        getString(R.string.downloading_error_copy));
                                            }
                                        }
                                    } finally {
                                        IOUtils.closeQuietly(out);
                                    }
                                } else {
                                    if (!isCancelled())
                                        addError(item, curElementName,
                                                getString(R.string.downloading_error_download));
                                }
                            }
                        }

                        for (int i = 0; i < attachments.size(); ++i) {
                            if (isCancelled())
                                throw new Exception();

                            AttachmentModel attachment = attachments.get(i);
                            String curFile = Attachments.getAttachmentLocalFileName(attachment, item.boardModel);
                            if (curFile == null)
                                continue;
                            String curElementName = getString(R.string.downloading_element_format, item.chanName,
                                    Attachments.getAttachmentLocalShortName(attachment, item.boardModel));
                            String curThumbElementName = getString(R.string.downloading_thumbnail_format,
                                    curElementName);
                            String curHash = ChanModels.hashAttachmentModel(attachment);
                            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                                progressNotifBuilder
                                        .setContentText("(" + i + "/" + attachments.size() + ") " + elementName);
                            }
                            curProgress = Math.round(100f * i / attachments.size());
                            progressNotifBuilder.setProgress(attachments.size(), i, false);
                            notifyForeground(DOWNLOADING_NOTIFICATION_ID, progressNotifBuilder.build());
                            sendBroadcast(new Intent(BROADCAST_UPDATED));

                            if (attachment.type != AttachmentModel.TYPE_OTHER_NOTFILE
                                    && !zip.hasFile(ORIGINALS_FOLDER + "/" + curFile)) {
                                File cur = new File(directory, curFile);
                                if (!cur.exists() || cur.isDirectory() || cur.length() == 0) {
                                    cur = fileCache.get(
                                            FileCache.PREFIX_ORIGINALS + ChanModels.hashAttachmentModel(attachment)
                                                    + Attachments.getAttachmentExtention(attachment));
                                    if (cur != null) {
                                        String curFilename = cur.getAbsolutePath();
                                        while (downloadingLocker.isLocked(curFilename))
                                            downloadingLocker.waitUnlock(curFilename);
                                        if (isCancelled())
                                            throw new Exception();
                                    }
                                    if (cur == null && item.downloadingThreadMode == MODE_DOWNLOAD_ALL) {
                                        cur = fileCache.create(FileCache.PREFIX_ORIGINALS
                                                + ChanModels.hashAttachmentModel(attachment)
                                                + Attachments.getAttachmentExtention(attachment));
                                        String curFilename = cur.getAbsolutePath();
                                        while (!downloadingLocker.lock(curFilename))
                                            downloadingLocker.waitUnlock(curFilename);
                                        if (isCancelled()) {
                                            fileCache.abort(cur);
                                            downloadingLocker.unlock(curFilename);
                                            throw new Exception();
                                        }
                                        FileOutputStream out = null;
                                        boolean success = true;
                                        try {
                                            out = new FileOutputStream(cur);
                                            MainApplication.getInstance().getChanModule(item.chanName)
                                                    .downloadFile(attachment.path, out, null, this);
                                            fileCache.put(cur);
                                        } catch (Exception e) {
                                            Logger.e(TAG, e);
                                            if (!isCancelled()) {
                                                if (IOUtils.isENOSPC(e)) {
                                                    throw new Exception(getString(R.string.error_no_space));
                                                } else {
                                                    addError(item, curElementName,
                                                            e instanceof InteractiveException ? getString(
                                                                    R.string.downloading_error_interactive_format,
                                                                    ((InteractiveException) e).getServiceName())
                                                                    : getMessageOrENOSPC(e));
                                                }
                                            }
                                            success = false;
                                        } finally {
                                            if (out != null)
                                                IOUtils.closeQuietly(out);
                                            if (!success && cur != null) {
                                                fileCache.abort(cur);
                                                cur = null;
                                            }
                                            downloadingLocker.unlock(curFilename);
                                        }
                                    }
                                }
                                if (isCancelled())
                                    throw new Exception();
                                if (cur != null) {
                                    InputStream in = null;
                                    OutputStream out = null;
                                    try {
                                        in = IOUtils.modifyInputStream(new FileInputStream(cur), null, this);
                                        out = zip.openStream(ORIGINALS_FOLDER + "/" + curFile);
                                        IOUtils.copyStream(in, out);
                                    } catch (Exception e) {
                                        Logger.e(TAG, e);
                                        if (!isCancelled()) {
                                            if (IOUtils.isENOSPC(e)) {
                                                throw new Exception(getString(R.string.error_no_space));
                                            } else {
                                                addError(item, curElementName,
                                                        getString(R.string.downloading_error_copy));
                                            }
                                        }
                                    } finally {
                                        IOUtils.closeQuietly(in);
                                        IOUtils.closeQuietly(out);
                                    }
                                }
                            }

                            if (isCancelled())
                                throw new Exception();

                            if (!zip.hasFile(String.format(Locale.US, THUMBNAIL_FILE_FORMAT, curHash))) {
                                Bitmap bmp = bitmapCache.getFromCache(curHash);
                                if (bmp == null
                                        && (attachment.thumbnail == null || attachment.thumbnail.length() == 0
                                                || item.downloadingThreadMode == MODE_ONLY_CACHE))
                                    continue;
                                if (bmp == null)
                                    bmp = bitmapCache.download(curHash, attachment.thumbnail,
                                            getResources().getDimensionPixelSize(R.dimen.post_thumbnail_size),
                                            MainApplication.getInstance().getChanModule(item.chanName), this);
                                if (isCancelled())
                                    throw new Exception();
                                if (bmp != null) {
                                    OutputStream out = null;
                                    try {
                                        out = zip.openStream(
                                                String.format(Locale.US, THUMBNAIL_FILE_FORMAT, curHash));
                                        bmp.compress(Bitmap.CompressFormat.PNG, 100, out);
                                    } catch (Exception e) {
                                        Logger.e(TAG, e);
                                        if (!isCancelled()) {
                                            if (IOUtils.isENOSPC(e)) {
                                                throw new Exception(getString(R.string.error_no_space));
                                            } else {
                                                addError(item, curThumbElementName,
                                                        getString(R.string.downloading_error_copy));
                                            }
                                        }
                                    } finally {
                                        IOUtils.closeQuietly(out);
                                    }
                                } else {
                                    if (!isCancelled())
                                        addError(item, curThumbElementName,
                                                getString(R.string.downloading_error_download));
                                }
                            }
                        }
                        try {
                            MainApplication.getInstance().database.addSavedThread(item.chanName, pageTitle,
                                    zipFile.getAbsolutePath());
                        } catch (Exception e) {
                            Logger.e(TAG, "database exception", e);
                        }

                    } catch (Exception e) {
                        Logger.e(TAG, e);
                        if (!isCancelled())
                            addError(item, elementName, getMessageOrENOSPC(e));
                        if (zip != null)
                            zip.cancel();
                    } finally {
                        try {
                            if (zip != null)
                                zip.close();
                        } catch (Exception e) {
                            if (!isCancelled())
                                addError(item, elementName, getString(R.string.downloading_error_save_container));
                        }
                    }
                }
            }
            currentItem = null;
            currentItemName = null;

            nowTaskRunning = false;
            if (!isCancelled()) {
                while (errorReport.length() > 0 && errorReport.charAt(errorReport.length() - 1) == '\n') {
                    errorReport.setLength(errorReport.length() - 1);
                }
                if (errorReport.length() == 0) {
                    progressNotifBuilder.setTicker(getString(R.string.downloading_success_ticker))
                            .setSmallIcon(android.R.drawable.stat_sys_download_done);
                    notifyForeground(DOWNLOADING_NOTIFICATION_ID, progressNotifBuilder.build());
                    Intent broadcast = new Intent(BROADCAST_UPDATED);
                    broadcast.putExtra(EXTRA_DOWNLOADING_REPORT, REPORT_OK);
                    sendBroadcast(broadcast);
                } else {
                    Intent intentToErrorReport = new Intent(DownloadingService.this,
                            DownloadingErrorReportActivity.class);
                    PendingIntent pIntentToErrorReport = PendingIntent.getActivity(DownloadingService.this, 0,
                            intentToErrorReport, PendingIntent.FLAG_CANCEL_CURRENT);
                    notificationManager.notify(ERROR_REPORT_NOTIFICATION_ID,
                            new NotificationCompat.Builder(DownloadingService.this)
                                    .setSmallIcon(android.R.drawable.stat_notify_error)
                                    .setTicker(getString(R.string.downloading_error_ticker))
                                    .setContentTitle(getString(R.string.downloading_error_title))
                                    .setContentText(getString(R.string.downloading_error_ticker))
                                    .setContentIntent(pIntentToErrorReport).setOngoing(false).setAutoCancel(true)
                                    .setCategory(NotificationCompat.CATEGORY_ERROR).build());
                    Intent broadcast = new Intent(BROADCAST_UPDATED);
                    broadcast.putExtra(EXTRA_DOWNLOADING_REPORT, REPORT_ERROR);
                    getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE).edit()
                            .putString(PREF_ERROR_REPORT, errorReport.toString())
                            .putString(PREF_ERROR_ITEMS, serializeErrorItems(errorItems)).commit();
                    sendBroadcast(broadcast);
                }
            }
            errorReport.setLength(0);
            errorReport.trimToSize();
            errorItems.clear();
            errorItems.trimToSize();
            Logger.d(TAG, "stopped downloading task");
            cancelForeground(DOWNLOADING_NOTIFICATION_ID);
            stopSelf(startId);
        }

        public SerializablePage getSerializablePage(DownloadingQueueItem item) throws Exception {
            if (item.type != DownloadingQueueItem.TYPE_THREAD)
                throw new Exception();
            SerializablePage page = MainApplication.getInstance().pagesCache
                    .getSerializablePage(ChanModels.hashUrlPageModel(item.threadUrlPage));
            if (isCancelled()) {
                throw new Exception();
            }
            if (page != null) {
                SerializablePage p = new SerializablePage(); //prevent concurrent modification
                p.pageModel = page.pageModel;
                p.boardModel = page.boardModel;
                p.posts = page.posts;
                p.threads = page.threads;
                return p;
            }
            page = new SerializablePage();
            page.pageModel = item.threadUrlPage;
            class LoaderCallback implements PageLoaderFromChan.PageLoaderCallback {
                public volatile String reason = null;

                @Override
                public void onSuccess() {
                    reason = null;
                }

                @Override
                public void onError(String message) {
                    reason = message;
                }

                @Override
                public void onInteractiveException(InteractiveException e) {
                    reason = getString(R.string.downloading_error_interactive_format, e.getServiceName());
                }
            }
            LoaderCallback cb = new LoaderCallback();
            new PageLoaderFromChan(page, cb, MainApplication.getInstance().getChanModule(item.chanName), this)
                    .run();
            if (isCancelled()) {
                throw new Exception();
            }
            if (cb.reason != null) {
                throw new Exception(cb.reason);
            }
            return page;
        }

        private void addError(DownloadingQueueItem item, String element, String error) {
            if (error == null)
                error = getString(R.string.downloading_error_unknown);
            errorReport.append(element).append('\n').append(error).append("\n\n");
            if (errorItems.size() > 0 && errorItems.get(errorItems.size() - 1).equals(item))
                return;
            // item    ? (?  )
            errorItems.add(item);
        }

        private String getMessageOrENOSPC(Exception e) {
            if (IOUtils.isENOSPC(e))
                return getString(R.string.error_no_space);
            return e.getMessage();
        }

        private void notifyMediaScanner(File file) {
            try {
                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
            } catch (Exception e) {
                Logger.e(TAG, e);
            }
        }
    }

    private static String serializeErrorItems(ArrayList<DownloadingQueueItem> list) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(
                    new GZIPOutputStream(new Base64OutputStream(baos, Base64.DEFAULT)));
            oos.writeObject(list);
            oos.close();
            return baos.toString("US-ASCII");
        } catch (Exception e) {
            Logger.e(TAG, e);
            return "";
        }
    }

    @SuppressWarnings("unchecked")
    public static ArrayList<DownloadingQueueItem> deserializeErrorItems(String data) {
        try {
            ObjectInputStream ois = new ObjectInputStream(
                    new GZIPInputStream(new ByteArrayInputStream(Base64.decode(data, Base64.DEFAULT))));
            return (ArrayList<DownloadingQueueItem>) ois.readObject();
        } catch (Exception e) {
            Logger.e(TAG, e);
            return null;
        }
    }

    /**
     * ??-?  
     * @author miku-nyan
     *
     */
    public static class DownloadingQueueItem implements Serializable {
        private static final long serialVersionUID = 1L;

        public static final int TYPE_ATTACHMENT = 1;
        public static final int TYPE_THREAD = 2;

        public final int type;
        public final AttachmentModel attachment;
        public final String subdirectory;
        public final String chanName;
        public final BoardModel boardModel;
        public final UrlPageModel threadUrlPage;
        public final int downloadingThreadMode;

        /**
         * ? ?  - -?
         * @param attachment  ?
         * @param subdirectory  ,   ??   (?    - null)
         * @param boardModel  ?, ?  ??? 
         */
        public DownloadingQueueItem(AttachmentModel attachment, String subdirectory, BoardModel boardModel) {
            this.type = TYPE_ATTACHMENT;
            this.attachment = attachment;
            if (attachment == null)
                throw new NullPointerException();
            this.subdirectory = subdirectory;
            this.chanName = boardModel.chan;
            this.boardModel = boardModel;
            this.threadUrlPage = null;
            this.downloadingThreadMode = -1;
        }

        /**
         * ? ?  - -?
         * @param attachment  ?
         * @param boardModel  ?, ?  ??? 
         */
        public DownloadingQueueItem(AttachmentModel attachment, BoardModel boardModel) {
            this(attachment, null, boardModel);
        }

        /**
         * ? ?  - ?-? 
         * @param threadUrlPage  ? 
         * @param downloadingThreadMode   ( ?,  ,    ?).
         * ?. {@link DownloadingService#MODE_DOWNLOAD_ALL}, {@link DownloadingService#MODE_DOWNLOAD_THUMBS},
         * {@link DownloadingService#MODE_ONLY_CACHE} 
         */
        public DownloadingQueueItem(UrlPageModel threadUrlPage, BoardModel boardModel, int downloadingThreadMode) {
            this.type = TYPE_THREAD;
            this.attachment = null;
            this.subdirectory = null;
            this.chanName = threadUrlPage.chanName;
            this.boardModel = boardModel;
            this.threadUrlPage = threadUrlPage;
            this.downloadingThreadMode = downloadingThreadMode;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o instanceof DownloadingQueueItem) {
                DownloadingQueueItem cmp = (DownloadingQueueItem) o;
                if (cmp.type != type)
                    return false;
                switch (type) {
                case TYPE_ATTACHMENT:
                    if (!stringsEqual(cmp.subdirectory, subdirectory))
                        return false;
                    if (cmp.attachment == null)
                        return attachment == null;
                    return ChanModels.hashAttachmentModel(cmp.attachment)
                            .equals(ChanModels.hashAttachmentModel(attachment));
                case TYPE_THREAD:
                    if (cmp.threadUrlPage == null)
                        return threadUrlPage == null;
                    return ChanModels.hashUrlPageModel(cmp.threadUrlPage)
                            .equals(ChanModels.hashUrlPageModel(threadUrlPage));
                }
            }
            return false;
        }

        private static boolean stringsEqual(String s1, String s2) {
            if (s1 == s2)
                return true;
            if (s1 == null)
                return s2 == null;
            return s1.equals(s2);
        }

        @Override
        public int hashCode() {
            return 0;
        }
    }

    public static class DownloadingServiceBinder extends Binder {
        private final WeakReference<DownloadingService> service;

        private DownloadingServiceBinder(DownloadingService service) {
            this.service = new WeakReference<>(service);
        }

        public void cancel() {
            DownloadingService service = this.service.get();
            if (service == null)
                return;
            if (service.currentTask != null)
                service.currentTask.cancel();
            if (!service.downloadingQueue.isEmpty())
                service.downloadingQueue.clear();
        }

        public int getCurrentProgress() {
            DownloadingService service = this.service.get();
            if (service == null)
                return -1;
            if (service.currentTask == null)
                return -1;
            return service.currentTask.getCurrentProgress();
        }

        public int getQueueSize() {
            DownloadingService service = this.service.get();
            if (service == null)
                return 0;
            if (service.downloadingQueue == null)
                return 0;
            return service.downloadingQueue.size();
        }

        public String getCurrentItemName() {
            DownloadingService service = this.service.get();
            if (service == null)
                return null;
            if (service.currentTask == null)
                return null;
            return service.currentTask.getCurrentItemName();
        }
    }
}