/* * Copyright (C) 2015 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hippo.ehviewer.cache; import android.os.Looper; import android.support.annotation.Nullable; import android.support.v4.util.LruCache; import com.hippo.ehviewer.util.Log; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public abstract class AnyCache { private static final String TAG = AnyCache.class.getSimpleName(); private @Nullable LruCache mMemoryCache; private @Nullable DiskCache mDiskCache; /** * Used to temporarily pause the disk cache while scrolling */ public boolean mPauseDiskAccess = false; private final Object mPauseLock = new Object(); protected abstract int sizeOf(String key, V value); protected abstract V read(InputStream is); protected abstract boolean write(OutputStream os, V value); public void setMemoryCache(int maxSize) { mMemoryCache = new MemoryCahce(maxSize, this); } public void setDiskCache(File cacheDir, int maxSize) { try { mDiskCache = new DiskCache(cacheDir, maxSize, this); } catch (IOException e) { Log.w(TAG, "Can't create disk cache", e); } } /** * Check if have memory cache * * @return true if have memory cache */ public boolean hasMemoryCache() { return mMemoryCache != null; } /** * Check if have disk cache * * @return true if have disk cache */ public boolean hasDiskCache() { return mDiskCache != null; } /** * Get value from memory cache * * @param key the key to get value * @return the value you get, null for miss or no memory cache */ public V getFromMemory(String key) { if (mMemoryCache != null) { return mMemoryCache.get(key); } else { return null; } } /** * Get value from memory cache * * @param key the key to get value * @return the value you get, null for miss or no memory cache or get error */ public V getFromDisk(String key) { if (mDiskCache != null) { // Wait for pause waitUntilUnpaused(); String diskKey = hashKeyForDisk(key); return mDiskCache.get(diskKey); } else { return null; } } /** * Get value from memory cache and disk cache. If miss in memory cache and * get in disk cache, it will put value from disk cache to memory cache. * * @param key the key to get value * @return the value you get */ public V get(String key) { V value; if (mMemoryCache != null) { value = mMemoryCache.get(key); if (value != null) { return value; } } if (mDiskCache != null) { // Wait for pause waitUntilUnpaused(); String diskKey = hashKeyForDisk(key); value = mDiskCache.get(diskKey); // Put value to memory cach if (value != null && mMemoryCache != null) { mMemoryCache.put(key, value); } return value; } else { return null; } } /** * Put value to memory cache * * @param key the key * @param value the value * @return false if no memory cache */ public boolean putToMemory(String key, V value) { if (mMemoryCache != null) { mMemoryCache.put(key, value); return true; } else { return false; } } /** * Put value to disk cache * * @param key the key * @param value the value * @return false if no disk cache or get error */ public boolean putToDisk(String key, V value) { if (mDiskCache != null) { // Wait for pause waitUntilUnpaused(); String diskKey = hashKeyForDisk(key); return mDiskCache.put(diskKey, value); } else { return false; } } /** * Put value to memory cache and disk cache * * @param key the key * @param value the value */ public void put(String key, V value) { if (mMemoryCache != null) { mMemoryCache.put(key, value); } if (mDiskCache != null) { // Wait for pause waitUntilUnpaused(); String diskKey = hashKeyForDisk(key); mDiskCache.put(diskKey, value); } } /** * Used to temporarily pause the disk cache while the user is scrolling to * improve scrolling. * * @param pause True to temporarily pause the disk cache, false otherwise. */ public void setPauseDiskCache(final boolean pause) { synchronized (mPauseLock) { if (mPauseDiskAccess != pause) { mPauseDiskAccess = pause; if (!pause) { mPauseLock.notify(); } } } } private void waitUntilUnpaused() { synchronized (mPauseLock) { if (Looper.myLooper() != Looper.getMainLooper()) { while (mPauseDiskAccess) { try { mPauseLock.wait(); } catch (InterruptedException e) { // ignored, we'll start waiting again } } } } } /** * Evicts all of the items from the memory cache and lets the system know * now would be a good time to garbage collect */ public void evictAll() { if (mMemoryCache != null) { mMemoryCache.evictAll(); } System.gc(); } /** * A hashing method that changes a string (like a URL) into a hash suitable * for using as a disk filename. * * @param key The key used to store the file */ private static String hashKeyForDisk(final String key) { String cacheKey; try { final MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(key.getBytes()); cacheKey = bytesToHexString(digest.digest()); } catch (final NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } /** * http://stackoverflow.com/questions/332079 * * @param bytes The bytes to convert. * @return A {@link String} converted from the bytes of a hashable key used * to store a filename on the disk, to hex digits. */ private static String bytesToHexString(final byte[] bytes) { final StringBuilder builder = new StringBuilder(); for (final byte b : bytes) { final String hex = Integer.toHexString(0xFF & b); if (hex.length() == 1) { builder.append('0'); } builder.append(hex); } return builder.toString(); } public class MemoryCahce extends LruCache { public AnyCache mParent; public MemoryCahce(int maxSize, AnyCache parent) { super(maxSize); mParent = parent; } @Override protected int sizeOf(String key, E value) { return mParent.sizeOf(key, value); } } public static class DiskCache { private static final int IO_BUFFER_SIZE = 8 * 1024; public DiskLruCache mDiskLruCache; public AnyCache mParent; public DiskCache(File cacheDir, int size, AnyCache parent) throws IOException { mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, size); mParent = parent; } public E get(String key) { DiskLruCache.Snapshot snapshot = null; try { snapshot = mDiskLruCache.get(key); if ( snapshot == null ) { // Miss return null; } final InputStream in = snapshot.getInputStream(0); if (in != null) { final BufferedInputStream buffIn = new BufferedInputStream(in, IO_BUFFER_SIZE); return mParent.read(buffIn); } else { // Can't get InputStream return null; } } catch (IOException e) { // e.printStackTrace(); return null; } finally { Util.closeQuietly(snapshot); } } public boolean put(String key, E value) { DiskLruCache.Editor editor = null; OutputStream os = null; try { editor = mDiskLruCache.edit(key); if (editor == null) { // The editor is in progress return false; } os = editor.newOutputStream(0); if (os != null) { final BufferedOutputStream buffOut = new BufferedOutputStream(os, IO_BUFFER_SIZE); boolean result = mParent.write(buffOut, value); mDiskLruCache.flush(); editor.commit(); return result; } else { // Can't get OutputStream editor.abort(); return false; } } catch (IOException e) { Util.closeQuietly(os); try { if (editor != null) { editor.abort(); } } catch (IOException ignored) { } return false; } } } }