/* * 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.cache; 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.OutputStream; import java.util.HashSet; import java.util.Locale; import java.util.Set; import java.util.concurrent.Executor; import nya.miku.wishmaster.api.ChanModule; import nya.miku.wishmaster.api.interfaces.CancellableTask; 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.lib.base64.Base64; import nya.miku.wishmaster.ui.downloading.DownloadingService; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Handler; import android.support.v4.util.LruCache; import android.widget.ImageView; /** * Трёхуровневый (Интернет -> Файловая система -> Оперативная память) кэш для картинок (Bitmap Cache) * @author miku-nyan * */ public class BitmapCache { private static final String TAG = "BitmapCache"; private final LruCache<String, Bitmap> lru; private final FileCache fileCache; private final Set<String> currentDownloads; private static final Bitmap EMPTY_BMP = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8); /** * Конструктор * @param maxSize максимальный размер кэша в памяти в байтах * @param fileCache объект файлового кэша */ public BitmapCache(int maxSize, FileCache fileCache) { this.fileCache = fileCache; this.lru = new LruCache<String, Bitmap>(maxSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; this.currentDownloads = new HashSet<String>(); } /** * Попытаться получить картинку из кэша в памяти * @param hash хэш (уникальный для картинки) * @return Bitmap с картинкой, или null, если отсутствует в памяти */ public Bitmap getFromMemory(String hash) { return lru.get(hash); } /** * Очистить LRU-кэш в памяти (вызывать в случае нехватки памяти) */ public void clearLru() { lru.evictAll(); } /** * Проверить, существует ли картинка в кэше * @param hash хэш (уникальный для картинки) * @return true, если картинка в наличии */ public boolean isInCache(String hash) { if (getFromMemory(hash) != null) return true; File file = fileCache.get(FileCache.PREFIX_BITMAPS + hash); if (file != null && file.exists()) return true; return false; } /** * Попытаться получить картинку из кэша (сначала берётся из памяти, в случае отсутствия - из файлового кэша) * @param hash хэш (уникальный для картинки) * @return Bitmap с картинкой, или null, если отсутствует и в памяти, и в файловом кэше */ public Bitmap getFromCache(String hash) { Bitmap fromLru = getFromMemory(hash); if (fromLru != null) return fromLru; synchronized (currentDownloads) { if (currentDownloads.contains(hash)) return null; } InputStream fileStream = null; Bitmap bmp = null; try { File file = fileCache.get(FileCache.PREFIX_BITMAPS + hash); if (file == null || !file.exists()) { return null; } fileStream = new FileInputStream(file); bmp = BitmapFactory.decodeStream(fileStream); } catch (Exception e) { Logger.e(TAG, e); } catch (OutOfMemoryError oom) { MainApplication.freeMemory(); Logger.e(TAG, oom); } finally { IOUtils.closeQuietly(fileStream); } if (bmp != null) lru.put(hash, bmp); return bmp; } /** * Попытаться получить картинку из локального контейнера-архива (сохранённого треда) * @param hash хэш (уникальный для картинки) * @param container объект-архив - источник картинок * @return Bitmap с картинкой, или null, если отсутствует в контейнере */ public Bitmap getFromContainer(String hash, ReadableContainer container) { Bitmap bmp = getFromMemory(hash); if (bmp != null) return bmp; if (container == null) return null; for (String fileFormatInContainer : new String[] { DownloadingService.THUMBNAIL_FILE_FORMAT, DownloadingService.ICON_FILE_FORMAT } ) { String filenameInContainer = String.format(Locale.US, fileFormatInContainer, hash); if (bmp == null && container.hasFile(filenameInContainer)) { InputStream is = null; try { is = container.openStream(filenameInContainer); bmp = BitmapFactory.decodeStream(is); if (bmp != null) lru.put(hash, bmp); } catch (Exception e) { Logger.e(TAG, e); bmp = null; } catch (OutOfMemoryError oom) { MainApplication.freeMemory(); Logger.e(TAG, oom); bmp = null; } finally { IOUtils.closeQuietly(is); } } } return bmp; } /** * Загрузить и поместить в кэш картинку из интернета * @param hash хэш (уникальный для картинки) * @param url адрес URL (абсолютный или относительный путь) * @param maxSize максимальный размер в пикселях, до которого картинка будет уменьшена, или 0, если требуется оставить как есть * @param chan модуль чана для загрузки * @param task отменяемая задача * @return Bitmap с картинкой, или null, если загрузить не удалось */ public Bitmap download(String hash, String url, int maxSize, ChanModule chan, CancellableTask task) { if (hash == null) { Logger.e(TAG, "received null hash; url: "+url); return null; } synchronized (currentDownloads) { while (currentDownloads.contains(hash)) { try { currentDownloads.wait(); } catch (Exception e) { Logger.e(TAG, e); } } currentDownloads.add(hash); } try { Bitmap bmp = getFromCache(hash); if (bmp != null) return bmp; try { class BufOutputStream extends ByteArrayOutputStream { public BufOutputStream() { super(1024); } public InputStream toInputStream() { return new ByteArrayInputStream(buf, 0, count); } } BufOutputStream data = new BufOutputStream(); chan.downloadFile(url, data, null, task); bmp = BitmapFactory.decodeStream(data.toInputStream()); } catch (Exception e) { Logger.e(TAG, e); if (url.startsWith("data:image")) { try { byte[] data = Base64.decode(url.substring(url.indexOf("base64,") + 7), Base64.DEFAULT); bmp = BitmapFactory.decodeByteArray(data, 0, data.length); } catch (Exception e1) { Logger.e(TAG, e1); } } } if (bmp == null || (task != null && task.isCancelled())) { return null; } if (maxSize > 0) { //ебучий шакал double scale = (double)maxSize / Math.max(bmp.getWidth(), bmp.getHeight()); if (scale < 1.0) { int width = (int) (bmp.getWidth() * scale); int height = (int) (bmp.getHeight() * scale); if (Math.min(width, height) > 0) { Bitmap scaled = Bitmap.createScaledBitmap(bmp, width, height, true); bmp.recycle(); bmp = scaled; } } } OutputStream fileStream = null; File file = null; boolean success = true; try { lru.put(hash, bmp); file = fileCache.create(FileCache.PREFIX_BITMAPS + hash); fileStream = new FileOutputStream(file); if (!bmp.compress(Bitmap.CompressFormat.PNG, 100, fileStream)) { throw new Exception(); } fileStream.close(); fileCache.put(file); fileStream = null; } catch (Exception e) { success = false; Logger.e(TAG, e); } finally { IOUtils.closeQuietly(fileStream); if (!success && file != null) fileCache.abort(file); } return bmp; } catch (OutOfMemoryError oom) { MainApplication.freeMemory(); Logger.e(TAG, oom); return null; } finally { synchronized (currentDownloads) { currentDownloads.remove(hash); currentDownloads.notifyAll(); } } } /** * Получить картинку асинхронно и установить в ImageView.<br> * На ImageView устанавливается тэг:<ul> * <li>строка с хэшем картинки, во время загрузки (прямо сейчас, в данный момент)</li> * <li>объект {@link Boolean#TRUE}, когда картинка загружена успешно</li> * <li>объект {@link Boolean#FALSE}, когда картинка не была загружена (в случае ошибки или downloadFromInternet == false)</li></ul> * @param hash хэш (уникальный для картинки) * @param url адрес URL (абсолютный или относительный) * @param maxSize максимальный размер в пикселях, до которого картинка будет уменьшена, или 0, если требуется оставить как есть * @param chan модуль чана для скачивания картинки из интернета * @param zipFile объект-архив - источник картинок для сохранённого треда (может принимать null) * @param task отменяемая задача * @param imageView объект {@link ImageView}, куда будет выведена картинка * @param executor асинхронный исполнитель * @param handler Handler UI потока * @param downloadFromInternet загружать ли картинку из интернета * @param defaultResId ID ресурса с картинкой ошибки, если картинка не загружена (не удалось или downloadFromInternet == false), * или 0 - если отображать ошибку не нужно */ public void asyncGet(String hash, String url, int maxSize, ChanModule chan, ReadableContainer zipFile, CancellableTask task, ImageView imageView, Executor executor, Handler handler, boolean downloadFromInternet, int defaultResId) { if (hash == null) { Logger.e(TAG, "received null hash for url: " + url); imageView.setTag(Boolean.FALSE); imageView.setImageResource(defaultResId); return; } Bitmap fromLru = getFromMemory(hash); if (fromLru != null) { imageView.setTag(Boolean.TRUE); imageView.setImageBitmap(fromLru); return; } else { imageView.setImageBitmap(EMPTY_BMP); } class ImageDownloader implements Runnable { private final String hash; private final String url; private final int maxSize; private final ChanModule chan; private final ReadableContainer zipFile; private final CancellableTask task; private final ImageView imageView; private final Handler handler; private final boolean downloadFromInternet; private final int defaultResId; public ImageDownloader(String hash, String url, int maxSize, ChanModule chan, ReadableContainer zipFile, CancellableTask task, ImageView imageView, Handler handler, boolean downloadFromInternet, int defaultResId) { this.hash = hash; this.url = url; this.maxSize = maxSize; this.chan = chan; this.zipFile = zipFile; this.task = task; this.imageView = imageView; this.handler = handler; this.downloadFromInternet = downloadFromInternet; this.defaultResId = defaultResId; } private Bitmap bmp; @Override public void run() { bmp = getFromCache(hash); if (bmp == null && zipFile != null) bmp = getFromContainer(hash, zipFile); if (bmp == null && downloadFromInternet) { bmp = download(hash, url, maxSize, chan, task); } if (task != null && task.isCancelled()) return; if (imageView.getTag() == null || !imageView.getTag().equals(hash)) return; if (bmp == null) { if (defaultResId == 0) { imageView.setTag(Boolean.FALSE); return; } } handler.post(new Runnable() { @Override public void run() { try { if (imageView.getTag() == null || !imageView.getTag().equals(hash)) return; if (bmp != null) { imageView.setTag(Boolean.TRUE); imageView.setImageBitmap(bmp); } else { imageView.setTag(Boolean.FALSE); imageView.setImageResource(defaultResId); } } catch (OutOfMemoryError oom) { MainApplication.freeMemory(); Logger.e(TAG, oom); } } }); } } if (task != null && task.isCancelled()) return; imageView.setTag(hash); executor.execute(new ImageDownloader(hash, url, maxSize, chan, zipFile, task, imageView, handler, downloadFromInternet, defaultResId)); } }