Skip to content

Commit

Permalink
Media scanner and image cache enhancements.
Browse files Browse the repository at this point in the history
Try to load info from MediaStore. If info and icon are available,
do not scan.

Image cache enhancements.

Expose bitmap cache to Android Auto via ContentProvider. AA should
receive icon URLs in the cache, instead of heavyweight Bitmap objects.
Fixes #45.
  • Loading branch information
AndreyPavlenko committed Oct 3, 2021
1 parent a3ded67 commit 4fddb43
Show file tree
Hide file tree
Showing 28 changed files with 715 additions and 271 deletions.
10 changes: 5 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ext {
def abi = project.properties['ABI']
VERSION_CODE = 134
VERSION_NAME = "1.8.2"
VERSION_CODE = 154
VERSION_NAME = "1.8.3"
SDK_MIN_VERSION = 23
SDK_TARGET_VERSION = 30
SDK_COMPILE_VERSION = 30
Expand All @@ -10,11 +10,11 @@ ext {
localProps = gradle.ext.localProps

ANDROID_MATERIAL_VERSION = '1.4.0'
ANDROID_PLAY_CORE_VERSION = '1.10.1'
ANDROID_PLAY_CORE_VERSION = '1.10.2'
ANDROIDX_CORE_VERSION = '1.3.2'
ANDROIDX_MEDIA_VERSION = '1.4.1'
ANDROIDX_MEDIA_VERSION = '1.4.2'
ANDROIDX_APPCOMPAT_VERSION = '1.3.1'
ANDROIDX_CONSTRAINTLAYOUT_VERSION = '2.1.0'
ANDROIDX_CONSTRAINTLAYOUT_VERSION = '2.1.1'
ANDROIDX_SWIPEREFRESHLAYOUT_VERSION = '1.1.0'
}

Expand Down
2 changes: 1 addition & 1 deletion depends/utils
Submodule utils updated from a4f1a4 to e0c18f
1 change: 1 addition & 0 deletions fermata/src/auto/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<uses-feature
Expand Down
6 changes: 6 additions & 0 deletions fermata/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>

<provider
android:name="me.aap.fermata.provider.FermataContentProvider"
android:authorities="${applicationId}"
android:exported="true"
android:grantUriPermissions="true" />
</application>

</manifest>
19 changes: 19 additions & 0 deletions fermata/src/main/java/me/aap/fermata/FermataApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.io.File;

import me.aap.fermata.addon.AddonManager;
import me.aap.fermata.media.engine.BitmapCache;
import me.aap.fermata.vfs.FermataVfsManager;
import me.aap.utils.app.App;
import me.aap.utils.app.NetSplitCompatApp;
import me.aap.utils.pref.PreferenceStore;
Expand All @@ -16,13 +18,30 @@
* @author Andrey Pavlenko
*/
public class FermataApplication extends NetSplitCompatApp {
private FermataVfsManager vfsManager;
private BitmapCache bitmapCache;
private volatile SharedPreferenceStore preferenceStore;
private volatile AddonManager addonManager;

public static FermataApplication get() {
return App.get();
}

@Override
public void onCreate() {
super.onCreate();
vfsManager = new FermataVfsManager();
bitmapCache = new BitmapCache();
}

public FermataVfsManager getVfsManager() {
return vfsManager;
}

public BitmapCache getBitmapCache() {
return bitmapCache;
}

public PreferenceStore getPreferenceStore() {
SharedPreferenceStore ps = preferenceStore;

Expand Down
155 changes: 100 additions & 55 deletions fermata/src/main/java/me/aap/fermata/media/engine/BitmapCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static me.aap.utils.async.Completed.failed;
import static me.aap.utils.io.FileUtils.getFileExtension;
import static me.aap.utils.net.http.HttpFileDownloader.MAX_AGE;
import static me.aap.utils.security.SecurityUtils.SHA1_DIGEST_LEN;
import static me.aap.utils.security.SecurityUtils.sha1;
import static me.aap.utils.text.TextUtils.appendHexString;
import static me.aap.utils.ui.UiUtils.resizedBitmap;
Expand All @@ -33,6 +34,7 @@
import androidx.core.content.res.ResourcesCompat;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -47,7 +49,7 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import me.aap.fermata.media.lib.MediaLib;
import me.aap.fermata.FermataApplication;
import me.aap.fermata.vfs.FermataVfsManager;
import me.aap.utils.app.App;
import me.aap.utils.async.FutureSupplier;
Expand All @@ -57,6 +59,7 @@
import me.aap.utils.io.MemOutputStream;
import me.aap.utils.log.Log;
import me.aap.utils.net.http.HttpFileDownloader;
import me.aap.utils.net.http.HttpFileDownloader.Status;
import me.aap.utils.pref.SharedPreferenceStore;
import me.aap.utils.resource.Rid;
import me.aap.utils.text.SharedTextBuilder;
Expand All @@ -67,7 +70,6 @@
* @author Andrey Pavlenko
*/
public class BitmapCache {
private final MediaLib lib;
private final File iconsCache;
private final File imageCache;
private final String iconsCacheUri;
Expand All @@ -78,30 +80,34 @@ public class BitmapCache {
private final PromiseQueue queue = new PromiseQueue(App.get().getExecutor());
private final Map<String, String> invalidBitmapUris = new ConcurrentHashMap<>();

public BitmapCache(MediaLib lib) {
this.lib = lib;
public BitmapCache() {
File cache = App.get().getExternalCacheDir();
if (cache == null) cache = App.get().getCacheDir();
iconsCache = new File(cache, "icons").getAbsoluteFile();
imageCache = new File(cache, "images").getAbsoluteFile();
iconsCacheUri = Uri.fromFile(iconsCache).toString() + '/';
imageCacheUri = Uri.fromFile(imageCache).toString() + '/';
prefs = lib.getContext().getSharedPreferences("image-cache", MODE_PRIVATE);
prefs = getContext().getSharedPreferences("image-cache", MODE_PRIVATE);
}

@Nullable
public Bitmap getCachedBitmap(String uri) {
synchronized (cache) {
clearRefs();
Ref r = cache.get(uri);
public boolean isResourceImageAvailable(Uri uri) {
try (AssetFileDescriptor afd = openResource(getContext(), uri, 0)) {
return afd.getLength() > 0;
} catch (Exception ex) {
return false;
}
}

if (r != null) {
Bitmap bm = r.get();
if (bm != null) return bm;
else cache.remove(uri);
}
public ParcelFileDescriptor openResourceImage(Uri uri) throws FileNotFoundException {
Context ctx = getContext();
int size = getIconSize(ctx);
String iconUri = toIconUri(uri.toString(), size);
File iconFile = new File(iconsCache, iconUri.substring(iconsCacheUri.length()));

return null;
if (iconFile.isFile()) {
return ctx.getContentResolver().openFileDescriptor(Uri.parse(iconUri), "r");
} else {
return openResource(ctx, uri, 0).getParcelFileDescriptor();
}
}

Expand Down Expand Up @@ -130,14 +136,33 @@ public FutureSupplier<Bitmap> getBitmap(Context ctx, String uri, boolean cache,
return queue.enqueue(() -> loadBitmap(ctx, uri, iconUri, cache, size));
}

@Nullable
private Bitmap getCachedBitmap(String uri) {
synchronized (cache) {
clearRefs();
Ref r = cache.get(uri);

if (r != null) {
Bitmap bm = r.get();
if (bm != null) return bm;
else cache.remove(uri);
}

return null;
}
}

private Bitmap loadBitmap(Context ctx, String uri, String iconUri, boolean cache, int size) {
Bitmap bm;

if (iconUri != null) {
bm = getCachedBitmap(iconUri);
if (bm != null) return bm;
File iconFile = new File(iconsCache, iconUri.substring(iconsCacheUri.length()));
if (iconFile.isFile()) bm = loadBitmap(ctx, iconUri, cache ? uri : null, 0);
if (bm != null) return bm;
bm = loadBitmap(ctx, uri, cache ? iconUri : null, size);
if (cache && (bm != null)) saveIcon(bm, iconUri);
if (cache && (bm != null)) saveIcon(bm, iconFile);
} else {
bm = getCachedBitmap(uri);
if (bm != null) return bm;
Expand All @@ -156,7 +181,7 @@ private Bitmap loadBitmap(Context ctx, String uri, String cacheUri, int size) {

switch (scheme) {
case "file":
try (ParcelFileDescriptor fd = ctx.getContentResolver().openFileDescriptor(Uri.parse(uri), "r")) {
try (ParcelFileDescriptor fd = ctx.getContentResolver().openFileDescriptor(u, "r")) {
if (fd != null) bm = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor());
}
break;
Expand All @@ -168,10 +193,11 @@ private Bitmap loadBitmap(Context ctx, String uri, String cacheUri, int size) {
if (d != null) bm = UiUtils.drawBitmap(d, Color.TRANSPARENT, Color.WHITE);
break;
case "content":
bm = loadContentBitmap(ctx, uri);
bm = loadContentBitmap(ctx, u, size);
size = 0;
break;
default:
FermataVfsManager vfs = lib.getVfsManager();
FermataVfsManager vfs = getVfsManager();
if (vfs.isSupportedScheme(scheme))
bm = loadUriBitmap(vfs.getHttpRid(Rid.create(u)).toString());
}
Expand All @@ -186,6 +212,27 @@ private Bitmap loadBitmap(Context ctx, String uri, String cacheUri, int size) {
}

private FutureSupplier<Bitmap> loadHttpBitmap(String uri, String cacheUri, int size) {
return downloadImage(uri).then(s -> {
if (s == null) return completedNull();
try (InputStream is = s.getFileStream(true)) {
Bitmap bm = BitmapFactory.decodeStream(is);

if (bm == null) {
invalidBitmapUris.put(uri, uri);
return failed(new IOException("Failed to decode image"));
} else {
if (size != 0) bm = resizedBitmap(bm, size);
if (cacheUri != null) bm = cacheBitmap(cacheUri, bm);
return completed(bm);
}
} catch (Exception ex) {
invalidBitmapUris.put(uri, uri);
return failed(ex);
}
});
}

public FutureSupplier<Status> downloadImage(String uri) {
if (invalidBitmapUris.containsKey(uri)) {
Log.d("Invalid bitmap uri: ", uri);
return completedNull();
Expand All @@ -205,44 +252,33 @@ private FutureSupplier<Bitmap> loadHttpBitmap(String uri, String cacheUri, int s
ImagePrefs ip = new ImagePrefs(prefs, path);
HttpFileDownloader d = new HttpFileDownloader();
d.setReturnExistingOnFail(true);
return d.download(uri, dst, ip).then(s -> {
try (InputStream is = s.getFileStream(true)) {
Bitmap bm = BitmapFactory.decodeStream(is);

if (bm == null) {
return failed(new IOException("Failed to decode image"));
} else {
if (size != 0) bm = resizedBitmap(bm, size);
if (cacheUri != null) bm = cacheBitmap(cacheUri, bm);
return completed(bm);
}
} catch (Exception ex) {
return failed(ex);
}
}).onFailure(ex -> {
Log.d(ex, "Failed to load image: ", uri);
return d.download(uri, dst, ip).onFailure(ex -> {
Log.d(ex, "Failed to download image: ", uri);
invalidBitmapUris.put(uri, uri);
});
}

private Bitmap loadContentBitmap(Context ctx, String uri) throws IOException {
ContentResolver cr = ctx.getContentResolver();
Uri u = Uri.parse(uri);
int s = getIconSize(ctx);

private Bitmap loadContentBitmap(Context ctx, Uri u, int size) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return cr.loadThumbnail(u, new Size(s, s), null);
ContentResolver cr = ctx.getContentResolver();
if (size == 0) size = getIconSize(ctx);
return cr.loadThumbnail(u, new Size(size, size), null);
} else {
final Bundle opts = new Bundle();
opts.putParcelable(ContentResolver.EXTRA_SIZE, new Point(s, s));

try (AssetFileDescriptor afd = cr.openTypedAssetFileDescriptor(u, "image/*", opts, null)) {
if (afd == null) return null;
return BitmapFactory.decodeFileDescriptor(afd.getFileDescriptor());
try (AssetFileDescriptor afd = openResource(ctx, u, size)) {
return (afd == null) ? null : BitmapFactory.decodeFileDescriptor(afd.getFileDescriptor());
}
}
}

private AssetFileDescriptor openResource(Context ctx, Uri u, int size)
throws FileNotFoundException {
if (size == 0) size = getIconSize(ctx);
ContentResolver cr = ctx.getContentResolver();
Bundle opts = new Bundle();
opts.putParcelable(ContentResolver.EXTRA_SIZE, new Point(size, size));
return cr.openTypedAssetFileDescriptor(u, "image/*", opts, null);
}

private Bitmap loadUriBitmap(String uri) throws IOException {
try (InputStream in = new URL(uri).openStream()) {
return BitmapFactory.decodeStream(in);
Expand Down Expand Up @@ -290,12 +326,12 @@ String getImageUri(byte[] hash, TextBuilder tb) {
tb.setLength(0);
tb.append(imageCacheUri);
int len = tb.length();
appendHexString(tb.append("X/"), hash).append(".jpg");
appendHexString(tb.append("X/"), hash, 0, SHA1_DIGEST_LEN).append(".jpg");
tb.setCharAt(len, tb.charAt(len + 2));
return tb.toString().intern();
}

byte[] saveBitmap(Bitmap bm, TextBuilder tb) {
synchronized byte[] saveBitmap(Bitmap bm, TextBuilder tb) {
if (bm == null) return null;

try {
Expand Down Expand Up @@ -327,16 +363,15 @@ byte[] saveBitmap(Bitmap bm, TextBuilder tb) {
}
}

private void saveIcon(Bitmap bm, String uri) {
File f = new File(iconsCache, uri.substring(iconsCacheUri.length()));
private synchronized void saveIcon(Bitmap bm, File f) {
File p = f.getParentFile();
if (p != null) //noinspection ResultOfMethodCallIgnored
p.mkdirs();

try (OutputStream out = new FileOutputStream(f)) {
bm.compress(Bitmap.CompressFormat.JPEG, 100, out);
} catch (Exception ex) {
Log.e(ex, "Failed to save icon: ", uri);
Log.e(ex, "Failed to save icon: ", f);
}
}

Expand All @@ -345,8 +380,10 @@ private String toIconUri(String imageUri, int size) {
tb.append(iconsCacheUri).append(size).append("/X/");
int len = tb.length();

if ((imageUri.startsWith(imageCacheUri)) || (imageUri.startsWith(iconsCacheUri))) {
tb.append(imageUri.substring(imageUri.lastIndexOf('/') + 1));
if (imageUri.startsWith(imageCacheUri)) {
tb.append(imageUri.substring(imageCacheUri.length()));
} else if (imageUri.startsWith(iconsCacheUri)) {
tb.append(imageUri.substring(iconsCacheUri.length()));
} else {
appendHexString(tb, sha1(imageUri)).append(".jpg");
}
Expand Down Expand Up @@ -386,6 +423,14 @@ public void cleanUpPrefs() {
if (removed) edit.apply();
}

private FermataApplication getContext() {
return FermataApplication.get();
}

private FermataVfsManager getVfsManager() {
return getContext().getVfsManager();
}

private static final class ImagePrefs implements SharedPreferenceStore {
private static final int IMAGE_MAX_AGE = 7 * 24 * 3600;
private final SharedPreferences prefs;
Expand Down
Loading

0 comments on commit 4fddb43

Please sign in to comment.