diff --git a/modules/file-chooser/build.gradle b/modules/file-chooser/build.gradle
new file mode 100644
index 00000000..3b82c159
--- /dev/null
+++ b/modules/file-chooser/build.gradle
@@ -0,0 +1,6 @@
+dependencies {
+ implementation project(":util")
+}
+
+ext.moduleName = 'com.gluonhq.attach.filechooser'
+ext.description = 'Common API to access file features'
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/FileChooserService.java b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/FileChooserService.java
new file mode 100644
index 00000000..695f0f04
--- /dev/null
+++ b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/FileChooserService.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2016, 2019 Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.filechooser;
+
+import java.io.File;
+import java.util.Optional;
+
+import com.gluonhq.attach.util.Services;
+
+/**
+ * The picture service allows the developer to load a picture from the device's local file
+ * system or from a picture taken directly using the device's camera.
+ *
+ *
Example
+ *
+ * {@code ImageView imageView = new ImageView();
+ * PicturesService.create().ifPresent(service -> {
+ * service.takePhoto(false).ifPresent(image -> imageView.setImage(image));
+ * });}
+ *
+ * It also allows the developer to retrieve the original file and work with it
+ * as needed, for instance sharing it with the ShareService.
+ *
+ * Example
+ *
+ * {@code ImageView imageView = new ImageView();
+ * PicturesService.create().ifPresent(service -> {
+ * service.loadImageFromGallery().ifPresent(image -> imageView.setImage(image));
+ * service.getImageFile().ifPresent(file ->
+ * ShareService.create().ifPresent(share ->
+ * share.share("image/jpeg", file)));
+ * });}
+ *
+ * Android Configuration
+ *
+ * Create the file {@code /src/android/res/xml/file_provider_paths.xml} with
+ * the following content that allows access to the external storage:
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ * }
+ *
+ *
+ * The permission android.permission.CAMERA needs to be added as well as the permissions
+ * android.permission.READ_EXTERNAL_STORAGE and android.permission.WRITE_EXTERNAL_STORAGE
+ * to be able to read and write images. Also a {@code provider} is required:
+ *
+ * Note: these modifications are handled automatically by Client plugin if it is used.
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ * ...
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ *
+ *
+ * iOS Configuration
+ * The following keys are required:
+ *
+ * {@code NSCameraUsageDescription
+ * Reason to use Camera Service (iOS 10+)
+ * NSPhotoLibraryUsageDescription
+ * Reason to use Photo Library (iOS 10+)
+ * NSPhotoLibraryAddUsageDescription
+ * Reason to use Photo Library (iOS 10+)}
+ *
+ * @since 3.0.0
+ */
+public interface FileChooserService {
+
+ /**
+ * Returns an instance of {@link FileChooserService}.
+ * @return An instance of {@link FileChooserService}.
+ */
+ static Optional create() {
+ return Services.get(FileChooserService.class);
+ }
+
+ /**
+ * Retrieve an image from the device's gallery of images
+ * @return an Optional with the Image or empty if it failed or it was cancelled
+ */
+ Optional loadFile();
+
+}
diff --git a/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/AndroidFileChooserService.java b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/AndroidFileChooserService.java
new file mode 100644
index 00000000..b6b89b21
--- /dev/null
+++ b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/AndroidFileChooserService.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2016, 2020, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.filechooser.impl;
+
+import com.gluonhq.attach.filechooser.FileChooserService;
+import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.io.File;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * Create the file {@code /src/android/res/xml/file_provider_paths.xml} with
+ * the following content that allows access to the external storage or to
+ * a temporal cache in case the file is not saved:
+ *
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ *
+ * The permission android.permission.CAMERA needs to be added as well as the permissions
+ * android.permission.READ_EXTERNAL_STORAGE and android.permission.WRITE_EXTERNAL_STORAGE
+ * to be able to read and write images. Also a {@code provider} is required:
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ * ...
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ */
+public class AndroidFileChooserService implements FileChooserService {
+
+ private static final Logger LOG = Logger.getLogger(AndroidFileChooserService.class.getName());
+
+ static {
+ System.loadLibrary("filechooser");
+ }
+
+ private static final ObjectProperty selectedFile = new SimpleObjectProperty<>();
+ private static ObjectProperty result;
+
+
+ public AndroidFileChooserService() {
+ }
+
+ @Override
+ public Optional loadFile() {
+ LOG.severe("Load image from gallery has been called.");
+ result = new SimpleObjectProperty<>();
+ selectFile();
+ try {
+ Platform.enterNestedEventLoop(result);
+ } catch (Exception e) {
+ LOG.severe("GalleryActivity: enterNestedEventLoop failed: " + e);
+ }
+
+ return Optional.ofNullable(result.get());
+
+ }
+
+ // native
+ public static native void selectFile();
+
+ // callback
+ public static void setResult(String filePath, int rotate) {
+ File file = new File(filePath);
+ selectedFile.set(file);
+ if (selectedFile.get() == null) {
+ result.set(selectedFile.get());
+ }
+ final File finalImage = selectedFile.get();
+ LOG.severe("Final File:");
+ LOG.severe(finalImage.getAbsolutePath());
+ Platform.runLater(() -> {
+ if (finalImage != null) {
+ result.set(selectedFile.get());
+ }
+ try {
+ Platform.exitNestedEventLoop(result, null);
+ } catch (Exception e) {
+ LOG.severe("GalleryActivity: exitNestedEventLoop failed: " + e);
+ }
+ });
+ }
+
+}
diff --git a/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/DummyFileChooserService.java b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/DummyFileChooserService.java
new file mode 100644
index 00000000..75b677ad
--- /dev/null
+++ b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/DummyFileChooserService.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.filechooser.impl;
+
+import com.gluonhq.attach.filechooser.FileChooserService;
+
+public abstract class DummyFileChooserService implements FileChooserService {
+}
diff --git a/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/IOSFileChooserService.java b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/IOSFileChooserService.java
new file mode 100644
index 00000000..9ac2c4d2
--- /dev/null
+++ b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/impl/IOSFileChooserService.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2016, 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.filechooser.impl;
+
+import com.gluonhq.attach.filechooser.FileChooserService;
+import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.image.Image;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.util.Base64;
+import java.util.Optional;
+
+/**
+ * Note:Since iOS 10 requires {@code NSCameraUsageDescription},
+ * {@code NSPhotoLibraryUsageDescription} and
+ * {@code NSPhotoLibraryAddUsageDescription} in pList.
+ */
+public class IOSFileChooserService implements FileChooserService {
+
+ static {
+ System.loadLibrary("FileChooser");
+ initFiles();
+ }
+
+ private static final ObjectProperty selectedFile = new SimpleObjectProperty<>();
+ private static ObjectProperty result;
+
+ @Override
+ public Optional loadFile() {
+ result = new SimpleObjectProperty<>();
+ selectFile();
+ try {
+ Platform.enterNestedEventLoop(result);
+ } catch (Exception e) {
+ System.out.println("GalleryActivity: enterNestedEventLoop failed: " + e);
+ }
+ return Optional.ofNullable(result.get());
+ }
+
+ // native
+ private static native void initFiles(); // init IDs for java callbacks from native
+ public static native void selectFile();
+
+ // callback
+ // Still needs some changes/adaptions to work for all files instead of just images.
+ public static void setResult(String v, String filePath) {
+ if (v != null && !v.isEmpty()) {
+ try {
+ byte[] fileBytes = Base64.getDecoder().decode(v.replaceAll("\\s+", "").getBytes());
+ selectedFile.set(new File(filePath));
+ Image temp = new Image(new ByteArrayInputStream(fileBytes));
+ result.set(new File(temp.getUrl()));
+ } catch (Exception ex) {
+ System.err.println("Error setResult: " + ex);
+ }
+ }
+ Platform.runLater(() -> {
+ try {
+ Platform.exitNestedEventLoop(result, null);
+ } catch (Exception e) {
+ System.out.println("GalleryActivity: exitNestedEventLoop failed: " + e);
+ }
+ });
+ }
+}
diff --git a/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/package-info.java b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/package-info.java
new file mode 100644
index 00000000..b3738459
--- /dev/null
+++ b/modules/file-chooser/src/main/java/com/gluonhq/attach/filechooser/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2016, 2019 Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Primary API package for Attach - Pictures plugin,
+ * contains the interface {@link com.gluonhq.attach.filechooser.FileChooserService} and related classes.
+ */
+package com.gluonhq.attach.filechooser;
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/java/module-info.java b/modules/file-chooser/src/main/java/module-info.java
new file mode 100644
index 00000000..d57388b4
--- /dev/null
+++ b/modules/file-chooser/src/main/java/module-info.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+module com.gluonhq.attach.filechooser {
+
+ requires javafx.graphics;
+ requires com.gluonhq.attach.util;
+
+ exports com.gluonhq.attach.filechooser;
+}
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/native/android/c/filechooser.c b/modules/file-chooser/src/main/native/android/c/filechooser.c
new file mode 100644
index 00000000..12c7014b
--- /dev/null
+++ b/modules/file-chooser/src/main/native/android/c/filechooser.c
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2020, 2021, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "util.h"
+
+static jclass jGraalFileChooserClass;
+static jmethodID jGraalSendFileMethod;
+
+static jclass jFileChooserServiceClass;
+static jobject jDalvikFileChooserService;
+static jmethodID jFileChooserServiceSelectFileMethod;
+
+void initializeFileChooserGraalHandles(JNIEnv *graalEnv) {
+ jGraalFileChooserClass = (*graalEnv)->NewGlobalRef(graalEnv, (*graalEnv)->FindClass(graalEnv, "com/gluonhq/attach/filechooser/impl/AndroidFileChooserService"));
+ jGraalSendFileMethod = (*graalEnv)->GetStaticMethodID(graalEnv, jGraalFileChooserClass, "setResult", "(Ljava/lang/String;I)V");
+}
+
+void initializeFileChooserDalvikHandles() {
+ jFileChooserServiceClass = GET_REGISTER_DALVIK_CLASS(jFileChooserServiceClass, "com/gluonhq/helloandroid/DalvikFileChooserService");
+ ATTACH_DALVIK();
+ jmethodID jFileChooserServiceInitMethod = (*dalvikEnv)->GetMethodID(dalvikEnv, jFileChooserServiceClass, "", "(Landroid/app/Activity;)V");
+ jFileChooserServiceSelectFileMethod = (*dalvikEnv)->GetMethodID(dalvikEnv, jFileChooserServiceClass, "selectFile", "()V");
+
+ jobject jActivity = substrateGetActivity();
+ jobject jtmpobj = (*dalvikEnv)->NewObject(dalvikEnv, jFileChooserServiceClass, jFileChooserServiceInitMethod, jActivity);
+ jDalvikFileChooserService = (*dalvikEnv)->NewGlobalRef(dalvikEnv, jtmpobj);
+ DETACH_DALVIK();
+
+}
+
+//////////////////////////
+// From Graal to native //
+//////////////////////////
+
+
+JNIEXPORT jint JNICALL
+JNI_OnLoad_filechooser(JavaVM *vm, void *reserved)
+{
+ATTACH_LOG_INFO("JNI_OnLoad_filechooser called");
+#ifdef JNI_VERSION_1_8
+ JNIEnv* graalEnv;
+ if ((*vm)->GetEnv(vm, (void **)&graalEnv, JNI_VERSION_1_8) != JNI_OK) {
+ ATTACH_LOG_WARNING("Error initializing native FileChooser from OnLoad");
+ return JNI_FALSE;
+ }
+ ATTACH_LOG_FINE("[FileChooser] Initializing native FileChooser from OnLoad");
+ initializeFileChooserGraalHandles(graalEnv);
+ initializeFileChooserDalvikHandles();
+ ATTACH_LOG_FINE("Initializing native FileChooser done");
+ return JNI_VERSION_1_8;
+#else
+ #error Error: Java 8+ SDK is required to compile Attach
+#endif
+}
+
+// from Java to Android
+
+JNIEXPORT void JNICALL Java_com_gluonhq_attach_filechooser_impl_AndroidFileChooserService_selectFile
+(JNIEnv *env, jclass jClass)
+{
+ ATTACH_DALVIK();
+ (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikFileChooserService, jFileChooserServiceSelectFileMethod);
+ DETACH_DALVIK();
+}
+
+///////////////////////////
+// From Dalvik to native //
+///////////////////////////
+
+JNIEXPORT void JNICALL Java_com_gluonhq_helloandroid_DalvikFileChooserService_sendFile(JNIEnv *env, jobject service, jstring path, jint rotate) {
+ ATTACH_LOG_FINE("Send File\n");
+ const char *pathChars = (*env)->GetStringUTFChars(env, path, NULL);
+ ATTACH_GRAAL();
+ jstring jpath = (*graalEnv)->NewStringUTF(graalEnv, pathChars);
+ (*graalEnv)->CallStaticVoidMethod(graalEnv, jGraalFileChooserClass, jGraalSendFileMethod, jpath, rotate);
+ DETACH_GRAAL();
+}
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/native/android/dalvik/DalvikFileChooserService.java b/modules/file-chooser/src/main/native/android/dalvik/DalvikFileChooserService.java
new file mode 100644
index 00000000..c91ff24b
--- /dev/null
+++ b/modules/file-chooser/src/main/native/android/dalvik/DalvikFileChooserService.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2020, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.helloandroid;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.ExifInterface;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import static android.app.Activity.RESULT_OK;
+
+public class DalvikFileChooserService {
+
+ private static final String TAG = Util.TAG;
+ private static final int RESULT_OK = -1;
+ private static final int SELECT_FILE = 1;
+
+ private final Activity activity;
+ private final boolean debug;
+ private boolean verified;
+
+ private final String authority;
+ private String filePath;
+
+ public DalvikFileChooserService(Activity activity) {
+ this.activity = activity;
+ this.debug = Util.isDebug();
+ authority = activity.getPackageName() + ".fileprovider";
+ clearCache();
+ }
+
+ private boolean verifyPermissions() {
+ if (!verified) {
+ verified = Util.verifyPermissions(Manifest.permission.CAMERA,
+ Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+ return verified;
+ }
+
+ private void selectFile() {
+ Log.v(TAG, "Select File method called.");
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setType("*/*");
+
+ if (activity == null) {
+ Log.e(TAG, "Activity not found. This service is not allowed when "
+ + "running in background mode or from wearable");
+ return;
+ }
+
+ IntentHandler intentHandler = new IntentHandler() {
+ @Override
+ public void gotActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == SELECT_FILE && resultCode == RESULT_OK) {
+ Log.v(TAG, "File successfully selected");
+ Uri selectedFileUri = intent.getData();
+ if (selectedFileUri != null) {
+ if (debug) {
+ Log.v(TAG, "Copy file");
+ }
+ File cachePhotoFile = copyFile(selectedFileUri);
+ if (debug) {
+ Log.v(TAG, "Setting file: " + cachePhotoFile.getAbsolutePath());
+ }
+ if (cachePhotoFile.exists()) {
+ if (debug) {
+ Log.v(TAG, "File located at " + cachePhotoFile.getAbsolutePath());
+ }
+ sendFile(cachePhotoFile.getAbsolutePath(), 0);
+ }
+ }
+ } else {
+ Log.v(TAG, "File not successfully selected");
+ }
+ }
+ };
+
+ Util.setOnActivityResultHandler(intentHandler);
+
+ // check for permissions
+ if (intent.resolveActivity(activity.getPackageManager()) != null) {
+ activity.startActivityForResult(Intent.createChooser(intent, "Select File"), SELECT_FILE);
+ } else {
+ Log.e(TAG, "GalleryActivity: resolveActivity failed");
+ }
+ }
+
+ private void clearCache() { // Adapted for all files
+ File[] files = activity.getCacheDir().listFiles();
+ for (File file : files) {
+ file.delete();
+ }
+ }
+
+ private String getFileName(Uri uri) {
+ try (Cursor cursor = activity.getContentResolver().query(uri, null, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
+ }
+ }
+ return "image.jpg";
+ }
+
+ private File copyFile(Uri uri) {
+ File selectedFile = new File(activity.getCacheDir(), getFileName(uri));
+ try (InputStream is = activity.getContentResolver().openInputStream(uri);
+ OutputStream os = new FileOutputStream(selectedFile)) {
+ byte[] buffer = new byte[8 * 1024];
+ int bytesRead;
+ while ((bytesRead = is.read(buffer)) != -1) {
+ os.write(buffer, 0, bytesRead);
+ }
+ } catch (FileNotFoundException ex) {
+ Log.e(TAG, "FILE NOT FOUND", ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "IO ISSUES", ex);
+ }
+ return selectedFile;
+ }
+
+ // native
+ private native void sendFile(String filePath, int rotate);
+
+}
diff --git a/modules/file-chooser/src/main/native/ios/FileChooser.h b/modules/file-chooser/src/main/native/ios/FileChooser.h
new file mode 100644
index 00000000..53b54ee7
--- /dev/null
+++ b/modules/file-chooser/src/main/native/ios/FileChooser.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2016, 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import
+#include "jni.h"
+#include "AttachMacros.h"
+
+@interface Pictures : UIViewController {}
+// - (void) takePicture;
+ - (void) selectFile;
+@end
+
+void sendFileChooserResult(NSString *picResult, NSString *picPath);
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/native/ios/FileChooser.m b/modules/file-chooser/src/main/native/ios/FileChooser.m
new file mode 100644
index 00000000..e47aacbe
--- /dev/null
+++ b/modules/file-chooser/src/main/native/ios/FileChooser.m
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2016, 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "FileChooser.h"
+
+JNIEnv *env;
+
+JNIEXPORT jint JNICALL
+JNI_OnLoad_filechooser(JavaVM *vm, void *reserved)
+{
+#ifdef JNI_VERSION_1_8
+ //min. returned JNI_VERSION required by JDK8 for builtin libraries
+ if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK) {
+ return JNI_VERSION_1_4;
+ }
+ return JNI_VERSION_1_8;
+#else
+ return JNI_VERSION_1_4;
+#endif
+}
+
+static int filesInited = 0;
+
+// FileChooser
+jclass mat_jFileChooserServiceClass;
+jmethodID mat_jFileChooserService_setResult = 0;
+FileChooser *_filechooser;
+BOOL saveFile;
+
+
+JNIEXPORT void JNICALL Java_com_gluonhq_attach_filechooser_impl_IOSFileChooserService_initFileChooser
+(JNIEnv *env, jclass jClass)
+{
+ if (filesInited)
+ {
+ return;
+ }
+ filesInited = 1;
+
+ mat_jFileChooserServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/filechooser/impl/IOSFileChooserService"));
+ mat_jFileChooserService_setResult = (*env)->GetStaticMethodID(env, mat_jFileChooserServiceClass, "setResult", "(Ljava/lang/String;Ljava/lang/String;)V");
+}
+
+void sendFileChooserResult(NSString *fileResult, NSString *filePath) {
+ if (fileResult)
+ {
+ const char *fileChars = [fileResult UTF8String];
+ jstring jfile = (*env)->NewStringUTF(env, fileChars);
+ const char *pathChars = [filePath UTF8String];
+ jstring jpath = (*env)->NewStringUTF(env, pathChars);
+ (*env)->CallStaticVoidMethod(env, mat_jFileChooserServiceClass, mat_jFileChooserService_setResult, jfile, jpath);
+ (*env)->DeleteLocalRef(env, jfile);
+ (*env)->DeleteLocalRef(env, jpath);
+ AttachLog(@"Finished sending file");
+ } else
+ {
+ (*env)->CallStaticVoidMethod(env, mat_jFileChooserServiceClass, mat_jFileChooserService_setResult, NULL, NULL);
+ }
+}
+
+JNIEXPORT void JNICALL Java_com_gluonhq_attach_filechooser_impl_IOSFileChooserService_selectFile
+(JNIEnv *env, jclass jClass)
+{
+ _files = [[FileChooser alloc] init];
+ [_files selectFile];
+ return;
+}
+
+@implementation FileChooser
+
+- (void)selectFile {
+ if(![[UIApplication sharedApplication] keyWindow])
+ {
+ AttachLog(@"key window was nil");
+ return;
+ }
+
+ NSArray *views = [[[UIApplication sharedApplication] keyWindow] subviews];
+ if(![views count]) {
+ AttachLog(@"views size was 0");
+ return;
+ }
+
+ UIView *_currentView = views[0];
+
+ UIImagePickerController *picker = [[UIImagePickerController alloc] init];
+ picker.delegate = self;
+ picker.allowsEditing = NO;
+ picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
+
+ [_currentView.window addSubview:picker.view];
+
+}
+
+- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
+
+ AttachLog(@"Encoding and sending retrieved file");
+ UIImage *originalImage = info[UIImagePickerControllerOriginalImage];
+
+ if (saveFile == YES)
+ {
+ AttachLog(@"Saving file...");
+ UIImageWriteToSavedPhotosAlbum(originalImage, nil, nil, nil);
+ }
+
+// The original image could be too big (ie 3264x2448) and not properly rotated,
+// what leads to: core.memory: GC Warning: Repeated allocation of very large block
+// and even: malloc: *** mach_vm_map(size=67108864) failed (error code=3) -> NPE at
+// com.sun.prism.impl.BaseGraphics.drawTexture(BaseGraphics.java)
+
+// Solution: limit max size to 1280x1280, and rotate properly:
+
+ UIImage *image = [self scaleAndRotateImage:originalImage];
+
+ NSData *imageData = UIImagePNGRepresentation(image);
+
+ NSString *base64StringOfImage = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
+
+ NSData *originalData = UIImagePNGRepresentation(originalImage);
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+ [formatter setDateFormat:@"yyyy-MM-dd_HH-mm-ss"];
+ NSString *stringFromDate = [formatter stringFromDate:[NSDate date]];
+
+ NSString *filePath = [[paths objectAtIndex:0] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_%@.png",@"Image",stringFromDate]];
+ [originalData writeToFile:filePath atomically:YES];
+
+ sendFileChooserResult(base64StringOfImage, filePath);
+
+ [picker dismissViewControllerAnimated:YES completion:nil];
+ [picker.view removeFromSuperview];
+ [picker release];
+
+}
+
+- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
+
+ AttachLog(@"Camera cancelled");
+
+ NSString *result = nil;
+ sendFileChooserResult(result, result);
+
+ [picker dismissViewControllerAnimated:YES completion:nil];
+ [picker.view removeFromSuperview];
+ [picker release];
+
+}
+
+- (UIImage *)scaleAndRotateImage:(UIImage *)image
+{
+// FIXME: hardcoded value, add it as a parameter
+ int kMaxResolution = 1280;
+
+ CGImageRef imgRef = image.CGImage;
+
+ CGFloat width = CGImageGetWidth(imgRef);
+ CGFloat height = CGImageGetHeight(imgRef);
+
+ CGAffineTransform transform = CGAffineTransformIdentity;
+ CGRect bounds = CGRectMake(0, 0, width, height);
+ if (width > kMaxResolution || height > kMaxResolution) {
+ CGFloat ratio = width/height;
+ if (ratio > 1) {
+ bounds.size.width = kMaxResolution;
+ bounds.size.height = bounds.size.width / ratio;
+ }
+ else {
+ bounds.size.height = kMaxResolution;
+ bounds.size.width = bounds.size.height * ratio;
+ }
+ }
+
+ CGFloat scaleRatio = bounds.size.width / width;
+ CGSize imageSize = CGSizeMake(CGImageGetWidth(imgRef), CGImageGetHeight(imgRef));
+ CGFloat boundHeight;
+ UIImageOrientation orient = image.imageOrientation;
+ switch(orient) {
+
+ case UIImageOrientationUp: //EXIF = 1
+ transform = CGAffineTransformIdentity;
+ break;
+
+ case UIImageOrientationUpMirrored: //EXIF = 2
+ transform = CGAffineTransformMakeTranslation(imageSize.width, 0.0);
+ transform = CGAffineTransformScale(transform, -1.0, 1.0);
+ break;
+
+ case UIImageOrientationDown: //EXIF = 3
+ transform = CGAffineTransformMakeTranslation(imageSize.width, imageSize.height);
+ transform = CGAffineTransformRotate(transform, M_PI);
+ break;
+
+ case UIImageOrientationDownMirrored: //EXIF = 4
+ transform = CGAffineTransformMakeTranslation(0.0, imageSize.height);
+ transform = CGAffineTransformScale(transform, 1.0, -1.0);
+ break;
+
+ case UIImageOrientationLeftMirrored: //EXIF = 5
+ boundHeight = bounds.size.height;
+ bounds.size.height = bounds.size.width;
+ bounds.size.width = boundHeight;
+ transform = CGAffineTransformMakeTranslation(imageSize.height, imageSize.width);
+ transform = CGAffineTransformScale(transform, -1.0, 1.0);
+ transform = CGAffineTransformRotate(transform, 3.0 * M_PI / 2.0);
+ break;
+
+ case UIImageOrientationLeft: //EXIF = 6
+ boundHeight = bounds.size.height;
+ bounds.size.height = bounds.size.width;
+ bounds.size.width = boundHeight;
+ transform = CGAffineTransformMakeTranslation(0.0, imageSize.width);
+ transform = CGAffineTransformRotate(transform, 3.0 * M_PI / 2.0);
+ break;
+
+ case UIImageOrientationRightMirrored: //EXIF = 7
+ boundHeight = bounds.size.height;
+ bounds.size.height = bounds.size.width;
+ bounds.size.width = boundHeight;
+ transform = CGAffineTransformMakeScale(-1.0, 1.0);
+ transform = CGAffineTransformRotate(transform, M_PI / 2.0);
+ break;
+
+ case UIImageOrientationRight: //EXIF = 8
+ boundHeight = bounds.size.height;
+ bounds.size.height = bounds.size.width;
+ bounds.size.width = boundHeight;
+ transform = CGAffineTransformMakeTranslation(imageSize.height, 0.0);
+ transform = CGAffineTransformRotate(transform, M_PI / 2.0);
+ break;
+
+ default:
+ [NSException raise:NSInternalInconsistencyException format:@"Invalid image orientation"];
+
+ }
+
+ UIGraphicsBeginImageContext(bounds.size);
+
+ CGContextRef context = UIGraphicsGetCurrentContext();
+
+ if (orient == UIImageOrientationRight || orient == UIImageOrientationLeft) {
+ CGContextScaleCTM(context, -scaleRatio, scaleRatio);
+ CGContextTranslateCTM(context, -height, 0);
+ }
+ else {
+ CGContextScaleCTM(context, scaleRatio, -scaleRatio);
+ CGContextTranslateCTM(context, 0, -height);
+ }
+
+ CGContextConcatCTM(context, transform);
+
+ CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, width, height), imgRef);
+ UIImage *imageCopy = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ return imageCopy;
+}
+
+@end
diff --git a/modules/file-chooser/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json b/modules/file-chooser/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json
new file mode 100644
index 00000000..9f9e0b90
--- /dev/null
+++ b/modules/file-chooser/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name" : "com.gluonhq.attach.filechooser.impl.AndroidFileChooserService",
+ "methods":[{"name":"setResult","parameterTypes":["java.lang.String", "int"] }]
+ }
+]
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/resources/META-INF/substrate/config/jniconfig-arm64-ios.json b/modules/file-chooser/src/main/resources/META-INF/substrate/config/jniconfig-arm64-ios.json
new file mode 100644
index 00000000..ce9cf50a
--- /dev/null
+++ b/modules/file-chooser/src/main/resources/META-INF/substrate/config/jniconfig-arm64-ios.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name" : "com.gluonhq.attach.filechooser.impl.IOSFileChooserService",
+ "methods":[{"name":"setResult","parameterTypes":["java.lang.String","java.lang.String"] }]
+ }
+]
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json b/modules/file-chooser/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json
new file mode 100644
index 00000000..9d8f32c8
--- /dev/null
+++ b/modules/file-chooser/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name" : "com.gluonhq.attach.filechooser.impl.AndroidFileChooserService",
+ "methods":[{"name":"","parameterTypes":[] }]
+ }
+]
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json b/modules/file-chooser/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json
new file mode 100644
index 00000000..f7764d87
--- /dev/null
+++ b/modules/file-chooser/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name" : "com.gluonhq.attach.filechooser.impl.IOSFileChooserService",
+ "methods":[{"name":"","parameterTypes":[] }]
+ }
+]
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml b/modules/file-chooser/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml
new file mode 100644
index 00000000..0af0a2d8
--- /dev/null
+++ b/modules/file-chooser/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/file-chooser/src/main/resources/META-INF/substrate/dalvik/res/xml/file_provider_paths.xml b/modules/file-chooser/src/main/resources/META-INF/substrate/dalvik/res/xml/file_provider_paths.xml
new file mode 100644
index 00000000..37004306
--- /dev/null
+++ b/modules/file-chooser/src/main/resources/META-INF/substrate/dalvik/res/xml/file_provider_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/open/build.gradle b/modules/open/build.gradle
new file mode 100644
index 00000000..71864f98
--- /dev/null
+++ b/modules/open/build.gradle
@@ -0,0 +1,6 @@
+dependencies {
+ implementation project(':util')
+}
+
+ext.moduleName = 'com.gluonhq.attach.open'
+ext.description = 'Common API to access opening features'
\ No newline at end of file
diff --git a/modules/open/src/main/java/com/gluonhq/attach/open/OpenService.java b/modules/open/src/main/java/com/gluonhq/attach/open/OpenService.java
new file mode 100644
index 00000000..ccfa77ac
--- /dev/null
+++ b/modules/open/src/main/java/com/gluonhq/attach/open/OpenService.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2017, 2019 Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.open;
+
+import com.gluonhq.attach.util.Services;
+
+import java.io.File;
+import java.util.Optional;
+
+/**
+ *
+ * The OpenService provides a way to open content (text and/or files) from the
+ * current app by using other suitable apps existing in the user device.
+ *
+ * Example
+ *
+ * {@code OpenService.create().ifPresent(service -> {
+ * service.open("This is the subject", "This is the content of the message");
+ * });}
+ *
+ * When opening files, the Attach
+ * StorageService can be used to create/read the file. Note that on Android,
+ * the file has to be located in a public folder (see
+ * StorageService#getPublicStorage), or opening it won't be allowed.
+ *
+ * Example
+ *
+ * {@code
+ * File root = StorageService.create()
+ * .flatMap(s -> s.getPublicStorage("Documents"))
+ * .orElseThrow(() -> new RuntimeException("Documents not available"));
+ *
+ * // select or create a file within Documents folder:
+ * File file = new File(root, "myFile.txt");
+ *
+ * // open the file
+ * OpenService.create().ifPresent(service -> {
+ * service.open("text/plain", file);
+ * });
+ * }
+ *
+ * Android Configuration
+ * Create the file {@code /src/android/res/xml/file_provider_paths.xml} with
+ * the following content that allows access to the external storage:
+ *
+ * Note: these modifications are handled automatically by Client plugin if it is used.
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ * }
+ *
+ *
+ * And add this {@code provider} to the manifest, within the Application tag:
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ *
+ * iOS Configuration: nothing is required, but if the app is opening
+ * images to the user's local gallery, the key
+ * {@code NSPhotoLibraryUsageDescription} is required in the app's plist file.
+ *
+ * Other similar keys could be required as well.
+ *
+ * @since 3.4.0
+ */
+public interface OpenService {
+
+ /**
+ * Returns an instance of {@link OpenService}.
+ * @return An instance of {@link OpenService}.
+ */
+ static Optional create() {
+ return Services.get(OpenService.class);
+ }
+
+
+ /**
+ * Allows opening a file, selecting from the suitable apps available in the
+ * user device.
+ *
+ * Note: On Android, the file has to be located in a public folder (see
+ * StorageService#getPublicStorage), or opening it won't be allowed.
+ *
+ * @param type On Android only, the MIME type of the file. It can be
+ * '∗/∗', but not empty. On iOS it can be null. Usual types are:
+ * - application/xml
+ * - application/zip
+ * - application/pdf
+ * - text/css
+ * - text/html
+ * - text/csv
+ * - text/plain
+ * - image/png
+ * - image/jpeg
+ * - image/gif
+ * - image/*
+ * @param file A valid file to be opened.
+ */
+ void open(String type, File file);
+
+ void open(File file);
+
+}
diff --git a/modules/open/src/main/java/com/gluonhq/attach/open/impl/AndroidOpenService.java b/modules/open/src/main/java/com/gluonhq/attach/open/impl/AndroidOpenService.java
new file mode 100644
index 00000000..8fc3a1b8
--- /dev/null
+++ b/modules/open/src/main/java/com/gluonhq/attach/open/impl/AndroidOpenService.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2017, 2020, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.open.impl;
+
+
+import com.gluonhq.attach.open.OpenService;
+import com.gluonhq.attach.util.Util;
+
+import java.io.File;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Create the file {@code /src/android/res/xml/file_provider_paths.xml} with
+ * the following content that allows access to the external storage:
+ *
+ *
+ * {@code
+ *
+ *
+ *
+ *
+ * }
+ *
+ *
+ * Add a {@code provider} to the AndroidManifest:
+ *
+ * {@code
+ *
+ * ...
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ */
+
+public class AndroidOpenService implements OpenService {
+
+ private static final Logger LOGGER = Logger.getLogger(AndroidOpenService.class.getName());
+
+ static {
+ System.loadLibrary("open");
+ }
+
+ public AndroidOpenService() {
+ }
+
+ /**
+ * Forces the specified MIME Type.
+ * @param type On Android only, the MIME type of the file. It can be
+ * '∗/∗', but not empty. On iOS it can be null. Usual types are:
+ * - application/xml
+ * - application/zip
+ * - application/pdf
+ * - text/css
+ * - text/html
+ * - text/csv
+ * - text/plain
+ * - image/png
+ * - image/jpeg
+ * - image/gif
+ * - image/*
+ * @param file A valid file to be opened.
+ */
+ @Override
+ public void open(String type, File file) {
+ if (file != null && file.exists()) {
+ if (Util.DEBUG) {
+ LOGGER.log(Level.INFO, "File to open: " + file);
+ }
+ } else {
+ LOGGER.log(Level.SEVERE, "Error: URL not valid for file: " + file);
+ return;
+ }
+ if (type == null || type.isEmpty()) {
+ LOGGER.log(Level.SEVERE, "Error: type not valid");
+ return;
+ }
+ openFileWithType(type,file.getAbsolutePath());
+ }
+
+ @Override
+ public void open(File file) {
+ if (file != null && file.exists()) {
+ if (Util.DEBUG) {
+ LOGGER.log(Level.INFO, "File to open: " + file);
+ }
+ } else {
+ LOGGER.log(Level.SEVERE, "Error: URL not valid for file: " + file);
+ return;
+ }
+ openFile(file.getAbsolutePath());
+ }
+
+
+ // native
+ private static native void openFile(String filePath);
+ private static native void openFileWithType(String type, String filePath);
+
+}
\ No newline at end of file
diff --git a/modules/open/src/main/java/com/gluonhq/attach/open/impl/DummyOpenService.java b/modules/open/src/main/java/com/gluonhq/attach/open/impl/DummyOpenService.java
new file mode 100644
index 00000000..ea033807
--- /dev/null
+++ b/modules/open/src/main/java/com/gluonhq/attach/open/impl/DummyOpenService.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.open.impl;
+
+import com.gluonhq.attach.open.OpenService;
+
+public abstract class DummyOpenService implements OpenService {
+
+}
diff --git a/modules/open/src/main/java/com/gluonhq/attach/open/impl/IOSOpenService.java b/modules/open/src/main/java/com/gluonhq/attach/open/impl/IOSOpenService.java
new file mode 100644
index 00000000..a4b4f349
--- /dev/null
+++ b/modules/open/src/main/java/com/gluonhq/attach/open/impl/IOSOpenService.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2017, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.attach.open.impl;
+
+
+import com.gluonhq.attach.open.OpenService;
+
+import java.io.File;
+
+
+public class IOSOpenService implements OpenService {
+
+ static {
+ System.loadLibrary("Open");
+ initOpen();
+ }
+
+ public IOSOpenService() {
+ }
+
+ @Override
+ public void open(File file) {
+ open(null, null, null, file);
+ }
+
+ @Override
+ public void open(String type, File file) {
+ open(null, null, type, file);
+ }
+
+ private void open(String subject, String contentText, String type, File file) {
+ if (subject == null) {
+ subject = "";
+ }
+ if (contentText == null) {
+ contentText = "";
+ }
+ if (file != null && file.exists()) {
+ System.out.println("File to open: " + file);
+ } else {
+ System.out.println("Error: URL not valid");
+ return;
+ }
+ nativeOpen(subject, contentText, file.getAbsolutePath());
+ }
+
+ // native
+ private static native void initOpen(); // init IDs for java callbacks from native
+ private static native void nativeOpen(String subject, String message, String filePath);
+
+}
\ No newline at end of file
diff --git a/modules/open/src/main/java/com/gluonhq/attach/open/package-info.java b/modules/open/src/main/java/com/gluonhq/attach/open/package-info.java
new file mode 100644
index 00000000..656c5576
--- /dev/null
+++ b/modules/open/src/main/java/com/gluonhq/attach/open/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2017, 2019 Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Primary API package for Attach - Open plugin,
+ * contains the interface {@link com.gluonhq.attach.open.OpenService} and related classes.
+ */
+package com.gluonhq.attach.open;
\ No newline at end of file
diff --git a/modules/open/src/main/java/module-info.java b/modules/open/src/main/java/module-info.java
new file mode 100644
index 00000000..7799144f
--- /dev/null
+++ b/modules/open/src/main/java/module-info.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+module com.gluonhq.attach.open {
+
+ requires com.gluonhq.attach.util;
+
+ exports com.gluonhq.attach.open;
+ exports com.gluonhq.attach.open.impl to com.gluonhq.attach.util;
+}
\ No newline at end of file
diff --git a/modules/open/src/main/native/android/c/open.c b/modules/open/src/main/native/android/c/open.c
new file mode 100644
index 00000000..50252e8c
--- /dev/null
+++ b/modules/open/src/main/native/android/c/open.c
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2020, 2021, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "util.h"
+
+static jclass jOpenServiceClass;
+static jobject jDalvikOpenService;
+static jmethodID jOpenServiceOpenFile;
+static jmethodID jOpenServiceOpenFileWithType;
+
+static void initializeOpenDalvikHandles() {
+ jOpenServiceClass = GET_REGISTER_DALVIK_CLASS(jOpenServiceClass, "com/gluonhq/helloandroid/DalvikOpenService");
+ ATTACH_DALVIK();
+ jmethodID jOpenServiceInitMethod = (*dalvikEnv)->GetMethodID(dalvikEnv, jOpenServiceClass, "", "(Landroid/app/Activity;)V");
+ jOpenServiceOpenFile = (*dalvikEnv)->GetMethodID(dalvikEnv, jOpenServiceClass, "openFile", "(Ljava/lang/String;)V");
+ jOpenServiceOpenFileWithType = (*dalvikEnv)->GetMethodID(dalvikEnv, jOpenServiceClass, "openFileWithType", "(Ljava/lang/String;Ljava/lang/String;)V");
+ jobject jActivity = substrateGetActivity();
+ jobject jtmpobj = (*dalvikEnv)->NewObject(dalvikEnv, jOpenServiceClass, jOpenServiceInitMethod, jActivity);
+ jDalvikOpenService = (*dalvikEnv)->NewGlobalRef(dalvikEnv, jtmpobj);
+ DETACH_DALVIK();
+}
+
+//////////////////////////
+// From Graal to native //
+//////////////////////////
+
+
+JNIEXPORT jint JNICALL
+JNI_OnLoad_open(JavaVM *vm, void *reserved)
+{
+ JNIEnv* graalEnv;
+ ATTACH_LOG_INFO("JNI_OnLoad_open called");
+#ifdef JNI_VERSION_1_8
+ if ((*vm)->GetEnv(vm, (void **)&graalEnv, JNI_VERSION_1_8) != JNI_OK) {
+ ATTACH_LOG_WARNING("Error initializing native Open from OnLoad");
+ return JNI_FALSE;
+ }
+ ATTACH_LOG_FINE("[Open Service] Initializing native Open from OnLoad");
+ initializeOpenDalvikHandles();
+ return JNI_VERSION_1_8;
+#else
+ #error Error: Java 8+ SDK is required to compile Attach
+#endif
+}
+
+// from Java to Android
+
+JNIEXPORT void JNICALL Java_com_gluonhq_attach_open_impl_AndroidOpenService_openFile
+(JNIEnv *env, jclass jClass, jstring jfilename)
+{
+ const char *filenameChars = (*env)->GetStringUTFChars(env, jfilename, NULL);
+ ATTACH_DALVIK();
+ jstring dfilename = (*dalvikEnv)->NewStringUTF(dalvikEnv, filenameChars);
+ if (isDebugAttach()) {
+ ATTACH_LOG_FINE("Open file, filename = %s\n",
+ filenameChars);
+ }
+ (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikOpenService, jOpenServiceOpenFile, dfilename);
+ DETACH_DALVIK();
+}
+
+JNIEXPORT void JNICALL Java_com_gluonhq_attach_open_impl_AndroidOpenService_openFileWithType
+(JNIEnv *env, jclass jClass, jstring jtype, jstring jfilename)
+{
+
+ const char *typeChars = (*env)->GetStringUTFChars(env, jtype, NULL);
+ const char *filenameChars = (*env)->GetStringUTFChars(env, jfilename, NULL);
+ ATTACH_DALVIK();
+ jstring dtype = (*dalvikEnv)->NewStringUTF(dalvikEnv, typeChars);
+ jstring dfilename = (*dalvikEnv)->NewStringUTF(dalvikEnv, filenameChars);
+ if (isDebugAttach()) {
+ ATTACH_LOG_FINE("Open file, type = %s, filename = %s\n",
+ typeChars, filenameChars);
+ }
+ (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikOpenService, jOpenServiceOpenFileWithType, dtype, dfilename);
+ DETACH_DALVIK();
+}
+
diff --git a/modules/open/src/main/native/android/dalvik/DalvikOpenService.java b/modules/open/src/main/native/android/dalvik/DalvikOpenService.java
new file mode 100644
index 00000000..22782447
--- /dev/null
+++ b/modules/open/src/main/native/android/dalvik/DalvikOpenService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2020, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.gluonhq.helloandroid;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+import java.io.File;
+import java.util.List;
+
+public class DalvikOpenService {
+
+ private static final String TAG = Util.TAG;
+
+ private final Activity activity;
+ private final String authority;
+ private final boolean debug;
+
+ public DalvikOpenService(Activity activity) {
+ this.activity = activity;
+ authority = activity.getPackageName() + ".fileprovider";
+ debug = Util.isDebug();
+ }
+
+ private void openFile(String fileName) {
+ Intent openingIntent = new Intent(Intent.ACTION_VIEW);
+ File file = new File(fileName);
+
+ if (file != null && file.exists()) {
+ final Uri uriFile = FileProvider.getUriForFile(activity, authority, file);
+ openingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ openingIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ openingIntent.setData(uriFile);
+ } else {
+ Log.e(TAG, "Error: A non empty file is required");
+ return;
+ }
+
+ Intent chooser = Intent.createChooser(openingIntent, "Open file");
+ chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ chooser.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ activity.startActivity(chooser);
+ }
+
+ private void openFileWithType(String type, String fileName) {
+ Intent openingIntent = new Intent(Intent.ACTION_VIEW);
+
+ if (type != null && !type.isEmpty()) {
+ openingIntent.setType(type);
+ } else {
+ Log.e(TAG, "Error: A non empty type is required");
+ return;
+ }
+ File file = new File(fileName);
+ if (file != null && file.exists()) {
+ if (debug) {
+ Log.v(TAG, String.format("File to open: %s", file));
+ Log.v(TAG, "Application name provider: " + authority);
+ }
+ final Uri uriFile = FileProvider.getUriForFile(activity, authority, file);
+ if (debug) {
+ Log.v(TAG, String.format("Opened file URI: %s", file));
+ }
+ openingIntent.putExtra(Intent.EXTRA_STREAM, uriFile);
+ openingIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ } else {
+ Log.e(TAG, "Error: A non empty file is required");
+ return;
+ }
+ if (debug) {
+ Log.v(TAG, "Start file opening intent");
+ }
+
+ Intent chooser = Intent.createChooser(openingIntent, null);
+ chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ chooser.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ activity.startActivity(chooser);
+
+ }
+
+
+}
diff --git a/modules/open/src/main/native/ios/Open.h b/modules/open/src/main/native/ios/Open.h
new file mode 100644
index 00000000..551f18c8
--- /dev/null
+++ b/modules/open/src/main/native/ios/Open.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2017, 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#import
+#include "jni.h"
+#include "AttachMacros.h"
+
+@interface Open : UIViewController
+{
+}
+ @property (nonatomic, strong) NSString *message;
+ @property (nonatomic, strong) NSString *subject;
+ @property (nonatomic, strong) NSString *filePath;
+ - (void) openText:(NSString *)subject message:(NSString *)message filePath:(NSString *)filePath;
+@end
diff --git a/modules/open/src/main/native/ios/Open.m b/modules/open/src/main/native/ios/Open.m
new file mode 100644
index 00000000..994ad561
--- /dev/null
+++ b/modules/open/src/main/native/ios/Open.m
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2017, 2019, Gluon
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "Open.h"
+
+JNIEnv *env;
+
+JNIEXPORT jint JNICALL
+JNI_OnLoad_Open(JavaVM *vm, void *reserved)
+{
+#ifdef JNI_VERSION_1_8
+ //min. returned JNI_VERSION required by JDK8 for builtin libraries
+ if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK) {
+ return JNI_VERSION_1_4;
+ }
+ return JNI_VERSION_1_8;
+#else
+ return JNI_VERSION_1_4;
+#endif
+}
+
+static int OpenInited = 0;
+
+Open *_open;
+
+JNIEXPORT void JNICALL Java_com_gluonhq_attach_open_impl_IOSOpenService_initOpen
+(JNIEnv *env, jclass jClass)
+{
+ if (OpenInited)
+ {
+ return;
+ }
+ OpenInited = 1;
+
+ AttachLog(@"Init Open");
+ _open = [[Open alloc] init];
+}
+
+JNIEXPORT void JNICALL Java_com_gluonhq_attach_open_impl_IOSOpenService_nativeOpen
+(JNIEnv *env, jclass jClass, jstring jsubject, jstring jmessage, jstring jfilePath) {
+
+ const jchar *charsSubject = (*env)->GetStringChars(env, jsubject, NULL);
+ NSString *subject = [NSString stringWithCharacters:(UniChar *)charsSubject length:(*env)->GetStringLength(env, jsubject)];
+ (*env)->ReleaseStringChars(env, jsubject, charsSubject);
+
+ const jchar *charsMessage = (*env)->GetStringChars(env, jmessage, NULL);
+ NSString *message = [NSString stringWithCharacters:(UniChar *)charsMessage length:(*env)->GetStringLength(env, jmessage)];
+ (*env)->ReleaseStringChars(env, jmessage, charsMessage);
+
+ const jchar *charsFilePath = (*env)->GetStringChars(env, jfilePath, NULL);
+ NSString *filePath = [NSString stringWithCharacters:(UniChar *)charsFilePath length:(*env)->GetStringLength(env, jfilePath)];
+ (*env)->ReleaseStringChars(env, jfilePath, charsFilePath);
+
+ [_open openText: subject message:message filePath:filePath];
+
+}
+
+
+@implementation Open
+
+- (void) openText: (NSString *)subject message:(NSString *)message filePath:(NSString *)filePath
+{
+ _subject = [[NSString alloc] initWithString:subject];
+ _message = [[NSString alloc] initWithString:message];
+ _filePath = [[NSString alloc] initWithString:filePath];
+
+ if(![[UIApplication openedApplication] keyWindow])
+ {
+ AttachLog(@"key window was nil");
+ return;
+ }
+
+ NSArray *views = [[[UIApplication openedApplication] keyWindow] subviews];
+ if(![views count]) {
+ AttachLog(@"views size was 0");
+ return;
+ }
+
+ UIViewController *rootViewController = [[[UIApplication openedApplication] keyWindow] rootViewController];
+ if(!rootViewController)
+ {
+ AttachLog(@"rootViewController was nil");
+ return;
+ }
+
+ NSMutableArray *items = [[NSMutableArray alloc] init];
+ [items addObject:self];
+
+ if ([_filePath length] > 0) {
+ [self logMessage:@"Open file: %@", _filePath];
+ NSURL *fileUrl = [NSURL fileURLWithPath:_filePath];
+ NSError *err;
+ if ([fileUrl checkResourceIsReachableAndReturnError:&err] == YES)
+ {
+ [self logMessage:@"Open fileUrl: %@", fileUrl];
+ [items addObject:fileUrl];
+ } else {
+ AttachLog(@"File resource not reachable: %@", err);
+ }
+ }
+
+ NSArray *itemsToOpen = [NSArray arrayWithArray:items];
+
+ UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:itemsToOpen applicationActivities:nil];
+ if ([activityViewController respondsToSelector:@selector(popoverPresentationController)]) {
+ activityViewController.popoverPresentationController.sourceView = views[0];
+ }
+
+ activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError){
+ [self logMessage:@"Activity Type selected: %@", activityType];
+ if (completed) {
+ [self logMessage:@"Selected activity was performed."];
+ } else {
+ if (activityType == NULL) {
+ [self logMessage:@"User dismissed the view controller without making a selection."];
+ } else {
+ [self logMessage:@"Activity was not performed."];
+ }
+ }
+ };
+
+ [rootViewController presentViewController:activityViewController animated:YES completion:nil];
+
+}
+
+- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController
+{
+ [self logMessage:@"Open activityViewControllerPlaceholderItem"];
+ return @"";
+}
+
+- (NSString *) activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(NSString *)activityType
+{
+ [self logMessage:@"Open subjectForActivityType %@ - Subject: %@", activityType, _subject];
+ return _subject;
+}
+
+- (nullable id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType
+{
+ [self logMessage:@"Open itemForActivityType %@ - Message: %@", activityType, _message];
+ return _message;
+}
+
+- (void) logMessage:(NSString *)format, ...;
+{
+ if (debugAttach)
+ {
+ va_list args;
+ va_start(args, format);
+ NSLogv([@"[Debug] " stringByAppendingString:format], args);
+ va_end(args);
+ }
+}
+@end
+
diff --git a/modules/open/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json b/modules/open/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json
new file mode 100644
index 00000000..36c98215
--- /dev/null
+++ b/modules/open/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name" : "com.gluonhq.attach.open.impl.AndroidOpenService",
+ "methods":[{"name":"","parameterTypes":[] }]
+ }
+]
\ No newline at end of file
diff --git a/modules/open/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json b/modules/open/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json
new file mode 100644
index 00000000..efd00f5b
--- /dev/null
+++ b/modules/open/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name" : "com.gluonhq.attach.open.impl.IOSOpenService",
+ "methods":[{"name":"","parameterTypes":[] }]
+ }
+]
\ No newline at end of file
diff --git a/modules/open/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml b/modules/open/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml
new file mode 100644
index 00000000..d3cfa87b
--- /dev/null
+++ b/modules/open/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/open/src/main/resources/META-INF/substrate/dalvik/res/xml/file_provider_paths.xml b/modules/open/src/main/resources/META-INF/substrate/dalvik/res/xml/file_provider_paths.xml
new file mode 100644
index 00000000..8248868e
--- /dev/null
+++ b/modules/open/src/main/resources/META-INF/substrate/dalvik/res/xml/file_provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 4cf9880c..c9bb83ee 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -23,10 +23,13 @@ include 'connectivity'
include 'device'
include 'dialer'
include 'display', 'display:desktop'
+include 'display'
+include 'file-chooser'
include 'in-app-billing'
include 'lifecycle', 'lifecycle:desktop'
include 'local-notifications'
include 'magnetometer'
+include 'open'
include 'orientation'
include 'pictures'
include 'position'
@@ -56,11 +59,13 @@ project(':device').projectDir = file('modules/device')
project(':dialer').projectDir = file('modules/dialer')
project(':display').projectDir = file('modules/display')
project(':display:desktop').projectDir = file('modules/display/desktop')
+project(':file-chooser').projectDir = file('modules/file-chooser')
project(':in-app-billing').projectDir = file('modules/in-app-billing')
project(':lifecycle').projectDir = file('modules/lifecycle')
project(':lifecycle:desktop').projectDir = file('modules/lifecycle/desktop')
project(':local-notifications').projectDir = file('modules/local-notifications')
project(':magnetometer').projectDir = file('modules/magnetometer')
+project(':open').projectDir = file('modules/open')
project(':orientation').projectDir = file('modules/orientation')
project(':pictures').projectDir = file('modules/pictures')
project(':position').projectDir = file('modules/position')