/*
* Overchan 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 nya.miku.wishmaster.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 nya.miku.wishmaster.R;
import nya.miku.wishmaster.api.ChanModule;
import nya.miku.wishmaster.api.interfaces.CancellableTask;
import nya.miku.wishmaster.api.interfaces.ProgressListener;
import nya.miku.wishmaster.api.models.AttachmentModel;
import nya.miku.wishmaster.api.models.BadgeIconModel;
import nya.miku.wishmaster.api.models.BoardModel;
import nya.miku.wishmaster.api.models.PostModel;
import nya.miku.wishmaster.api.models.UrlPageModel;
import nya.miku.wishmaster.api.util.ChanModels;
import nya.miku.wishmaster.api.util.PageLoaderFromChan;
import nya.miku.wishmaster.cache.BitmapCache;
import nya.miku.wishmaster.cache.FileCache;
import nya.miku.wishmaster.cache.SerializablePage;
import nya.miku.wishmaster.common.Async;
import nya.miku.wishmaster.common.IOUtils;
import nya.miku.wishmaster.common.Logger;
import nya.miku.wishmaster.common.MainApplication;
import nya.miku.wishmaster.containers.WriteableContainer;
import nya.miku.wishmaster.http.interactive.InteractiveException;
import nya.miku.wishmaster.lib.base64.Base64;
import nya.miku.wishmaster.lib.base64.Base64OutputStream;
import nya.miku.wishmaster.ui.Attachments;
import nya.miku.wishmaster.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 = "nya.miku.wishmaster.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();
}
}
}