/*
* 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.gallery;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.tuple.Triple;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.IBinder;
import android.os.RemoteException;
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.BoardModel;
import nya.miku.wishmaster.api.models.UrlPageModel;
import nya.miku.wishmaster.api.util.ChanModels;
import nya.miku.wishmaster.cache.BitmapCache;
import nya.miku.wishmaster.cache.FileCache;
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.ReadableContainer;
import nya.miku.wishmaster.http.interactive.InteractiveException;
import nya.miku.wishmaster.ui.Attachments;
import nya.miku.wishmaster.ui.downloading.DownloadingLocker;
import nya.miku.wishmaster.ui.downloading.DownloadingService;
import nya.miku.wishmaster.ui.presentation.BoardFragment;
import nya.miku.wishmaster.ui.presentation.PresentationModel;
import nya.miku.wishmaster.ui.settings.ApplicationSettings;
import nya.miku.wishmaster.ui.tabs.TabModel;
import nya.miku.wishmaster.ui.tabs.TabsState;
import nya.miku.wishmaster.ui.tabs.TabsSwitcher;
public class GalleryBackend extends Service {
private static final String TAG = "GalleryBackend";
private IBinder binder;
private ApplicationSettings settings;
private DownloadingLocker downloadingLocker;
private CancellableTask tnDownloadingTask;
private FileCache fileCache;
private BitmapCache bitmapCache;
private List<GalleryContext> contexts;
@Override
public void onCreate() {
super.onCreate();
settings = MainApplication.getInstance().settings;
downloadingLocker = MainApplication.getInstance().downloadingLocker;
tnDownloadingTask = new CancellableTask.BaseCancellableTask();
fileCache = MainApplication.getInstance().fileCache;
bitmapCache = MainApplication.getInstance().bitmapCache;
contexts = new ArrayList<>();
binder = new MyBinder(this).asBinder();
}
@Override
public void onDestroy() {
super.onDestroy();
if (tnDownloadingTask != null) tnDownloadingTask.cancel();
for (GalleryContext context : contexts) {
if (context.localFile != null) {
try {
context.localFile.close();
} catch (Exception e) {
Logger.e(TAG, "cannot close local file", e);
}
}
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
private static class MyBinder extends GalleryBinder.Stub {
private final WeakReference<GalleryBackend> service;
private MyBinder(GalleryBackend service) {
this.service = new WeakReference<>(service);
}
@Override
public boolean isPageLoaded(String pagehash) {
return MainApplication.getInstance().pagesCache.getPresentationModel(pagehash) != null;
}
@Override
public int initContext(GalleryInitData initData) {
int result = -1;
GalleryBackend service = this.service.get();
if (service != null) {
GalleryContext context = service.new GalleryContext(initData);
synchronized (service.contexts) {
result = service.contexts.size();
service.contexts.add(context);
}
}
return result;
}
@Override
public GalleryInitResult getInitResult(int contextId) {
GalleryBackend service = this.service.get();
if (service == null) return null;
return service.contexts.get(contextId).getInitResult();
}
@Override
public Bitmap getBitmapFromMemory(int contextId, String hash) {
GalleryBackend service = this.service.get();
if (service == null) return null;
return service.contexts.get(contextId).getBitmapFromMemory(hash);
}
@Override
public Bitmap getBitmap(int contextId, String hash, String url) {
GalleryBackend service = this.service.get();
if (service == null) return null;
return service.contexts.get(contextId).getBitmap(hash, url);
}
@Override
public String getAttachment(int contextId, GalleryAttachmentInfo attachment, GalleryGetterCallback callback) {
GalleryBackend service = this.service.get();
if (service == null) return null;
try {
File file = service.contexts.get(contextId).getFile(attachment.hash, attachment.attachment, callback);
if (file == null) return null;
return file.getPath();
} catch (Exception e) {
Logger.e(TAG, e);
return null;
}
}
@Override
public String getAbsoluteUrl(int contextId, String url) {
GalleryBackend service = this.service.get();
if (service == null) return null;
return service.contexts.get(contextId).getAbsoluteUrl(url);
}
@Override
public void tryScrollParent(int contextId, String postNumber) {
GalleryBackend service = this.service.get();
if (service == null) return;
service.contexts.get(contextId).tryScrollParent(postNumber);
}
}
private class GalleryContext {
private ChanModule chan;
private String customSubdir;
private BoardModel boardModel;
private ReadableContainer localFile;
private GalleryInitResult initResult;
public GalleryContext(GalleryInitData initData) {
initResult = new GalleryInitResult();
boardModel = initData.boardModel;
chan = MainApplication.getInstance().getChanModule(boardModel.chan);
if (initData.localFileName != null) {
try {
localFile = ReadableContainer.obtain(new File(initData.localFileName));
} catch (Exception e) {
Logger.e(TAG, "cannot open local file", e);
}
}
PresentationModel presentationModel = MainApplication.getInstance().pagesCache.getPresentationModel(initData.pageHash);
if (presentationModel != null) {
boolean isThread = presentationModel.source.pageModel.type == UrlPageModel.TYPE_THREADPAGE;
this.customSubdir = BoardFragment.getCustomSubdir(presentationModel.source.pageModel);
List<Triple<AttachmentModel, String, String>> attachments = presentationModel.getAttachments();
presentationModel = null;
if (attachments != null) {
List<Triple<AttachmentModel, String, String>> list = attachments;
int index = -1;
String attachmentHash = initData.attachmentHash;
for (int i=0; i<list.size(); ++i) {
if (list.get(i).getMiddle().equals(attachmentHash)) {
index = i;
break;
}
}
if (index != -1) {
if (isThread) {
initResult.attachments = list;
initResult.initPosition = index;
} else {
int leftOffset = 0, rightOffset = 0;
String threadNumber = list.get(index).getRight();
int it = index; while (it > 0 && list.get(--it).getRight().equals(threadNumber)) ++leftOffset;
it = index; while (it < (list.size()-1) && list.get(++it).getRight().equals(threadNumber)) ++rightOffset;
initResult.attachments = list.subList(index - leftOffset, index + rightOffset + 1);
initResult.initPosition = leftOffset;
}
}
}
} else {
initResult.shouldWaitForPageLoaded = true;
}
if (initResult.attachments == null) {
initResult.attachments = Collections.singletonList(
Triple.of(initData.attachment, ChanModels.hashAttachmentModel(initData.attachment), (String)null));
initResult.initPosition = 0;
}
}
public GalleryInitResult getInitResult() {
GalleryInitResult result = initResult;
initResult = null;
return result;
}
public Bitmap getBitmapFromMemory(String hash) {
return bitmapCache.getFromMemory(hash);
}
public Bitmap getBitmap(String hash, String url) {
Bitmap bmp = bitmapCache.getFromCache(hash);
if (bmp == null && localFile != null) {
bmp = bitmapCache.getFromContainer(hash, localFile);
}
if (bmp == null && url != null && url.length() != 0) {
bmp = bitmapCache.download(hash, url, getResources().getDimensionPixelSize(R.dimen.post_thumbnail_size), chan, tnDownloadingTask);
}
return bmp;
}
public File getFile(String attachmentHash, AttachmentModel attachmentModel, GalleryGetterCallback callback) throws RemoteException {
AsyncCallback asyncCallback = new AsyncCallback(callback);
try {
Async.runAsync(asyncCallback);
return getFile(attachmentHash, attachmentModel, asyncCallback);
} finally {
asyncCallback.stop();
}
}
public File getFile(String attachmentHash, AttachmentModel attachmentModel, final AsyncCallback callback) throws RemoteException {
File file = fileCache.get(FileCache.PREFIX_ORIGINALS + attachmentHash + Attachments.getAttachmentExtention(attachmentModel));
if (file != null) {
String filename = file.getAbsolutePath();
while (downloadingLocker.isLocked(filename)) downloadingLocker.waitUnlock(filename);
if (callback.isCancelled()) return null;
}
if (file == null || !file.exists() || file.isDirectory() || file.length() == 0) {
File dir = new File(settings.getDownloadDirectory(), chan.getChanName());
file = new File(dir, Attachments.getAttachmentLocalFileName(attachmentModel, boardModel));
String filename = file.getAbsolutePath();
while (downloadingLocker.isLocked(filename)) downloadingLocker.waitUnlock(filename);
if (callback.isCancelled()) return null;
}
if (customSubdir != null) {
if (file == null || !file.exists() || file.isDirectory() || file.length() == 0) {
File dir = new File(settings.getDownloadDirectory(), chan.getChanName());
dir = new File(dir, customSubdir);
file = new File(dir, Attachments.getAttachmentLocalFileName(attachmentModel, boardModel));
String filename = file.getAbsolutePath();
while (downloadingLocker.isLocked(filename)) downloadingLocker.waitUnlock(filename);
if (callback.isCancelled()) return null;
}
}
if (!file.exists() || file.isDirectory() || file.length() == 0) {
callback.getCallback().showLoading();
file = fileCache.create(FileCache.PREFIX_ORIGINALS + attachmentHash + Attachments.getAttachmentExtention(attachmentModel));
String filename = file.getAbsolutePath();
while (!downloadingLocker.lock(filename)) downloadingLocker.waitUnlock(filename);
InputStream fromLocal = null;
OutputStream out = null;
boolean success = false;
try {
out = new FileOutputStream(file);
String localName = DownloadingService.ORIGINALS_FOLDER + "/" +
Attachments.getAttachmentLocalFileName(attachmentModel, boardModel);
if (localFile != null && localFile.hasFile(localName)) {
fromLocal = IOUtils.modifyInputStream(localFile.openStream(localName), null, callback);
IOUtils.copyStream(fromLocal, out);
} else {
chan.downloadFile(attachmentModel.path, out, callback, callback);
}
fileCache.put(file);
success = true;
} catch (final Exception e) {
if (callback.isCancelled()) return null;
if (e instanceof InteractiveException) {
callback.getCallback().onInteractiveException(new GalleryInteractiveExceptionHolder((InteractiveException) e));
} else if (IOUtils.isENOSPC(e)) {
callback.getCallback().onException(getString(R.string.error_no_space));
} else {
callback.getCallback().onException(e.getMessage());
}
return null;
} finally {
IOUtils.closeQuietly(fromLocal);
IOUtils.closeQuietly(out);
if (file != null && !success) fileCache.abort(file);
downloadingLocker.unlock(filename);
}
}
return file;
}
public String getAbsoluteUrl(String url) {
return chan.fixRelativeUrl(url);
}
public void tryScrollParent(final String postNumber) {
try {
TabsState tabsState = MainApplication.getInstance().tabsState;
final TabsSwitcher tabsSwitcher = MainApplication.getInstance().tabsSwitcher;
if (tabsSwitcher.currentFragment instanceof BoardFragment) {
TabModel tab = tabsState.findTabById(tabsSwitcher.currentId);
if (tab != null && tab.pageModel != null && tab.pageModel.type == UrlPageModel.TYPE_THREADPAGE) {
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
((BoardFragment) tabsSwitcher.currentFragment).scrollToItem(postNumber);
}
});
}
}
} catch (Exception e) {
Logger.e(TAG, e);
}
}
}
public static class AsyncCallback implements ProgressListener, CancellableTask, Runnable {
private final static long DELAY = 50;
private final GalleryGetterCallback callback;
private final AtomicLong progress, maxValue;
private final AtomicBoolean indeterminate, taskCancelled;
private volatile boolean working = true;
public AsyncCallback(GalleryGetterCallback callback) {
this.callback = callback;
this.progress = new AtomicLong(Long.MIN_VALUE);
this.maxValue = new AtomicLong(Long.MIN_VALUE);
this.indeterminate = new AtomicBoolean(false);
this.taskCancelled = new AtomicBoolean(false);
}
public GalleryGetterCallback getCallback() {
return callback;
}
@Override
public void setMaxValue(long value) {
maxValue.set(value);
}
@Override
public void setProgress(long value) {
progress.set(value);
}
@Override
public void setIndeterminate() {
indeterminate.set(true);
}
@Override
public boolean isCancelled() {
return taskCancelled.get();
}
@Override
public void run() {
while (working) {
try {
long curProgress = progress.getAndSet(Long.MIN_VALUE);
long curMaxValue = maxValue.getAndSet(Long.MIN_VALUE);
boolean curIndeterminate = indeterminate.getAndSet(false);
if (callback.isTaskCancelled()) taskCancelled.set(true);
if (curProgress != Long.MIN_VALUE) callback.setProgress(curProgress);
if (curMaxValue != Long.MIN_VALUE) callback.setProgressMaxValue(curMaxValue);
if (curIndeterminate) callback.setProgressIndeterminate();
Thread.sleep(DELAY);
} catch (Exception e) {
Logger.e(TAG, e);
}
}
}
public void stop() {
working = false;
}
@Override
public void cancel() {
throw new UnsupportedOperationException();
}
}
}