emoji table and cache fix, fixed crash

This commit is contained in:
nuclearfog 2023-08-19 18:33:32 +02:00
parent 2988726d50
commit 182e69f35f
No known key found for this signature in database
GPG Key ID: 03488A185C476379
7 changed files with 97 additions and 58 deletions

View File

@ -2,7 +2,7 @@ package org.nuclearfog.twidda;
import android.app.Application; import android.app.Application;
import org.nuclearfog.twidda.backend.image.ImageCache; import org.nuclearfog.twidda.backend.image.EmojiCache;
import org.nuclearfog.twidda.backend.image.PicassoBuilder; import org.nuclearfog.twidda.backend.image.PicassoBuilder;
import org.nuclearfog.twidda.backend.utils.BlurHashDecoder; import org.nuclearfog.twidda.backend.utils.BlurHashDecoder;
import org.nuclearfog.twidda.config.GlobalSettings; import org.nuclearfog.twidda.config.GlobalSettings;
@ -38,7 +38,7 @@ public class ClientApplication extends Application {
@Override @Override
public void onLowMemory() { public void onLowMemory() {
ImageCache.clear(); EmojiCache.clear();
PicassoBuilder.clear(); PicassoBuilder.clear();
BlurHashDecoder.clearCache(); BlurHashDecoder.clearCache();
super.onLowMemory(); super.onLowMemory();

View File

@ -49,10 +49,14 @@ public class ImageDownloader extends AsyncExecutor<ImageDownloader.ImageLoaderPa
} }
// create file // create file
String ext = '.' + mimeType.substring(mimeType.indexOf('/') + 1); String ext = '.' + mimeType.substring(mimeType.indexOf('/') + 1);
File imageFile = new File(request.cache, StringUtils.getRandomString() + ext); // use deterministic filename depending on the url
imageFile.createNewFile(); File imageFile = new File(request.cacheFolder, StringUtils.getMD5signature(request.uri.toString()) + ext);
// if file exists with this signature, use this file
if (imageFile.exists()) {
return new ImageLoaderResult(Uri.fromFile(imageFile), null);
}
// copy image to cache folder // copy image to cache folder
imageFile.createNewFile();
FileOutputStream output = new FileOutputStream(imageFile); FileOutputStream output = new FileOutputStream(imageFile);
int length; int length;
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
@ -60,7 +64,6 @@ public class ImageDownloader extends AsyncExecutor<ImageDownloader.ImageLoaderPa
output.write(buffer, 0, length); output.write(buffer, 0, length);
input.close(); input.close();
output.close(); output.close();
// create Uri from cached image // create Uri from cached image
return new ImageLoaderResult(Uri.fromFile(imageFile), null); return new ImageLoaderResult(Uri.fromFile(imageFile), null);
} catch (ConnectionException exception) { } catch (ConnectionException exception) {
@ -75,11 +78,11 @@ public class ImageDownloader extends AsyncExecutor<ImageDownloader.ImageLoaderPa
*/ */
public static class ImageLoaderParam { public static class ImageLoaderParam {
final File cache;
final Uri uri; final Uri uri;
final File cacheFolder;
public ImageLoaderParam(Uri uri, File cache) { public ImageLoaderParam(Uri uri, File cacheFolder) {
this.cache = cache; this.cacheFolder = cacheFolder;
this.uri = uri; this.uri = uri;
} }
} }

View File

@ -11,7 +11,7 @@ import androidx.annotation.Nullable;
import org.nuclearfog.twidda.backend.api.Connection; import org.nuclearfog.twidda.backend.api.Connection;
import org.nuclearfog.twidda.backend.api.ConnectionManager; import org.nuclearfog.twidda.backend.api.ConnectionManager;
import org.nuclearfog.twidda.backend.helper.MediaStatus; import org.nuclearfog.twidda.backend.helper.MediaStatus;
import org.nuclearfog.twidda.backend.image.ImageCache; import org.nuclearfog.twidda.backend.image.EmojiCache;
import org.nuclearfog.twidda.model.Emoji; import org.nuclearfog.twidda.model.Emoji;
import java.io.InputStream; import java.io.InputStream;
@ -26,14 +26,14 @@ import java.util.TreeMap;
public class TextEmojiLoader extends AsyncExecutor<TextEmojiLoader.EmojiParam, TextEmojiLoader.EmojiResult> { public class TextEmojiLoader extends AsyncExecutor<TextEmojiLoader.EmojiParam, TextEmojiLoader.EmojiResult> {
private Connection connection; private Connection connection;
private ImageCache cache; private EmojiCache cache;
/** /**
* *
*/ */
public TextEmojiLoader(Context context) { public TextEmojiLoader(Context context) {
connection = ConnectionManager.getDefaultConnection(context); connection = ConnectionManager.getDefaultConnection(context);
cache = ImageCache.get(context); cache = EmojiCache.get(context);
} }
@ -42,12 +42,12 @@ public class TextEmojiLoader extends AsyncExecutor<TextEmojiLoader.EmojiParam, T
try { try {
Map<String, Bitmap> result = new TreeMap<>(); Map<String, Bitmap> result = new TreeMap<>();
for (Emoji emoji : param.emojis) { for (Emoji emoji : param.emojis) {
Bitmap icon = cache.getImage(emoji.getCode()); Bitmap icon = cache.getImage(emoji.getUrl());
if (icon == null) { if (icon == null) {
MediaStatus media = connection.downloadImage(emoji.getUrl()); MediaStatus media = connection.downloadImage(emoji.getUrl());
InputStream input = media.getStream(); InputStream input = media.getStream();
icon = BitmapFactory.decodeStream(input); icon = BitmapFactory.decodeStream(input);
cache.putImage(emoji.getCode(), icon); cache.putImage(emoji.getUrl(), icon);
} }
if (icon.getHeight() > 0 && icon.getWidth() > 0) { if (icon.getHeight() > 0 && icon.getWidth() > 0) {
icon = Bitmap.createScaledBitmap(icon, icon.getWidth() * param.size / icon.getHeight(), param.size, false); icon = Bitmap.createScaledBitmap(icon, icon.getWidth() * param.size / icon.getHeight(), param.size, false);

View File

@ -8,6 +8,7 @@ import android.util.LruCache;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.nuclearfog.twidda.BuildConfig; import org.nuclearfog.twidda.BuildConfig;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -24,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap;
* *
* @author nuclearfog * @author nuclearfog
*/ */
public class ImageCache { public class EmojiCache {
/** /**
* size of the lru cache (max entry count) * size of the lru cache (max entry count)
@ -41,7 +42,7 @@ public class ImageCache {
*/ */
private static final String FOLDER = "images"; private static final String FOLDER = "images";
private static ImageCache instance; private static EmojiCache instance;
private final LruCache<String, Bitmap> cache; private final LruCache<String, Bitmap> cache;
private Map<String, File> files; private Map<String, File> files;
@ -50,7 +51,7 @@ public class ImageCache {
/** /**
* @param context context used to determine cache folder path * @param context context used to determine cache folder path
*/ */
private ImageCache(Context context) { private EmojiCache(Context context) {
files = new ConcurrentHashMap<>(); files = new ConcurrentHashMap<>();
cache = new LruCache<>(SIZE); cache = new LruCache<>(SIZE);
try { try {
@ -83,9 +84,9 @@ public class ImageCache {
* *
* @return singleton instance of this class * @return singleton instance of this class
*/ */
public static ImageCache get(Context context) { public static EmojiCache get(Context context) {
if (instance == null) if (instance == null)
instance = new ImageCache(context); instance = new EmojiCache(context);
return instance; return instance;
} }
@ -103,20 +104,21 @@ public class ImageCache {
/** /**
* put image to cache and save as file if not exists * put image to cache and save as file if not exists
* *
* @param key key of the image (tag) * @param url url of the image
* @param image image bitmap * @param image image bitmap
*/ */
public void putImage(String key, Bitmap image) { public void putImage(String url, Bitmap image) {
synchronized (instance.cache) { synchronized (instance.cache) {
cache.put(key, image); String hash = StringUtils.getMD5signature(url);
if (!files.containsKey(key)) { cache.put(hash, image);
if (!files.containsKey(hash)) {
try { try {
File file = new File(imageFolder, key); File file = new File(imageFolder, hash);
if ((file.exists() && file.canWrite()) || file.createNewFile()) { if ((file.exists() && file.canWrite()) || file.createNewFile()) {
FileOutputStream output = new FileOutputStream(file); FileOutputStream output = new FileOutputStream(file);
image.compress(Bitmap.CompressFormat.PNG, 1, output); image.compress(Bitmap.CompressFormat.PNG, 1, output);
output.close(); output.close();
files.put(key, file); files.put(hash, file);
} }
} catch (IOException | SecurityException exception) { } catch (IOException | SecurityException exception) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -130,20 +132,21 @@ public class ImageCache {
/** /**
* get image from cache or file * get image from cache or file
* *
* @param key key of the image (tag) * @param url url of the image
* @return image bitmap or null if not found * @return image bitmap or null if not found
*/ */
@Nullable @Nullable
public Bitmap getImage(String key) { public Bitmap getImage(String url) {
synchronized (instance.cache) { synchronized (instance.cache) {
Bitmap result = cache.get(key); String hash = StringUtils.getMD5signature(url);
Bitmap result = cache.get(hash);
if (result == null) { if (result == null) {
try { try {
File file = files.get(key); File file = files.get(hash);
if (file != null && file.canRead()) { if (file != null && file.canRead()) {
result = BitmapFactory.decodeFile(file.getAbsolutePath()); result = BitmapFactory.decodeFile(file.getAbsolutePath());
if (result != null) { if (result != null) {
cache.put(key, result); cache.put(hash, result);
} }
} }
} catch (SecurityException exception) { } catch (SecurityException exception) {
@ -161,28 +164,30 @@ public class ImageCache {
* and reduce cache size by deleting old image files * and reduce cache size by deleting old image files
*/ */
public void trimCache() { public void trimCache() {
File[] files = imageFolder.listFiles(); synchronized (instance.cache) {
if (files != null) { File[] files = imageFolder.listFiles();
long size = 0L; if (files != null) {
for (File file : files) { long size = 0L;
size += file.length();
}
if (size > CACHE_SIZE) {
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File file1, File file2) {
if (file1.lastModified() > file2.lastModified())
return 1;
else if (file1.lastModified() < file2.lastModified())
return -1;
return 0;
}
});
for (File file : files) { for (File file : files) {
size -= file.length(); size += file.length();
file.delete(); }
if (size < CACHE_SIZE) { if (size > CACHE_SIZE) {
break; Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File file1, File file2) {
if (file1.lastModified() > file2.lastModified())
return 1;
else if (file1.lastModified() < file2.lastModified())
return -1;
return 0;
}
});
for (File file : files) {
size -= file.length();
file.delete();
if (size < CACHE_SIZE) {
break;
}
} }
} }
} }

View File

@ -10,7 +10,10 @@ import org.nuclearfog.twidda.BuildConfig;
import org.nuclearfog.twidda.R; import org.nuclearfog.twidda.R;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -187,6 +190,27 @@ public class StringUtils {
return new String(Base64.encode(randomBytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)); return new String(Base64.encode(randomBytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP));
} }
/**
* generate MD5 hash from string
*
* @param text text to hash
* @return hash string
*/
public static String getMD5signature(String text) {
try {
MessageDigest m = MessageDigest.getInstance("MD5");
m.reset();
m.update(text.getBytes());
byte[] digest = m.digest();
BigInteger bigInt = new BigInteger(1, digest);
return bigInt.toString(16);
} catch (NoSuchAlgorithmException exception) {
if (text.length() > 200)
return text.substring(0, 200);
return text;
}
}
/** /**
* convert to percentage string (RFC 3986) * convert to percentage string (RFC 3986)
* *

View File

@ -988,10 +988,12 @@ public class GlobalSettings {
* notify listener when settings changes and clear old instances * notify listener when settings changes and clear old instances
*/ */
private void notifySettingsChange() { private void notifySettingsChange() {
for (SettingsChangeObserver observer : settingsChangeObservers) { synchronized (this) {
observer.onSettingsChange(); for (SettingsChangeObserver observer : settingsChangeObservers) {
observer.onSettingsChange();
}
settingsChangeObservers.clear();
} }
settingsChangeObservers.clear();
} }
/** /**

View File

@ -20,7 +20,7 @@ public class DatabaseAdapter {
/** /**
* database version * database version
*/ */
private static final int DB_VERSION = 19; private static final int DB_VERSION = 20;
/** /**
* database file name * database file name
@ -137,9 +137,9 @@ public class DatabaseAdapter {
*/ */
private static final String TABLE_EMOJI = "CREATE TABLE IF NOT EXISTS " private static final String TABLE_EMOJI = "CREATE TABLE IF NOT EXISTS "
+ EmojiTable.NAME + "(" + EmojiTable.NAME + "("
+ EmojiTable.CODE + " TEXT PRIMARY KEY," + EmojiTable.URL + " TEXT PRIMARY KEY,"
+ EmojiTable.CATEGORY + " TEXT," + EmojiTable.CATEGORY + " TEXT,"
+ EmojiTable.URL + " TEXT);"; + EmojiTable.CODE + " TEXT);";
/** /**
* SQL query to create a poll table * SQL query to create a poll table
@ -412,8 +412,13 @@ public class DatabaseAdapter {
db.execSQL(UPDATE_MEDIA_ADD_DESCRIPTION); db.execSQL(UPDATE_MEDIA_ADD_DESCRIPTION);
db.setVersion(18); db.setVersion(18);
} }
if (db.getVersion() < DB_VERSION) { if (db.getVersion() < 19) {
db.execSQL(UPDATE_MEDIA_ADD_BLUR_HASH); db.execSQL(UPDATE_MEDIA_ADD_BLUR_HASH);
db.setVersion(19);
}
if (db.getVersion() < DB_VERSION) {
db.delete(EmojiTable.NAME, null, null);
db.execSQL(TABLE_EMOJI);
db.setVersion(DB_VERSION); db.setVersion(DB_VERSION);
} }
} }