|
import android.content.Context; |
|
import android.content.pm.PackageInfo; |
|
import android.content.pm.PackageManager; |
|
import android.os.Build; |
|
import android.os.Parcel; |
|
import android.os.Parcelable; |
|
import android.text.TextUtils; |
|
|
|
import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain; |
|
import com.facebook.crypto.Crypto; |
|
import com.facebook.crypto.Entity; |
|
import com.facebook.crypto.util.SystemNativeCryptoLibrary; |
|
import com.jakewharton.disklrucache.DiskLruCache; |
|
|
|
import java.io.ByteArrayOutputStream; |
|
import java.io.File; |
|
import java.io.IOException; |
|
import java.io.InputStream; |
|
import java.io.OutputStream; |
|
import java.util.ArrayList; |
|
import java.util.List; |
|
import java.util.concurrent.Executor; |
|
import java.util.concurrent.Executors; |
|
import java.util.regex.Matcher; |
|
import java.util.regex.Pattern; |
|
|
|
public abstract class AbstractParcelDiskCache implements DiskCache<Parcelable> { |
|
|
|
private static final String LIST = "list"; |
|
private static final String PARCELABLE = "parcelable"; |
|
private static final String VALIDATE_KEY_REGEX = "[a-z0-9_-]{1,5}"; |
|
private static final int MAX_KEY_SYMBOLS = 62; |
|
private DiskLruCache cache; |
|
private Executor storeExecutor; |
|
private boolean saveInUI = true; |
|
private static Crypto crypto; |
|
|
|
/** |
|
* @param context Context |
|
* @param name The name of the sub-directory under the cache/store directory. |
|
* @param maxSize in bytes |
|
* @throws IOException |
|
*/ |
|
AbstractParcelDiskCache(Context context, String name, long maxSize) throws IOException { |
|
storeExecutor = Executors.newSingleThreadExecutor(); |
|
File dir = new File(getCacheDirectory(context), name); |
|
int version = getVersionCode(context) + Build.VERSION.SDK_INT; |
|
this.cache = DiskLruCache.open(dir, version, 1, maxSize); |
|
if (crypto == null) { |
|
crypto = new Crypto( |
|
new SharedPrefsBackedKeyChain(context.getApplicationContext()), |
|
new SystemNativeCryptoLibrary()); |
|
} |
|
} |
|
|
|
abstract File getCacheDirectory(Context context); |
|
|
|
// public static AbstractParcelDiskCache open(Context context, String name, long maxSize) throws IOException { |
|
// return new AbstractParcelDiskCache(context, name, maxSize); |
|
// } |
|
|
|
public void set(String key, Parcelable value) { |
|
key = validateKey(key); |
|
Parcel parcel = Parcel.obtain(); |
|
parcel.writeString(PARCELABLE); |
|
parcel.writeParcelable(value, 0); |
|
if (saveInUI) { |
|
saveValue(cache, parcel, key); |
|
} else { |
|
storeExecutor.execute(new StoreParcelableValueTask(cache, parcel, key)); |
|
} |
|
} |
|
|
|
public void set(String key, List<Parcelable> values) { |
|
key = validateKey(key); |
|
Parcel parcel = Parcel.obtain(); |
|
parcel.writeString(LIST); |
|
parcel.writeList(values); |
|
if (saveInUI) { |
|
saveValue(cache, parcel, key); |
|
} else { |
|
storeExecutor.execute(new StoreParcelableValueTask(cache, parcel, key)); |
|
} |
|
} |
|
|
|
@Override |
|
public <T> T get(String key, Class<T> clazz) { |
|
key = validateKey(key); |
|
Parcel parcel = getParcel(key); |
|
if (parcel != null) { |
|
try { |
|
String type = parcel.readString(); |
|
if (type.equals(LIST)) { |
|
throw new IllegalAccessError("get list data with getList method"); |
|
} |
|
if (!type.equals(PARCELABLE)) { |
|
throw new IllegalAccessError("Parcel doesn't contain parcelable data"); |
|
} |
|
return parcel.readParcelable(clazz.getClassLoader()); |
|
} catch (Exception e) { |
|
e.printStackTrace(); |
|
} finally { |
|
parcel.recycle(); |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
private Parcel getParcel(String key) { |
|
key = validateKey(key); |
|
byte[] value; |
|
DiskLruCache.Snapshot snapshot = null; |
|
try { |
|
snapshot = cache.get(key); |
|
if (snapshot == null) { |
|
return null; |
|
} |
|
value = getBytesFromStream(snapshot.getInputStream(0)); |
|
Parcel parcel = Parcel.obtain(); |
|
parcel.unmarshall(value, 0, value.length); |
|
parcel.setDataPosition(0); |
|
return parcel; |
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
} finally { |
|
if (snapshot != null) { |
|
snapshot.close(); |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
private String validateKey(String key) { |
|
Matcher keyMatcher = getPattern(VALIDATE_KEY_REGEX).matcher(key); |
|
StringBuilder newKey = new StringBuilder(); |
|
while (keyMatcher.find()) { |
|
String group = keyMatcher.group(); |
|
if (newKey.length() + group.length() > MAX_KEY_SYMBOLS) { |
|
break; |
|
} |
|
|
|
newKey.append(group); |
|
} |
|
return newKey.toString().toLowerCase(); |
|
} |
|
|
|
public Pattern getPattern(String bodyRegex) { |
|
int flags = Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE; |
|
return Pattern.compile(bodyRegex, flags); |
|
} |
|
|
|
public <T> List<T> getList(String key, Class itemClass) { |
|
key = validateKey(key); |
|
ArrayList<T> res = new ArrayList<>(); |
|
Parcel parcel = getParcel(key); |
|
if (parcel != null) { |
|
try { |
|
String type = parcel.readString(); |
|
if (type.equals(PARCELABLE)) { |
|
throw new IllegalAccessError("Get not a list data with get method"); |
|
} |
|
if (!type.equals(LIST)) { |
|
throw new IllegalAccessError("Parcel doesn't contain list data"); |
|
} |
|
parcel.readList(res, itemClass != null ? itemClass.getClassLoader() : ArrayList.class.getClassLoader()); |
|
} catch (Exception e) { |
|
e.printStackTrace(); |
|
} finally { |
|
parcel.recycle(); |
|
} |
|
} |
|
return res; |
|
} |
|
|
|
@SuppressWarnings("unused") |
|
public <T> List<T> getList(String key) { |
|
return getList(key, null); |
|
} |
|
|
|
public boolean remove(String key) { |
|
key = validateKey(key); |
|
try { |
|
return cache.remove(key.toLowerCase()); |
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
} |
|
return false; |
|
} |
|
|
|
@SuppressWarnings("unchecked") |
|
public <T> List<T> getAll(Class<T> clazz) { |
|
return getAll(null, clazz); |
|
} |
|
|
|
public <T> List<T> getAll(String prefix, Class<T> clazz) { |
|
List<T> list = new ArrayList<>(1); |
|
File dir = cache.getDirectory(); |
|
File[] files = dir.listFiles(); |
|
if (files != null) { |
|
list = new ArrayList<>(files.length); |
|
for (File file : files) { |
|
String fileName = file.getName(); |
|
if ((!TextUtils.isEmpty(prefix) && fileName.startsWith(prefix) && fileName.indexOf(".") > 0) |
|
|| (TextUtils.isEmpty(prefix) && fileName.indexOf(".") > 0)) { |
|
String key = fileName.substring(0, fileName.indexOf(".")); |
|
T value = get(key, clazz); |
|
list.add(value); |
|
} |
|
} |
|
} |
|
return list; |
|
} |
|
|
|
public void clear() { |
|
try { |
|
cache.delete(); |
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
} |
|
} |
|
|
|
public boolean exists(String key) { |
|
key = validateKey(key); |
|
DiskLruCache.Snapshot snapshot = null; |
|
try { |
|
snapshot = cache.get(key.toLowerCase()); |
|
return snapshot != null && snapshot.getLength(0) > 0; |
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
} finally { |
|
if (snapshot != null) { |
|
snapshot.close(); |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
@Override |
|
public void close() { |
|
try { |
|
cache.close(); |
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
} |
|
} |
|
|
|
@SuppressWarnings("unused") |
|
public void shouldSaveInUI() { |
|
this.saveInUI = true; |
|
} |
|
|
|
private static class StoreParcelableValueTask implements Runnable { |
|
|
|
private final DiskLruCache cache; |
|
private final Parcel value; |
|
private final String key; |
|
|
|
public StoreParcelableValueTask(DiskLruCache cache, Parcel value, String key) { |
|
this.value = value; |
|
this.key = key; |
|
this.cache = cache; |
|
} |
|
|
|
@Override |
|
public void run() { |
|
saveValue(cache, value, key); |
|
} |
|
} |
|
|
|
private static void saveValue(DiskLruCache cache, Parcel value, String key) { |
|
if (cache == null) return; |
|
key = key.toLowerCase(); |
|
try { |
|
final String skey = key.intern(); |
|
synchronized (skey) { |
|
DiskLruCache.Editor editor = cache.edit(key); |
|
OutputStream outputStream = editor.newOutputStream(0); |
|
writeBytesToStream(outputStream, value.marshall()); |
|
editor.commit(); |
|
} |
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
} finally { |
|
value.recycle(); |
|
} |
|
} |
|
|
|
public static byte[] getBytesFromStream(InputStream is) throws IOException { |
|
InputStream ins = is; |
|
if (crypto.isAvailable()) { |
|
try { |
|
ins = crypto.getCipherInputStream(ins, new Entity("data.dat")); |
|
} catch (Exception e) { |
|
e.printStackTrace(); |
|
} |
|
} |
|
|
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
|
try { |
|
byte[] data = new byte[1024]; |
|
int count; |
|
while ((count = ins.read(data, 0, data.length)) != -1) { |
|
buffer.write(data, 0, count); |
|
} |
|
buffer.flush(); |
|
return buffer.toByteArray(); |
|
} finally { |
|
ins.close(); |
|
buffer.close(); |
|
|
|
} |
|
} |
|
|
|
public static void writeBytesToStream(OutputStream outputStream, byte[] bytes) throws IOException { |
|
OutputStream os = outputStream; |
|
if (crypto.isAvailable()) { |
|
try { |
|
os = crypto.getCipherOutputStream(outputStream, new Entity("data.dat")); |
|
} catch (Exception e) { |
|
e.printStackTrace(); |
|
} |
|
} |
|
os.write(bytes); |
|
os.flush(); |
|
os.close(); |
|
} |
|
|
|
public static int getVersionCode(Context context) { |
|
int result = 0; |
|
try { |
|
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); |
|
result = pInfo.versionCode; |
|
} catch (PackageManager.NameNotFoundException e) { |
|
e.printStackTrace(); |
|
} |
|
return result; |
|
} |
|
} |