fix(android): image asset handling regarding requestLegacyExternalStorage (#9373)

This commit is contained in:
Osei Fortune
2021-05-09 22:29:44 -04:00
committed by GitHub
parent 879b5f6a22
commit f311151496
8 changed files with 380 additions and 97 deletions

View File

@ -349,31 +349,31 @@ declare class WeakRef<T> {
}
/**
* Create a Java long from a number
*/
* Create a Java long from a number
*/
declare function long(value: number): any;
/**
* Create a Java byte from a number
*/
* Create a Java byte from a number
*/
declare function byte(value: number): any;
/**
* Create a Java short from a number
*/
* Create a Java short from a number
*/
declare function short(value: number): any;
/**
* Create a Java double from a number
*/
* Create a Java double from a number
*/
declare function double(value: number): any;
/**
* Create a Java float from a number
*/
* Create a Java float from a number
*/
declare function float(value: number): any;
/**
* Create a Java char from a string
*/
* Create a Java char from a string
*/
declare function char(value: string): any;

View File

@ -1,6 +1,7 @@
import { ImageAssetBase, getRequestedImageSize } from './image-asset-common';
import { path as fsPath, knownFolders } from '../file-system';
import { ad } from '../utils';
import { Screen } from '../platform';
export * from './image-asset-common';
export class ImageAsset extends ImageAssetBase {
@ -25,66 +26,20 @@ export class ImageAsset extends ImageAssetBase {
}
public getImageAsync(callback: (image, error) => void) {
const bitmapOptions = new android.graphics.BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
// read only the file size
let bitmap = android.graphics.BitmapFactory.decodeFile(this.android, bitmapOptions);
const sourceSize = {
width: bitmapOptions.outWidth,
height: bitmapOptions.outHeight,
};
const requestedSize = getRequestedImageSize(sourceSize, this.options);
const sampleSize = org.nativescript.widgets.image.Fetcher.calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.width, requestedSize.height);
const finalBitmapOptions = new android.graphics.BitmapFactory.Options();
finalBitmapOptions.inSampleSize = sampleSize;
try {
let error = null;
// read as minimum bitmap as possible (slightly bigger than the requested size)
bitmap = android.graphics.BitmapFactory.decodeFile(this.android, finalBitmapOptions);
if (bitmap) {
if (requestedSize.width !== bitmap.getWidth() || requestedSize.height !== bitmap.getHeight()) {
// scale to exact size
bitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, requestedSize.width, requestedSize.height, true);
}
const rotationAngle = calculateAngleFromFile(this.android);
if (rotationAngle !== 0) {
const matrix = new android.graphics.Matrix();
matrix.postRotate(rotationAngle);
bitmap = android.graphics.Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}
if (!bitmap) {
error = "Asset '" + this.android + "' cannot be found.";
}
callback(bitmap, error);
} catch (ex) {
callback(null, ex);
}
org.nativescript.widgets.Utils.loadImageAsync(
ad.getApplicationContext(),
this.android,
JSON.stringify(this.options || {}),
Screen.mainScreen.widthPixels,
Screen.mainScreen.heightPixels,
new org.nativescript.widgets.Utils.AsyncImageCallback({
onSuccess(bitmap) {
callback(bitmap, null);
},
onError(ex) {
callback(null, ex);
},
})
);
}
}
function calculateAngleFromFile(filename: string) {
let rotationAngle = 0;
const ei = new android.media.ExifInterface(filename);
const orientation = ei.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case android.media.ExifInterface.ORIENTATION_ROTATE_90:
rotationAngle = 90;
break;
case android.media.ExifInterface.ORIENTATION_ROTATE_180:
rotationAngle = 180;
break;
case android.media.ExifInterface.ORIENTATION_ROTATE_270:
rotationAngle = 270;
break;
}
return rotationAngle;
}

View File

@ -1,11 +1,6 @@
declare module org {
module nativescript {
module widgets {
export class Utils {
public static drawBoxShadow(view: android.view.View, value: string): void;
}
export class BoxShadowDrawable {
public constructor(drawable: android.graphics.drawable.Drawable, value: string);
public getWrappedDrawable(): android.graphics.drawable.Drawable;
@ -632,3 +627,34 @@
}
}
}
declare module org {
export module nativescript {
export module widgets {
export class Utils {
public static class: java.lang.Class<org.nativescript.widgets.Utils>;
public static loadImageAsync(param0: globalAndroid.content.Context, param1: string, param2: string, param3: number, param4: number, param5: org.nativescript.widgets.Utils.AsyncImageCallback): void;
public static drawBoxShadow(param0: globalAndroid.view.View, param1: string): void;
public constructor();
}
export module Utils {
export class AsyncImageCallback {
public static class: java.lang.Class<org.nativescript.widgets.Utils.AsyncImageCallback>;
/**
* Constructs a new instance of the org.nativescript.widgets.Utils$AsyncImageCallback interface with the provided implementation. An empty constructor exists calling super() when extending the interface class.
*/
public constructor(implementation: {
onSuccess(param0: globalAndroid.graphics.Bitmap): void;
onError(param0: java.lang.Exception): void;
});
public constructor();
public onSuccess(param0: globalAndroid.graphics.Bitmap): void;
public onError(param0: java.lang.Exception): void;
}
export class ImageAssetOptions {
public static class: java.lang.Class<org.nativescript.widgets.Utils.ImageAssetOptions>;
}
}
}
}
}

View File

@ -78,6 +78,7 @@ dependencies {
implementation 'androidx.viewpager:viewpager:' + androidxVersion
implementation 'androidx.fragment:fragment:' + androidxVersion
implementation 'androidx.transition:transition:' + androidxVersion
implementation "androidx.exifinterface:exifinterface:1.3.2"
} else {
println 'Using support library'
implementation 'com.android.support:support-v4:' + computeSupportVersion()
@ -97,4 +98,4 @@ task copyAar << {
assemble.dependsOn(cleanBuildDir)
copyAar.dependsOn(assemble)
build.dependsOn(copyAar)
build.dependsOn(copyAar)

View File

@ -17,13 +17,17 @@
package org.nativescript.widgets.image;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import androidx.exifinterface.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import android.util.TypedValue;
@ -34,6 +38,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -257,8 +262,9 @@ public class Fetcher extends Worker {
if (debuggable > 0) {
Log.v(TAG, "process: " + uri);
}
if (uri.startsWith(FILE_PREFIX)) {
if(uri.startsWith(CONTENT_PREFIX)){
return decodeSampledBitmapFromContent(uri,mResolver , decodeWidth, decodeHeight, keepAspectRatio, getCache());
}else if (uri.startsWith(FILE_PREFIX)) {
String filename = uri.substring(FILE_PREFIX.length());
return decodeSampledBitmapFromFile(filename, decodeWidth, decodeHeight, keepAspectRatio, getCache());
} else if (uri.startsWith(RESOURCE_PREFIX)) {
@ -474,6 +480,62 @@ public class Fetcher extends Worker {
return scaleAndRotateBitmap(bitmap, ei, reqWidth, reqHeight, keepAspectRatio);
}
private static void closePfd(ParcelFileDescriptor pfd){
if(pfd != null){
try {
pfd.close();
} catch (IOException ignored) {}
}
}
/**
* Decode and sample down a bitmap from a file to the requested width and height.
*
* @param content The content uri of the file to decode
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @param cache The Cache used to find candidate bitmaps for use with inBitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromContent(String content, ContentResolver resolver, int reqWidth, int reqHeight,
boolean keepAspectRatio, Cache cache) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Uri uri = android.net.Uri.parse(content);
ParcelFileDescriptor pfd = null;
try {
pfd = resolver.openFileDescriptor(uri, "r");
BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor(), null, options);
} catch (FileNotFoundException e) {
Log.v(TAG, "File not found " + content);
closePfd(pfd);
return null;
}
options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
// If we're running on Honeycomb or newer, try to use inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
final Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor(), null, options);
ExifInterface ei = getExifInterface(pfd.getFileDescriptor());
closePfd(pfd);
return scaleAndRotateBitmap(bitmap, ei, reqWidth, reqHeight, keepAspectRatio);
}
private static Bitmap scaleAndRotateBitmap(Bitmap bitmap, ExifInterface ei, int reqWidth, int reqHeight,
boolean keepAspectRatio) {
if (bitmap == null) {
@ -669,4 +731,4 @@ public class Fetcher extends Worker {
}
//END_INCLUDE(add_bitmap_options)
}
}
}

View File

@ -16,6 +16,7 @@
package org.nativescript.widgets.image;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@ -37,6 +38,7 @@ public abstract class Worker {
protected static final String RESOURCE_PREFIX = "res://";
protected static final String FILE_PREFIX = "file:///";
protected static final String CONTENT_PREFIX = "content://";
static final String TAG = "JS";
private static final int FADE_IN_TIME = 200;
@ -49,7 +51,7 @@ public abstract class Worker {
protected boolean mPauseWork = false;
protected Resources mResources;
protected ContentResolver mResolver;
private static final int MESSAGE_CLEAR = 0;
private static final int MESSAGE_INIT_DISK_CACHE = 1;
private static final int MESSAGE_FLUSH = 2;
@ -59,7 +61,7 @@ public abstract class Worker {
protected Worker(Context context) {
mResources = context.getResources();
mResolver = context.getContentResolver();
// Negative means not initialized.
if (debuggable < 0) {
try {
@ -539,4 +541,4 @@ public abstract class Worker {
public void closeCache() {
new CacheAsyncTask().execute(MESSAGE_CLOSE);
}
}
}

View File

@ -1,12 +1,33 @@
package org.nativescript.widgets;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import androidx.exifinterface.media.ExifInterface;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class Utils {
public static void drawBoxShadow(View view, String value) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
@ -16,14 +37,14 @@ public class Utils {
Drawable currentBg = view.getBackground();
if(currentBg != null) {
if (currentBg != null) {
Log.d("BoxShadowDrawable", "current BG is: " + currentBg.getClass().getName());
}
if(currentBg == null) {
if (currentBg == null) {
Log.d("BoxShadowDrawable", "view had no background!");
currentBg = new ColorDrawable(Color.TRANSPARENT);
} else if(currentBg instanceof BoxShadowDrawable) {
} else if (currentBg instanceof BoxShadowDrawable) {
currentBg = ((BoxShadowDrawable) view.getBackground()).getWrappedDrawable();
Log.d("BoxShadowDrawable", "already a BoxShadowDrawable, getting wrapped drawable:" + currentBg.getClass().getName());
}
@ -33,21 +54,237 @@ public class Utils {
view.setBackground(new BoxShadowDrawable(currentBg, value));
Drawable bg = view.getBackground();
if(bg != null) {
if (bg != null) {
Log.d("BoxShadowDrawable", "new current bg: " + bg.getClass().getName());
}
int count = 0;
while (view.getParent() != null && view.getParent() instanceof ViewGroup) {
count++;
ViewGroup parent = (ViewGroup) view.getParent();
parent.setClipChildren(false);
parent.setClipToPadding(false);
// removing clipping from all breaks the ui
if (count == 1) {
count++;
ViewGroup parent = (ViewGroup) view.getParent();
parent.setClipChildren(false);
parent.setClipToPadding(false);
// removing clipping from all breaks the ui
if (count == 1) {
break;
}
}
}
public interface AsyncImageCallback {
void onSuccess(Bitmap bitmap);
void onError(Exception exception);
}
static class ImageAssetOptions {
int width;
int height;
boolean keepAspectRatio;
boolean autoScaleFactor;
}
private static final Executor executors = Executors.newCachedThreadPool();
private static Pair<Integer, Integer> getAspectSafeDimensions(float sourceWidth, float sourceHeight, float reqWidth, float reqHeight) {
float widthCoef = sourceWidth / reqWidth;
float heightCoef = sourceHeight / reqHeight;
float aspectCoef = Math.min(widthCoef, heightCoef);
return new Pair<>((int) Math.floor(sourceWidth / aspectCoef), (int) Math.floor(sourceHeight / aspectCoef));
}
private static Pair<Integer, Integer> getRequestedImageSize(Pair<Integer, Integer> src, Pair<Integer, Integer> maxSize, ImageAssetOptions options) {
int reqWidth = options.width;
if (reqWidth <= 0) {
reqWidth = Math.min(src.first, maxSize.first);
}
int reqHeight = options.height;
if (reqHeight <= 0) {
reqHeight = Math.min(src.second, maxSize.second);
}
if (options.keepAspectRatio) {
Pair<Integer, Integer> safeAspectSize = getAspectSafeDimensions(src.first, src.second, reqWidth, reqHeight);
reqWidth = safeAspectSize.first;
reqHeight = safeAspectSize.second;
}
return new Pair<>(reqWidth, reqHeight);
}
private static void closePfd(ParcelFileDescriptor pfd) {
if (pfd != null) {
try {
pfd.close();
} catch (IOException ignored) {
}
}
}
private static int calculateAngleFromFile(String filename) {
int rotationAngle = 0;
ExifInterface ei;
try {
ei = new ExifInterface(filename);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
rotationAngle = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotationAngle = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
rotationAngle = 270;
break;
}
} catch (IOException ignored) {
}
return rotationAngle;
}
private static int calculateAngleFromFileDescriptor(FileDescriptor fd) {
int rotationAngle = 0;
ExifInterface ei;
try {
ei = new ExifInterface(fd);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
rotationAngle = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotationAngle = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
rotationAngle = 270;
break;
}
} catch (IOException ignored) {
}
return rotationAngle;
}
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
public static void loadImageAsync(final Context context, final String src, final String options, final int maxWidth, final int maxHeight, final AsyncImageCallback callback) {
executors.execute(new Runnable() {
@Override
public void run() {
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
try {
Bitmap bitmap;
ParcelFileDescriptor pfd = null;
if (src.startsWith("content://")) {
Uri uri = android.net.Uri.parse(src);
ContentResolver resolver = context.getContentResolver();
try {
pfd = resolver.openFileDescriptor(uri, "r");
} catch (final FileNotFoundException e) {
mainHandler.post(new Runnable() {
@Override
public void run() {
callback.onError(e);
}
});
closePfd(pfd);
return;
}
android.graphics.BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor(), null, bitmapOptions);
} else {
android.graphics.BitmapFactory.decodeFile(src, bitmapOptions);
}
ImageAssetOptions opts = new ImageAssetOptions();
opts.keepAspectRatio = true;
opts.autoScaleFactor = true;
try {
JSONObject object = new JSONObject(options);
opts.width = object.optInt("width", 0);
opts.height = object.optInt("height", 0);
opts.keepAspectRatio = object.optBoolean("keepAspectRatio", true);
opts.autoScaleFactor = object.optBoolean("autoScaleFactor", true);
} catch (JSONException ignored) {
}
Pair<Integer, Integer> sourceSize = new Pair<>(bitmapOptions.outWidth, bitmapOptions.outHeight);
Pair<Integer, Integer> maxSize = new Pair<>(maxWidth, maxHeight);
Pair<Integer, Integer> requestedSize = getRequestedImageSize(sourceSize, maxSize, opts);
int sampleSize = org.nativescript.widgets.image.Fetcher.calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.first, requestedSize.second);
BitmapFactory.Options finalBitmapOptions = new BitmapFactory.Options();
finalBitmapOptions.inSampleSize = sampleSize;
String error = null;
// read as minimum bitmap as possible (slightly bigger than the requested size)
if (pfd != null) {
bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor(), null, finalBitmapOptions);
} else {
bitmap = android.graphics.BitmapFactory.decodeFile(src, finalBitmapOptions);
}
if (bitmap != null) {
if (requestedSize.first != bitmap.getWidth() || requestedSize.second != bitmap.getHeight()) {
// scale to exact size
bitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, requestedSize.first, requestedSize.second, true);
}
int rotationAngle;
if (pfd != null) {
rotationAngle = calculateAngleFromFileDescriptor(pfd.getFileDescriptor());
closePfd(pfd);
} else {
rotationAngle = calculateAngleFromFile(src);
}
if (rotationAngle != 0) {
Matrix matrix = new android.graphics.Matrix();
matrix.postRotate(rotationAngle);
bitmap = android.graphics.Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}
if (bitmap == null) {
error = "Asset '" + src + "' cannot be found.";
}
final String finalError = error;
final Bitmap finalBitmap = bitmap;
mainHandler.post(new Runnable() {
@Override
public void run() {
if (finalError != null) {
callback.onError(new Exception(finalError));
} else {
callback.onSuccess(finalBitmap);
}
}
});
} catch (final Exception ex) {
mainHandler.post(new Runnable() {
@Override
public void run() {
callback.onError(ex);
}
});
}
}
});
}
// public static void clearBoxShadow(View view) {