|
package com.github.jsierles.reactnativeviewsnapshot; |
|
|
|
import android.content.Context; |
|
import android.graphics.Bitmap; |
|
import android.os.AsyncTask; |
|
import android.util.DisplayMetrics; |
|
|
|
import com.facebook.react.bridge.GuardedAsyncTask; |
|
import com.facebook.react.bridge.JSApplicationIllegalArgumentException; |
|
import com.facebook.react.bridge.Promise; |
|
import com.facebook.react.bridge.ReactApplicationContext; |
|
import com.facebook.react.bridge.ReactContext; |
|
import com.facebook.react.bridge.ReactContextBaseJavaModule; |
|
import com.facebook.react.bridge.ReactMethod; |
|
import com.facebook.react.bridge.ReadableMap; |
|
import com.facebook.react.uimanager.NativeViewHierarchyManager; |
|
import com.facebook.react.uimanager.UIBlock; |
|
import com.facebook.react.uimanager.UIImplementation; |
|
import com.facebook.react.uimanager.UIManagerModule; |
|
|
|
import java.io.File; |
|
import java.io.FilenameFilter; |
|
import java.io.IOException; |
|
|
|
public class ViewSnapshotModule extends ReactContextBaseJavaModule { |
|
private final ReactApplicationContext reactContext; |
|
|
|
public ViewSnapshotModule(ReactApplicationContext reactContext) { |
|
super(reactContext); |
|
this.reactContext = reactContext; |
|
} |
|
|
|
@Override |
|
public String getName() { |
|
return "ViewSnapshot"; |
|
} |
|
|
|
@Override |
|
public void onCatalystInstanceDestroy() { |
|
super.onCatalystInstanceDestroy(); |
|
new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
|
} |
|
|
|
@ReactMethod |
|
public void takeSnapshot(int tag, ReadableMap options, Promise promise) { |
|
ReactApplicationContext context = getReactApplicationContext(); |
|
String format = options.hasKey("format") ? options.getString("format") : "png"; |
|
Bitmap.CompressFormat compressFormat = |
|
format.equals("png") |
|
? Bitmap.CompressFormat.PNG |
|
: format.equals("jpg")||format.equals("jpeg") |
|
? Bitmap.CompressFormat.JPEG |
|
: format.equals("webm") |
|
? Bitmap.CompressFormat.WEBP |
|
: null; |
|
if (compressFormat == null) { |
|
throw new JSApplicationIllegalArgumentException("Unsupported image format: " + format); |
|
} |
|
double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0; |
|
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); |
|
Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null; |
|
Integer height = options.hasKey("height") ? (int)(displayMetrics.density * options.getDouble("height")) : null; |
|
try { |
|
File tmpFile = createTempFile(getReactApplicationContext(), format); |
|
UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class); |
|
uiManager.addUIBlock(new ViewSnapshot(tag, compressFormat, quality, width, height, tmpFile, promise)); |
|
} |
|
catch (Exception e) { |
|
promise.reject(ViewSnapshot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag); |
|
} |
|
} |
|
|
|
private static final String TEMP_FILE_PREFIX = "ReactNative_snapshot_image_"; |
|
|
|
/** |
|
* Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped |
|
* image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting |
|
* down) and when the module is instantiated, to handle the case where the app crashed. |
|
*/ |
|
private static class CleanTask extends GuardedAsyncTask<Void, Void> { |
|
private final Context mContext; |
|
|
|
private CleanTask(ReactContext context) { |
|
super(context); |
|
mContext = context; |
|
} |
|
|
|
@Override |
|
protected void doInBackgroundGuarded(Void... params) { |
|
cleanDirectory(mContext.getCacheDir()); |
|
File externalCacheDir = mContext.getExternalCacheDir(); |
|
if (externalCacheDir != null) { |
|
cleanDirectory(externalCacheDir); |
|
} |
|
} |
|
|
|
private void cleanDirectory(File directory) { |
|
File[] toDelete = directory.listFiles( |
|
new FilenameFilter() { |
|
@Override |
|
public boolean accept(File dir, String filename) { |
|
return filename.startsWith(TEMP_FILE_PREFIX); |
|
} |
|
}); |
|
if (toDelete != null) { |
|
for (File file: toDelete) { |
|
file.delete(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Create a temporary file in the cache directory on either internal or external storage, |
|
* whichever is available and has more free space. |
|
*/ |
|
private File createTempFile(Context context, String ext) |
|
throws IOException { |
|
File externalCacheDir = context.getExternalCacheDir(); |
|
File internalCacheDir = context.getCacheDir(); |
|
File cacheDir; |
|
if (externalCacheDir == null && internalCacheDir == null) { |
|
throw new IOException("No cache directory available"); |
|
} |
|
if (externalCacheDir == null) { |
|
cacheDir = internalCacheDir; |
|
} |
|
else if (internalCacheDir == null) { |
|
cacheDir = externalCacheDir; |
|
} else { |
|
cacheDir = externalCacheDir.getFreeSpace() > internalCacheDir.getFreeSpace() ? |
|
externalCacheDir : internalCacheDir; |
|
} |
|
return File.createTempFile(TEMP_FILE_PREFIX, ext, cacheDir); |
|
} |
|
|
|
} |