Skip to content

[RNA][OTA] Adding image support #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package com.facebook.react.views.imagehelper;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.DisplayMetrics;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;
import java.util.concurrent.ConcurrentHashMap;

public class ImageOTAUtils {
private static final ConcurrentHashMap<String, String> mResourceCacheMap = new ConcurrentHashMap<>();
private static final String CACHE_DRAWABLE_DIRECTORY_SCHEME = "otas/app/src/main/res";
private static final String[] RESOURCE_EXTENSIONS = {
"xml",
"png",
"svg",
"jpg"
};

private static int densityDpi;

@Nullable
static Drawable getResourceDrawable(Context context, @Nullable String name) {
if (name == null || name.isEmpty()) {
return null;
}

name = sanitizeResourceDrawableId(name);

// Checks to see if we have an ota version of the file, otherwise default to normal behavior.
File otaFile = getDrawableFileFromCache(context, name);
if (otaFile != null) {
return Drawable.createFromPath(otaFile.getAbsolutePath());
}

return null;
}

@Nullable
static Uri getResourceUri(Context context, @Nullable String name) {
if (name == null || name.isEmpty()) {
return Uri.EMPTY;
}

name = sanitizeResourceDrawableId(name);

// Checks to see if we have an ota version of the file, otherwise default to normal behavior.
File otaFile = ImageOTAUtils.getDrawableFileFromCache(context, name);
if (otaFile != null) {
return Uri.fromFile(otaFile);
}

return null;
}

/**
* Checks the cache to see if there is a drawable file downloaded via OTA.
*/
@Nullable
private static File getDrawableFileFromCache(Context context, @Nullable String fileName) {
if (fileName == null || fileName.isEmpty()) {
return null;
}

String cacheMapFileName = mResourceCacheMap.get(fileName);

// Check the cache to see if we've already looked up the file before.
if (cacheMapFileName != null) {
return new File(cacheMapFileName);
}

File file = null;
int densityDpi = getDensityDpi(context);
PhoneDensity[] phoneDensities = PhoneDensity.values();

// We start from the medium dpi and go up.
for (PhoneDensity phoneDensity : phoneDensities) {
String drawableFileParent = String.format("drawable-%s", phoneDensity.fileParentSuffix);
String mipMapFileParent = String.format("mipmap-%s", phoneDensity.fileParentSuffix);

String[] parentFileNames = { drawableFileParent, mipMapFileParent };

File resourceFile = checkFiles(context, parentFileNames, fileName);

if (resourceFile != null) {
file = resourceFile;
}

// If we've found a file at our current dpi level, return it.
// Otherwise continue looking up the chain.
if (densityDpi <= phoneDensity.density) {
if (file != null) {
mResourceCacheMap.put(fileName, file.getAbsolutePath());
return file;
}
}
}

// As a last resort, check the drawable/raw folders.
String[] parentFileNames = { "drawable", "raw" };
file = checkFiles(context, parentFileNames, fileName);

if (file != null) {
mResourceCacheMap.put(fileName, file.getAbsolutePath());
}

return file;
}

/**
* Given a list of files, check if any of them exist.
* Checks multiple extension types.
*/
private static File checkFiles(Context context, String[] parentFileNames, String fileName) {
for(String parentFileName : parentFileNames) {
for (String extension : RESOURCE_EXTENSIONS) {
File file = getFile(context, parentFileName, fileName, extension);
if (file.exists()) {
return file;
}
}
}

return null;
}

/**
* Returns a file object with the correct directory extensions.
*/
private static File getFile(
Context context,
String parentFileName,
String fileName,
String extension
) {
String fullDrawableFileName = String.format(
"%s/%s/%s.%s",
CACHE_DRAWABLE_DIRECTORY_SCHEME,
parentFileName,
fileName,
extension
);

return new File(context.getCacheDir(), fullDrawableFileName);
}

/**
* Returns the density dpi for the device.
*/
private static int getDensityDpi(Context context) {
// Cache this so we only have to do this once.
if (densityDpi == 0) {
DisplayMetrics metrics = new DisplayMetrics();

WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getMetrics(metrics);

densityDpi = metrics.densityDpi;
}

return densityDpi;
}

private static String sanitizeResourceDrawableId(@NonNull String name) {
return name.toLowerCase().replace("-", "_");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.facebook.react.views.imagehelper;

import android.util.DisplayMetrics;

import androidx.annotation.NonNull;

enum PhoneDensity {
Medium(DisplayMetrics.DENSITY_MEDIUM, "mdpi"),
High(DisplayMetrics.DENSITY_HIGH, "hdpi"),
XHigh(DisplayMetrics.DENSITY_XHIGH, "xhdpi"),
XXHigh(DisplayMetrics.DENSITY_XXHIGH, "xxhdpi"),
XXXHigh(DisplayMetrics.DENSITY_XXXHIGH, "xxxhdpi");

int density;

@NonNull
String fileParentSuffix;

PhoneDensity(
int density,
@NonNull String fileParentSuffix
) {
this.density = density;
this.fileParentSuffix = fileParentSuffix;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public synchronized void clear() {
mResourceDrawableIdMap.clear();
}

public int getResourceDrawableId(Context context, @Nullable String name) {
private int getResourceDrawableId(Context context, @Nullable String name) {
if (name == null || name.isEmpty()) {
return 0;
}
Expand All @@ -67,11 +67,23 @@ public int getResourceDrawableId(Context context, @Nullable String name) {
}

public @Nullable Drawable getResourceDrawable(Context context, @Nullable String name) {
Drawable otaDrawable = ImageOTAUtils.getResourceDrawable(context, name);

if (otaDrawable != null) {
return otaDrawable;
}

int resId = getResourceDrawableId(context, name);
return resId > 0 ? context.getResources().getDrawable(resId) : null;
}

public Uri getResourceDrawableUri(Context context, @Nullable String name) {
Uri otaUri = ImageOTAUtils.getResourceUri(context, name);

if (otaUri != null) {
return otaUri;
}

int resId = getResourceDrawableId(context, name);
return resId > 0
? new Uri.Builder().scheme(LOCAL_RESOURCE_SCHEME).path(String.valueOf(resId)).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,9 @@ public void setTextAlignVertical(ReactEditText view, @Nullable String textAlignV

@ReactProp(name = "inlineImageLeft")
public void setInlineImageLeft(ReactEditText view, @Nullable String resource) {
int id =
ResourceDrawableIdHelper.getInstance().getResourceDrawableId(view.getContext(), resource);
view.setCompoundDrawablesWithIntrinsicBounds(id, 0, 0, 0);
Drawable drawable =
ResourceDrawableIdHelper.getInstance().getResourceDrawable(view.getContext(), resource);
view.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
}

@ReactProp(name = "inlineImagePadding")
Expand Down