From 5dc1ca03af20182617e934c908eb7bbef79227bc Mon Sep 17 00:00:00 2001 From: Raul Candelario Date: Sun, 10 Dec 2023 16:33:07 -0600 Subject: [PATCH] add methods --- src/android/Chooser.java | 230 +++++++++++++++++++++++++-------------- src/ios/Chooser.swift | 106 ++++++++++++------ www/chooser.js | 54 +++------ 3 files changed, 233 insertions(+), 157 deletions(-) diff --git a/src/android/Chooser.java b/src/android/Chooser.java index 04978bf..8fb14ff 100755 --- a/src/android/Chooser.java +++ b/src/android/Chooser.java @@ -5,11 +5,15 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.provider.MediaStore; import android.util.Base64; +import android.provider.OpenableColumns; +import android.webkit.MimeTypeMap; +import java.io.File; +import java.io.FileOutputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.io.OutputStream; import java.io.IOException; import java.lang.Exception; @@ -20,56 +24,45 @@ import org.json.JSONException; import org.json.JSONObject; - public class Chooser extends CordovaPlugin { private static final String ACTION_OPEN = "getFile"; private static final int PICK_FILE_REQUEST = 1; private static final String TAG = "Chooser"; - /** @see https://stackoverflow.com/a/17861016/459881 */ - public static byte[] getBytesFromInputStream (InputStream is) throws IOException { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - byte[] buffer = new byte[0xFFFF]; - - for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { - os.write(buffer, 0, len); - } - - return os.toByteArray(); - } - - /** @see https://stackoverflow.com/a/23270545/459881 */ - public static String getDisplayName (ContentResolver contentResolver, Uri uri) { - String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME}; - Cursor metaCursor = contentResolver.query(uri, projection, null, null, null); - - if (metaCursor != null) { - try { - if (metaCursor.moveToFirst()) { - return metaCursor.getString(0); - } - } finally { - metaCursor.close(); - } - } - - return "File"; - } - - private CallbackContext callback; - private Boolean includeData; + private int maxFileSize = 0; - public void chooseFile (CallbackContext callbackContext, String accept, Boolean includeData) { + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) { + try { + if (action.equals(Chooser.ACTION_OPEN)) { + this.chooseFile(callbackContext, args); + return true; + } + } catch (JSONException err) { + this.callback.error("Execute failed: " + err.toString()); + } + + return false; + } + + public void chooseFile (CallbackContext callbackContext, JSONArray args) throws JSONException { + JSONObject options = args.optJSONObject(0); + String mimeTypes = options.optString("mimeTypes"); + int maxFileSize = options.optInt("maxFileSize"); + + if (maxFileSize != 0) { + this.maxFileSize = maxFileSize; + } + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); - if (!accept.equals("*/*")) { - intent.putExtra(Intent.EXTRA_MIME_TYPES, accept.split(",")); - } + if (!mimeTypes.equals("*/*")) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.split(",")); + } intent.addCategory(Intent.CATEGORY_OPENABLE); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); - this.includeData = includeData; Intent chooser = Intent.createChooser(intent, "Select File"); cordova.startActivityForResult(this, chooser, Chooser.PICK_FILE_REQUEST); @@ -80,25 +73,6 @@ public void chooseFile (CallbackContext callbackContext, String accept, Boolean callbackContext.sendPluginResult(pluginResult); } - @Override - public boolean execute ( - String action, - JSONArray args, - CallbackContext callbackContext - ) { - try { - if (action.equals(Chooser.ACTION_OPEN)) { - this.chooseFile(callbackContext, args.getString(0), args.getBoolean(1)); - return true; - } - } - catch (JSONException err) { - this.callback.error("Execute failed: " + err.toString()); - } - - return false; - } - @Override public void onActivityResult (int requestCode, int resultCode, Intent data) { try { @@ -107,35 +81,83 @@ public void onActivityResult (int requestCode, int resultCode, Intent data) { Uri uri = data.getData(); if (uri != null) { - ContentResolver contentResolver = - this.cordova.getActivity().getContentResolver() - ; - - String name = Chooser.getDisplayName(contentResolver, uri); + Activity activity = cordova.getActivity(); + ContentResolver contentResolver = activity.getContentResolver(); + InputStream inputStream = contentResolver.openInputStream(uri); + String displayName = "File"; + String uriString = uri.toString(); + String mimeType = null; + String extension = null; + String filePath = null; + JSONObject result = new JSONObject(); - String mediaType = contentResolver.getType(uri); - if (mediaType == null || mediaType.isEmpty()) { - mediaType = "application/octet-stream"; + int size = inputStream.available(); + if (this.maxFileSize != 0) { + if (size > this.maxFileSize) { + result.put("error", true); + result.put("code", 1); + result.put("code_name", "FILE_SIZE_EXCEEDED"); + result.put("message", "Invalid size"); + this.callback.error(result); + return; + } + } + + if (uriString.startsWith("content://")) { + Cursor cursor = null; + try { + cursor = contentResolver.query(uri, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + displayName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); + } + mimeType = contentResolver.getType(uri); + } finally { + assert cursor != null; + cursor.close(); + } + } else if (uriString.startsWith("file://")) { + displayName = new File(uriString).getName(); + String[] parts = uriString.split("\\."); + String ext = parts[parts.length - 1]; + if (ext != null) { + MimeTypeMap mime = MimeTypeMap.getSingleton(); + mimeType = mime.getMimeTypeFromExtension(ext); + } } - String base64 = ""; - - if (this.includeData) { - byte[] bytes = Chooser.getBytesFromInputStream( - contentResolver.openInputStream(uri) - ); + if (mimeType != null) { + MimeTypeMap mime = MimeTypeMap.getSingleton(); + extension = mime.getExtensionFromMimeType(mimeType); + } - base64 = Base64.encodeToString(bytes, Base64.DEFAULT); - } - - JSONObject result = new JSONObject(); + if (mimeType == null || mimeType.isEmpty()) { + mimeType = "application/octet-stream"; + } - result.put("data", base64); - result.put("mediaType", mediaType); - result.put("name", name); - result.put("uri", uri.toString()); + filePath = activity.getCacheDir().getAbsolutePath() + '/' + displayName; + copyInputStreamToFile(inputStream, filePath); + + ContentResolver contentB64 = this.cordova.getActivity().getContentResolver(); + String base64 = ""; - this.callback.success(result.toString()); + byte[] bytes = this.getBytesFromInputStream(contentB64.openInputStream(uri)); + base64 = Base64.encodeToString(bytes, Base64.DEFAULT); + + try { + result.put("path", new File(filePath).exists() ? "file://" + filePath : ""); + result.put("name", this.getFileName(displayName)); // without extension + result.put("displayName", displayName); // with extension + result.put("mimeType", mimeType); + result.put("extension", extension); + result.put("size", size); + result.put("data", base64); + result.put("uri", uriString); + + this.callback.success(result); + // this.callback.success(result.toString()); + } catch (JSONException e) { + this.callback.error("JSON Object not supported"); + } } else { this.callback.error("File URI was null."); @@ -153,4 +175,50 @@ else if (resultCode == Activity.RESULT_CANCELED) { this.callback.error("Failed to read file: " + err.toString()); } } + + private void copyInputStreamToFile(InputStream inputStream, String file) { + OutputStream out = null; + + try { + out = new FileOutputStream(file); + byte[] buf = new byte[1024]; + int len; + while ((len = inputStream.read(buf)) > 0) { + out.write(buf, 0, len); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + // Ensure that the InputStreams are closed even if there's an exception. + try { + if (out != null) { + out.close(); + } + + // If you want to close the "in" InputStream yourself then remove this + // from here but ensure that you close it yourself eventually. + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public String getFileName(String fileName) { + if(!fileName.contains(".")){ + return fileName; + } + return fileName.substring(0,fileName.lastIndexOf(".")); + } + + public byte[] getBytesFromInputStream (InputStream is) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + byte[] buffer = new byte[0xFFFF]; + + for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { + os.write(buffer, 0, len); + } + + return os.toByteArray(); + } } diff --git a/src/ios/Chooser.swift b/src/ios/Chooser.swift index 24ece6e..419baec 100755 --- a/src/ios/Chooser.swift +++ b/src/ios/Chooser.swift @@ -1,20 +1,20 @@ +import Cordova import UIKit import MobileCoreServices import Foundation - class ChooserUIDocumentPickerViewController : UIDocumentPickerViewController { - var includeData: Bool = false + var maxFileSize: Int = 0 } @objc(Chooser) class Chooser : CDVPlugin { var commandCallback: String? - func callPicker (includeData: Bool, utis: [String]) { + func callPicker (maxFileSize: Int, utis: [String]) { let picker = ChooserUIDocumentPickerViewController(documentTypes: utis, in: .import) picker.delegate = self - picker.includeData = includeData + picker.maxFileSize = maxFileSize self.viewController.present(picker, animated: true, completion: nil) } @@ -35,14 +35,43 @@ class Chooser : CDVPlugin { return "application/octet-stream" } - func documentWasSelected (includeData: Bool, url: URL) { + func documentWasSelected (maxFileSize: Int, url: URL) { var error: NSError? - - NSFileCoordinator().coordinate( - readingItemAt: url, - options: [], - error: &error - ) { newURL in + let fileSize: Int = 0; + + do { + let resources = try url.resourceValues(forKeys:[.fileSizeKey]) + let fileSize = resources.fileSize! + print ("\(fileSize)") + if(fileSize > maxFileSize){ + let resError = [ + "error": true, + "code": 1, + "code_name": "FILE_SIZE_EXCEEDED", + "message": "File size exceeded the limit of \(maxFileSize) bytes." + ] as [AnyHashable : Any] + + if let callbackId = self.commandCallback { + self.commandCallback = nil + + let pluginResult = CDVPluginResult( + status: CDVCommandStatus_ERROR, + messageAs: resError + ) + + self.commandDelegate!.send( + pluginResult, + callbackId: callbackId + ) + } + return + } + } catch { + self.sendError("Error: \(error)") + } + + NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &error) { + newURL in let maybeData = try? Data(contentsOf: newURL, options: []) guard let data = maybeData else { @@ -51,25 +80,30 @@ class Chooser : CDVPlugin { } do { - let result = [ - "data": includeData ? data.base64EncodedString() : "", - "mediaType": self.detectMimeType(newURL), - "name": newURL.lastPathComponent, + let result: [String: any] = [ + "path": newURL.absoluteString, + "name": newURL.deletingPathExtension().lastPathComponent, // without extension + "displayName": newURL.lastPathComponent, // with extension + "mimeType": self.detectMimeType(newURL), + "extension": newURL.pathExtension, + "size": fileSize, + "data": data.base64EncodedString(), "uri": newURL.absoluteString - ] - - if let message = try String( - data: JSONSerialization.data( - withJSONObject: result, - options: [] - ), - encoding: String.Encoding.utf8 - ) { - self.send(message) - } - else { - self.sendError("Serializing result failed.") - } + ] as [AnyHashable : Any] + + if let callbackId = self.commandCallback { + self.commandCallback = nil + + let pluginResult = CDVPluginResult( + status: CDVCommandStatus_OK, + messageAs: result + ) + + self.commandDelegate!.send( + pluginResult, + callbackId: callbackId + ) + } newURL.stopAccessingSecurityScopedResource() } @@ -89,11 +123,11 @@ class Chooser : CDVPlugin { func getFile (command: CDVInvokedUrlCommand) { self.commandCallback = command.callbackId - let accept = command.arguments.first as! String - let includeData = command.arguments.last as! Bool - let mimeTypes = accept.components(separatedBy: ",") + let options = (command.arguments[0] as! [String : AnyObject]) + let mimeTypes = options["mimeTypes"] as! String + let maxFileSize = options["maxFileSize"] as! Int - let utis = mimeTypes.map { (mimeType: String) -> String in + let utis = mimeTypes.components(separatedBy: ",").map { (mimeType: String) -> String in switch mimeType { case "audio/*": return kUTTypeAudio as String @@ -126,7 +160,7 @@ class Chooser : CDVPlugin { return kUTTypeItem as String } - self.callPicker(includeData: includeData, utis: utis) + self.callPicker(maxFileSize: maxFileSize, utis: utis) } func send (_ message: String, _ status: CDVCommandStatus = CDVCommandStatus_OK) { @@ -158,7 +192,7 @@ extension Chooser : UIDocumentPickerDelegate { ) { let picker = controller as! ChooserUIDocumentPickerViewController if let url = urls.first { - self.documentWasSelected(includeData: picker.includeData, url: url) + self.documentWasSelected(maxFileSize: picker.maxFileSize, url: url) } } @@ -167,7 +201,7 @@ extension Chooser : UIDocumentPickerDelegate { didPickDocumentAt url: URL ) { let picker = controller as! ChooserUIDocumentPickerViewController - self.documentWasSelected(includeData: picker.includeData, url: url) + self.documentWasSelected(maxFileSize: picker.maxFileSize, url: url) } func documentPickerWasCancelled (_ controller: UIDocumentPickerViewController) { diff --git a/www/chooser.js b/www/chooser.js index bbcf5e5..08c89c5 100755 --- a/www/chooser.js +++ b/www/chooser.js @@ -1,3 +1,5 @@ +var exec = require('cordova/exec'); + /** @see sodiumutil */ function from_base64 (sBase64, nBlocksSize) { function _b64ToUint6 (nChr) { @@ -42,44 +44,24 @@ function from_base64 (sBase64, nBlocksSize) { return taBytes; } -function getFileInternal ( - accept, - includeData, - successCallback, - failureCallback -) { - if (typeof accept === 'function') { - failureCallback = successCallback; - successCallback = accept; - accept = undefined; - } +function getFileInternal (options, successCallback, failureCallback) { + var chooserOptions = Object.assign({ mimeTypes: '*/*', maxFileSize: 0 }, options); var result = new Promise(function (resolve, reject) { - cordova.exec( - function (json) { - if (json === 'RESULT_CANCELED') { + exec( + function (result) { + if (result === 'RESULT_CANCELED') { resolve(); return; } try { - var o = JSON.parse(json); + var base64Data = result.data.replace(/[^A-Za-z0-9\+\/]/g, ''); - if (includeData) { - var base64Data = o.data.replace( - /[^A-Za-z0-9\+\/]/g, - '' - ); + result.data = from_base64(base64Data); + result.dataURI = 'data:' + result.mimeType + ';base64,' + base64Data; - o.data = from_base64(base64Data); - o.dataURI = - 'data:' + o.mediaType + ';base64,' + base64Data; - } - else { - delete o.data; - } - - resolve(o); + resolve(result); } catch (err) { reject(err); @@ -88,12 +70,7 @@ function getFileInternal ( reject, 'Chooser', 'getFile', - [ - (typeof accept === 'string' ? - accept.toLowerCase().replace(/\s/g, '') : - undefined) || '*/*', - includeData - ] + [ chooserOptions ] ); }); @@ -108,10 +85,7 @@ function getFileInternal ( } module.exports = { - getFile: function (accept, successCallback, failureCallback) { - return getFileInternal(accept, true, successCallback, failureCallback); + getFile: function (options, successCallback, failureCallback) { + return getFileInternal(options, successCallback, failureCallback); }, - getFileMetadata: function (accept, successCallback, failureCallback) { - return getFileInternal(accept, false, successCallback, failureCallback); - } };