diff --git a/gradle/include/AttachMacros.h b/gradle/include/AttachMacros.h new file mode 100644 index 00000000..ef4c4b4c --- /dev/null +++ b/gradle/include/AttachMacros.h @@ -0,0 +1,49 @@ +/* + * 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. + */ + +#include + +static __inline__ void AttachLog(const char *file, int lineNumber, const char *funcName, NSString* format, ...) +{ + va_list argList; + va_start(argList, format); + NSString* formattedMessage = [[NSString alloc] initWithFormat: format arguments: argList]; + va_end(argList); + NSLog(@"[AttachLog] %@", formattedMessage); + + static NSDateFormatter* dateFormatter; + if (!dateFormatter) { + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"]; + [dateFormatter setTimeZone:[NSTimeZone systemTimeZone]]; + } + fprintf(stderr, "[AttachLog] %s %s:%3d %s\n", [[dateFormatter stringFromDate:[NSDate date]] UTF8String], funcName, lineNumber, [formattedMessage UTF8String]); + [formattedMessage release]; +} + +#define AttachLog(MSG, ...) AttachLog(__FILE__, __LINE__, __PRETTY_FUNCTION__, MSG, ## __VA_ARGS__ ) diff --git a/gradle/native-build.gradle b/gradle/native-build.gradle index 4133d535..8c4e0eb0 100644 --- a/gradle/native-build.gradle +++ b/gradle/native-build.gradle @@ -23,7 +23,10 @@ ext.nativeBuild = { buildDir, projectDir, name, os -> } def JAVAHOME = System.getenv("JAVA_HOME") - def includeFlags = "-I$JAVAHOME/include" + def includeFlags = [ + "-I$JAVAHOME/include", + "-I$projectDir/../../gradle/include", + ] def osIncludeFlags = "" if (os == "ios") { @@ -101,6 +104,12 @@ ext.nativeBuild = { buildDir, projectDir, name, os -> args lipoArgs } + println("native build for $name finished") + File n = new File(lipoOutput) + if (n.exists()) { + println "Adding $n to native jar" + n + } } else { // TODO def compileOutput = "$buildDir/native/$os" @@ -111,19 +120,18 @@ ext.nativeBuild = { buildDir, projectDir, name, os -> def cargs = [ "-c", includeFlags, osIncludeFlags, sharedSources, osSources ].flatten() - + exec { executable "/usr/bin/gcc" args cargs workingDir compileOutput } - } - - println("native build for $name finished") - File n = new File("$buildDir/native/${os}") - if (n.exists()) { - println "Adding lib${name} to native jar" - fileTree("$buildDir/native/${os}").filter { it.isFile() }.files - .first() + // TODO + File n = new File("$buildDir/native/${os}") + if (n.exists()) { + println "Adding lib${name} to native jar" + fileTree("$buildDir/native/${os}").filter { it.isFile() }.files + .first() + } } } \ No newline at end of file diff --git a/modules/accelerometer/build.gradle b/modules/accelerometer/build.gradle index 7e86f7b5..f229a630 100644 --- a/modules/accelerometer/build.gradle +++ b/modules/accelerometer/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { implementation project(':util') + implementation project(":lifecycle") } ext.description = 'Common API to access accelerometer features' \ No newline at end of file diff --git a/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/impl/DummyAccelerometerService.java b/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/impl/DummyAccelerometerService.java index aa509351..fe5c9c31 100644 --- a/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/impl/DummyAccelerometerService.java +++ b/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/impl/DummyAccelerometerService.java @@ -1,3 +1,30 @@ +/* + * 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.accelerometer.impl; import com.gluonhq.attach.accelerometer.AccelerometerService; diff --git a/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/impl/IOSAccelerometerService.java b/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/impl/IOSAccelerometerService.java new file mode 100644 index 00000000..dedc9a7b --- /dev/null +++ b/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/impl/IOSAccelerometerService.java @@ -0,0 +1,85 @@ +/* + * 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.accelerometer.impl; + +import com.gluonhq.attach.accelerometer.Acceleration; +import com.gluonhq.attach.accelerometer.AccelerometerService; +import com.gluonhq.attach.lifecycle.LifecycleEvent; +import com.gluonhq.attach.lifecycle.LifecycleService; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public class IOSAccelerometerService implements AccelerometerService { + + static { + System.loadLibrary("Accelerometer"); + initAccelerometer(); + } + + private static ReadOnlyObjectWrapper acceleration; + + public IOSAccelerometerService() { + acceleration = new ReadOnlyObjectWrapper<>(); + + LifecycleService.create().ifPresent(l -> { + l.addListener(LifecycleEvent.PAUSE, IOSAccelerometerService::stopObserver); + l.addListener(LifecycleEvent.RESUME, () -> startObserver(FILTER_GRAVITY, FREQUENCY)); + }); + startObserver(FILTER_GRAVITY, FREQUENCY); + } + + @Override + public Acceleration getCurrentAcceleration() { + return acceleration.get(); + } + + @Override + public ReadOnlyObjectProperty accelerationProperty() { + return acceleration.getReadOnlyProperty(); + } + + // native + private static native void initAccelerometer(); + private static native void startObserver(boolean filterGravity, int rateInMillis); + private static native void stopObserver(); + + // callback + private static void notifyAcceleration(double x, double y, double z, double t) { + Acceleration a = new Acceleration(x, y, z, toLocalDateTime(t)); + Platform.runLater(() -> acceleration.setValue(a)); + } + + private static LocalDateTime toLocalDateTime(double t) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli((long) t), ZoneId.systemDefault()); + } +} diff --git a/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/package-info.java b/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/package-info.java index e3f2b00b..6cb12e60 100644 --- a/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/package-info.java +++ b/modules/accelerometer/src/main/java/com/gluonhq/attach/accelerometer/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Accelerometer plugin, + * Primary API package for Attach - Accelerometer plugin, * contains the interface {@link com.gluonhq.attach.accelerometer.AccelerometerService} and related classes. */ package com.gluonhq.attach.accelerometer; \ No newline at end of file diff --git a/modules/accelerometer/src/main/java/module-info.java b/modules/accelerometer/src/main/java/module-info.java index c096bfbe..0e85a140 100644 --- a/modules/accelerometer/src/main/java/module-info.java +++ b/modules/accelerometer/src/main/java/module-info.java @@ -27,8 +27,9 @@ */ module com.gluonhq.attach.accelerometer { - requires javafx.base; + requires javafx.graphics; requires com.gluonhq.attach.util; + requires com.gluonhq.attach.lifecycle; exports com.gluonhq.attach.accelerometer; exports com.gluonhq.attach.accelerometer.impl to com.gluonhq.attach.util; diff --git a/modules/accelerometer/src/main/native/ios/Accelerometer.h b/modules/accelerometer/src/main/native/ios/Accelerometer.h new file mode 100644 index 00000000..160290d9 --- /dev/null +++ b/modules/accelerometer/src/main/native/ios/Accelerometer.h @@ -0,0 +1,40 @@ +/* + * 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" +#import + +@interface Accelerometer : UIViewController {} + @property (strong, nonatomic) CMMotionManager *motionManager; + - (void) startObserver; + - (void) stopObserver; +@end + +void sendAcceleration(CMAccelerometerData *accelerometerData); diff --git a/modules/accelerometer/src/main/native/ios/Accelerometer.m b/modules/accelerometer/src/main/native/ios/Accelerometer.m new file mode 100644 index 00000000..ff895b77 --- /dev/null +++ b/modules/accelerometer/src/main/native/ios/Accelerometer.m @@ -0,0 +1,151 @@ +/* + * 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 "Accelerometer.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Accelerometer(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 AccelerometerInited = 0; + +// Accelerometer +jclass mat_jAccelerometerServiceClass; +jmethodID mat_jAccelerometerService_notifyAcceleration = 0; +Accelerometer *_accelerometer; +BOOL filterGravity; +double rate = 0.01; +double gravity[3]; +double alpha = 0.8; +double offset = 0; +double g = 9.81; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_accelerometer_impl_IOSAccelerometerService_initAccelerometer +(JNIEnv *env, jclass jClass) +{ + if (AccelerometerInited) + { + return; + } + AccelerometerInited = 1; + + mat_jAccelerometerServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/accelerometer/impl/IOSAccelerometerService")); + mat_jAccelerometerService_notifyAcceleration = (*env)->GetStaticMethodID(env, mat_jAccelerometerServiceClass, "notifyAcceleration", "(DDDD)V"); + + _accelerometer = [[Accelerometer alloc] init]; + + NSTimeInterval uptime = [NSProcessInfo processInfo].systemUptime; + NSTimeInterval nowTimeIntervalSince1970 = [[NSDate date] timeIntervalSince1970]; + offset = nowTimeIntervalSince1970 - uptime; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_accelerometer_impl_IOSAccelerometerService_startObserver +(JNIEnv *env, jclass jClass, jboolean jfilterGravity, jint jfrequency) +{ + filterGravity = jfilterGravity; + if (jfrequency > 0) { + rate = 1.0 / ((double) jfrequency); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [_accelerometer startObserver]; + }); + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_accelerometer_impl_IOSAccelerometerService_stopObserver +(JNIEnv *env, jclass jClass) +{ + [_accelerometer stopObserver]; + return; +} + +void sendAcceleration(CMAccelerometerData *accelerometerData) { + double x = accelerometerData.acceleration.x * g; + double y = accelerometerData.acceleration.y * g; + double z = accelerometerData.acceleration.z * g; + + if (filterGravity) { + // filter to remove gravity + gravity[0] = alpha * gravity[0] + (1 - alpha) * x; + gravity[1] = alpha * gravity[1] + (1 - alpha) * y; + gravity[2] = alpha * gravity[2] + (1 - alpha) * z; + + x -= gravity[0]; + y -= gravity[1]; + z -= gravity[2]; + } + double t = (accelerometerData.timestamp + offset) * 1000; + (*env)->CallStaticVoidMethod(env, mat_jAccelerometerServiceClass, mat_jAccelerometerService_notifyAcceleration, x, y, z, t); +} + +@implementation Accelerometer + +- (void) startObserver +{ + + if (!self.motionManager) { + self.motionManager = [[CMMotionManager alloc] init]; + } + + if (self.motionManager.accelerometerAvailable) + { + self.motionManager.accelerometerUpdateInterval = rate; // in seconds + [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] + withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) { + sendAcceleration(accelerometerData); + }]; + } else + { + AttachLog(@"Error: No Accelerometer or Gyroscope Available"); + } + +} + +- (void) stopObserver +{ + if (self.motionManager) + { + [self.motionManager stopAccelerometerUpdates]; + } +} + +@end + diff --git a/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/AudioRecordingService.java b/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/AudioRecordingService.java index b34a1575..321a044b 100644 --- a/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/AudioRecordingService.java +++ b/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/AudioRecordingService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, Gluon + * 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 diff --git a/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/impl/IOSAudioRecordingService.java b/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/impl/IOSAudioRecordingService.java new file mode 100644 index 00000000..7e5f91c6 --- /dev/null +++ b/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/impl/IOSAudioRecordingService.java @@ -0,0 +1,76 @@ +/* + * 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.audiorecording.impl; + +import com.gluonhq.attach.util.Constants; +import javafx.application.Platform; + +import java.util.function.Function; + + +public class IOSAudioRecordingService extends DefaultAudioRecordingService { + + static { + System.loadLibrary("AudioRecording"); + initAudioRecording(); + } + private static Function addChunk; + + public IOSAudioRecordingService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + } + + @Override + protected void start(float sampleRate, int sampleSizeInBits, int channels, int chunkRecordTime, Function addChunk) { + startAudioRecording(getAudioFolder().getName(), sampleRate, sampleSizeInBits, channels, chunkRecordTime); + IOSAudioRecordingService.addChunk = addChunk; + } + + @Override + protected void stop() { + stopAudioRecording(); + } + + // native + private static native void initAudioRecording(); + private native void startAudioRecording(String audioFolderName, float sampleRate, int sampleSizeInBits, int channels, int chunkRecordTime); + private native void stopAudioRecording(); + private static native void enableDebug(); + + // callback + private static void notifyRecordingStatus(boolean value) { + Platform.runLater(() -> updateRecordingStatus(value)); + } + + private static void notifyRecordingChunk(String file) { + Platform.runLater(() -> addChunk.apply(file)); + } + +} \ No newline at end of file diff --git a/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/package-info.java b/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/package-info.java index 56c19f99..c6e15dbd 100644 --- a/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/package-info.java +++ b/modules/audio-recording/src/main/java/com/gluonhq/attach/audiorecording/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Audio Recording plugin, + * Primary API package for Attach - Audio Recording plugin, * contains the interface {@link com.gluonhq.attach.audiorecording.AudioRecordingService} and related classes. */ package com.gluonhq.attach.audiorecording; \ No newline at end of file diff --git a/modules/audio-recording/src/main/native/ios/AudioRecording.h b/modules/audio-recording/src/main/native/ios/AudioRecording.h new file mode 100644 index 00000000..ea8c17ae --- /dev/null +++ b/modules/audio-recording/src/main/native/ios/AudioRecording.h @@ -0,0 +1,49 @@ +/* + * 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" +#import + +@interface AudioRecording : UIViewController +{ +} + + @property (nonatomic, retain) AVAudioRecorder *avAudioRecorderController; + @property (strong, nonatomic) NSTimer *timer; + @property BOOL restart; + + - (void) playAudioRecorder; + - (void) startRecording:(AVAudioSession *)session; + - (void) stopAudioRecorder; + - (void) restartAudioRecorder; +@end + +void sendRecordingStatus(BOOL recording); +void sendRecordingChunk(NSString *fileName); \ No newline at end of file diff --git a/modules/audio-recording/src/main/native/ios/AudioRecording.m b/modules/audio-recording/src/main/native/ios/AudioRecording.m new file mode 100644 index 00000000..5d0360c0 --- /dev/null +++ b/modules/audio-recording/src/main/native/ios/AudioRecording.m @@ -0,0 +1,280 @@ +/* + * 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 "AudioRecording.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_AudioRecording(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 audioRecordingInited = 0; + +jclass mat_jAudioRecordingServiceClass; +jmethodID mat_jAudioRecordingService_notifyRecordingStatus = 0; +jmethodID mat_jAudioRecordingService_notifyRecordingChunk = 0; + +// AudioRecording +AudioRecording *_audioRecording; + +NSMutableString *dataPath; +NSMutableDictionary *recordSettings; +NSDateFormatter *format; + +double timerInterval = 20.0f; +int counter = 0; +BOOL debugAudioRecording; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_audiorecording_impl_IOSAudioRecordingService_initAudioRecording +(JNIEnv *env, jclass jClass) +{ + if (audioRecordingInited) + { + return; + } + audioRecordingInited = 1; + + mat_jAudioRecordingServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/audiorecording/impl/IOSAudioRecordingService")); + mat_jAudioRecordingService_notifyRecordingStatus = (*env)->GetStaticMethodID(env, mat_jAudioRecordingServiceClass, "notifyRecordingStatus", "(Z)V"); + mat_jAudioRecordingService_notifyRecordingChunk = (*env)->GetStaticMethodID(env, mat_jAudioRecordingServiceClass, "notifyRecordingChunk", "(Ljava/lang/String;)V"); + + AttachLog(@"Initialize IOSAudioRecordingService"); + + format = [[NSDateFormatter alloc] init]; + [format setDateFormat:@"yyyy-MM-dd.HH-mm-ss.SSS"]; + + _audioRecording = [[AudioRecording alloc] init]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_audiorecording_impl_IOSAudioRecordingService_startAudioRecording +(JNIEnv *env, jclass jClass, jstring jAudioFolderName, jfloat sampleRate, jint sampleSizeInBits, jint channels, jint chunkRecordTime) +{ + dataPath = [[NSMutableString alloc] init]; + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + const jchar *charsAudioFolder = (*env)->GetStringChars(env, jAudioFolderName, NULL); + NSString *audioFolder = [NSString stringWithCharacters:(UniChar *)charsAudioFolder length:(*env)->GetStringLength(env, jAudioFolderName)]; + (*env)->ReleaseStringChars(env, jAudioFolderName, charsAudioFolder); + + [dataPath setString:[[paths objectAtIndex:0] stringByAppendingPathComponent:audioFolder]]; + + timerInterval = chunkRecordTime; + + // Define the recorder setting + recordSettings = [[NSMutableDictionary alloc] init]; + + [recordSettings setValue:[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey]; + [recordSettings setValue:[NSNumber numberWithFloat:sampleRate] forKey:AVSampleRateKey]; + [recordSettings setValue:[NSNumber numberWithInt:AVAudioQualityMin] forKey:AVEncoderAudioQualityKey]; + [recordSettings setValue:[NSNumber numberWithInt:sampleSizeInBits] forKey:AVEncoderBitRateKey]; + [recordSettings setValue:[NSNumber numberWithInt:channels] forKey:AVNumberOfChannelsKey]; + + AttachLog(@"Start Recording"); + [_audioRecording playAudioRecorder]; + + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_audiorecording_impl_IOSAudioRecordingService_stopAudioRecording +(JNIEnv *env, jclass jClass) +{ + AttachLog(@"Stop Recording"); + [_audioRecording stopAudioRecorder]; + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_audiorecording_impl_IOSAudioRecordingService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugAudioRecording = YES; +} + +void sendRecordingStatus(BOOL recording) { + AttachLog(@"Recording Status is %s", recording ? "true" : "false"); + (*env)->CallStaticVoidMethod(env, mat_jAudioRecordingServiceClass, mat_jAudioRecordingService_notifyRecordingStatus, recording ? JNI_TRUE : JNI_FALSE); +} + +void sendRecordingChunk(NSString *fileName) { + if (debugAudioRecording) { + AttachLog(@"Send chunk file: %@", fileName); + } + const char *chunkChars = [fileName UTF8String]; + jstring arg = (*env)->NewStringUTF(env, chunkChars); + (*env)->CallStaticVoidMethod(env, mat_jAudioRecordingServiceClass, mat_jAudioRecordingService_notifyRecordingChunk, arg); + (*env)->DeleteLocalRef(env, arg); +} + +@implementation AudioRecording +@synthesize restart; + +- (void)playAudioRecorder +{ + if(_avAudioRecorderController) + { + _avAudioRecorderController = nil; + } + + // set audio session + AVAudioSession *session = [AVAudioSession sharedInstance]; + [session setCategory:AVAudioSessionCategoryRecord error:nil]; + [session setActive:YES error:nil]; + + // Check mic permission + [session requestRecordPermission:^(BOOL granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (granted) { + [self logMessage:@"Microphone enabled"]; + [self startRecording:session]; + } + else { + AttachLog(@"Microphone disabled"); + } + }); + }]; +} + +- (void) startRecording:(AVAudioSession *)session +{ + // Initiate and prepare the recorder + counter = 0; + NSString *recorderFilePath = [NSString stringWithFormat:@"%@/audioFile.wav", dataPath]; + [self logMessage:@"recorderFilePath: %@",recorderFilePath]; + NSURL *outputFileURL = [NSURL fileURLWithPath:recorderFilePath]; + + NSError *error = nil; + _avAudioRecorderController = [[AVAudioRecorder alloc] initWithURL:outputFileURL settings:recordSettings error:&error]; + _avAudioRecorderController.delegate = self; + _avAudioRecorderController.meteringEnabled = YES; + if ([_avAudioRecorderController prepareToRecord] == YES) { + restart = NO; + [self logMessage:@"starting timer"]; + _timer = [[NSTimer alloc] initWithFireDate: [NSDate dateWithTimeIntervalSinceNow: timerInterval] + interval: timerInterval + target: self + selector:@selector(restartAudioRecorder) + userInfo:nil repeats:YES]; + + NSRunLoop *runner = [NSRunLoop currentRunLoop]; + [runner addTimer:_timer forMode: NSDefaultRunLoopMode]; + + [_avAudioRecorderController record]; + [self logMessage:@"recording started"]; + sendRecordingStatus(YES); + } else { + AttachLog(@"Error recorder: %@ %@ %@", [error domain], [error localizedDescription], [[error userInfo] description]); + AttachLog(@"recording failed"); + } +} + +- (void)restartAudioRecorder +{ + if(!_avAudioRecorderController) + { + return; + } + [self logMessage:@"restart recorder"]; + restart = YES; + [_avAudioRecorderController stop]; +} + +- (void)stopAudioRecorder +{ + if(!_avAudioRecorderController) + { + return; + } + [self logMessage:@"stop recorder"]; + restart = NO; + [_avAudioRecorderController stop]; +} + +- (void) audioRecorderDidFinishRecording:(AVAudioRecorder *)avrecorder successfully:(BOOL)flag { + NSString *recorderFilePath = [NSString stringWithFormat:@"%@/audioFile.wav", dataPath]; + NSString *recorderFileName = [NSString stringWithFormat:@"audioFile-%03d-%@.wav", counter, [format stringFromDate:NSDate.date]]; + NSString *recorderFileFinalPath = [NSString stringWithFormat:@"%@/%@", dataPath, recorderFileName]; + counter = counter + 1; + [self logMessage:@"Copy to recorderFileFinalPath: %@",recorderFileFinalPath]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + BOOL result = [fileManager copyItemAtPath:recorderFilePath toPath:recorderFileFinalPath error:&error]; + if(!result) { + AttachLog(@"Error copying file: %@", error); + } + sendRecordingChunk(recorderFileName); + if (restart) { + [fileManager release]; + [self logMessage:@"stopped and restarted"]; + + if(_avAudioRecorderController) + { + // resume recording again + [_avAudioRecorderController record]; + } + + } else { + [self logMessage:@"Finished recording"]; + [fileManager removeItemAtPath:recorderFilePath error:&error]; + [fileManager release]; + + if(_timer) + { + [_timer invalidate]; + [_timer release]; + } + if(_avAudioRecorderController) + { + [recordSettings release]; + [_avAudioRecorderController release]; + sendRecordingStatus(NO); + } + } +} + +- (void) logMessage:(NSString *)format, ...; +{ + if (debugAudioRecording) + { + va_list args; + va_start(args, format); + NSLogv([@"[Debug] " stringByAppendingString:format], args); + va_end(args); + } +} +@end \ No newline at end of file diff --git a/modules/augmented-reality/build.gradle b/modules/augmented-reality/build.gradle index 66e77309..ff6867a6 100644 --- a/modules/augmented-reality/build.gradle +++ b/modules/augmented-reality/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { diff --git a/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/AugmentedRealityService.java b/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/AugmentedRealityService.java index 10c55123..c8e27efe 100644 --- a/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/AugmentedRealityService.java +++ b/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/AugmentedRealityService.java @@ -27,8 +27,11 @@ */ package com.gluonhq.attach.ar; +import com.gluonhq.attach.util.Services; import javafx.beans.property.ReadOnlyBooleanProperty; +import java.util.Optional; + /** * The Augmented Reality Service allows accesing the native AR kit, if it is available. * @@ -125,10 +128,18 @@ */ public interface AugmentedRealityService { - public enum Availability { + enum Availability { AR_NOT_SUPPORTED, ARCORE_NOT_INSTALLED, ARCORE_OUTDATED, IOS_NOT_UPDATED, AR_SUPPORTED } - + + /** + * Returns an instance of {@link AugmentedRealityService}. + * @return An instance of {@link AugmentedRealityService}. + */ + static Optional create() { + return Services.get(AugmentedRealityService.class); + } + /** * Checks if device supports AR * @param afterInstall action that can be performed if AR is installed @@ -146,7 +157,7 @@ public enum Availability { * {@code /src/ios/assets/} for iOS, while for Android these can be placed * under {@code /src/android/assets/} or {@code /src/main/resources/assets/}. * - * @param model + * @param model the entity model */ void setModel(ARModel model); @@ -158,7 +169,7 @@ public enum Availability { /** * Shows debug information * - * @param enable + * @param enable set to true to get verbose output */ void debugAR(boolean enable); diff --git a/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/impl/IOSAugmentedRealityService.java b/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/impl/IOSAugmentedRealityService.java new file mode 100644 index 00000000..7a1b3c9c --- /dev/null +++ b/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/impl/IOSAugmentedRealityService.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2018, 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.ar.impl; + +import com.gluonhq.attach.ar.ARModel; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class IOSAugmentedRealityService extends DefaultAugmentedRealityService { + + private static final Logger LOG = Logger.getLogger(IOSAugmentedRealityService.class.getName()); + + private static final int CHECK_AR; + private static final int ARKIT_NOT_SUPPORTED = 0; + private static final int IOS_NOT_UPDATED = 1; + private static final int ARKIT_SUPPORTED = 2; + + private static final ReadOnlyBooleanWrapper CANCELLED = new ReadOnlyBooleanWrapper(); + + static { + System.loadLibrary("AugmentedReality"); + CHECK_AR = initAR(); + } + + public IOSAugmentedRealityService() { + if (debug) { + enableDebug(); + } + } + + @Override + public Availability checkAR(Runnable afterInstall) { + if (CHECK_AR == ARKIT_NOT_SUPPORTED) { + return Availability.AR_NOT_SUPPORTED; + } else if (CHECK_AR == IOS_NOT_UPDATED) { + return Availability.IOS_NOT_UPDATED; + } + return Availability.AR_SUPPORTED; + } + + @Override + public void setModel(ARModel model) { + setARModel(model.getObjFilename(), model.getScale()); + } + + @Override + public void showAR() { + if (debug) LOG.log(Level.INFO, "Show AR..."); + CANCELLED.setValue(false); + showNativeAR(); + } + + @Override + public void debugAR(boolean enable) { + if (enable) { + enableDebugAR(); + } + } + + @Override + public ReadOnlyBooleanProperty cancelled() { + return CANCELLED.getReadOnlyProperty(); + } + + // native + private static native int initAR(); // init IDs for java callbacks from native + private native void showNativeAR(); + private native void setARModel(String objFileName, double scale); + + private static native void enableDebug(); + private static native void enableDebugAR(); + + private static void notifyCancel() { + if (CANCELLED != null && ! CANCELLED.get()) { + Platform.runLater(() -> CANCELLED.set(true)); + } + } + +} diff --git a/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/package-info.java b/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/package-info.java index 4dc809c9..b16f7643 100644 --- a/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/package-info.java +++ b/modules/augmented-reality/src/main/java/com/gluonhq/attach/ar/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Augmented Reality plugin, + * Primary API package for Attach - Augmented Reality plugin, * contains the interface {@link com.gluonhq.attach.ar.AugmentedRealityService} and related classes. */ package com.gluonhq.attach.ar; \ No newline at end of file diff --git a/modules/augmented-reality/src/main/java/module-info.java b/modules/augmented-reality/src/main/java/module-info.java index f5d43943..517adff6 100644 --- a/modules/augmented-reality/src/main/java/module-info.java +++ b/modules/augmented-reality/src/main/java/module-info.java @@ -27,7 +27,7 @@ */ module com.gluonhq.attach.audio.recording { - requires javafx.base; + requires javafx.graphics; requires com.gluonhq.attach.util; requires com.gluonhq.attach.storage; diff --git a/modules/augmented-reality/src/main/native/ios/AugmentedReality.h b/modules/augmented-reality/src/main/native/ios/AugmentedReality.h new file mode 100644 index 00000000..20ae3d4f --- /dev/null +++ b/modules/augmented-reality/src/main/native/ios/AugmentedReality.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018, 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" +#import +#import + +API_AVAILABLE(ios(11.3)) +@interface AugmentedReality : UIViewController +{ +} + + @property(nonatomic, strong) IBOutlet ARSCNView *sceneView API_AVAILABLE(ios(11.3)); + @property(nonatomic, strong) IBOutlet UIButton *cancelButton; + @property(nonatomic, strong) IBOutlet NSString *modelFileName; + + - (void) showAR; + - (void)setARModel:(NSString *)fileName scale:(double)scale; + - (void) hideAR; +@end + +void sendCancelled(); \ No newline at end of file diff --git a/modules/augmented-reality/src/main/native/ios/AugmentedReality.m b/modules/augmented-reality/src/main/native/ios/AugmentedReality.m new file mode 100644 index 00000000..1a254d3b --- /dev/null +++ b/modules/augmented-reality/src/main/native/ios/AugmentedReality.m @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2018, 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 "AugmentedReality.h" + +// JNIEnv *env = NULL; + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_AugmentedReality(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 AugmentedRealityInited = 0; + +// AugmentedReality +jclass mat_jAugmentedRealityServiceClass; +jmethodID mat_jAugmentedRealityService_notifyCancel = 0; + +API_AVAILABLE(ios(11.3)) +AugmentedReality *_ar; + +BOOL debugAugmentedReality; +BOOL enableDebugAugmentedReality; + +JNIEXPORT jint JNICALL Java_com_gluonhq_attach_ar_impl_IOSAugmentedRealityService_initAR +(JNIEnv *myenv, jclass jClass) +{ + if (AugmentedRealityInited) + { + return 0; + } + AugmentedRealityInited = 1; + + mat_jAugmentedRealityServiceClass = (*myenv)->NewGlobalRef(myenv, (*myenv)->FindClass(myenv, "com/gluonhq/attach/ar/impl/IOSAugmentedRealityService")); + mat_jAugmentedRealityService_notifyCancel = (*myenv)->GetStaticMethodID(myenv, mat_jAugmentedRealityServiceClass, "notifyCancel", "()V"); + + AttachLog(@"Init AugmentedReality"); + if (@available(iOS 11.0, *)) { // First of all, ARConfiguration requires iOS 11.0+ + if (ARConfiguration.isSupported) { // Then, AR requires chip A9+ that supports AR + if (@available(iOS 11.3, *)) { // this app uses APIs that require iOS 11.3+ + AttachLog(@"ARKit is supported and iOS is at least 11.3"); + _ar = [[AugmentedReality alloc] init]; + return 2; + } else { + AttachLog(@"ARKit requires at least 11.3. Please update your device"); + return 1; + } + } + } + AttachLog(@"ARKit is not supported"); + return 0; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ar_impl_IOSAugmentedRealityService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugAugmentedReality = YES; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ar_impl_IOSAugmentedRealityService_enableDebugAR +(JNIEnv *env, jclass jClass) +{ + enableDebugAugmentedReality = YES; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ar_impl_IOSAugmentedRealityService_showNativeAR +(JNIEnv *env, jclass jClass) +{ + if (@available(iOS 11.3, *)) { + if (_ar) + { + [_ar showAR]; + } + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ar_impl_IOSAugmentedRealityService_setARModel +(JNIEnv *env, jclass jClass, jstring jObjFileName, jdouble scale) +{ + const jchar *charsObjFileName = (*env)->GetStringChars(env, jObjFileName, NULL); + NSString *objFileName = [NSString stringWithCharacters:(UniChar *)charsObjFileName length:(*env)->GetStringLength(env, jObjFileName)]; + (*env)->ReleaseStringChars(env, jObjFileName, charsObjFileName); + + if (@available(iOS 11.3, *)) { + if (_ar) + { + [_ar setARModel:objFileName scale:scale]; + } + } + return; +} + +void sendCancelled() { + AttachLog(@"Sending cancel action"); + (*env)->CallStaticVoidMethod(env, mat_jAugmentedRealityServiceClass, mat_jAugmentedRealityService_notifyCancel); +} + +@implementation AugmentedReality + +#pragma mark - Overriding UIViewController + +double modelScale = 1.0; + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +- (void)setARModel:(NSString *)fileName scale:(double)scale +{ + self.modelFileName = fileName; + modelScale = scale; + [self logMessage:@"Set ARModel: %@ %.2f", self.modelFileName, modelScale]; +} + +- (void)showAR +{ + [self logMessage:@"showing AR"]; + + if(![[UIApplication sharedApplication] keyWindow]) + { + AttachLog(@"key window was nil"); + return; + } + UIWindow* window = [UIApplication sharedApplication].keyWindow; + + NSArray *views = [window subviews]; + if(![views count]) { + AttachLog(@"views size was 0"); + return; + } + + UIView *_currentView = views[0]; + + UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; + if(!rootViewController) + { + AttachLog(@"rootViewController was nil"); + return; + } + + // Stop the screen from dimming while we are using the app + [UIApplication.sharedApplication setIdleTimerDisabled:YES]; + + [self logMessage:@"adding sceneView"]; + self.sceneView = [[ARSCNView alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.sceneView.contentMode = UIViewContentModeScaleToFill; + self.sceneView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + self.sceneView.multipleTouchEnabled = YES; + self.sceneView.antialiasingMode = SCNAntialiasingModeMultisampling4X; + self.sceneView.autoenablesDefaultLighting = YES; + self.sceneView.automaticallyUpdatesLighting = NO; + + self.sceneView.delegate = self; + [self logMessage:@"got sceneView %@", self.sceneView]; + if (enableDebugAugmentedReality) { + self.sceneView.showsStatistics = YES; + self.sceneView.debugOptions = ARSCNDebugOptionShowFeaturePoints | ARSCNDebugOptionShowWorldOrigin; + } + self.sceneView.userInteractionEnabled = YES; + + [self logMessage:@"SceneView: %@", self.sceneView]; + + [self logMessage:@"adding scene"]; + SCNScene *scene = [[SCNScene alloc] init]; + self.sceneView.scene = scene; + + [self logMessage:@"adding subView"]; + [_currentView addSubview:self.sceneView]; + [_currentView bringSubviewToFront:self.sceneView]; + + + ARWorldTrackingConfiguration *configuration = [ARWorldTrackingConfiguration new]; + [configuration setWorldAlignment:ARWorldAlignmentGravity]; + [configuration setPlaneDetection:ARPlaneDetectionHorizontal]; + configuration.lightEstimationEnabled = YES; + + [self logMessage:@"run sceneView"]; + self.sceneView.session = [[ARSession alloc] init]; + [self.sceneView.session runWithConfiguration:configuration]; + self.sceneView.session.delegate = self; + + [self logMessage:@"***** Running %@", self.sceneView.session]; + + self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [self.cancelButton setTitle:@"CANCEL" forState:UIControlStateNormal]; + [self.cancelButton addTarget:self action:@selector(cancelButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + self.cancelButton.frame = CGRectMake(16.0, 44.0, 100.0, 30.0); + [self.sceneView addSubview:self.cancelButton]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self startObserver]; + }); + + UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)]; + [self.sceneView addGestureRecognizer:singleFingerTap]; +} + +- (void)hideAR { + [UIApplication.sharedApplication setIdleTimerDisabled:NO]; + if (! self.sceneView.scene) { + [self logMessage:@"Remove nodes"]; + for (SCNNode *childNode in [[self.sceneView.scene rootNode] childNodes]) { + [childNode removeFromParentNode]; + } + } + [self logMessage:@"Stop session"]; + [self.sceneView.session pause]; + + [self logMessage:@"Remove sceneView"]; + [self.sceneView removeFromSuperview]; + [self stopObserver]; +} + +#pragma mark - ARSCNViewDelegate + +- (void) renderer:(id)renderer didAddNode:(nonnull SCNNode *)node forAnchor:(nonnull ARAnchor *)anchor { + if (enableDebugAugmentedReality && [anchor isKindOfClass:[ARPlaneAnchor class]]) { + [self logMessage:@"didAddNode: add plane"]; + ARPlaneAnchor *planeAnchor = (ARPlaneAnchor *)anchor; + + CGFloat width = planeAnchor.extent.x; + CGFloat height = planeAnchor.extent.z; + SCNPlane *plane = [SCNPlane planeWithWidth:width height:height]; + + plane.materials.firstObject.diffuse.contents = + [UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.3f]; + + SCNNode *planeNode = [SCNNode nodeWithGeometry:plane]; + + CGFloat x = planeAnchor.center.x; + CGFloat y = planeAnchor.center.y; + CGFloat z = planeAnchor.center.z; + planeNode.position = SCNVector3Make(x, y, z); + planeNode.eulerAngles = SCNVector3Make(-M_PI / 2, 0, 0); + + [node addChildNode:planeNode]; + } +} + +- (void)renderer:(id)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor { + if (enableDebugAugmentedReality && [anchor isKindOfClass:[ARPlaneAnchor class]]) { + ARPlaneAnchor *planeAnchor = (ARPlaneAnchor *)anchor; + + SCNNode *planeNode = node.childNodes.firstObject; + SCNPlane *plane = (SCNPlane *)planeNode.geometry; + + CGFloat width = planeAnchor.extent.x; + CGFloat height = planeAnchor.extent.z; + plane.width = width; + plane.height = height; + + CGFloat x = planeAnchor.center.x; + CGFloat y = planeAnchor.center.y; + CGFloat z = planeAnchor.center.z; + planeNode.position = SCNVector3Make(x, y, z); + } +} + +- (void)renderer:(id)renderer didRemoveNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor { + if ([anchor isKindOfClass:[ARPlaneAnchor class]]) { + [self logMessage:@"didRemoveNode: remove plane"]; + SCNNode *planeNode = node.childNodes.firstObject; + [planeNode removeFromParentNode]; + } +} + +- (void)renderer:(id )renderer updateAtTime:(NSTimeInterval)time { + ARLightEstimate *estimate = self.sceneView.session.currentFrame.lightEstimate; + if (! estimate) { + return; + } + //AttachLog(@"light estimate: %f", estimate.ambientIntensity); + + CGFloat intensity = estimate.ambientIntensity / 1000.0; + self.sceneView.scene.lightingEnvironment.intensity = intensity; +} + +- (void)session:(ARSession *)session didFailWithError:(NSError *)error { + // Present an error message to the user + AttachLog(@"Session error: %@", error); +} + +- (void)sessionWasInterrupted:(ARSession *)session { + // Inform the user that the session has been interrupted, for example, by presenting an overlay + AttachLog(@"session was interrupted: %@", session); +} + +- (void)sessionInterruptionEnded:(ARSession *)session { + // Reset tracking and/or remove existing anchors if consistent tracking is required + AttachLog(@"session interruption ended: %@", session); +} + +#pragma mark - ARSessionDelegate + +- (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame +{ +} + +# pragma mark - Actions + +- (void)cancelButtonPressed:(UIButton*)sender { + [self logMessage:@"Cancel AR session"]; + [self hideAR]; + sendCancelled(); +} + +//The event handling method +- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer +{ + CGPoint touchLocation = [recognizer locationInView:[recognizer.view superview]]; + [self logMessage:@"TapGesture: %@", NSStringFromCGPoint(touchLocation)]; + + NSArray *hitTestResults = [self.sceneView hitTest:touchLocation types:ARHitTestResultTypeExistingPlane | + ARHitTestResultTypeExistingPlaneUsingExtent | + ARHitTestResultTypeEstimatedHorizontalPlane]; + [self logMessage:@"HitTestResults: %@", hitTestResults]; + if (hitTestResults.count > 0) { + ARHitTestResult *result = [hitTestResults firstObject]; + [self logMessage:@"Result %@", result]; + + SCNNode *model = [[SCNNode new] autorelease]; + // Create a new scene + if ([self.modelFileName length] > 0) { + SCNScene *scene = [SCNScene sceneNamed:self.modelFileName]; + [self logMessage:@"Adding new scene %@", scene]; + for (SCNNode *childNode in [[scene rootNode] childNodes]) { + [model addChildNode:childNode]; + } + } else { + AttachLog(@"No model was set. Use AugmentedRealityService::setModel"); + } + [self logMessage:@"node: %@", model]; + SCNMatrix4 sc = SCNMatrix4MakeScale(modelScale, modelScale, modelScale); + model.transform = SCNMatrix4Mult(SCNMatrix4FromMat4(result.worldTransform), sc); + [self.sceneView.scene.rootNode addChildNode:model]; + } +} + +- (void) startObserver +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; +} + +- (void) stopObserver +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; +} + +-(void)OrientationDidChange:(NSNotification*)notification +{ + [self logMessage:@"adjustiong sceneView frame"]; + self.sceneView.frame = [[UIScreen mainScreen] bounds]; +} + +- (void) logMessage:(NSString *)format, ...; +{ + if (debugAugmentedReality) + { + va_list args; + va_start(args, format); + NSString* formattedMessage = [[NSString alloc] initWithFormat: format arguments: args]; + AttachLog([@"[Debug] " stringByAppendingString:formattedMessage]); + va_end(args); + [formattedMessage release]; + } +} +@end diff --git a/modules/barcode-scan/build.gradle b/modules/barcode-scan/build.gradle index 2de54b12..c09a8af4 100644 --- a/modules/barcode-scan/build.gradle +++ b/modules/barcode-scan/build.gradle @@ -1,3 +1,9 @@ +apply plugin: 'org.openjfx.javafxplugin' + +javafx { + modules 'javafx.graphics' +} + dependencies { implementation project(':util') } diff --git a/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/BarcodeScanService.java b/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/BarcodeScanService.java index 4153cdf5..073698f9 100644 --- a/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/BarcodeScanService.java +++ b/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/BarcodeScanService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Gluon + * 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 diff --git a/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/impl/IOSBarcodeScanService.java b/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/impl/IOSBarcodeScanService.java new file mode 100644 index 00000000..58f88aa2 --- /dev/null +++ b/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/impl/IOSBarcodeScanService.java @@ -0,0 +1,87 @@ +/* + * 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.barcode.impl; + +import com.gluonhq.attach.barcode.BarcodeScanService; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.util.Optional; + +/** + * Note: Since iOS 10, the key {@code NSCameraUsageDescription} is required in + * the plist file in order to use this service + */ +public class IOSBarcodeScanService implements BarcodeScanService { + + static { + System.loadLibrary("BarcodeScan"); + initBarcodeScan(); + } + + private static StringProperty result; + + @Override + public Optional scan() { + return scan("", "", ""); + } + + @Override + public Optional scan(String title, String legend, String resultText) { + result = new SimpleStringProperty(); + startBarcodeScan(title != null ? title : "", legend != null ? legend : "", resultText != null ? resultText : ""); + try { + Platform.enterNestedEventLoop(result); + } catch (Exception e) { + System.out.println("ScanActivity: enterNestedEventLoop failed: " + e); + } + return Optional.ofNullable(result.get()); + } + + // callback + + public static void setResult(String v) { + result.set(v); + Platform.runLater(() -> { + try { + Platform.exitNestedEventLoop(result, null); + } catch (Exception e) { + System.out.println("ScanActivity: exitNestedEventLoop failed: " + e); + } + }); + } + + // native + + private static native void initBarcodeScan(); // init IDs for java callbacks from native + + // scanning service + private static native void startBarcodeScan(String title, String legend, String resultText); + +} diff --git a/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/package-info.java b/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/package-info.java index 4509922c..41b6cb33 100644 --- a/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/package-info.java +++ b/modules/barcode-scan/src/main/java/com/gluonhq/attach/barcode/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - BarcodeScan plugin, + * Primary API package for Attach - BarcodeScan plugin, * contains the interface {@link com.gluonhq.attach.barcode.BarcodeScanService} and related classes. */ package com.gluonhq.attach.barcode; \ No newline at end of file diff --git a/modules/barcode-scan/src/main/java/module-info.java b/modules/barcode-scan/src/main/java/module-info.java index 0308f428..20b78a26 100644 --- a/modules/barcode-scan/src/main/java/module-info.java +++ b/modules/barcode-scan/src/main/java/module-info.java @@ -27,6 +27,7 @@ */ module com.gluonhq.attach.barcode { + requires javafx.graphics; requires com.gluonhq.attach.util; exports com.gluonhq.attach.barcode; diff --git a/modules/barcode-scan/src/main/native/ios/BarcodeScan.h b/modules/barcode-scan/src/main/native/ios/BarcodeScan.h new file mode 100644 index 00000000..ca290bff --- /dev/null +++ b/modules/barcode-scan/src/main/native/ios/BarcodeScan.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" +#import + +@interface BarcodeScan : UIViewController {} + - (void) display:(NSString *)title legend:(NSString *)legend resultText:(NSString *)resultText; +@end + +void sendScanResult(NSString *scanResult); diff --git a/modules/barcode-scan/src/main/native/ios/BarcodeScan.m b/modules/barcode-scan/src/main/native/ios/BarcodeScan.m new file mode 100644 index 00000000..a47a331e --- /dev/null +++ b/modules/barcode-scan/src/main/native/ios/BarcodeScan.m @@ -0,0 +1,317 @@ +/* + * 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 "BarcodeScan.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_BarcodeScan(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 BarcodeScanInited = 0; + +// BarcodeScan +jclass mat_jScanServiceClass; +jmethodID mat_jScanService_setResult = 0; +BarcodeScan *_barcodeScan; + + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_barcode_impl_IOSBarcodeScanService_initBarcodeScan +(JNIEnv *env, jclass jClass) +{ + if (BarcodeScanInited) + { + return; + } + BarcodeScanInited = 1; + + mat_jScanServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/barcode/impl/IOSBarcodeScanService")); + mat_jScanService_setResult = (*env)->GetStaticMethodID(env, mat_jScanServiceClass, "setResult", "(Ljava/lang/String;)V"); +} + +void sendScanResult(NSString *scanResult) { + const char *scanChars = [scanResult UTF8String]; + jstring arg = (*env)->NewStringUTF(env, scanChars); + (*env)->CallStaticVoidMethod(env, mat_jScanServiceClass, mat_jScanService_setResult, arg); + (*env)->DeleteLocalRef(env, arg); + AttachLog(@"Finished sending scan result"); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_barcode_impl_IOSBarcodeScanService_startBarcodeScan +(JNIEnv *env, jclass jClass, jstring jTitle, jstring jLegend, jstring jResult) +{ + + const jchar *charsTitle = (*env)->GetStringChars(env, jTitle, NULL); + NSString *sTitle = [NSString stringWithCharacters:(UniChar *)charsTitle length:(*env)->GetStringLength(env, jTitle)]; + (*env)->ReleaseStringChars(env, jTitle, charsTitle); + + const jchar *charsLegend = (*env)->GetStringChars(env, jLegend, NULL); + NSString *sLegend = [NSString stringWithCharacters:(UniChar *)charsLegend length:(*env)->GetStringLength(env, jLegend)]; + (*env)->ReleaseStringChars(env, jLegend, charsLegend); + + const jchar *charsResult = (*env)->GetStringChars(env, jResult, NULL); + NSString *sResult = [NSString stringWithCharacters:(UniChar *)charsResult length:(*env)->GetStringLength(env, jResult)]; + (*env)->ReleaseStringChars(env, jResult, charsResult); + + _barcodeScan = [[BarcodeScan alloc] init]; + [_barcodeScan display:sTitle legend:sLegend resultText:sResult]; + return; +} + +@implementation BarcodeScan + +AVCaptureSession *_session; +AVCaptureDevice *_device; +AVCaptureDeviceInput *_input; +AVCaptureMetadataOutput *_output; +AVCaptureVideoPreviewLayer *_prevLayer; +UINavigationItem *currentItem; +UINavigationBar *navBar; +NSString *resultString; + +- (void)display:(NSString *)title legend:(NSString *)legend resultText:(NSString *)resultText +{ + if(![[UIApplication sharedApplication] keyWindow]) + { + AttachLog(@"key window was nil"); + return; + } + + // get the root view controller + UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; + if(!rootViewController) + { + AttachLog(@"rootViewController was nil"); + return; + } + + resultString = resultText; + + // get the view + UIView *view = self.view; + + _session = [[AVCaptureSession alloc] init]; + _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + NSError *error = nil; + + _input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error]; + if (_input) { + [_session addInput:_input]; + } else { + AttachLog(@"Error: %@", error); + } + + _output = [[AVCaptureMetadataOutput alloc] init]; + [_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; + [_session addOutput:_output]; + + _output.metadataObjectTypes = [_output availableMetadataObjectTypes]; + + _prevLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session]; + _prevLayer.frame = view.bounds; + _prevLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; + _prevLayer.connection.videoOrientation = [self videoOrientationFromCurrentDeviceOrientation]; + [view.layer addSublayer:_prevLayer]; + + CGRect sbFrame = [[UIApplication sharedApplication] statusBarFrame]; + int ofs = sbFrame.size.height; + + navBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0, ofs, self.view.frame.size.width, 44)]; + [navBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; + navBar.shadowImage = [UIImage new]; + navBar.translucent = YES; + [navBar setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor whiteColor]}]; + + currentItem = [[UINavigationItem alloc] init]; + if ([title length] != 0) { + currentItem.title = title; + } + + UIBarButtonItem *leftButton = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStylePlain + target:self action:@selector(cancel:)]; + currentItem.leftBarButtonItem = leftButton; + + navBar.items = @[ currentItem ]; + [view addSubview:navBar]; + + // show view controller + [rootViewController presentViewController:self animated:YES completion:nil]; + [_session startRunning]; + + if ([legend length] != 0) { + UIAlertController *toast = [UIAlertController alertControllerWithTitle:nil message:legend preferredStyle:UIAlertControllerStyleAlert]; + [self presentViewController:toast animated:YES completion:nil]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [NSThread sleepForTimeInterval:2.0f]; + dispatch_async(dispatch_get_main_queue(), ^{ + [toast dismissViewControllerAnimated:YES completion:nil]; + }); + }); + } +} + +// hide barcodeScan preview and view controller +- (IBAction)cancel:(id)sender +{ + AttachLog(@"Scan cancelled"); + NSString *result = nil; + sendScanResult(result); + [self end]; +} + +- (void)end +{ + if([_session isRunning]) + { + [_session stopRunning]; + } + [_session removeInput:_input]; + [_session removeOutput:_output]; + [_prevLayer removeFromSuperlayer]; + [currentItem release]; + currentItem = nil; + [navBar removeFromSuperview]; + [navBar release]; + navBar = nil; + [self dismissViewControllerAnimated:YES completion:nil]; + _prevLayer = nil; + _session = nil; + resultString = nil; +} + +// device will / did rotate +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + + // Place code here which is performed before the rotation animation starts. + + [coordinator animateAlongsideTransition:^(id context) + { + // Perform this code here during rotation animation + + } completion:^(id context) + { + + // rotation finished, resize preview layer + _prevLayer.frame = self.view.bounds; + // rotate camera based on new orientation + _prevLayer.connection.videoOrientation = [self videoOrientationFromCurrentDeviceOrientation]; + + CGRect sbFrame = [[UIApplication sharedApplication] statusBarFrame]; + int ofs = sbFrame.size.height; + navBar.frame = CGRectMake(0, ofs, self.view.frame.size.width, 44); + + }]; +} + +- (AVCaptureVideoOrientation) videoOrientationFromCurrentDeviceOrientation { + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + + if (orientation == UIDeviceOrientationPortraitUpsideDown) + return AVCaptureVideoOrientationPortraitUpsideDown; + else if(orientation == UIInterfaceOrientationPortrait) + return AVCaptureVideoOrientationPortrait; + else if(orientation == UIInterfaceOrientationLandscapeLeft) + return AVCaptureVideoOrientationLandscapeLeft; + else if(orientation == UIInterfaceOrientationLandscapeRight) + return AVCaptureVideoOrientationLandscapeRight; + + return AVCaptureVideoOrientationPortrait; +} + +- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection +{ + NSString *detectionString = nil; + NSArray *barCodeTypes = @[AVMetadataObjectTypeUPCECode, AVMetadataObjectTypeCode39Code, AVMetadataObjectTypeCode39Mod43Code, + AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode93Code, AVMetadataObjectTypeCode128Code, + AVMetadataObjectTypePDF417Code, AVMetadataObjectTypeQRCode, AVMetadataObjectTypeAztecCode]; + + for (AVMetadataObject *metadata in metadataObjects) { + for (NSString *type in barCodeTypes) { + if ([metadata.type isEqualToString:type]) + { + detectionString = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; + break; + } + } + + if (detectionString != nil) + { + break; + } + else + { + AttachLog(@"String: none"); + NSString *result = nil; + sendScanResult(result); + } + } + + if (detectionString != nil) + { + AttachLog(@"String: %@", detectionString); + if ([resultString length] != 0) { + if([_session isRunning]) + { + [_session stopRunning]; + } + [_session removeInput:_input]; + [_session removeOutput:_output]; + UIAlertController *toast =[UIAlertController alertControllerWithTitle:nil + message:[NSString stringWithFormat:@"%@: %@",resultString, detectionString] + preferredStyle:UIAlertControllerStyleAlert]; + [self presentViewController:toast animated:YES completion:nil]; + + int duration = 2; // in seconds + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [toast dismissViewControllerAnimated:YES completion:^{ + sendScanResult(detectionString); + [self end]; + }]; + }); + } else { + sendScanResult(detectionString); + [self end]; + } + } + +} +@end diff --git a/modules/battery/build.gradle b/modules/battery/build.gradle index a69d920d..fe457a59 100644 --- a/modules/battery/build.gradle +++ b/modules/battery/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { implementation project(":util") + implementation project(":lifecycle") } ext.description = 'Common API to access battery features' \ No newline at end of file diff --git a/modules/battery/src/main/java/com/gluonhq/attach/battery/BatteryService.java b/modules/battery/src/main/java/com/gluonhq/attach/battery/BatteryService.java index 0c0bebd5..0c614f07 100644 --- a/modules/battery/src/main/java/com/gluonhq/attach/battery/BatteryService.java +++ b/modules/battery/src/main/java/com/gluonhq/attach/battery/BatteryService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Gluon + * 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 diff --git a/modules/battery/src/main/java/com/gluonhq/attach/battery/impl/DummyBatteryService.java b/modules/battery/src/main/java/com/gluonhq/attach/battery/impl/DummyBatteryService.java index 1359b7a0..d476732a 100644 --- a/modules/battery/src/main/java/com/gluonhq/attach/battery/impl/DummyBatteryService.java +++ b/modules/battery/src/main/java/com/gluonhq/attach/battery/impl/DummyBatteryService.java @@ -1,3 +1,30 @@ +/* + * 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.battery.impl; import com.gluonhq.attach.battery.BatteryService; diff --git a/modules/battery/src/main/java/com/gluonhq/attach/battery/impl/IOSBatteryService.java b/modules/battery/src/main/java/com/gluonhq/attach/battery/impl/IOSBatteryService.java new file mode 100644 index 00000000..20316287 --- /dev/null +++ b/modules/battery/src/main/java/com/gluonhq/attach/battery/impl/IOSBatteryService.java @@ -0,0 +1,99 @@ +/* + * 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.battery.impl; + +import com.gluonhq.attach.battery.BatteryService; +import com.gluonhq.attach.lifecycle.LifecycleEvent; +import com.gluonhq.attach.lifecycle.LifecycleService; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyFloatProperty; +import javafx.beans.property.ReadOnlyFloatWrapper; + +public class IOSBatteryService implements BatteryService { + + static { + System.loadLibrary("Battery"); + initBattery(); + } + + private static final ReadOnlyBooleanWrapper PLUGGED_IN = new ReadOnlyBooleanWrapper(); + private static final ReadOnlyFloatWrapper BATTERY_LEVEL = new ReadOnlyFloatWrapper(); + + public IOSBatteryService() { + LifecycleService.create().ifPresent(l -> { + l.addListener(LifecycleEvent.PAUSE, IOSBatteryService::stopObserver); + l.addListener(LifecycleEvent.RESUME, IOSBatteryService::startObserver); + }); + startObserver(); + } + + @Override + public float getBatteryLevel() { + return BATTERY_LEVEL.get(); + } + + @Override + public ReadOnlyFloatProperty batteryLevelProperty() { + return BATTERY_LEVEL.getReadOnlyProperty(); + } + + @Override + public boolean isPluggedIn() { + return PLUGGED_IN.get(); + } + + @Override + public ReadOnlyBooleanProperty pluggedInProperty() { + return PLUGGED_IN.getReadOnlyProperty(); + } + + // native + private static native void initBattery(); + private static native void startObserver(); + private static native void stopObserver(); + + // callback + private static void notifyBatteryState(String state) { + if (state == null) { + return; + } + // ios docs: charging -> device is plugged into power and the battery is less than 100% charged + // or full -> device is plugged into power and the battery is 100% charged + boolean plugged = state.equals("Charging") || state.equals("Full"); + if (PLUGGED_IN != null && PLUGGED_IN.get() != plugged) { + Platform.runLater(() -> PLUGGED_IN.set(plugged)); + } + } + private static void notifyBatteryLevel(float level) { + if (BATTERY_LEVEL != null && BATTERY_LEVEL.get() != level) { + Platform.runLater(() -> BATTERY_LEVEL.set(level)); + } + } +} \ No newline at end of file diff --git a/modules/battery/src/main/java/com/gluonhq/attach/battery/package-info.java b/modules/battery/src/main/java/com/gluonhq/attach/battery/package-info.java index 410b60ff..360a40dd 100644 --- a/modules/battery/src/main/java/com/gluonhq/attach/battery/package-info.java +++ b/modules/battery/src/main/java/com/gluonhq/attach/battery/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Battery plugin, + * Primary API package for Attach - Battery plugin, * contains the interface {@link com.gluonhq.attach.battery.BatteryService} and related classes. */ package com.gluonhq.attach.battery; \ No newline at end of file diff --git a/modules/battery/src/main/java/module-info.java b/modules/battery/src/main/java/module-info.java index 8c2e67d7..f8021136 100644 --- a/modules/battery/src/main/java/module-info.java +++ b/modules/battery/src/main/java/module-info.java @@ -27,8 +27,9 @@ */ module com.gluonhq.attach.battery { - requires javafx.base; + requires javafx.graphics; requires com.gluonhq.attach.util; + requires com.gluonhq.attach.lifecycle; exports com.gluonhq.attach.battery; exports com.gluonhq.attach.battery.impl to com.gluonhq.attach.util; diff --git a/modules/battery/src/main/native/ios/Battery.h b/modules/battery/src/main/native/ios/Battery.h new file mode 100644 index 00000000..920fc073 --- /dev/null +++ b/modules/battery/src/main/native/ios/Battery.h @@ -0,0 +1,40 @@ +/* + * 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 Battery : NSObject {} + - (void) startObserver; + - (void) stopObserver; + - (NSString*) getBatteryState; +@end + +void sendBatteryState(); +void sendBatteryLevel(); diff --git a/modules/battery/src/main/native/ios/Battery.m b/modules/battery/src/main/native/ios/Battery.m new file mode 100644 index 00000000..0fb71242 --- /dev/null +++ b/modules/battery/src/main/native/ios/Battery.m @@ -0,0 +1,150 @@ +/* + * 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 "Battery.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Battery(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 BatteryInited = 0; + +// Battery +jclass mat_jBatteryServiceClass; +jmethodID mat_jBatteryService_notifyBatteryLevel = 0; +jmethodID mat_jBatteryService_notifyBatteryState = 0; +Battery *_battery; + + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_battery_impl_IOSBatteryService_initBattery +(JNIEnv *env, jclass jClass) +{ + if (BatteryInited) + { + return; + } + BatteryInited = 1; + + mat_jBatteryServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/battery/impl/IOSBatteryService")); + mat_jBatteryService_notifyBatteryLevel = (*env)->GetStaticMethodID(env, mat_jBatteryServiceClass, "notifyBatteryLevel", "(F)V"); + mat_jBatteryService_notifyBatteryState = (*env)->GetStaticMethodID(env, mat_jBatteryServiceClass, "notifyBatteryState", "(Ljava/lang/String;)V"); + + _battery = [[Battery alloc] init]; + +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_battery_impl_IOSBatteryService_startObserver +(JNIEnv *env, jclass jClass) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [_battery startObserver]; + }); + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_battery_impl_IOSBatteryService_stopObserver +(JNIEnv *env, jclass jClass) +{ + [_battery stopObserver]; + return; +} + +void sendBatteryState() { + NSString *battery = [_battery getBatteryState]; + const char *batteryChars = [battery UTF8String]; + jstring arg = (*env)->NewStringUTF(env, batteryChars); + + (*env)->CallStaticVoidMethod(env, mat_jBatteryServiceClass, mat_jBatteryService_notifyBatteryState, arg); + (*env)->DeleteLocalRef(env, arg); +} + +void sendBatteryLevel() { + float batteryLevel = [[UIDevice currentDevice] batteryLevel]; + (*env)->CallStaticVoidMethod(env, mat_jBatteryServiceClass, mat_jBatteryService_notifyBatteryLevel, batteryLevel); +} + +@implementation Battery + +- (void) startObserver +{ + [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(BatteryStateDidChange:) name:UIDeviceBatteryStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(BatteryLevelDidChange:) name:UIDeviceBatteryLevelDidChangeNotification object:nil]; + sendBatteryState(); + sendBatteryLevel(); +} + +- (void) stopObserver +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceBatteryStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceBatteryLevelDidChangeNotification object:nil]; + [[UIDevice currentDevice] setBatteryMonitoringEnabled:NO]; +} + +- (NSString*) getBatteryState +{ + int state = [[UIDevice currentDevice] batteryState]; + + NSMutableString *value; + if(state == UIDeviceBatteryStateUnknown) + value = [NSMutableString stringWithString: @"Unknown"]; + else if(state == UIDeviceBatteryStateUnplugged) + value = [NSMutableString stringWithString: @"Unplugged"]; + else if(state == UIDeviceBatteryStateCharging) + value = [NSMutableString stringWithString: @"Charging"]; + else if(state == UIDeviceBatteryStateFull) + value = [NSMutableString stringWithString: @"Full"]; + else + value = [NSMutableString stringWithString: @"Unknown"]; + return value; +} + +-(void)BatteryStateDidChange:(NSNotification*)notification +{ + sendBatteryState(); +} + +-(void)BatteryLevelDidChange:(NSNotification*)notification +{ + sendBatteryLevel(); +} + +@end + diff --git a/modules/ble/build.gradle b/modules/ble/build.gradle index 0e832568..34928e10 100644 --- a/modules/ble/build.gradle +++ b/modules/ble/build.gradle @@ -1,3 +1,9 @@ +apply plugin: 'org.openjfx.javafxplugin' + +javafx { + modules 'javafx.graphics' +} + dependencies { implementation project(':util') } diff --git a/modules/ble/src/main/java/com/gluonhq/attach/ble/impl/DummyBleService.java b/modules/ble/src/main/java/com/gluonhq/attach/ble/impl/DummyBleService.java index 5d2d93e6..086e0e13 100644 --- a/modules/ble/src/main/java/com/gluonhq/attach/ble/impl/DummyBleService.java +++ b/modules/ble/src/main/java/com/gluonhq/attach/ble/impl/DummyBleService.java @@ -1,3 +1,30 @@ +/* + * 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.ble.impl; import com.gluonhq.attach.ble.BleService; diff --git a/modules/ble/src/main/java/com/gluonhq/attach/ble/impl/IOSBleService.java b/modules/ble/src/main/java/com/gluonhq/attach/ble/impl/IOSBleService.java new file mode 100644 index 00000000..9d9e7567 --- /dev/null +++ b/modules/ble/src/main/java/com/gluonhq/attach/ble/impl/IOSBleService.java @@ -0,0 +1,102 @@ +/* + * 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.ble.impl; + +import com.gluonhq.attach.ble.BleService; +import com.gluonhq.attach.ble.Configuration; +import com.gluonhq.attach.ble.ScanDetection; +import javafx.application.Platform; + +import java.util.function.Consumer; + +/** + * iOS implementation of BleService + * + * Important note: it requires adding to the info.plist file: + * + * {@code + * NSLocationUsageDescription + * Reason to use Location Service (iOS 6+) + * NSLocationAlwaysUsageDescription + * Reason to use Location Service (iOS 8+) + * } +*/ +public class IOSBleService implements BleService { + + static { + System.loadLibrary("Ble"); + initBle(); + } + + private static Consumer callback; + + /** + * startScanning is called with a given uuid and a callback for the beacon found. + * iOS iBeacon only scans for given uuid's + * + * iOS apps using BleService require the use of the key + * NSLocationAlwaysDescription in the plist file so the user is + * asked about allowing location services + * + * @param region Containing the beacon uuid + * @param callback Callback added to the beacon + */ + + @Override + public void startScanning(Configuration region, Consumer callback) { + this.callback = callback; + String[] uuids = new String[region.getUuids().size()]; + uuids = region.getUuids().toArray(uuids); + startObserver(uuids); + } + + /** + * stopScanning, if the manager is initialized + */ + @Override + public void stopScanning() { + stopObserver(); + } + + + // native + private static native void initBle(); // init IDs for java callbacks from native + private static native void startObserver(String[] uuids); + private static native void stopObserver(); + + // callback + private static void setDetection(String uuid, int major, int minor, int rssi, int proximity) { + ScanDetection detection = new ScanDetection(); + detection.setUuid(uuid); + detection.setMajor(major); + detection.setMinor(minor); + detection.setRssi(rssi); + detection.setProximity(proximity); + Platform.runLater(() -> callback.accept(detection)); + } +} diff --git a/modules/ble/src/main/java/module-info.java b/modules/ble/src/main/java/module-info.java index 25a56c93..3d46b031 100644 --- a/modules/ble/src/main/java/module-info.java +++ b/modules/ble/src/main/java/module-info.java @@ -27,6 +27,7 @@ */ module com.gluonhq.attach.ble { + requires javafx.graphics; requires com.gluonhq.attach.util; exports com.gluonhq.attach.ble; diff --git a/modules/ble/src/main/native/ios/BLE.h b/modules/ble/src/main/native/ios/BLE.h new file mode 100644 index 00000000..33d89ee7 --- /dev/null +++ b/modules/ble/src/main/native/ios/BLE.h @@ -0,0 +1,44 @@ +/* + * 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" +#import +#import + +@interface Ble : UIViewController +{ +} + @property(nonatomic, strong) CLLocationManager *locationManager; + @property(nonatomic, strong) CBCentralManager *bluetoothManager; + - (void) startObserver; + - (void) stopObserver; +@end + +void setDetection(CLBeacon *foundBeacon); \ No newline at end of file diff --git a/modules/ble/src/main/native/ios/BLE.m b/modules/ble/src/main/native/ios/BLE.m new file mode 100644 index 00000000..f207f782 --- /dev/null +++ b/modules/ble/src/main/native/ios/BLE.m @@ -0,0 +1,223 @@ +/* + * 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 "BLE.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Ble(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 BleInited = 0; + +// Ble +jclass mat_jBleServiceClass; +jmethodID mat_jBleService_setDetection = 0; +Ble *_Ble; +NSArray *arrayOfUuids; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ble_impl_IOSBleService_initBle +(JNIEnv *env, jclass jClass) +{ + if (BleInited) + { + return; + } + BleInited = 1; + + mat_jBleServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/ble/impl/IOSBleService")); + mat_jBleService_setDetection = (*env)->GetStaticMethodID(env, mat_jBleServiceClass, "setDetection", "(Ljava/lang/String;IIII)V"); + + _Ble = [[Ble alloc] init]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ble_impl_IOSBleService_startObserver +(JNIEnv *env, jclass jClass, jobjectArray jUuidsArray) +{ + int uuidCount = (*env)->GetArrayLength(env, jUuidsArray); + NSMutableArray *uuids = [[NSMutableArray alloc] init]; + + for (int i=0; iGetObjectArrayElement(env, jUuidsArray, i)); + const jchar *uuidString = (*env)->GetStringChars(env, juuid, NULL); + NSString *uuid = [NSString stringWithCharacters:(UniChar *)uuidString length:(*env)->GetStringLength(env, juuid)]; + (*env)->ReleaseStringChars(env, juuid, uuidString); + [uuids addObject:uuid]; + } + arrayOfUuids = [NSArray arrayWithArray:uuids]; + + [_Ble startObserver]; + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ble_impl_IOSBleService_stopObserver +(JNIEnv *env, jclass jClass) +{ + [_Ble stopObserver]; + return; +} + +void setDetection(CLBeacon *foundBeacon) { + if (foundBeacon) + { + NSString *uuid = foundBeacon.proximityUUID.UUIDString; + int major = [foundBeacon.major intValue]; + int minor = [foundBeacon.minor intValue]; + int rssi = foundBeacon.rssi; + int proximity = 0; + switch (foundBeacon.proximity) + { + case CLProximityUnknown: { proximity = 0; } break; + case CLProximityImmediate: { proximity = 1; } break; + case CLProximityNear: { proximity = 2; } break; + case CLProximityFar: { proximity = 3; } break; + default: { proximity = 0; } break; + } + const char *uuidChars = [uuid UTF8String]; + jstring juuid = (*env)->NewStringUTF(env, uuidChars); + (*env)->CallStaticVoidMethod(env, mat_jBleServiceClass, mat_jBleService_setDetection, juuid, major, minor, rssi, proximity); + (*env)->DeleteLocalRef(env, juuid); + } +} + +@implementation Ble + +- (void)startObserver +{ + if (!self.locationManager) { + self.locationManager = [[CLLocationManager alloc] init]; + self.locationManager.delegate = self; + + _bluetoothManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; + } + + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) + { + // Requires Always. With WhenInUse it won't work + [self.locationManager requestAlwaysAuthorization]; + } + + AttachLog(@"Start monitoring for regions"); + for (NSString* uuidString in arrayOfUuids) + { + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; + CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uuid + identifier:[NSString stringWithFormat:@"com.gluonhq.beacon.%@", uuidString]]; + [self.locationManager startMonitoringForRegion:beaconRegion]; + [self.locationManager startRangingBeaconsInRegion:beaconRegion]; + } +} + +- (void)stopObserver +{ + AttachLog(@"Stop monitoring for regions"); + if (self.locationManager) + { + NSSet *setOfRegions = [self.locationManager monitoredRegions]; + for (CLRegion *region in setOfRegions) { + [self.locationManager stopMonitoringForRegion:(CLBeaconRegion *) region]; + [self.locationManager stopRangingBeaconsInRegion:(CLBeaconRegion *) region]; + } + } +} + +- (void)locationManager:(CLLocationManager*)manager didEnterRegion:(CLRegion*)region +{ + [self.locationManager startRangingBeaconsInRegion:(CLBeaconRegion *) region]; +} + +-(void)locationManager:(CLLocationManager*)manager didExitRegion:(CLRegion*)region +{ + [self.locationManager stopRangingBeaconsInRegion:(CLBeaconRegion *) region]; +} + +- (void)locationManager:(CLLocationManager *)manager rangingBeaconsDidFailForRegion:(CLBeaconRegion *)region withError:(NSError *)error +{ + AttachLog(@"Ranging Beacons failed with error: %@", [error localizedDescription]); +} + +-(void)locationManager:(CLLocationManager*)manager didRangeBeacons:(NSArray*)beacons inRegion:(CLBeaconRegion*)region +{ + // sorted by proximity + CLBeacon *foundBeacon = [beacons firstObject]; + setDetection(foundBeacon); +} + +- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error +{ + AttachLog(@"Region monitoring failed with error: %@", [error localizedDescription]); +} + + +- (void)centralManagerDidUpdateState:(CBCentralManager *)central +{ + NSString *stateString = nil; + if (@available(iOS 10.0, *)) + { + switch(_bluetoothManager.state) + { + case CBManagerStateResetting: stateString = @"The connection with the system service was momentarily lost, update imminent."; break; + case CBManagerStateUnsupported: stateString = @"The platform doesn't support Bluetooth Low Energy."; break; + case CBManagerStateUnauthorized: stateString = @"The app is not authorized to use Bluetooth Low Energy."; break; + case CBManagerStatePoweredOff: stateString = @"Bluetooth is currently powered off."; break; + case CBManagerStatePoweredOn: stateString = @"Bluetooth is currently powered on and available to use."; break; + default: stateString = @"State unknown, update imminent."; break; + } + } + else + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + switch(_bluetoothManager.state) + { + case CBCentralManagerStateResetting: stateString = @"The connection with the system service was momentarily lost, update imminent."; break; + case CBCentralManagerStateUnsupported: stateString = @"The platform doesn't support Bluetooth Low Energy."; break; + case CBCentralManagerStateUnauthorized: stateString = @"The app is not authorized to use Bluetooth Low Energy."; break; + case CBCentralManagerStatePoweredOff: stateString = @"Bluetooth is currently powered off."; break; + case CBCentralManagerStatePoweredOn: stateString = @"Bluetooth is currently powered on and available to use."; break; + default: stateString = @"State unknown, update imminent."; break; + } + + #pragma clang diagnostic pop + } + AttachLog(@"Bluetooth State: %@",stateString); +} + +@end diff --git a/modules/browser/src/main/java/com/gluonhq/attach/browser/BrowserService.java b/modules/browser/src/main/java/com/gluonhq/attach/browser/BrowserService.java index c228f635..23d3bcb2 100644 --- a/modules/browser/src/main/java/com/gluonhq/attach/browser/BrowserService.java +++ b/modules/browser/src/main/java/com/gluonhq/attach/browser/BrowserService.java @@ -62,8 +62,8 @@ static Optional create() { * Launches the user-default browser to show a specified URL. * * @param url The URL to load when the browser application opens. - * @throws java.io.IOException - * @throws java.net.URISyntaxException + * @throws java.io.IOException If the URL can't be opened + * @throws java.net.URISyntaxException If it is not a valid URL string */ void launchExternalBrowser(String url) throws IOException, URISyntaxException; } diff --git a/modules/browser/src/main/java/com/gluonhq/attach/browser/impl/DummyBrowserService.java b/modules/browser/src/main/java/com/gluonhq/attach/browser/impl/DummyBrowserService.java index d7425e7b..adf4f877 100644 --- a/modules/browser/src/main/java/com/gluonhq/attach/browser/impl/DummyBrowserService.java +++ b/modules/browser/src/main/java/com/gluonhq/attach/browser/impl/DummyBrowserService.java @@ -1,3 +1,30 @@ +/* + * 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.browser.impl; import com.gluonhq.attach.browser.BrowserService; diff --git a/modules/browser/src/main/java/com/gluonhq/attach/browser/impl/IOSBrowserService.java b/modules/browser/src/main/java/com/gluonhq/attach/browser/impl/IOSBrowserService.java new file mode 100644 index 00000000..aa66132c --- /dev/null +++ b/modules/browser/src/main/java/com/gluonhq/attach/browser/impl/IOSBrowserService.java @@ -0,0 +1,50 @@ +/* + * 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.browser.impl; + + +import com.gluonhq.attach.browser.BrowserService; + +import java.io.IOException; + +public class IOSBrowserService implements BrowserService { + + static { + System.loadLibrary("Browser"); + } + + @Override + public void launchExternalBrowser(String url) throws IOException { + if (!launchURL(url)) { + throw new IOException("Error launching url " + url); + } + } + + private native boolean launchURL(String url); + +} diff --git a/modules/browser/src/main/java/com/gluonhq/attach/browser/package-info.java b/modules/browser/src/main/java/com/gluonhq/attach/browser/package-info.java index a3253884..90be6d59 100644 --- a/modules/browser/src/main/java/com/gluonhq/attach/browser/package-info.java +++ b/modules/browser/src/main/java/com/gluonhq/attach/browser/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Browser plugin, + * Primary API package for Attach - Browser plugin, * contains the interface {@link com.gluonhq.attach.browser.BrowserService} and related classes. */ package com.gluonhq.attach.browser; \ No newline at end of file diff --git a/modules/browser/src/main/native/ios/Browser.m b/modules/browser/src/main/native/ios/Browser.m new file mode 100644 index 00000000..67f31fa6 --- /dev/null +++ b/modules/browser/src/main/native/ios/Browser.m @@ -0,0 +1,81 @@ +/* + * 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" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Browser(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 +} + +JNIEXPORT jboolean JNICALL Java_com_gluonhq_attach_browser_impl_IOSBrowserService_launchURL +(JNIEnv *env, jclass jClass, jstring jUrl) +{ + const jchar *chars = (*env)->GetStringChars(env, jUrl, NULL); + NSString *url = [NSString stringWithCharacters:(UniChar *)chars length:(*env)->GetStringLength(env, jUrl)]; + (*env)->ReleaseStringChars(env, jUrl, chars); + + NSURL *nsUrl = [NSURL URLWithString:url]; + if ([[UIApplication sharedApplication] canOpenURL:nsUrl]) { + if (@available(iOS 10.0, *)) + { + [[UIApplication sharedApplication] openURL:nsUrl options:@{} + completionHandler:^(BOOL success) { + if (success) { + AttachLog(@"Opened url successfully"); + } + }]; + } + else { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + [[UIApplication sharedApplication] openURL:nsUrl]; + + #pragma clang diagnostic pop + } + AttachLog(@"Done opening url %@", url); + return JNI_TRUE; + } else { + AttachLog(@"Can't open url %@", url); + return JNI_FALSE; + } +} + diff --git a/modules/cache/src/main/java/com/gluonhq/attach/cache/package-info.java b/modules/cache/src/main/java/com/gluonhq/attach/cache/package-info.java index be2c0780..be1fdf59 100644 --- a/modules/cache/src/main/java/com/gluonhq/attach/cache/package-info.java +++ b/modules/cache/src/main/java/com/gluonhq/attach/cache/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 Gluon + * 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 @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Cache plugin, + * Primary API package for Attach - Cache plugin, * contains the interface {@link com.gluonhq.attach.cache.CacheService} and related classes. */ package com.gluonhq.attach.cache; \ No newline at end of file diff --git a/modules/compass/build.gradle b/modules/compass/build.gradle index 80625449..efd45399 100644 --- a/modules/compass/build.gradle +++ b/modules/compass/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { implementation project(":util") + implementation project(":magnetometer") } ext.description = 'Common API to access compass features' \ No newline at end of file diff --git a/modules/compass/src/main/java/com/gluonhq/attach/compass/impl/DummyCompassService.java b/modules/compass/src/main/java/com/gluonhq/attach/compass/impl/DummyCompassService.java index 3cd07999..85f46ea6 100644 --- a/modules/compass/src/main/java/com/gluonhq/attach/compass/impl/DummyCompassService.java +++ b/modules/compass/src/main/java/com/gluonhq/attach/compass/impl/DummyCompassService.java @@ -1,3 +1,30 @@ +/* + * 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.compass.impl; import com.gluonhq.attach.compass.CompassService; diff --git a/modules/compass/src/main/java/com/gluonhq/attach/compass/impl/IOSCompassService.java b/modules/compass/src/main/java/com/gluonhq/attach/compass/impl/IOSCompassService.java new file mode 100644 index 00000000..3af77da5 --- /dev/null +++ b/modules/compass/src/main/java/com/gluonhq/attach/compass/impl/IOSCompassService.java @@ -0,0 +1,58 @@ +/* + * 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.compass.impl; + +import com.gluonhq.attach.compass.CompassService; +import com.gluonhq.attach.magnetometer.MagnetometerService; +import com.gluonhq.attach.util.Services; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; + +public class IOSCompassService implements CompassService { + + private final ReadOnlyDoubleWrapper heading; + + public IOSCompassService() { + heading = new ReadOnlyDoubleWrapper(); + + Services.get(MagnetometerService.class).ifPresent(m -> { + m.readingProperty().addListener((obs, ov, nv) -> heading.setValue(nv.getAzimuth())); + }); + } + + @Override + public double getHeading() { + return heading.get(); + } + + @Override + public ReadOnlyDoubleProperty headingProperty() { + return heading.getReadOnlyProperty(); + } + +} diff --git a/modules/compass/src/main/java/com/gluonhq/attach/compass/package-info.java b/modules/compass/src/main/java/com/gluonhq/attach/compass/package-info.java index 62acb789..d6e8e8b7 100644 --- a/modules/compass/src/main/java/com/gluonhq/attach/compass/package-info.java +++ b/modules/compass/src/main/java/com/gluonhq/attach/compass/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 Gluon + * 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 @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Compass plugin, + * Primary API package for Attach - Compass plugin, * contains the interface {@link com.gluonhq.attach.compass.CompassService} and related classes. */ package com.gluonhq.attach.compass; \ No newline at end of file diff --git a/modules/compass/src/main/java/module-info.java b/modules/compass/src/main/java/module-info.java index 32b7c10d..c611c9f3 100644 --- a/modules/compass/src/main/java/module-info.java +++ b/modules/compass/src/main/java/module-info.java @@ -27,8 +27,9 @@ */ module com.gluonhq.attach.compass { - requires javafx.base; + requires javafx.graphics; requires com.gluonhq.attach.util; + requires com.gluonhq.attach.magnetometer; exports com.gluonhq.attach.compass; exports com.gluonhq.attach.compass.impl to com.gluonhq.attach.util; diff --git a/modules/connectivity/build.gradle b/modules/connectivity/build.gradle index bc8f221d..dc077cd8 100644 --- a/modules/connectivity/build.gradle +++ b/modules/connectivity/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { diff --git a/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/impl/DummyConnectivityService.java b/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/impl/DummyConnectivityService.java index e91e6e22..4bf453f1 100644 --- a/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/impl/DummyConnectivityService.java +++ b/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/impl/DummyConnectivityService.java @@ -1,3 +1,30 @@ +/* + * 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.connectivity.impl; import com.gluonhq.attach.connectivity.ConnectivityService; diff --git a/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/impl/IOSConnectivityService.java b/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/impl/IOSConnectivityService.java new file mode 100644 index 00000000..70910bbb --- /dev/null +++ b/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/impl/IOSConnectivityService.java @@ -0,0 +1,64 @@ +/* + * 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.connectivity.impl; + +import com.gluonhq.attach.connectivity.ConnectivityService; +import com.gluonhq.attach.util.PropertyWatcher; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; + +public class IOSConnectivityService implements ConnectivityService { + + static { + System.loadLibrary("Connectivity"); + } + + private native boolean singleCheck(); + + private ReadOnlyBooleanWrapper connectedProperty; + + @Override + public ReadOnlyBooleanProperty connectedProperty() { + if (connectedProperty == null) { + connectedProperty = new ReadOnlyBooleanWrapper(); + PropertyWatcher.addPropertyWatcher(() -> { + final boolean connected = isConnected(); + if (connectedProperty.getValue() != connected) { + Platform.runLater(() -> connectedProperty.setValue(connected)); + } + }); + } + return connectedProperty.getReadOnlyProperty(); + } + + @Override + public boolean isConnected() { + return singleCheck(); + } +} \ No newline at end of file diff --git a/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/package-info.java b/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/package-info.java index d73ea4d6..6b4b8f08 100644 --- a/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/package-info.java +++ b/modules/connectivity/src/main/java/com/gluonhq/attach/connectivity/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Gluon + * 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 @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Connectivity plugin, + * Primary API package for Attach - Connectivity plugin, * contains the interface {@link com.gluonhq.attach.connectivity.ConnectivityService} and related classes. */ package com.gluonhq.attach.connectivity; \ No newline at end of file diff --git a/modules/connectivity/src/main/java/module-info.java b/modules/connectivity/src/main/java/module-info.java index 316a9c14..c39af644 100644 --- a/modules/connectivity/src/main/java/module-info.java +++ b/modules/connectivity/src/main/java/module-info.java @@ -27,7 +27,7 @@ */ module com.gluonhq.attach.connectivity { - requires javafx.base; + requires javafx.graphics; requires com.gluonhq.attach.util; exports com.gluonhq.attach.connectivity; diff --git a/modules/connectivity/src/main/native/ios/Connectivity.m b/modules/connectivity/src/main/native/ios/Connectivity.m new file mode 100644 index 00000000..c4c7f103 --- /dev/null +++ b/modules/connectivity/src/main/native/ios/Connectivity.m @@ -0,0 +1,63 @@ +/* + * 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" +#include +#include + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Connectivity(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 +} + +JNIEXPORT jboolean JNICALL Java_com_gluonhq_attach_connectivity_impl_IOSConnectivityService_singleCheck +(JNIEnv *env, jclass jClass) { + + char *hostname; + struct hostent *hostinfo; + hostname = "apple.com"; + hostinfo = gethostbyname(hostname); + if (hostinfo == NULL) { + return JNI_FALSE; + } + else { + return JNI_TRUE; + }; +} \ No newline at end of file diff --git a/modules/device/src/main/java/com/gluonhq/attach/device/impl/DummyDeviceServiceImpl.java b/modules/device/src/main/java/com/gluonhq/attach/device/impl/DummyDeviceServiceImpl.java index 53b561a4..a8e7c9ee 100644 --- a/modules/device/src/main/java/com/gluonhq/attach/device/impl/DummyDeviceServiceImpl.java +++ b/modules/device/src/main/java/com/gluonhq/attach/device/impl/DummyDeviceServiceImpl.java @@ -1,3 +1,30 @@ +/* + * 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.device.impl; import com.gluonhq.attach.device.DeviceService; diff --git a/modules/device/src/main/java/com/gluonhq/attach/device/package-info.java b/modules/device/src/main/java/com/gluonhq/attach/device/package-info.java new file mode 100644 index 00000000..813fee50 --- /dev/null +++ b/modules/device/src/main/java/com/gluonhq/attach/device/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 - Device plugin, + * contains the interface {@link com.gluonhq.attach.device.DeviceService} and related classes. + */ +package com.gluonhq.attach.device; \ No newline at end of file diff --git a/modules/device/src/main/native/ios/Device.m b/modules/device/src/main/native/ios/Device.m index 5141aebe..1b8407b3 100644 --- a/modules/device/src/main/native/ios/Device.m +++ b/modules/device/src/main/native/ios/Device.m @@ -27,6 +27,7 @@ */ #import #include "jni.h" +#include "AttachMacros.h" JNIEnv *env; diff --git a/modules/dialer/src/main/java/com/gluonhq/attach/dialer/impl/DummyDialerService.java b/modules/dialer/src/main/java/com/gluonhq/attach/dialer/impl/DummyDialerService.java index 985ad1cd..3b357b1b 100644 --- a/modules/dialer/src/main/java/com/gluonhq/attach/dialer/impl/DummyDialerService.java +++ b/modules/dialer/src/main/java/com/gluonhq/attach/dialer/impl/DummyDialerService.java @@ -1,3 +1,30 @@ +/* + * 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.dialer.impl; import com.gluonhq.attach.dialer.DialerService; diff --git a/modules/dialer/src/main/java/com/gluonhq/attach/dialer/impl/IOSDialerService.java b/modules/dialer/src/main/java/com/gluonhq/attach/dialer/impl/IOSDialerService.java new file mode 100644 index 00000000..0672c9ba --- /dev/null +++ b/modules/dialer/src/main/java/com/gluonhq/attach/dialer/impl/IOSDialerService.java @@ -0,0 +1,48 @@ +/* + * 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.dialer.impl; + + +import com.gluonhq.attach.dialer.DialerService; + +public class IOSDialerService implements DialerService { + + static { + System.loadLibrary("Dialer"); + } + + @Override + public void call(String number) { + if (number != null && !number.isEmpty()) { + callNumber(number); + } + } + + private native void callNumber(String number); + +} diff --git a/modules/dialer/src/main/java/com/gluonhq/attach/dialer/package-info.java b/modules/dialer/src/main/java/com/gluonhq/attach/dialer/package-info.java index bb8a279c..a437199e 100644 --- a/modules/dialer/src/main/java/com/gluonhq/attach/dialer/package-info.java +++ b/modules/dialer/src/main/java/com/gluonhq/attach/dialer/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Dialer plugin, + * Primary API package for Attach - Dialer plugin, * contains the interface {@link com.gluonhq.attach.dialer.DialerService} and related classes. */ package com.gluonhq.attach.dialer; \ No newline at end of file diff --git a/modules/dialer/src/main/native/ios/Dialer.m b/modules/dialer/src/main/native/ios/Dialer.m new file mode 100644 index 00000000..00190d0c --- /dev/null +++ b/modules/dialer/src/main/native/ios/Dialer.m @@ -0,0 +1,74 @@ +/* + * 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" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Dialer(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 +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_dialer_impl_IOSDialerService_callNumber +(JNIEnv *env, jclass jClass, jstring jNumber) +{ + const jchar *chars = (*env)->GetStringChars(env, jNumber, NULL); + NSString *number = [NSString stringWithCharacters:(UniChar *)chars length:(*env)->GetStringLength(env, jNumber)]; + (*env)->ReleaseStringChars(env, jNumber, chars); + NSURL *phoneUrl = [NSURL URLWithString:[NSString stringWithFormat:@"tel://%@",number]]; + if ([[UIApplication sharedApplication] canOpenURL:phoneUrl]) { + if (@available(iOS 10.0, *)) + { + [[UIApplication sharedApplication] openURL:phoneUrl options:@{} + completionHandler:nil]; + } + else { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + [[UIApplication sharedApplication] openURL:phoneUrl]; + + #pragma clang diagnostic pop + } + AttachLog(@"Done calling to %@", number); + } else { + AttachLog(@"Can't call to %@", number); + } +} + diff --git a/modules/display/src/main/native/ios/Display.h b/modules/display/src/main/native/ios/Display.h index 2ccb2381..cef07fc8 100644 --- a/modules/display/src/main/native/ios/Display.h +++ b/modules/display/src/main/native/ios/Display.h @@ -28,6 +28,7 @@ #import #include "jni.h" +#include "AttachMacros.h" @interface Display : UIViewController {} - (void) isIPhoneX; diff --git a/modules/display/src/main/native/ios/Display.m b/modules/display/src/main/native/ios/Display.m index b69c7c6b..93600ef1 100644 --- a/modules/display/src/main/native/ios/Display.m +++ b/modules/display/src/main/native/ios/Display.m @@ -60,7 +60,7 @@ return; } DisplayInited = 1; - printf("INIT\n "); + mat_jDisplayServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/display/impl/IOSDisplayService")); mat_jDisplayService_notifyDisplay = (*env)->GetStaticMethodID(env, mat_jDisplayServiceClass, "notifyDisplay", "(Ljava/lang/String;)V"); @@ -146,7 +146,7 @@ void sendNotch() { NSString *notch = [_display getNotch]; - NSLog(@"Notch is %@", notch); + AttachLog(@"Notch is %@", notch); const char *notchChars = [notch UTF8String]; jstring arg = (*env)->NewStringUTF(env, notchChars); (*env)->CallStaticVoidMethod(env, mat_jDisplayServiceClass, mat_jDisplayService_notifyDisplay, arg); @@ -159,7 +159,9 @@ - (void) isIPhoneX { iPhoneX = NO; if ([[UIDevice currentDevice].model hasPrefix:@"iPhone"] && - [[UIScreen mainScreen] nativeBounds].size.height == 2436) + ([[UIScreen mainScreen] nativeBounds].size.height == 1792 || // XR + [[UIScreen mainScreen] nativeBounds].size.height == 2436 || // X, XS + [[UIScreen mainScreen] nativeBounds].size.height == 2688)) // XS MAX { iPhoneX = YES; } diff --git a/modules/in-app-billing/src/main/java/com/gluonhq/attach/inappbilling/impl/DummyInAppBillingService.java b/modules/in-app-billing/src/main/java/com/gluonhq/attach/inappbilling/impl/DummyInAppBillingService.java index d049fd1f..d0069745 100644 --- a/modules/in-app-billing/src/main/java/com/gluonhq/attach/inappbilling/impl/DummyInAppBillingService.java +++ b/modules/in-app-billing/src/main/java/com/gluonhq/attach/inappbilling/impl/DummyInAppBillingService.java @@ -1,3 +1,30 @@ +/* + * 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.inappbilling.impl; import com.gluonhq.attach.inappbilling.InAppBillingService; diff --git a/modules/in-app-billing/src/main/java/com/gluonhq/attach/inappbilling/impl/IOSInAppBillingService.java b/modules/in-app-billing/src/main/java/com/gluonhq/attach/inappbilling/impl/IOSInAppBillingService.java new file mode 100644 index 00000000..86980e47 --- /dev/null +++ b/modules/in-app-billing/src/main/java/com/gluonhq/attach/inappbilling/impl/IOSInAppBillingService.java @@ -0,0 +1,304 @@ +/* + * 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.inappbilling.impl; + +import com.gluonhq.attach.inappbilling.InAppBillingException; +import com.gluonhq.attach.inappbilling.InAppBillingQueryResult; +import com.gluonhq.attach.inappbilling.InAppBillingQueryResultListener; +import com.gluonhq.attach.inappbilling.InAppBillingService; +import com.gluonhq.attach.inappbilling.Product; +import com.gluonhq.attach.inappbilling.ProductDetails; +import com.gluonhq.attach.inappbilling.ProductOrder; +import com.gluonhq.attach.util.Constants; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableMap; +import javafx.concurrent.Task; +import javafx.concurrent.Worker; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class IOSInAppBillingService implements InAppBillingService { + + static { + System.loadLibrary("InAppBilling"); + initInAppBilling(); + } + + private static final ReadOnlyBooleanWrapper ready = new ReadOnlyBooleanWrapper(false); + + private static InAppBillingQueryResultListener queryResultListener; + + private static List registeredProducts = new LinkedList<>(); + private final List productIds = new LinkedList<>(); + private final List subscriptionIds = new LinkedList<>(); + private final static List detailedProducts = new LinkedList<>(); + private static boolean supported = true; + private static final BooleanProperty FETCH = new SimpleBooleanProperty(); + private static final ObservableMap MAP_ORDERS = FXCollections.observableMap(new HashMap<>()); + + @Override + public boolean isSupported() { + return supported; + } + + @Override + public void setQueryResultListener(InAppBillingQueryResultListener listener) { + this.queryResultListener = listener; + } + + @Override + public void setRegisteredProducts(List registeredProducts) { + this.registeredProducts = registeredProducts; + } + + @Override + public void initialize(String androidPublicKey, List registeredProducts) { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + this.registeredProducts = registeredProducts; + } + + @Override + public boolean isReady() { + return ready.get(); + } + + @Override + public ReadOnlyBooleanProperty readyProperty() { + return ready.getReadOnlyProperty(); + } + + @Override + public Worker> fetchProductDetails() { + if (!isReady()) { + return null; + } + Task> task = new Task>() { + + @Override + protected List call() throws Exception { + for (Product registeredProduct : registeredProducts) { + switch (registeredProduct.getType()) { + case CONSUMABLE: + case NON_CONSUMABLE: + productIds.add(registeredProduct.getId()); + break; + case FREE_SUBSCRIPTION: + case RENEWABLE_SUBSCRIPTION: + case NON_RENEWABLE_SUBSCRIPTION: + subscriptionIds.add(registeredProduct.getId()); + break; + } + } + + CountDownLatch latch = new CountDownLatch(1); + + FETCH.addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { + FETCH.removeListener(this); + latch.countDown(); + } + }); + + String[] ids = new String[productIds.size()]; + fetchProducts(productIds.toArray(ids)); + + if (latch.await(5, TimeUnit.MINUTES)) { + return detailedProducts; + } else { + throw new InAppBillingException("Products fetch operation timed out."); + } + } + }; + FETCH.set(false); + Thread thread = new Thread(task); + thread.start(); + return task; + } + + @Override + public Worker order(Product product) { + if (!isReady()) { + return null; + } + + final String key = UUID.randomUUID().toString(); + MAP_ORDERS.put(key, null); + Task task = new Task() { + + @Override + protected ProductOrder call() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + MAP_ORDERS.addListener(new MapChangeListener() { + @Override + public void onChanged(MapChangeListener.Change change) { + if (key.equals(change.getKey())) { + MAP_ORDERS.removeListener(this); + latch.countDown(); + } + } + }); + + purchaseProduct(key, product.getId()); + + if (latch.await(15, TimeUnit.MINUTES)) { + ProductOrder productOrder = MAP_ORDERS.remove(key); + if (productOrder == null) { + throw new InAppBillingException("There was an error purchasing the product"); + } + return productOrder; + } else { + throw new InAppBillingException("Product order operation timed out."); + } + } + }; + Thread thread = new Thread(task); + thread.start(); + return task; + } + + @Override + public Worker finish(ProductOrder productOrder) { + if (!isReady()) { + return null; + } + Task task = new Task() { + + @Override + protected Product call() throws Exception { + if (productOrder != null && productOrder.getProduct() != null) { + Product product = productOrder.getProduct(); + product.getDetails().setState(ProductDetails.State.FINISHED); + return product; + } + return null; + } + }; + Thread thread = new Thread(task); + thread.start(); + return task; + } + + // native + private static native void initInAppBilling(); // init IDs for java callbacks from native + private static native void fetchProducts(String[] ids); + private static native void purchaseProduct(String key, String id); + private static native void enableDebug(); + + // callbacks + private static void setInAppReady(boolean value) { + supported = value; + Platform.runLater(() -> ready.set(value)); + } + + private static void setProduct(String id, String title, String description, String price, String currency) { + ProductDetails details = new ProductDetails(); + details.setTitle(title); + details.setDescription(description); + details.setPrice(price); + details.setCurrency(currency); + + for (Product product : registeredProducts) { + if (product.getId().equals(id)) { + Product detailedProduct = new Product(product.getId(), product.getType()); + detailedProduct.setDetails(details); + detailedProducts.add(detailedProduct); + break; + } + } + } + + private static void doneFetching(boolean value) { + if (!value) { + System.out.println("There was an error fetching products"); + } + Platform.runLater(() -> FETCH.set(true)); + } + + private static void restorePurchases(String[] puchasesId) { + InAppBillingQueryResult result = new InAppBillingQueryResult(); + for (String id: puchasesId) { + for (Product product : detailedProducts) { + if (product.getId().equals(id)) { + product.getDetails().setState(ProductDetails.State.APPROVED); + + ProductOrder productOrder = new ProductOrder(); + productOrder.setPlatform(com.gluonhq.attach.util.Platform.IOS); + productOrder.setProduct(product); + // TODO: Restoring purchases doesn't take the fields for now + //productOrder.setFields(fields); + result.getProductOrders().add(productOrder); + break; + } + } + } + Platform.runLater(() -> queryResultListener.onQueryResultReceived(result)); + } + + private static void setPurchase(String key, String purchasedId, String transactionId, String transactionReceipt) { + if (purchasedId == null || purchasedId.isEmpty()) { + Platform.runLater(() -> MAP_ORDERS.put(key, null)); + return; + } + + ProductOrder productOrder = new ProductOrder(); + productOrder.setPlatform(com.gluonhq.attach.util.Platform.IOS); + for (Product product : detailedProducts) { + if (product.getId().equals(purchasedId)) { + productOrder.setProduct(product); + break; + } + } + Map fields = new HashMap<>(); + fields.put("productId", purchasedId); + fields.put("orderId", transactionId); + fields.put("token", transactionReceipt); + + // TODO: validate transactionReceipt from the server side + // https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1 + productOrder.setFields(fields); + Platform.runLater(() -> MAP_ORDERS.put(key, productOrder)); + } +} diff --git a/modules/in-app-billing/src/main/native/ios/InAppBilling.h b/modules/in-app-billing/src/main/native/ios/InAppBilling.h new file mode 100644 index 00000000..8b6c1766 --- /dev/null +++ b/modules/in-app-billing/src/main/native/ios/InAppBilling.h @@ -0,0 +1,54 @@ +/* + * 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" +#import + +@interface InAppBilling : UIViewController +{ + SKProductsRequest *productsRequest; +} + + - (void) setup; + - (void) fetchProducts; + - (void) purchaseProduct:(NSString *)productId; + + - (void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions; + - (void) paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error; + - (void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue; + - (void) logMessage:(NSString *)format, ...; + +@end + +void sendProduct(SKProduct *product); +void ready(BOOL value); +void doneFetching(BOOL value); +void sendPurchases(NSArray *purchasedIDs); +void sendPurchase(NSString *purchasedID, NSString *transactionId, NSString *transactionReceipt); diff --git a/modules/in-app-billing/src/main/native/ios/InAppBilling.m b/modules/in-app-billing/src/main/native/ios/InAppBilling.m new file mode 100644 index 00000000..1031e627 --- /dev/null +++ b/modules/in-app-billing/src/main/native/ios/InAppBilling.m @@ -0,0 +1,396 @@ +/* + * 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 "InAppBilling.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_InAppBilling(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 InAppBillingInited = 0; + +// InAppBilling +jclass mat_jInAppBillingServiceClass; +jmethodID mat_jInAppBillingService_setInAppReady = 0; +jmethodID mat_jInAppBillingService_setProduct = 0; +jmethodID mat_jInAppBillingService_doneFetching = 0; +jmethodID mat_jInAppBillingService_restorePurchases = 0; +jmethodID mat_jInAppBillingService_setPurchase = 0; + +InAppBilling *_InAppBilling; +NSArray *arrayOfProductIds; +NSMutableArray *arrayOfProducts; +NSNumberFormatter *numberFormatter; +NSMutableDictionary *orders; +BOOL debugInAppBilling; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_inappbilling_impl_IOSInAppBillingService_initInAppBilling +(JNIEnv *env, jclass jClass) +{ + if (InAppBillingInited) + { + return; + } + InAppBillingInited = 1; + + mat_jInAppBillingServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/inappbilling/impl/IOSInAppBillingService")); + mat_jInAppBillingService_setInAppReady = (*env)->GetStaticMethodID(env, mat_jInAppBillingServiceClass, "setInAppReady", "(Z)V"); + mat_jInAppBillingService_setProduct = (*env)->GetStaticMethodID(env, mat_jInAppBillingServiceClass, "setProduct", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + mat_jInAppBillingService_doneFetching = (*env)->GetStaticMethodID(env, mat_jInAppBillingServiceClass, "doneFetching", "(Z)V"); + mat_jInAppBillingService_restorePurchases = (*env)->GetStaticMethodID(env, mat_jInAppBillingServiceClass, "restorePurchases", "([Ljava/lang/String;)V"); + mat_jInAppBillingService_setPurchase = (*env)->GetStaticMethodID(env, mat_jInAppBillingServiceClass, "setPurchase", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + + debugInAppBilling = NO; + orders = [[NSMutableDictionary alloc] init]; + + AttachLog(@"Init InAppBilling"); + _InAppBilling = [[InAppBilling alloc] init]; + + [_InAppBilling setup]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_inappbilling_impl_IOSInAppBillingService_fetchProducts +(JNIEnv *env, jclass jClass, jobjectArray jProductIdsArray) +{ + int productIdCount = (*env)->GetArrayLength(env, jProductIdsArray); + NSMutableArray *productIds = [[NSMutableArray alloc] init]; + + for (int i=0; iGetObjectArrayElement(env, jProductIdsArray, i)); + const jchar *productIdString = (*env)->GetStringChars(env, jproductId, NULL); + NSString *productId = [NSString stringWithCharacters:(UniChar *)productIdString length:(*env)->GetStringLength(env, jproductId)]; + (*env)->ReleaseStringChars(env, jproductId, productIdString); + [productIds addObject:productId]; + } + arrayOfProductIds = [NSArray arrayWithArray:productIds]; + + AttachLog(@"Fetching products"); + [_InAppBilling fetchProducts]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_inappbilling_impl_IOSInAppBillingService_purchaseProduct +(JNIEnv *env, jclass jClass, jstring jKey, jstring jProductId) +{ + const jchar *keyString = (*env)->GetStringChars(env, jKey, NULL); + NSString *key = [NSString stringWithCharacters:(UniChar *)keyString length:(*env)->GetStringLength(env, jKey)]; + (*env)->ReleaseStringChars(env, jKey, keyString); + + const jchar *productIdString = (*env)->GetStringChars(env, jProductId, NULL); + NSString *productId = [NSString stringWithCharacters:(UniChar *)productIdString length:(*env)->GetStringLength(env, jProductId)]; + (*env)->ReleaseStringChars(env, jProductId, productIdString); + + AttachLog(@"Purchasing product %@ with key %@", productId, key); + [orders setObject:productId forKey:key]; + [_InAppBilling purchaseProduct:productId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_inappbilling_impl_IOSInAppBillingService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugInAppBilling = YES; +} + +void ready(BOOL value) { + (*env)->CallStaticVoidMethod(env, mat_jInAppBillingServiceClass, mat_jInAppBillingService_setInAppReady, (value) ? JNI_TRUE : JNI_FALSE); +} + +void sendProduct(SKProduct *product) { + const char *product0Chars = [product.productIdentifier UTF8String]; + jstring arg0 = (*env)->NewStringUTF(env, product0Chars); + const char *product1Chars = [product.localizedTitle UTF8String]; + jstring arg1 = (*env)->NewStringUTF(env, product1Chars); + const char *product2Chars = [product.localizedDescription UTF8String]; + jstring arg2 = (*env)->NewStringUTF(env, product2Chars); + [numberFormatter setLocale:product.priceLocale]; + const char *product3Chars = [[numberFormatter stringFromNumber:product.price] UTF8String]; + jstring arg3 = (*env)->NewStringUTF(env, product3Chars); + const char *product4Chars = [product.priceLocale.currencyCode UTF8String]; + jstring arg4 = (*env)->NewStringUTF(env, product4Chars); + (*env)->CallStaticVoidMethod(env, mat_jInAppBillingServiceClass, mat_jInAppBillingService_setProduct, arg0, arg1, arg2, arg3, arg4); + (*env)->DeleteLocalRef(env, arg0); + (*env)->DeleteLocalRef(env, arg1); + (*env)->DeleteLocalRef(env, arg2); + (*env)->DeleteLocalRef(env, arg3); + (*env)->DeleteLocalRef(env, arg4); + + AttachLog(@"Finished sending product"); +} + +void doneFetching(BOOL value) +{ + (*env)->CallStaticVoidMethod(env, mat_jInAppBillingServiceClass, mat_jInAppBillingService_doneFetching, (value) ? JNI_TRUE : JNI_FALSE); +} + +void sendPurchases(NSArray *purchasedIDs) +{ + int size = [purchasedIDs count]; + jobjectArray ret = (*env)->NewObjectArray(env, size, (*env)->FindClass(env, "java/lang/String"), NULL); + + int i; + for (i = 0; i < size; i++) + { + const char *purchasedID = [purchasedIDs[i] UTF8String]; + (*env)->SetObjectArrayElement(env, ret, i, (*env)->NewStringUTF(env, purchasedID)); + } + (*env)->CallStaticVoidMethod(env, mat_jInAppBillingServiceClass, mat_jInAppBillingService_restorePurchases, ret); + (*env)->DeleteLocalRef(env, ret); +} + +void sendPurchase(NSString *purchasedID, NSString *transactionId, NSString *transactionReceipt) +{ + AttachLog(@"Sending purchase %@", purchasedID); + NSString* key; + for (NSString* k in orders) + { + NSString *v = [orders objectForKey:k]; + if ([v isEqualToString:purchasedID]) + { + key = k; + break; + } + } + if (!key) + { + AttachLog(@"Error retrieving key from orders for product %@", purchasedID); + return; + } + + const char *keyChars = [key UTF8String]; + jstring arg0 = (*env)->NewStringUTF(env, keyChars); + [orders removeObjectForKey:key]; + const char *productIdChars = [purchasedID UTF8String]; + jstring arg1 = (*env)->NewStringUTF(env, productIdChars); + const char *transactionIdChars = [transactionId UTF8String]; + jstring arg2 = (*env)->NewStringUTF(env, transactionIdChars); + const char *transactionReceiptChars = [transactionReceipt UTF8String]; + jstring arg3 = (*env)->NewStringUTF(env, transactionReceiptChars); + (*env)->CallStaticVoidMethod(env, mat_jInAppBillingServiceClass, mat_jInAppBillingService_setPurchase, arg0, arg1, arg2, arg3); + (*env)->DeleteLocalRef(env, arg0); + (*env)->DeleteLocalRef(env, arg1); + (*env)->DeleteLocalRef(env, arg2); + (*env)->DeleteLocalRef(env, arg3); +} + +@implementation InAppBilling + +- (void) setup +{ + if (![SKPaymentQueue canMakePayments]) + { + AttachLog(@"Can't make payments. Please enable In App Purchase in Settings"); + ready(NO); + } + else + { + [self logMessage:@"In App Purchase enabled"]; + + [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; + + numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + + ready(YES); + } +} + +- (void) fetchProducts +{ + productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:arrayOfProductIds]]; + productsRequest.delegate = self; + + [self logMessage:@"Start fetching products"]; + [productsRequest start]; +} + +- (void) purchaseProduct:(NSString *)productId +{ + [self logMessage:@"Purchase product %@", productId]; + if ([arrayOfProducts count]) { + [self logMessage:@"Available products %d", [arrayOfProducts count]]; + for (SKProduct *product in arrayOfProducts) + { + [self logMessage:@"Trying product %@", product.productIdentifier]; + if ([product.productIdentifier isEqualToString:productId]) + { + [self logMessage:@"Start purchasing product %@", productId]; + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [[SKPaymentQueue defaultQueue] addPayment:payment]; + break; + } + } + } + else + { + [self logMessage:@"No products to purchase"]; + } +} + +-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response +{ + + arrayOfProducts = [[NSMutableArray alloc] init]; + + NSArray *products = response.products; + + if (products.count != 0) + { + for (SKProduct *product in products) + { + [arrayOfProducts addObject:product]; + sendProduct(product); + } + } else { + AttachLog(@"Products not found"); + } + + for (SKProduct *product in response.invalidProductIdentifiers) + { + [self logMessage:@"Invalid product found: %@", product]; + } + + [productsRequest release]; + + // restoring purchases + [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + +} + +// SKPaymentTransactionObserver methods +// called when the transaction status is updated +- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions +{ + NSString *state, *transactionIdentifier, *transactionReceipt, *productId; + NSData *receiptData; + + [self logMessage:@"paymentQueue"]; + for (SKPaymentTransaction *transaction in transactions) + { + switch (transaction.transactionState) + { + case SKPaymentTransactionStatePurchased: + [self logMessage:@"completeTransaction"]; + state = @"PaymentTransactionStatePurchased"; + transactionIdentifier = transaction.transactionIdentifier; + receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]; + if (!receiptData) { + transactionReceipt = @"No receipt"; + } else { + transactionReceipt = [receiptData base64EncodedStringWithOptions:0]; + } + productId = transaction.payment.productIdentifier; + [self logMessage:@"transaction state: %@ with id: %@ for product: %@", state, transactionIdentifier, productId]; + + sendPurchase(productId, transactionIdentifier, transactionReceipt); + + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + break; + case SKPaymentTransactionStateFailed: + [self logMessage:@"failedTransaction: error %d %@", transaction.error.code, transaction.error.localizedDescription]; + state = @"PaymentTransactionStateFailed"; + sendPurchase(@"", @"", @""); + + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + break; + case SKPaymentTransactionStateRestored: + [self logMessage:@"restoreTransaction"]; + state = @"PaymentTransactionStateRestored"; + transactionIdentifier = transaction.originalTransaction.transactionIdentifier; + receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]; + if (!receiptData) { + transactionReceipt = @"No receipt"; + } else { + transactionReceipt = [receiptData base64EncodedStringWithOptions:0]; + } + productId = transaction.originalTransaction.payment.productIdentifier; + [self logMessage:@"transaction state: %@ with id: %@ for product: %@", state, transactionIdentifier, productId]; + + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + break; + default: + break; + } + + } +} + +- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue +{ + [self logMessage:@"paymentQueueRestoreCompletedTransactionsFinished"]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterMediumStyle; + dateFormatter.timeStyle = NSDateFormatterShortStyle; + + NSMutableArray *purchasedItemIDs = [[NSMutableArray alloc] init]; + + [self logMessage:@"received restored transactions: %li", queue.transactions.count]; + for (SKPaymentTransaction *transaction in queue.transactions) + { + if (transaction.transactionState == SKPaymentTransactionStateRestored || transaction.transactionState == SKPaymentTransactionStatePurchased) { + NSString *productID = transaction.payment.productIdentifier; + [self logMessage:@"product %@ purchased on %@", productID, [dateFormatter stringFromDate:transaction.transactionDate]]; + [purchasedItemIDs addObject:productID]; + } else { + [self logMessage:@"Transaction for %@ failed, error: %d %@", transaction.payment.productIdentifier, transaction.error.code, transaction.error.localizedDescription]; + } + } + sendPurchases([purchasedItemIDs copy]); + + doneFetching(YES); + +} + +-(void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error +{ + AttachLog(@"restoreCompletedTransactionsFailedWithError %@", error); + doneFetching(NO); +} + +- (void) logMessage:(NSString *)format, ...; +{ + if (debugInAppBilling) + { + va_list args; + va_start(args, format); + NSLogv(format, args); + va_end(args); + } +} +@end diff --git a/modules/lifecycle/src/main/native/ios/Lifecycle.h b/modules/lifecycle/src/main/native/ios/Lifecycle.h index e0615857..a9347ac3 100644 --- a/modules/lifecycle/src/main/native/ios/Lifecycle.h +++ b/modules/lifecycle/src/main/native/ios/Lifecycle.h @@ -28,6 +28,7 @@ #import #include "jni.h" +#include "AttachMacros.h" @interface Lifecycle : UIViewController {} - (void) initEvents; diff --git a/modules/lifecycle/src/main/native/ios/Lifecycle.m b/modules/lifecycle/src/main/native/ios/Lifecycle.m index 3ffd4889..52ea1a5a 100644 --- a/modules/lifecycle/src/main/native/ios/Lifecycle.m +++ b/modules/lifecycle/src/main/native/ios/Lifecycle.m @@ -99,7 +99,7 @@ - (void)initEvents { } - (void)stopEvents { - NSLog(@"Unregistering sending event"); + AttachLog(@"Unregistering sending event"); [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:[UIApplication sharedApplication]]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification diff --git a/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/impl/IOSLocalNotificationsService.java b/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/impl/IOSLocalNotificationsService.java new file mode 100644 index 00000000..b6e07656 --- /dev/null +++ b/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/impl/IOSLocalNotificationsService.java @@ -0,0 +1,71 @@ +/* + * 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.localnotifications.impl; + + +import com.gluonhq.attach.localnotifications.Notification; +import com.gluonhq.attach.util.Constants; + +/** + * iOS implementation of LocalNotificationsService. + */ +public class IOSLocalNotificationsService extends LocalNotificationsServiceBase { + + static { + System.loadLibrary("LocalNotifications"); + initLocalNotification(); + } + + public IOSLocalNotificationsService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + } + + @Override + protected void unscheduleNotification(Notification notification) { + if (notification != null) { + unregisterNotification(notification.getId()); + } + } + + @Override + protected void scheduleNotification(Notification notification) { + registerNotification(notification.getTitle() == null ? "" : notification.getTitle(), + notification.getText(), notification.getId(), notification.getDateTime().toEpochSecond()); + } + + private native void registerNotification(String title, String text, String identifier, double seconds); + + private native void unregisterNotification(String identifier); + + private static native void initLocalNotification(); + private static native void enableDebug(); + + +} diff --git a/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/impl/LocalNotificationsServiceBase.java b/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/impl/LocalNotificationsServiceBase.java index 73e6d6d5..26d0343b 100644 --- a/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/impl/LocalNotificationsServiceBase.java +++ b/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/impl/LocalNotificationsServiceBase.java @@ -27,7 +27,7 @@ */ package com.gluonhq.attach.localnotifications.impl; -import com.gluonhq.attach.runtime.RuntimeArgsService; +import com.gluonhq.attach.runtimeargs.RuntimeArgsService; import java.util.logging.Logger; import com.gluonhq.attach.util.Services; import com.gluonhq.attach.localnotifications.LocalNotificationsService; diff --git a/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/package-info.java b/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/package-info.java index aab86e2b..63b0bdde 100644 --- a/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/package-info.java +++ b/modules/local-notifications/src/main/java/com/gluonhq/attach/localnotifications/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Local Notifications plugin, + * Primary API package for Attach - Local Notifications plugin, * contains the interface {@link com.gluonhq.attach.localnotifications.LocalNotificationsService} and related classes. */ package com.gluonhq.attach.localnotifications; \ No newline at end of file diff --git a/modules/local-notifications/src/main/native/ios/LocalNotifications.h b/modules/local-notifications/src/main/native/ios/LocalNotifications.h new file mode 100644 index 00000000..1af1d174 --- /dev/null +++ b/modules/local-notifications/src/main/native/ios/LocalNotifications.h @@ -0,0 +1,39 @@ +/* + * 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" +#import + +API_AVAILABLE(ios(10.0)) +@interface LocalNotifications : NSObject +{ +} + +@end \ No newline at end of file diff --git a/modules/local-notifications/src/main/native/ios/LocalNotifications.m b/modules/local-notifications/src/main/native/ios/LocalNotifications.m new file mode 100644 index 00000000..59cf1f10 --- /dev/null +++ b/modules/local-notifications/src/main/native/ios/LocalNotifications.m @@ -0,0 +1,253 @@ +/* + * 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 "LocalNotifications.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_LocalNotifications(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 notificationsInited = 0; +BOOL debugLocalNotifications; + +// Notifications + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_localnotifications_impl_IOSLocalNotificationsService_initLocalNotification +(JNIEnv *env, jclass jClass) +{ + if (notificationsInited) + { + return; + } + notificationsInited = 1; + + if (@available(iOS 10.0, *)) + { + AttachLog(@"Initialize UNUserNotificationCenter iOS 10+"); + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + UNAuthorizationOptions options = UNAuthorizationOptionAlert + UNAuthorizationOptionBadge + UNAuthorizationOptionSound; + [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) { + if (!granted) { + AttachLog(@"Error granting notification options"); + } + }]; + + [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { + // Authorization status of the UNNotificationSettings object + switch (settings.authorizationStatus) { + case UNAuthorizationStatusAuthorized: + AttachLog(@"UNNotificationSettings Status Authorized"); + break; + case UNAuthorizationStatusDenied: + AttachLog(@"UNNotificationSettings Status Denied"); + break; + case UNAuthorizationStatusNotDetermined: + AttachLog(@"UNNotificationSettings Status Undetermined"); + break; + default: + break; + } + + // Status of specific settings + if (settings.alertSetting != UNAuthorizationStatusAuthorized) { + AttachLog(@"Alert settings not authorized"); + } + + if (settings.badgeSetting != UNAuthorizationStatusAuthorized) { + AttachLog(@"Badge settings not authorized"); + } + + if (settings.soundSetting != UNAuthorizationStatusAuthorized) { + AttachLog(@"Sound settings not authorized"); + } + }]; + } + else + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + AttachLog(@"Initialize UIUserNotificationSettings iOS 10-"); + UIUserNotificationSettings* settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]; + [[UIApplication sharedApplication] registerUserNotificationSettings: settings]; + #pragma clang diagnostic pop + } + +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_localnotifications_impl_IOSLocalNotificationsService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugLocalNotifications = YES; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_localnotifications_impl_IOSLocalNotificationsService_registerNotification +(JNIEnv *env, jobject obj, jstring jTitle, jstring jText, jstring jIdentifier, jdouble seconds) +{ + if (debugLocalNotifications) { + AttachLog(@"Register notification"); + } + const jchar *charsTitle = (*env)->GetStringChars(env, jTitle, NULL); + NSString *name = [NSString stringWithCharacters:(UniChar *)charsTitle length:(*env)->GetStringLength(env, jTitle)]; + (*env)->ReleaseStringChars(env, jTitle, charsTitle); + const jchar *charsText = (*env)->GetStringChars(env, jText, NULL); + NSString *text = [NSString stringWithCharacters:(UniChar *)charsText length:(*env)->GetStringLength(env, jText)]; + (*env)->ReleaseStringChars(env, jText, charsText); + const jchar *charsIdentifier = (*env)->GetStringChars(env, jIdentifier, NULL); + NSString *identifier = [NSString stringWithCharacters:(UniChar *)charsIdentifier length:(*env)->GetStringLength(env, jIdentifier)]; + (*env)->ReleaseStringChars(env, jIdentifier, charsIdentifier); + + if (@available(iOS 10.0, *)) + { + UNMutableNotificationContent *content = [UNMutableNotificationContent new]; + content.title = name; + content.body = text; + content.sound = [UNNotificationSound defaultSound]; + content.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:identifier, @"userId", nil]; + + NSDate *date = [NSDate dateWithTimeIntervalSince1970:seconds]; + NSDateComponents *triggerDate = [[NSCalendar currentCalendar] + components:NSCalendarUnitYear + + NSCalendarUnitMonth + NSCalendarUnitDay + + NSCalendarUnitHour + NSCalendarUnitMinute + + NSCalendarUnitSecond fromDate:date]; + UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:triggerDate + repeats:NO]; + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier + content:content trigger:trigger]; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (error != nil) { + AttachLog(@"Something went wrong scheduling local notification: %@",error); + } else if (debugLocalNotifications) { + AttachLog(@"done register notifications for %@ with identifier %@", name, identifier); + } + }]; + } + else + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + UILocalNotification* localNotification = [[UILocalNotification alloc] init]; + localNotification.fireDate = [NSDate dateWithTimeIntervalSince1970:seconds]; + // Not supported by iOS 8.1 + // localNotification.alertTitle = name; + if ([name length] == 0) { + localNotification.alertBody = text; + } else { + localNotification.alertBody = [name stringByAppendingFormat:@"%@%@",@"\n",text]; + } + localNotification.soundName = UILocalNotificationDefaultSoundName; + localNotification.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:identifier, @"userId", nil]; + localNotification.timeZone = [NSTimeZone defaultTimeZone]; + localNotification.category = @"sessionReminderCategory"; + [[UIApplication sharedApplication] scheduleLocalNotification: localNotification]; + if (debugLocalNotifications) { + AttachLog(@"done register notifications for %@ with identifier %@", name, identifier); + } + #pragma clang diagnostic pop + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_localnotifications_impl_IOSLocalNotificationsService_unregisterNotification +(JNIEnv *env, jclass jClass, jstring jIdentifier) +{ + const jchar *charsIdentifier = (*env)->GetStringChars(env, jIdentifier, NULL); + NSString *identifier = [NSString stringWithCharacters:(UniChar *)charsIdentifier length:(*env)->GetStringLength(env, jIdentifier)]; + (*env)->ReleaseStringChars(env, jIdentifier, charsIdentifier); + + if (@available(iOS 10.0, *)) + { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + NSArray *array = [NSArray arrayWithObjects:identifier, nil]; + [center removePendingNotificationRequestsWithIdentifiers:array]; + if (debugLocalNotifications) { + AttachLog(@"We did remove the notification with id: %@", identifier); + } + } else { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + NSArray *nots = [[UIApplication sharedApplication] scheduledLocalNotifications]; + for (int i=0; i<[nots count]; i++) { + UILocalNotification* candidate = [nots objectAtIndex:i]; + NSDictionary *myUserInfo = candidate.userInfo; + NSString *myId = [myUserInfo objectForKey:@"userId"]; + if ([myId isEqualToString:identifier]) { + [[UIApplication sharedApplication] cancelLocalNotification:candidate]; + if (debugLocalNotifications) { + AttachLog(@"We did remove the notification with id: %@", identifier); + } + } + } + #pragma clang diagnostic pop + } +} + +@implementation LocalNotifications + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler +{ + // called when a user selects an action in a delivered notification + [self logMessage:@"didReceiveNotificationResponse %@", response]; + completionHandler(); +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler +{ + // called when a notification is delivered to a foreground app + [self logMessage:@"willPresentNotification %@", notification]; + completionHandler(UNNotificationPresentationOptionAlert + UNNotificationPresentationOptionSound); +} + +- (void) logMessage:(NSString *)format, ...; +{ + if (debugLocalNotifications) + { + va_list args; + va_start(args, format); + NSLogv([@"[Debug] " stringByAppendingString:format], args); + va_end(args); + } +} +@end diff --git a/modules/magnetometer/build.gradle b/modules/magnetometer/build.gradle index dc23fa2a..58d57c1d 100644 --- a/modules/magnetometer/build.gradle +++ b/modules/magnetometer/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { implementation project(":util") + implementation project(":lifecycle") } ext.description = 'Common API to access magnetometer features' \ No newline at end of file diff --git a/modules/magnetometer/src/main/java/com/gluonhq/attach/magnetometer/impl/IOSMagnetometerService.java b/modules/magnetometer/src/main/java/com/gluonhq/attach/magnetometer/impl/IOSMagnetometerService.java new file mode 100644 index 00000000..0fde5670 --- /dev/null +++ b/modules/magnetometer/src/main/java/com/gluonhq/attach/magnetometer/impl/IOSMagnetometerService.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016, 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.magnetometer.impl; + +import com.gluonhq.attach.lifecycle.LifecycleEvent; +import com.gluonhq.attach.lifecycle.LifecycleService; +import com.gluonhq.attach.magnetometer.MagnetometerReading; +import com.gluonhq.attach.magnetometer.MagnetometerService; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; + +public class IOSMagnetometerService implements MagnetometerService { + + static { + System.loadLibrary("Magnetometer"); + initMagnetometer(); + } + + private static final ReadOnlyObjectWrapper reading = new ReadOnlyObjectWrapper<>(); + + public IOSMagnetometerService() { + LifecycleService.create().ifPresent(l -> { + l.addListener(LifecycleEvent.PAUSE, IOSMagnetometerService::stopObserver); + l.addListener(LifecycleEvent.RESUME, () -> startObserver(FREQUENCY)); + }); + startObserver(FREQUENCY); + } + + @Override + public ReadOnlyObjectProperty readingProperty() { + return reading.getReadOnlyProperty(); + } + + @Override + public MagnetometerReading getReading() { + return reading.get(); + } + + // native + private static native void initMagnetometer(); + private static native void startObserver(int rateInMillis); + private static native void stopObserver(); + + // callback + private static void notifyReading(double x, double y, double z, double m, double a, double p, double r) { + MagnetometerReading read = new MagnetometerReading(x, y, z, m, a, p, r); + Platform.runLater(() -> reading.setValue(read)); + } +} diff --git a/modules/magnetometer/src/main/java/com/gluonhq/attach/magnetometer/package-info.java b/modules/magnetometer/src/main/java/com/gluonhq/attach/magnetometer/package-info.java index 395630b8..d44a126a 100644 --- a/modules/magnetometer/src/main/java/com/gluonhq/attach/magnetometer/package-info.java +++ b/modules/magnetometer/src/main/java/com/gluonhq/attach/magnetometer/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Magnetometer plugin, + * Primary API package for Attach - Magnetometer plugin, * contains the interface {@link com.gluonhq.attach.magnetometer.MagnetometerService} and related classes. */ package com.gluonhq.attach.magnetometer; \ No newline at end of file diff --git a/modules/magnetometer/src/main/java/module-info.java b/modules/magnetometer/src/main/java/module-info.java index 58e57d07..d932fb77 100644 --- a/modules/magnetometer/src/main/java/module-info.java +++ b/modules/magnetometer/src/main/java/module-info.java @@ -27,8 +27,9 @@ */ module com.gluonhq.attach.magnetometer { - requires javafx.base; + requires javafx.graphics; requires com.gluonhq.attach.util; + requires com.gluonhq.attach.lifecycle; exports com.gluonhq.attach.magnetometer; exports com.gluonhq.attach.magnetometer.impl to com.gluonhq.attach.util; diff --git a/modules/magnetometer/src/main/native/ios/Magnetometer.h b/modules/magnetometer/src/main/native/ios/Magnetometer.h new file mode 100644 index 00000000..cacf4f80 --- /dev/null +++ b/modules/magnetometer/src/main/native/ios/Magnetometer.h @@ -0,0 +1,40 @@ +/* + * 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" +#import + +@interface Magnetometer : UIViewController {} + @property (strong, nonatomic) CMMotionManager *motionManager; + - (void) startObserver; + - (void) stopObserver; +@end + +void sendReading(CMDeviceMotion *motionData); diff --git a/modules/magnetometer/src/main/native/ios/Magnetometer.m b/modules/magnetometer/src/main/native/ios/Magnetometer.m new file mode 100644 index 00000000..a404cfd3 --- /dev/null +++ b/modules/magnetometer/src/main/native/ios/Magnetometer.m @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2016, 2019, Gluon + * + * This program is fr_aee 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 "Magnetometer.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Magnetometer(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 MagnetometerInited = 0; + +// Magnetometer +jclass mat_jMagnetometerServiceClass; +jmethodID mat_jMagnetometerService_notifyReading = 0; +Magnetometer *_magnetometer; +double magRate = 0.1; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_magnetometer_impl_IOSMagnetometerService_initMagnetometer +(JNIEnv *env, jclass jClass) +{ + if (MagnetometerInited) + { + return; + } + MagnetometerInited = 1; + + mat_jMagnetometerServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/magnetometer/impl/IOSMagnetometerService")); + mat_jMagnetometerService_notifyReading = (*env)->GetStaticMethodID(env, mat_jMagnetometerServiceClass, "notifyReading", "(DDDDDDD)V"); + + _magnetometer = [[Magnetometer alloc] init]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_magnetometer_impl_IOSMagnetometerService_startObserver +(JNIEnv *env, jclass jClass, jint jfrequency) +{ + if (jfrequency > 0) { + magRate = 1.0 / ((double) jfrequency); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [_magnetometer startObserver]; + }); + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_magnetometer_impl_IOSMagnetometerService_stopObserver +(JNIEnv *env, jclass jClass) +{ + [_magnetometer stopObserver]; + return; +} + +void sendReading0(CMMagnetometerData *magnetometerData) { + if (magnetometerData) + { + double x = magnetometerData.magneticField.x; + double y = magnetometerData.magneticField.y; + double z = magnetometerData.magneticField.z; + double m = sqrt(x * x + y * y + z * z); + (*env)->CallStaticVoidMethod(env, mat_jMagnetometerServiceClass, mat_jMagnetometerService_notifyReading, x, y, z, m, 0, 0, 0); + } +} +void sendReading(CMDeviceMotion *motionData) { + if (motionData) + { + double x = motionData.magneticField.field.x; + double y = motionData.magneticField.field.y; + double z = motionData.magneticField.field.z; + double m = sqrt(x * x + y * y + z * z); + + double defaultYaw = motionData.attitude.yaw; // 0 right side device towards North, + Pi/2 towards West, Pi South, - Pi/2 East. + double yaw = M_PI / 2 + defaultYaw; // 0 front side device towards North + if (yaw > M_PI) { + yaw -= 2 * M_PI; + } else if (yaw < -M_PI) { + yaw += 2 * M_PI; + } + yaw = - yaw; // + Pi/2 towards East + + double defaultPitch = motionData.attitude.pitch; // 0 parallel to ground, -Pi/2 top side towards ground + double pitch = -defaultPitch; // +PI/2 top side towards ground + + double roll = motionData.attitude.roll; + + (*env)->CallStaticVoidMethod(env, mat_jMagnetometerServiceClass, mat_jMagnetometerService_notifyReading, + x, y, z, m, yaw, pitch, roll); + } +} + +@implementation Magnetometer + +- (void) startObserver +{ + + if (!self.motionManager) { + self.motionManager = [[CMMotionManager alloc] init]; + } + + if ([self.motionManager isDeviceMotionAvailable]) + { + self.motionManager.deviceMotionUpdateInterval = magRate; // in seconds + [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXMagneticNorthZVertical + toQueue:[NSOperationQueue mainQueue] + withHandler:^(CMDeviceMotion *motionData, NSError *error) { + sendReading(motionData); + }]; + } else + { + AttachLog(@"Error: No Magnetometer Available"); + } + +} + +- (void) stopObserver +{ + if (self.motionManager) + { + [self.motionManager stopDeviceMotionUpdates]; + } +} + +@end + diff --git a/modules/orientation/src/main/java/com/gluonhq/attach/orientation/package-info.java b/modules/orientation/src/main/java/com/gluonhq/attach/orientation/package-info.java index c1203588..ef37042f 100644 --- a/modules/orientation/src/main/java/com/gluonhq/attach/orientation/package-info.java +++ b/modules/orientation/src/main/java/com/gluonhq/attach/orientation/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Orientation plugin, + * Primary API package for Attach - Orientation plugin, * contains the interface {@link com.gluonhq.attach.orientation.OrientationService} and related classes. */ package com.gluonhq.attach.orientation; \ No newline at end of file diff --git a/modules/orientation/src/main/native/ios/Orientation.h b/modules/orientation/src/main/native/ios/Orientation.h index 473d7e05..9ab38afc 100644 --- a/modules/orientation/src/main/native/ios/Orientation.h +++ b/modules/orientation/src/main/native/ios/Orientation.h @@ -28,6 +28,7 @@ #import #include "jni.h" +#include "AttachMacros.h" @interface Orientation : UIViewController {} - (void) startObserver; diff --git a/modules/orientation/src/main/native/ios/Orientation.m b/modules/orientation/src/main/native/ios/Orientation.m index d6d2dcef..b0ffeb3d 100644 --- a/modules/orientation/src/main/native/ios/Orientation.m +++ b/modules/orientation/src/main/native/ios/Orientation.m @@ -85,7 +85,7 @@ void sendOrientation() { NSString *orientation = [_orientation getOrientation]; - NSLog(@"Orientation is %@", orientation); + AttachLog(@"Orientation is %@", orientation); const char *orientationChars = [orientation UTF8String]; jstring arg = (*env)->NewStringUTF(env, orientationChars); (*env)->CallStaticVoidMethod(env, mat_jOrientationServiceClass, mat_jOrientationService_notifyOrientation, arg); diff --git a/modules/pictures/src/main/java/com/gluonhq/attach/pictures/impl/DummyPicturesService.java b/modules/pictures/src/main/java/com/gluonhq/attach/pictures/impl/DummyPicturesService.java index 1e1a7a1f..e6e05280 100644 --- a/modules/pictures/src/main/java/com/gluonhq/attach/pictures/impl/DummyPicturesService.java +++ b/modules/pictures/src/main/java/com/gluonhq/attach/pictures/impl/DummyPicturesService.java @@ -1,3 +1,30 @@ +/* + * 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.pictures.impl; import com.gluonhq.attach.pictures.PicturesService; diff --git a/modules/pictures/src/main/java/com/gluonhq/attach/pictures/impl/IOSPicturesService.java b/modules/pictures/src/main/java/com/gluonhq/attach/pictures/impl/IOSPicturesService.java new file mode 100644 index 00000000..44231c6e --- /dev/null +++ b/modules/pictures/src/main/java/com/gluonhq/attach/pictures/impl/IOSPicturesService.java @@ -0,0 +1,109 @@ +/* + * 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.pictures.impl; + +import com.gluonhq.attach.pictures.PicturesService; +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 IOSPicturesService implements PicturesService { + + static { + System.loadLibrary("Pictures"); + initPictures(); + } + + private static final ObjectProperty imageFile = new SimpleObjectProperty<>(); + private static ObjectProperty result; + + @Override + public Optional takePhoto(boolean savePhoto) { + result = new SimpleObjectProperty<>(); + takePicture(savePhoto); + try { + Platform.enterNestedEventLoop(result); + } catch (Exception e) { + System.out.println("GalleryActivity: enterNestedEventLoop failed: " + e); + } + return Optional.ofNullable(result.get()); + } + + @Override + public Optional loadImageFromGallery() { + result = new SimpleObjectProperty<>(); + selectPicture(); + try { + Platform.enterNestedEventLoop(result); + } catch (Exception e) { + System.out.println("GalleryActivity: enterNestedEventLoop failed: " + e); + } + return Optional.ofNullable(result.get()); + } + + @Override + public Optional getImageFile() { + return Optional.ofNullable(imageFile.get()); + } + + // native + private static native void initPictures(); // init IDs for java callbacks from native + public static native void takePicture(boolean savePhoto); + public static native void selectPicture(); + + // callback + public static void setResult(String v, String filePath) { + if (v != null && !v.isEmpty()) { + try { + byte[] imageBytes = Base64.getDecoder().decode(v.replaceAll("\\s+", "").getBytes()); + imageFile.set(new File(filePath)); + result.set(new Image(new ByteArrayInputStream(imageBytes))); + } 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/pictures/src/main/java/com/gluonhq/attach/pictures/package-info.java b/modules/pictures/src/main/java/com/gluonhq/attach/pictures/package-info.java index 18ea5088..43a75d70 100644 --- a/modules/pictures/src/main/java/com/gluonhq/attach/pictures/package-info.java +++ b/modules/pictures/src/main/java/com/gluonhq/attach/pictures/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Pictures plugin, + * Primary API package for Attach - Pictures plugin, * contains the interface {@link com.gluonhq.attach.pictures.PicturesService} and related classes. */ package com.gluonhq.attach.pictures; \ No newline at end of file diff --git a/modules/pictures/src/main/native/ios/Pictures.h b/modules/pictures/src/main/native/ios/Pictures.h new file mode 100644 index 00000000..6c181e40 --- /dev/null +++ b/modules/pictures/src/main/native/ios/Pictures.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) selectPicture; +@end + +void sendPicturesResult(NSString *picResult, NSString *picPath); \ No newline at end of file diff --git a/modules/pictures/src/main/native/ios/Pictures.m b/modules/pictures/src/main/native/ios/Pictures.m new file mode 100644 index 00000000..ffbb38c4 --- /dev/null +++ b/modules/pictures/src/main/native/ios/Pictures.m @@ -0,0 +1,321 @@ +/* + * 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 "Pictures.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Pictures(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 picturesInited = 0; + +// Pictures +jclass mat_jPicturesServiceClass; +jmethodID mat_jPicturesService_setResult = 0; +Pictures *_pictures; +BOOL savePhoto; + + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_pictures_impl_IOSPicturesService_initPictures +(JNIEnv *env, jclass jClass) +{ + if (picturesInited) + { + return; + } + picturesInited = 1; + + mat_jPicturesServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/pictures/impl/IOSPicturesService")); + mat_jPicturesService_setResult = (*env)->GetStaticMethodID(env, mat_jPicturesServiceClass, "setResult", "(Ljava/lang/String;Ljava/lang/String;)V"); +} + +void sendPicturesResult(NSString *picResult, NSString *picPath) { + if (picResult) + { + const char *picChars = [picResult UTF8String]; + jstring jpic = (*env)->NewStringUTF(env, picChars); + const char *pathChars = [picPath UTF8String]; + jstring jpath = (*env)->NewStringUTF(env, pathChars); + (*env)->CallStaticVoidMethod(env, mat_jPicturesServiceClass, mat_jPicturesService_setResult, jpic, jpath); + (*env)->DeleteLocalRef(env, jpic); + (*env)->DeleteLocalRef(env, jpath); + AttachLog(@"Finished sending picture"); + } else + { + (*env)->CallStaticVoidMethod(env, mat_jPicturesServiceClass, mat_jPicturesService_setResult, NULL, NULL); + } +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_pictures_impl_IOSPicturesService_takePicture +(JNIEnv *env, jclass jClass, jboolean jSavePhoto) +{ + savePhoto = jSavePhoto; + _pictures = [[Pictures alloc] init]; + [_pictures takePicture]; + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_pictures_impl_IOSPicturesService_selectPicture +(JNIEnv *env, jclass jClass) +{ + _pictures = [[Pictures alloc] init]; + [_pictures selectPicture]; + return; +} + +@implementation Pictures + +- (void)takePicture { + 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; + } + + if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + AttachLog(@"Device has no camera"); + return; + } + + UIView *_currentView = views[0]; + + UIImagePickerController *picker = [[UIImagePickerController alloc] init]; + picker.delegate = self; + picker.allowsEditing = NO; + picker.sourceType = UIImagePickerControllerSourceTypeCamera; + + [_currentView.window addSubview:picker.view]; + +} + +- (void)selectPicture { + 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 picture"); + UIImage *originalImage = info[UIImagePickerControllerOriginalImage]; + + if (savePhoto == YES) + { + AttachLog(@"Saving picture..."); + 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]; + + sendPicturesResult(base64StringOfImage, filePath); + + [picker dismissViewControllerAnimated:YES completion:nil]; + [picker.view removeFromSuperview]; + [picker release]; + +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { + + AttachLog(@"Camera cancelled"); + + NSString *result = nil; + sendPicturesResult(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/position/build.gradle b/modules/position/build.gradle index 60863414..02261943 100644 --- a/modules/position/build.gradle +++ b/modules/position/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { implementation project(':util') + implementation project(':lifecycle') } ext.description = 'Common API to access position features' \ No newline at end of file diff --git a/modules/position/src/main/java/com/gluonhq/attach/position/impl/IOSPositionService.java b/modules/position/src/main/java/com/gluonhq/attach/position/impl/IOSPositionService.java new file mode 100644 index 00000000..45163b02 --- /dev/null +++ b/modules/position/src/main/java/com/gluonhq/attach/position/impl/IOSPositionService.java @@ -0,0 +1,144 @@ +/* + * 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.position.impl; + +import com.gluonhq.attach.lifecycle.LifecycleEvent; +import com.gluonhq.attach.lifecycle.LifecycleService; +import com.gluonhq.attach.position.Parameters; +import com.gluonhq.attach.position.Position; +import com.gluonhq.attach.position.PositionService; +import com.gluonhq.attach.util.Constants; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; + +/** + * An implementation of the + * {@link PositionService PositionService} for the + * iOS platform. + * + * Important note: it requires adding to the info.plist file: + * + * {@code + * NSLocationUsageDescription + * Reason to use Location Service (iOS 6+) + * NSLocationWhenInUseUsageDescription + * Reason to use Location Service (iOS 8+) + * } + * + * With Background mode enabled + * {@code + * UIBackgroundModes + * + * location + * + * NSLocationUsageDescription + * Reason to use Location Service (iOS 6+) + * NSLocationAlwaysUsageDescription + * Reason to use Location Service (iOS 8+) in background + * NSLocationAlwaysAndWhenInUseUsageDescription + * Reason to use Location Service (iOS 8+) in background + * } + */ +public class IOSPositionService implements PositionService { + + static { + System.loadLibrary("Position"); + initPosition(); + } + + private static ReadOnlyObjectWrapper position; + private static boolean running; + private Parameters parameters; + + public IOSPositionService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + position = new ReadOnlyObjectWrapper<>(); + + LifecycleService.create().ifPresent(l -> { + l.addListener(LifecycleEvent.PAUSE, () -> { + if (! parameters.isBackgroundModeEnabled()) { + stopObserver(); + } + }); + l.addListener(LifecycleEvent.RESUME, () -> { + if (! parameters.isBackgroundModeEnabled()) { + startObserver(parameters.getAccuracy().name(), parameters.getTimeInterval(), + parameters.getDistanceFilter(), parameters.isBackgroundModeEnabled()); + } + }); + }); + } + + @Override + public void start() { + start(DEFAULT_PARAMETERS); + } + + @Override + public void start(Parameters parameters) { + if (running) { + stop(); + } + this.parameters = parameters; + startObserver(parameters.getAccuracy().name(), parameters.getTimeInterval(), + parameters.getDistanceFilter(), parameters.isBackgroundModeEnabled()); + running = true; + } + + @Override + public void stop() { + stopObserver(); + running = false; + } + + @Override + public ReadOnlyObjectProperty positionProperty() { + return position.getReadOnlyProperty(); + } + + @Override + public Position getPosition() { + return positionProperty().get(); + } + + // native + private static native void initPosition(); // init IDs for java callbacks from native + private static native void startObserver(String accuracy, long timeInterval, float distanceFilter, boolean backgroundModeEnabled); + private static native void stopObserver(); + private static native void enableDebug(); + + // callback + private static void setLocation(double lat, double lon, double alt) { + Position p = new Position(lat, lon, alt); + Platform.runLater(() -> position.set(p)); + } + +} diff --git a/modules/position/src/main/java/com/gluonhq/attach/position/package-info.java b/modules/position/src/main/java/com/gluonhq/attach/position/package-info.java index ef5ea769..22bf9375 100644 --- a/modules/position/src/main/java/com/gluonhq/attach/position/package-info.java +++ b/modules/position/src/main/java/com/gluonhq/attach/position/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Position plugin, + * Primary API package for Attach - Position plugin, * contains the interface {@link com.gluonhq.attach.position.PositionService} and related classes. */ package com.gluonhq.attach.position; \ No newline at end of file diff --git a/modules/position/src/main/java/module-info.java b/modules/position/src/main/java/module-info.java index 3c038b8b..8f3694b5 100644 --- a/modules/position/src/main/java/module-info.java +++ b/modules/position/src/main/java/module-info.java @@ -27,8 +27,9 @@ */ module com.gluonhq.attach.position { - requires javafx.base; + requires javafx.graphics; requires com.gluonhq.attach.util; + requires com.gluonhq.attach.lifecycle; exports com.gluonhq.attach.position; exports com.gluonhq.attach.position.impl to com.gluonhq.attach.util; diff --git a/modules/position/src/main/native/ios/Position.h b/modules/position/src/main/native/ios/Position.h new file mode 100644 index 00000000..70a28d71 --- /dev/null +++ b/modules/position/src/main/native/ios/Position.h @@ -0,0 +1,40 @@ +/* + * 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" +#import + +@interface Position : UIViewController {} + @property(nonatomic, strong) CLLocationManager *locationManager; + - (void) startObserver:(NSString *)accuracy interval:(long)interval distance:(CGFloat)distance background:(BOOL)background; + - (void) stopObserver; +@end + +void setLocation(CLLocation *newLocation); \ No newline at end of file diff --git a/modules/position/src/main/native/ios/Position.m b/modules/position/src/main/native/ios/Position.m new file mode 100644 index 00000000..160ddfa4 --- /dev/null +++ b/modules/position/src/main/native/ios/Position.m @@ -0,0 +1,230 @@ +/* + * 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 "Position.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Position(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 positionInited = 0; + +// Position +jclass mat_jPositionServiceClass; +jmethodID mat_jPositionService_setLocation = 0; +Position *_position; +BOOL debugPosition; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_position_impl_IOSPositionService_initPosition +(JNIEnv *env, jclass jClass) +{ + if (positionInited) + { + return; + } + positionInited = 1; + + mat_jPositionServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/position/impl/IOSPositionService")); + mat_jPositionService_setLocation = (*env)->GetStaticMethodID(env, mat_jPositionServiceClass, "setLocation", "(DDD)V"); + + _position = [[Position alloc] init]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_position_impl_IOSPositionService_startObserver +(JNIEnv *env, jclass jClass, jstring jAccuracy, jlong jInterval, jfloat jDistance, jboolean jBackground) +{ + const jchar *charsAccuracy = (*env)->GetStringChars(env, jAccuracy, NULL); + NSString *sAccuracy = [NSString stringWithCharacters:(UniChar *)charsAccuracy length:(*env)->GetStringLength(env, jAccuracy)]; + (*env)->ReleaseStringChars(env, jAccuracy, charsAccuracy); + + dispatch_async(dispatch_get_main_queue(), ^{ + [_position startObserver:sAccuracy interval:jInterval distance:jDistance background:jBackground]; + }); + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_position_impl_IOSPositionService_stopObserver +(JNIEnv *env, jclass jClass) +{ + [_position stopObserver]; + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_position_impl_IOSPositionService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugPosition = YES; +} + +void setLocation(CLLocation *newLocation) { + if (newLocation) + { + double lat = newLocation.coordinate.latitude; + double lon = newLocation.coordinate.longitude; + double alt = newLocation.altitude; + (*env)->CallStaticVoidMethod(env, mat_jPositionServiceClass, mat_jPositionService_setLocation, lat, lon, alt); + } + +} + +@implementation Position + +- (void)startObserver:(NSString *)accuracy interval:(long)interval distance:(CGFloat)distance background:(BOOL)background +{ + + self.locationManager = [[CLLocationManager alloc] init]; + self.locationManager.delegate = self; + self.locationManager.distanceFilter = distance; + if ([accuracy isEqualToString:@"HIGHEST"]) { + self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation; + } else if ([accuracy isEqualToString:@"HIGH"]) { + self.locationManager.desiredAccuracy = kCLLocationAccuracyBest; + } else if ([accuracy isEqualToString:@"MEDIUM"]) { + self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters; + } else if ([accuracy isEqualToString:@"LOW"]) { + self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters; + } else if ([accuracy isEqualToString:@"LOWEST"]) { + self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer; + } + if (background) { + self.locationManager.allowsBackgroundLocationUpdates = YES; + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 9.0) { +#ifdef __IPHONE_11_0 + if (@available(iOS 11, *)) { + self.locationManager.showsBackgroundLocationIndicator = YES; + } +#endif + } + } + + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) + { + if (background) { + [self.locationManager requestAlwaysAuthorization]; + } else { + // try to save battery by using GPS only when app is used: + [self.locationManager requestWhenInUseAuthorization]; + } + } + + if (debugPosition) + { + AttachLog(@"Start updating location with accuracy: %f", self.locationManager.desiredAccuracy); + } + [self.locationManager startUpdatingLocation]; + + // Request a location update + [self.locationManager requestLocation]; + +} + +- (void)stopObserver +{ + if (debugPosition) + { + AttachLog(@"Stop updating location"); + } + [self.locationManager stopUpdatingLocation]; +} + +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + CLLocation *newLocation = [locations lastObject]; + if (debugPosition) + { + AttachLog(@"NewLocation: %f, %f, %f", newLocation.coordinate.latitude, newLocation.coordinate.longitude, newLocation.altitude); + } + if (newLocation.horizontalAccuracy < 0) + { + if (debugPosition) + { + AttachLog(@"iOS location update, horizontal accuracy too small: %.2f", newLocation.horizontalAccuracy); + } + // return; + } + + NSTimeInterval interval = [newLocation.timestamp timeIntervalSinceNow]; + if (interval < -5) + { + if (debugPosition) + { + AttachLog(@"iOS location update, time interval to large (probably cached): %.2f", interval); + } + // return; + } + + setLocation(newLocation); +} + +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error +{ + if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) + { + AttachLog(@"User has denied location services"); + } + else + { + AttachLog(@"Location manager did fail with error: %@", error); + switch([error code]) + { + case kCLErrorNetwork: // general, network-related error + { + AttachLog(@"ErrorNetwork"); + } + break; + case kCLErrorDenied: + { + AttachLog(@"ErrorDenied"); + } + break; + case kCLErrorLocationUnknown: + { + AttachLog(@"ErrorLocationUnknown"); + } + break; + default: + { + AttachLog(@"Unknown error: %@", error); + } + break; + } + } +} + +@end diff --git a/modules/push-notifications/build.gradle b/modules/push-notifications/build.gradle index 44de96a9..415a9879 100644 --- a/modules/push-notifications/build.gradle +++ b/modules/push-notifications/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'org.openjfx.javafxplugin' javafx { - modules 'javafx.base' + modules 'javafx.graphics' } dependencies { implementation project(":util") + implementation project(":runtime-args") } ext.description = 'Common API to access push notifications features' \ No newline at end of file diff --git a/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/impl/DummyPushNotificationsService.java b/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/impl/DummyPushNotificationsService.java index 343fa5c4..acff742f 100644 --- a/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/impl/DummyPushNotificationsService.java +++ b/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/impl/DummyPushNotificationsService.java @@ -1,3 +1,30 @@ +/* + * 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.pushnotifications.impl; import com.gluonhq.attach.pushnotifications.PushNotificationsService; diff --git a/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/impl/IOSPushNotificationsService.java b/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/impl/IOSPushNotificationsService.java new file mode 100644 index 00000000..eeb56fbb --- /dev/null +++ b/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/impl/IOSPushNotificationsService.java @@ -0,0 +1,90 @@ +/* + * 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.pushnotifications.impl; + +import com.gluonhq.attach.pushnotifications.PushNotificationsService; +import com.gluonhq.attach.runtimeargs.RuntimeArgsService; +import com.gluonhq.attach.util.Constants; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyStringWrapper; + +/** + * iOS implementation of PushNotificationsService. + */ +public class IOSPushNotificationsService implements PushNotificationsService { + + static { + System.loadLibrary("PushNotifications"); + } + + /** + * A string property to wrap the token device when received from the native layer + */ + private static final ReadOnlyStringWrapper TOKEN = new ReadOnlyStringWrapper(); + + public IOSPushNotificationsService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + + // Initialize RAS service + RuntimeArgsService.create(); + } + + @Override + public ReadOnlyStringProperty tokenProperty() { + return TOKEN.getReadOnlyProperty(); + } + + @Override + public void register(String authorizedEntity) { + initPushNotifications(); + } + + // native + private static native void initPushNotifications(); + private static native void enableDebug(); + + /** + * @param s String with the error description + */ + private static void failToRegisterForRemoteNotifications(String s) { + Platform.runLater(() -> System.out.println("Failed registering Push Notifications with error: " + s)); + } + + /** + * @param token String with the device token description + */ + private static void didRegisterForRemoteNotifications(String token) { + if (token == null) { + return; + } + Platform.runLater(() -> TOKEN.setValue(token)); + } +} \ No newline at end of file diff --git a/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/package-info.java b/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/package-info.java new file mode 100644 index 00000000..e18fbab7 --- /dev/null +++ b/modules/push-notifications/src/main/java/com/gluonhq/attach/pushnotifications/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 - Push Notifications plugin, + * contains the interface {@link com.gluonhq.attach.pushnotifications.PushNotificationsService} and related classes. + */ +package com.gluonhq.attach.pushnotifications; \ No newline at end of file diff --git a/modules/push-notifications/src/main/java/module-info.java b/modules/push-notifications/src/main/java/module-info.java index 3816e787..6fa85485 100644 --- a/modules/push-notifications/src/main/java/module-info.java +++ b/modules/push-notifications/src/main/java/module-info.java @@ -27,9 +27,10 @@ */ module com.gluonhq.attach.pushnotifications { - requires transitive javafx.base; + requires transitive javafx.graphics; requires com.gluonhq.attach.util; + requires com.gluonhq.attach.runtime.args; exports com.gluonhq.attach.pushnotifications; exports com.gluonhq.attach.pushnotifications.impl to com.gluonhq.attach.util; diff --git a/modules/push-notifications/src/main/native/ios/PushNotifications.h b/modules/push-notifications/src/main/native/ios/PushNotifications.h new file mode 100644 index 00000000..2d8d14fc --- /dev/null +++ b/modules/push-notifications/src/main/native/ios/PushNotifications.h @@ -0,0 +1,42 @@ +/* + * 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" +#import + +@interface PushNotifications : NSObject { } +@end + +@interface PushNotifications (NotificationsAdditions) + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error; + +@end \ No newline at end of file diff --git a/modules/push-notifications/src/main/native/ios/PushNotifications.m b/modules/push-notifications/src/main/native/ios/PushNotifications.m new file mode 100644 index 00000000..78077ec3 --- /dev/null +++ b/modules/push-notifications/src/main/native/ios/PushNotifications.m @@ -0,0 +1,159 @@ +/* + * 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 "PushNotifications.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_PushNotifications(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 notificationsInitied = 0; + +// Push Notifications + +jclass mat_jPushNotificationsClass; +jmethodID mat_failToRegisterForRemoteNotifications = 0; +jmethodID mat_didRegisterForRemoteNotifications = 0; +BOOL debugPushNotifications; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_pushnotifications_impl_IOSPushNotificationsService_initPushNotifications +(JNIEnv *env, jclass jClass) +{ + if (notificationsInitied) + { + return; + } + notificationsInitied = 1; + + // Push Notifications + mat_jPushNotificationsClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/pushnotifications/impl/IOSPushNotificationsService")); + mat_failToRegisterForRemoteNotifications = (*env)->GetStaticMethodID(env, mat_jPushNotificationsClass, "failToRegisterForRemoteNotifications", "(Ljava/lang/String;)V"); + mat_didRegisterForRemoteNotifications = (*env)->GetStaticMethodID(env, mat_jPushNotificationsClass, "didRegisterForRemoteNotifications", "(Ljava/lang/String;)V"); + + if (@available(iOS 10.0, *)) + { + if (debugPushNotifications) { + AttachLog(@"Initialize UIUserNotificationSettings - Push >= 10"); + } + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){ + if (!error) + { + if (debugPushNotifications) { + AttachLog(@"Registering notifications"); + } + [[UIApplication sharedApplication] registerForRemoteNotifications]; + } + else + { + AttachLog(@"Registering notifications failed with error %@", [error localizedDescription]); + } + }]; + } + else + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + if (debugPushNotifications) { + AttachLog(@"Initialize UIUserNotificationSettings - Push < 10"); + } + UIUserNotificationSettings* settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]; + [[UIApplication sharedApplication] registerUserNotificationSettings: settings]; + [[UIApplication sharedApplication] registerForRemoteNotifications]; + + #pragma clang diagnostic pop + } +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_pushnotifications_impl_IOSPushNotificationsService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugPushNotifications = YES; +} + +@implementation PushNotifications (NotificationsAdditions) + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + { + + NSString * deviceTokenString = [[[[deviceToken description] + stringByReplacingOccurrencesOfString: @"<" withString: @""] + stringByReplacingOccurrencesOfString: @">" withString: @""] + stringByReplacingOccurrencesOfString: @" " withString: @""]; + + const char *deviceTokenChars = [deviceTokenString UTF8String]; + jstring argToken = (*env)->NewStringUTF(env, deviceTokenChars); + + [self logMessage:@"Sending token %@", deviceTokenString]; + (*env)->CallStaticVoidMethod(env, mat_jPushNotificationsClass, mat_didRegisterForRemoteNotifications, argToken); + (*env)->DeleteLocalRef(env, argToken); + } + [pool drain]; +} + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + AttachLog(@"Error registering remote notifications %@", [error localizedDescription]); + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + { + NSString *errorDescString = [error localizedDescription]; + const char *errorDescChars = [errorDescString UTF8String]; + jstring arg = (*env)->NewStringUTF(env, errorDescChars); + [self logMessage:@"Sending error %@", errorDescString]; + (*env)->CallStaticVoidMethod(env, mat_jPushNotificationsClass, mat_failToRegisterForRemoteNotifications, arg); + (*env)->DeleteLocalRef(env, arg); + } + [pool drain]; +} + +- (void) logMessage:(NSString *)format, ...; +{ + if (debugPushNotifications) + { + va_list args; + va_start(args, format); + NSLogv([@"[Debug] " stringByAppendingString:format], args); + va_end(args); + } +} +@end diff --git a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/RuntimeArgsService.java b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/RuntimeArgsService.java similarity index 99% rename from modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/RuntimeArgsService.java rename to modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/RuntimeArgsService.java index 067c5208..a05a49a2 100644 --- a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/RuntimeArgsService.java +++ b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/RuntimeArgsService.java @@ -25,7 +25,7 @@ * (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.runtime; +package com.gluonhq.attach.runtimeargs; import com.gluonhq.attach.util.Services; diff --git a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DefaultRuntimeArgsService.java b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DefaultRuntimeArgsService.java similarity index 97% rename from modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DefaultRuntimeArgsService.java rename to modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DefaultRuntimeArgsService.java index 4b2f6d4d..51aa81a4 100644 --- a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DefaultRuntimeArgsService.java +++ b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DefaultRuntimeArgsService.java @@ -25,9 +25,9 @@ * (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.runtime.impl; +package com.gluonhq.attach.runtimeargs.impl; -import com.gluonhq.attach.runtime.RuntimeArgsService; +import com.gluonhq.attach.runtimeargs.RuntimeArgsService; import java.util.HashMap; import java.util.Map; diff --git a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DesktopRuntimeArgsService.java b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DesktopRuntimeArgsService.java similarity index 97% rename from modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DesktopRuntimeArgsService.java rename to modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DesktopRuntimeArgsService.java index 6aa53312..0da3791a 100644 --- a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DesktopRuntimeArgsService.java +++ b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DesktopRuntimeArgsService.java @@ -25,7 +25,7 @@ * (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.runtime.impl; +package com.gluonhq.attach.runtimeargs.impl; /** * An implementation of RuntimeArgsService on desktop diff --git a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DummyRuntimeArgsService.java b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DummyRuntimeArgsService.java similarity index 93% rename from modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DummyRuntimeArgsService.java rename to modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DummyRuntimeArgsService.java index b5bafc74..ee9dd6f0 100644 --- a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/impl/DummyRuntimeArgsService.java +++ b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/DummyRuntimeArgsService.java @@ -25,9 +25,9 @@ * (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.runtime.impl; +package com.gluonhq.attach.runtimeargs.impl; -import com.gluonhq.attach.runtime.RuntimeArgsService; +import com.gluonhq.attach.runtimeargs.RuntimeArgsService; // no-op public abstract class DummyRuntimeArgsService implements RuntimeArgsService { diff --git a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/IOSRuntimeArgsService.java b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/IOSRuntimeArgsService.java new file mode 100644 index 00000000..42187563 --- /dev/null +++ b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/impl/IOSRuntimeArgsService.java @@ -0,0 +1,65 @@ +/* + * 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.runtimeargs.impl; + +import com.gluonhq.attach.runtimeargs.RuntimeArgsService; +import com.gluonhq.attach.util.Constants; + +/** + * An implementation of the + * {@link RuntimeArgsService RuntimeArgsService} for the + * iOS platform. + */ +public class IOSRuntimeArgsService extends DefaultRuntimeArgsService { + + static { + System.loadLibrary("RuntimeArgs"); + initRuntimeArgs(); + } + + private static IOSRuntimeArgsService instance; + + public IOSRuntimeArgsService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + instance = this; + } + + // Native + + private static native void initRuntimeArgs(); + private static native void enableDebug(); + + // callback + private static void processRuntimeArgs(String key, String value) { + if (instance != null) { + instance.fire(key, value); + } + } +} diff --git a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/package-info.java b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/package-info.java similarity index 87% rename from modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/package-info.java rename to modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/package-info.java index a38aaa0d..81072865 100644 --- a/modules/runtime-args/src/main/java/com/gluonhq/attach/runtime/package-info.java +++ b/modules/runtime-args/src/main/java/com/gluonhq/attach/runtimeargs/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Vibration plugin, - * contains the interface {@link com.gluonhq.attach.runtime.RuntimeArgsService} and related classes. + * Primary API package for Attach - RuntimeArgs plugin, + * contains the interface {@link com.gluonhq.attach.runtimeargs.RuntimeArgsService} and related classes. */ -package com.gluonhq.attach.runtime; \ No newline at end of file +package com.gluonhq.attach.runtimeargs; \ No newline at end of file diff --git a/modules/runtime-args/src/main/java/module-info.java b/modules/runtime-args/src/main/java/module-info.java index 2f7541d3..b8cafbb8 100644 --- a/modules/runtime-args/src/main/java/module-info.java +++ b/modules/runtime-args/src/main/java/module-info.java @@ -29,6 +29,6 @@ requires com.gluonhq.attach.util; - exports com.gluonhq.attach.runtime; - exports com.gluonhq.attach.runtime.impl to com.gluonhq.attach.util; + exports com.gluonhq.attach.runtimeargs; + exports com.gluonhq.attach.runtimeargs.impl to com.gluonhq.attach.util; } \ No newline at end of file diff --git a/modules/runtime-args/src/main/native/ios/RuntimeArgs.h b/modules/runtime-args/src/main/native/ios/RuntimeArgs.h new file mode 100644 index 00000000..aa802b2c --- /dev/null +++ b/modules/runtime-args/src/main/native/ios/RuntimeArgs.h @@ -0,0 +1,53 @@ +/* + * 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" +#import + +@interface RuntimeArgs : NSObject { } +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification; +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler; +#pragma clang diagnostic pop + +@end + +@interface RasDelegate : NSObject +-(void)register; +-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler; +-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler; + +@end + +void processRuntimeArgs(NSString* key, NSString* value); \ No newline at end of file diff --git a/modules/runtime-args/src/main/native/ios/RuntimeArgs.m b/modules/runtime-args/src/main/native/ios/RuntimeArgs.m new file mode 100644 index 00000000..a2fd28d2 --- /dev/null +++ b/modules/runtime-args/src/main/native/ios/RuntimeArgs.m @@ -0,0 +1,231 @@ +/* + * 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 "RuntimeArgs.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_RuntimeArgs(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 runtimeArgsInited = 0; + +jclass mat_jRuntimeArgsClass; +jmethodID mat_jProcessRuntimeArgsMethod = 0; +RasDelegate *_rasDelegate; +BOOL debugRuntimeArgs; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_runtimeargs_impl_IOSRuntimeArgsService_initRuntimeArgs +(JNIEnv *env, jclass jClass) +{ + if (runtimeArgsInited) + { + return; + } + runtimeArgsInited = 1; + + mat_jRuntimeArgsClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/runtimeargs/impl/IOSRuntimeArgsService")); + mat_jProcessRuntimeArgsMethod = (*env)->GetStaticMethodID(env, mat_jRuntimeArgsClass, "processRuntimeArgs", "(Ljava/lang/String;Ljava/lang/String;)V"); + + _rasDelegate = [[RasDelegate alloc] init]; + [_rasDelegate register]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_runtimeargs_impl_IOSRuntimeArgsService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugRuntimeArgs = YES; +} + +void processRuntimeArgs(NSString* key, NSString* value) { + const char *keyChars = [key UTF8String]; + jstring jkey = (*env)->NewStringUTF(env, keyChars); + const char *valueChars = [value UTF8String]; + jstring jvalue = (*env)->NewStringUTF(env, valueChars); + (*env)->CallStaticVoidMethod(env, mat_jRuntimeArgsClass, mat_jProcessRuntimeArgsMethod, jkey, jvalue); + (*env)->DeleteLocalRef(env, jkey); + (*env)->DeleteLocalRef(env, jvalue); +} + +@implementation RuntimeArgs + +// TODO: Add the rest of methods that allow opening externally the application + +// iOS 4 - 9 +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + { + if (debugRuntimeArgs) { + AttachLog(@"OpenURL called: %@", url.absoluteString); + } + processRuntimeArgs(@"Launch.URL", url.absoluteString); + } + [pool drain]; + return TRUE; +} + +// iOS 10 +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url + options:(NSDictionary *)options +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + { + if (debugRuntimeArgs) { + AttachLog(@"OpenURL called: %@", url.absoluteString); + } + processRuntimeArgs(@"Launch.URL", url.absoluteString); + } + [pool drain]; + return TRUE; +} + +// called with app opened either on front or in the background, when user clicks on notification + +// Local Notifications iOS 4 - 10 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + { + NSDictionary *myUserInfo = notification.userInfo; + NSString *myId = [myUserInfo objectForKey:@"userId"]; + if (debugRuntimeArgs) { + AttachLog(@"Sending this notification with id %@", myId); + } + processRuntimeArgs(@"Launch.LocalNotification", myId); + } + [pool drain]; +} +#pragma clang diagnostic pop + +// Remote Notifications iOS < 10 + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler; +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + { + NSError *err; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error: &err]; + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + if (debugRuntimeArgs) { + AttachLog(@"Received remote notification, forward to RAS"); + } + processRuntimeArgs(@"Launch.PushNotification", jsonString); + if (debugRuntimeArgs) { + AttachLog(@"Processed remote notification"); + } + } + [pool drain]; + + if(application.applicationState == UIApplicationStateInactive) { + if (debugRuntimeArgs) { + AttachLog(@"App was Inactive"); + } + //Show the view with the content of the push + } else if (application.applicationState == UIApplicationStateBackground) { + if (debugRuntimeArgs) { + AttachLog(@"App was in Background"); + } + //Refresh the local model + } else { + if (debugRuntimeArgs) { + AttachLog(@"App is Active"); + } + //Show an in-app banner + } + if (debugRuntimeArgs) { + AttachLog(@"call completionhandler after remote notification"); + } + completionHandler(UIBackgroundFetchResultNewData); + +} +@end + +// Remote Notifications iOS 10 + +@implementation RasDelegate + +- (void)register +{ + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = self; +} + +-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{ + + //When a notification is delivered to a foreground app, this will show it on top: + completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound); +} + +-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{ + + //Called when a notification is delivered to foreground or background app. + if (debugRuntimeArgs) { + AttachLog(@"Received remote notification: Userinfo %@",response.notification.request.content.userInfo); + } + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + { + NSError *err; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:response.notification.request.content.userInfo options:0 error: &err]; + if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { + if (debugRuntimeArgs) { + AttachLog(@"Handling Push notification"); + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + processRuntimeArgs(@"Launch.PushNotification", jsonString); + } else { + if (debugRuntimeArgs) { + AttachLog(@"Handling Local notification"); + } + NSDictionary *myUserInfo = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err]; + NSString *myId = [myUserInfo objectForKey:@"userId"]; + if (debugRuntimeArgs) { + AttachLog(@"Sending local notification with id %@", myId); + } + processRuntimeArgs(@"Launch.LocalNotification", myId); + } + } + [pool drain]; + completionHandler(); +} + +@end diff --git a/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/DummySettingsService.java b/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/DummySettingsService.java index c366b938..1830beae 100644 --- a/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/DummySettingsService.java +++ b/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/DummySettingsService.java @@ -1,3 +1,30 @@ +/* + * 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.settings.impl; import com.gluonhq.attach.settings.SettingsService; diff --git a/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/IOSSettingsService.java b/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/IOSSettingsService.java index b68b0822..4b454dfc 100644 --- a/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/IOSSettingsService.java +++ b/modules/settings/src/main/java/com/gluonhq/attach/settings/impl/IOSSettingsService.java @@ -29,6 +29,7 @@ import com.gluonhq.attach.settings.SettingsService; +import com.gluonhq.attach.util.Constants; /** * An implementation of the @@ -42,6 +43,12 @@ public class IOSSettingsService implements SettingsService { initSettings(); } + public IOSSettingsService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + } + @Override public void store(String key, String value) { settingsStore(key, value); @@ -61,5 +68,6 @@ public String retrieve(String key) { private static native void settingsStore(String key, String value); private static native void settingsRemove(String key); private static native String settingsRetrieve(String key); + private static native void enableDebug(); } diff --git a/modules/settings/src/main/java/com/gluonhq/attach/settings/package-info.java b/modules/settings/src/main/java/com/gluonhq/attach/settings/package-info.java index f069e61e..8d9d511d 100644 --- a/modules/settings/src/main/java/com/gluonhq/attach/settings/package-info.java +++ b/modules/settings/src/main/java/com/gluonhq/attach/settings/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Settings plugin, + * Primary API package for Attach - Settings plugin, * contains the interface {@link com.gluonhq.attach.settings.SettingsService} and related classes. */ package com.gluonhq.attach.settings; \ No newline at end of file diff --git a/modules/settings/src/main/native/ios/Settings.m b/modules/settings/src/main/native/ios/Settings.m index 240fb107..b2149994 100644 --- a/modules/settings/src/main/native/ios/Settings.m +++ b/modules/settings/src/main/native/ios/Settings.m @@ -28,6 +28,7 @@ #import #include "jni.h" +#include "AttachMacros.h" JNIEnv *env; @@ -46,6 +47,7 @@ } static int settingsInited = 0; +BOOL debugSettings; JNIEXPORT void JNICALL Java_com_gluonhq_attach_settings_impl_IOSSettingsService_initSettings (JNIEnv *env, jclass jClass) @@ -61,6 +63,13 @@ [defaults registerDefaults:defaultPreferences]; } +JNIEXPORT void JNICALL Java_com_gluonhq_attach_settings_impl_IOSSettingsService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugSettings = YES; +} + + JNIEXPORT void JNICALL Java_com_gluonhq_attach_settings_impl_IOSSettingsService_settingsStore (JNIEnv *env, jclass jClass, jstring jKey, jstring jValue) { @@ -73,8 +82,9 @@ (*env)->ReleaseStringChars(env, jValue, charsVal); [[NSUserDefaults standardUserDefaults] setObject:value forKey:key]; - NSLog(@"Done storing %@ to %@", key, value); - + if (debugSettings) { + AttachLog(@"Done storing %@ to %@", key, value); + } } JNIEXPORT void JNICALL Java_com_gluonhq_attach_settings_impl_IOSSettingsService_settingsRemove @@ -85,7 +95,9 @@ (*env)->ReleaseStringChars(env, jKey, charsKey); [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; - NSLog(@"Done removing %@", key); + if (debugSettings) { + AttachLog(@"Done removing %@", key); + } } JNIEXPORT jstring JNICALL Java_com_gluonhq_attach_settings_impl_IOSSettingsService_settingsRetrieve @@ -97,10 +109,12 @@ NSString *value = [[NSUserDefaults standardUserDefaults] objectForKey:key]; if (!value) { - NSLog(@"%@ not found", key); + AttachLog(@"Error: %@ not found", key); return NULL; } - NSLog(@"Done retreiving %@", key); + if (debugSettings) { + AttachLog(@"Done retreiving %@", key); + } const char *valueChars = [value UTF8String]; return (*env)->NewStringUTF(env, valueChars); } \ No newline at end of file diff --git a/modules/share/src/main/java/com/gluonhq/attach/share/impl/IOSShareService.java b/modules/share/src/main/java/com/gluonhq/attach/share/impl/IOSShareService.java new file mode 100644 index 00000000..f6a3dc7e --- /dev/null +++ b/modules/share/src/main/java/com/gluonhq/attach/share/impl/IOSShareService.java @@ -0,0 +1,94 @@ +/* + * 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.share.impl; + + +import com.gluonhq.attach.share.ShareService; +import com.gluonhq.attach.util.Constants; + +import java.io.File; + + +public class IOSShareService implements ShareService { + + static { + System.loadLibrary("Share"); + initShare(); + } + + public IOSShareService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + } + + @Override + public void share(String contentText) { + share(null, contentText); + } + + @Override + public void share(String subject, String contentText) { + if (subject == null) { + subject = ""; + } + if (contentText == null || contentText.isEmpty()) { + System.out.println("Error: contentText not valid"); + return; + } + nativeShare(subject, contentText, ""); + } + + @Override + public void share(String type, File file) { + share(null, null, type, file); + } + + @Override + public void share(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 share: " + file); + } else { + System.out.println("Error: URL not valid"); + return; + } + nativeShare(subject, contentText, file.getAbsolutePath()); + } + + // native + private static native void initShare(); // init IDs for java callbacks from native + private static native void enableDebug(); + private static native void nativeShare(String subject, String message, String filePath); + +} \ No newline at end of file diff --git a/modules/share/src/main/java/com/gluonhq/attach/share/package-info.java b/modules/share/src/main/java/com/gluonhq/attach/share/package-info.java index c1ccaa7e..207273fd 100644 --- a/modules/share/src/main/java/com/gluonhq/attach/share/package-info.java +++ b/modules/share/src/main/java/com/gluonhq/attach/share/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Share plugin, + * Primary API package for Attach - Share plugin, * contains the interface {@link com.gluonhq.attach.share.ShareService} and related classes. */ package com.gluonhq.attach.share; \ No newline at end of file diff --git a/modules/share/src/main/native/ios/Share.h b/modules/share/src/main/native/ios/Share.h new file mode 100644 index 00000000..f33cd578 --- /dev/null +++ b/modules/share/src/main/native/ios/Share.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 Share : UIViewController +{ +} + @property (nonatomic, strong) NSString *message; + @property (nonatomic, strong) NSString *subject; + @property (nonatomic, strong) NSString *filePath; + - (void) shareText:(NSString *)subject message:(NSString *)message filePath:(NSString *)filePath; +@end diff --git a/modules/share/src/main/native/ios/Share.m b/modules/share/src/main/native/ios/Share.m new file mode 100644 index 00000000..260b7992 --- /dev/null +++ b/modules/share/src/main/native/ios/Share.m @@ -0,0 +1,186 @@ +/* + * 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 "Share.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Share(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 ShareInited = 0; + +Share *_share; + +BOOL debugShare; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_share_impl_IOSShareService_initShare +(JNIEnv *env, jclass jClass) +{ + if (ShareInited) + { + return; + } + ShareInited = 1; + + AttachLog(@"Init Share"); + _share = [[Share alloc] init]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_share_impl_IOSShareService_nativeShare +(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); + + [_share shareText: subject message:message filePath:filePath]; + +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_share_impl_IOSShareService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugShare = YES; +} + +@implementation Share + +- (void) shareText: (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 sharedApplication] keyWindow]) + { + AttachLog(@"key window was nil"); + return; + } + + NSArray *views = [[[UIApplication sharedApplication] keyWindow] subviews]; + if(![views count]) { + AttachLog(@"views size was 0"); + return; + } + + UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; + if(!rootViewController) + { + AttachLog(@"rootViewController was nil"); + return; + } + + NSMutableArray *items = [[NSMutableArray alloc] init]; + [items addObject:self]; + + if ([_filePath length] > 0) { + [self logMessage:@"Share file: %@", _filePath]; + NSURL *fileUrl = [NSURL fileURLWithPath:_filePath]; + NSError *err; + if ([fileUrl checkResourceIsReachableAndReturnError:&err] == YES) + { + [self logMessage:@"Share fileUrl: %@", fileUrl]; + [items addObject:fileUrl]; + } else { + AttachLog(@"File resource not reachable: %@", err); + } + } + + NSArray *itemsToShare = [NSArray arrayWithArray:items]; + + UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:itemsToShare 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:@"Share activityViewControllerPlaceholderItem"]; + return @""; +} + +- (NSString *) activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(NSString *)activityType +{ + [self logMessage:@"Share subjectForActivityType %@ - Subject: %@", activityType, _subject]; + return _subject; +} + +- (nullable id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType +{ + [self logMessage:@"Share itemForActivityType %@ - Message: %@", activityType, _message]; + return _message; +} + +- (void) logMessage:(NSString *)format, ...; +{ + if (debugShare) + { + va_list args; + va_start(args, format); + NSLogv([@"[Debug] " stringByAppendingString:format], args); + va_end(args); + } +} +@end + diff --git a/modules/statusbar/src/main/java/com/gluonhq/attach/statusbar/impl/IOSStatusBarService.java b/modules/statusbar/src/main/java/com/gluonhq/attach/statusbar/impl/IOSStatusBarService.java new file mode 100644 index 00000000..f4dd1969 --- /dev/null +++ b/modules/statusbar/src/main/java/com/gluonhq/attach/statusbar/impl/IOSStatusBarService.java @@ -0,0 +1,45 @@ +/* + * 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.statusbar.impl; + +import com.gluonhq.attach.statusbar.StatusBarService; +import javafx.scene.paint.Color; + +public class IOSStatusBarService implements StatusBarService { + + static { + System.loadLibrary("StatusBar"); + } + + @Override + public void setColor(Color color) { + setNativeColor(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity()); + } + + private native void setNativeColor(double red, double green, double blue, double opacity); +} \ No newline at end of file diff --git a/modules/statusbar/src/main/java/com/gluonhq/attach/statusbar/package-info.java b/modules/statusbar/src/main/java/com/gluonhq/attach/statusbar/package-info.java index dba8ad9a..76792505 100644 --- a/modules/statusbar/src/main/java/com/gluonhq/attach/statusbar/package-info.java +++ b/modules/statusbar/src/main/java/com/gluonhq/attach/statusbar/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Status Bar plugin, + * Primary API package for Attach - Status Bar plugin, * contains the interface {@link com.gluonhq.attach.statusbar.StatusBarService} and related classes. */ package com.gluonhq.attach.statusbar; \ No newline at end of file diff --git a/modules/statusbar/src/main/native/ios/StatusBar.m b/modules/statusbar/src/main/native/ios/StatusBar.m new file mode 100644 index 00000000..2d115c91 --- /dev/null +++ b/modules/statusbar/src/main/native/ios/StatusBar.m @@ -0,0 +1,56 @@ +/* + * 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" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_StatusBar(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 +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_statusbar_impl_IOSStatusBarService_setNativeColor +(JNIEnv *env, jclass jClass, jdouble red, jdouble green, jdouble blue, jdouble opacity) +{ + UIView *statusBar = [[[UIApplication sharedApplication] valueForKey:@"statusBarWindow"] valueForKey:@"statusBar"]; + + if ([statusBar respondsToSelector:@selector(setBackgroundColor:)]) { + statusBar.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:opacity]; + } +} \ No newline at end of file diff --git a/modules/storage/src/main/java/com/gluonhq/attach/storage/impl/IOSStorageService.java b/modules/storage/src/main/java/com/gluonhq/attach/storage/impl/IOSStorageService.java index dab8079e..d1a2ad36 100644 --- a/modules/storage/src/main/java/com/gluonhq/attach/storage/impl/IOSStorageService.java +++ b/modules/storage/src/main/java/com/gluonhq/attach/storage/impl/IOSStorageService.java @@ -28,6 +28,7 @@ package com.gluonhq.attach.storage.impl; import com.gluonhq.attach.storage.StorageService; +import com.gluonhq.attach.util.Constants; import java.io.File; import java.util.HashMap; @@ -40,6 +41,12 @@ public class IOSStorageService implements StorageService { System.loadLibrary("Storage"); } + public IOSStorageService() { + if ("true".equals(System.getProperty(Constants.ATTACH_DEBUG))) { + enableDebug(); + } + } + @Override public Optional getPrivateStorage() { try { @@ -90,4 +97,5 @@ private synchronized String getPublicStorageURL(String dir) { private native String privateStorageURL(); private native String publicStorageURL(String dir); + private static native void enableDebug(); } diff --git a/modules/storage/src/main/native/ios/Storage.m b/modules/storage/src/main/native/ios/Storage.m index a4570197..026040ea 100644 --- a/modules/storage/src/main/native/ios/Storage.m +++ b/modules/storage/src/main/native/ios/Storage.m @@ -27,6 +27,7 @@ */ #import #include "jni.h" +#include "AttachMacros.h" JNIEnv *env; @@ -44,6 +45,14 @@ #endif } +BOOL debugStorage; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_storage_impl_IOSStorageService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugStorage = YES; +} + JNIEXPORT jstring JNICALL Java_com_gluonhq_attach_storage_impl_IOSStorageService_privateStorageURL (JNIEnv *env, jclass jClass) { @@ -53,14 +62,16 @@ NSString *folderPath = [documentsDir stringByAppendingPathComponent:folder]; if (!folderPath) { - NSLog(@"Error getting the private storage path"); + AttachLog(@"Error getting the private storage path"); return NULL; } NSFileManager *manager = [NSFileManager defaultManager]; [manager createDirectoryAtPath: folderPath withIntermediateDirectories: NO attributes: nil error: nil]; - NSLog(@"Done creating private storage %@", folderPath); + if (debugStorage) { + AttachLog(@"Done creating private storage %@", folderPath); + } const char *valueChars = [folderPath UTF8String]; return (*env)->NewStringUTF(env, valueChars); } @@ -81,10 +92,12 @@ [manager createDirectoryAtPath: folderPath withIntermediateDirectories: NO attributes: nil error: nil]; if (!folderPath) { - NSLog(@"Error creating public storage path"); + AttachLog(@"Error creating public storage path"); return NULL; } - NSLog(@"Done creating public storage %@", folderPath); + if (debugStorage) { + AttachLog(@"Done creating public storage %@", folderPath); + } const char *valueChars = [folderPath UTF8String]; return (*env)->NewStringUTF(env, valueChars); } \ No newline at end of file diff --git a/modules/util/src/main/java/com/gluonhq/attach/util/Platform.java b/modules/util/src/main/java/com/gluonhq/attach/util/Platform.java index dd198ae8..668e5feb 100644 --- a/modules/util/src/main/java/com/gluonhq/attach/util/Platform.java +++ b/modules/util/src/main/java/com/gluonhq/attach/util/Platform.java @@ -65,18 +65,11 @@ public enum Platform { private static final Logger LOGGER = Logger.getLogger(Platform.class.getName()); static { - String s = System.getProperty("javafx.platform", null); - if (s == null) { - String os = System.getProperty("os.target", null); - if (os != null) { - LOGGER.info("javafx.platform is not defined, using: " + os + " from os.target"); - s = os; - } else { - LOGGER.severe("javafx.platform is not defined. Desktop will be assumed by default."); - s = DESKTOP.getName(); - } + String os = System.getProperty("os.name", DESKTOP.getName()).toLowerCase(Locale.ROOT); + if (os.contains("mac") || os.contains("win") || os.contains("nux")) { + os = DESKTOP.getName(); } - String name = s.toUpperCase(Locale.ROOT); + String name = os.toUpperCase(Locale.ROOT); current = valueOf(name); LOGGER.fine("Current platform: " + current); } diff --git a/modules/util/src/main/java/com/gluonhq/attach/util/impl/PropertyWatcher.java b/modules/util/src/main/java/com/gluonhq/attach/util/PropertyWatcher.java similarity index 98% rename from modules/util/src/main/java/com/gluonhq/attach/util/impl/PropertyWatcher.java rename to modules/util/src/main/java/com/gluonhq/attach/util/PropertyWatcher.java index 9a4e9b69..92c8dd5f 100644 --- a/modules/util/src/main/java/com/gluonhq/attach/util/impl/PropertyWatcher.java +++ b/modules/util/src/main/java/com/gluonhq/attach/util/PropertyWatcher.java @@ -25,7 +25,7 @@ * (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.util.impl; +package com.gluonhq.attach.util; import java.util.ArrayList; import java.util.List; diff --git a/modules/vibration/src/main/java/com/gluonhq/attach/vibration/impl/IOSVibrationService.java b/modules/vibration/src/main/java/com/gluonhq/attach/vibration/impl/IOSVibrationService.java new file mode 100644 index 00000000..493759f8 --- /dev/null +++ b/modules/vibration/src/main/java/com/gluonhq/attach/vibration/impl/IOSVibrationService.java @@ -0,0 +1,51 @@ +/* + * 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.vibration.impl; + + +import com.gluonhq.attach.vibration.VibrationService; + +public class IOSVibrationService implements VibrationService { + + static { + System.loadLibrary("Vibration"); + } + + @Override + public void vibrate() { + doVibrate(); + } + + @Override + public void vibrate(long... pattern) { + // pattern not supported on iOS + vibrate(); + } + + private native static void doVibrate(); +} \ No newline at end of file diff --git a/modules/vibration/src/main/java/com/gluonhq/attach/vibration/package-info.java b/modules/vibration/src/main/java/com/gluonhq/attach/vibration/package-info.java index bc60a567..fba8eabe 100644 --- a/modules/vibration/src/main/java/com/gluonhq/attach/vibration/package-info.java +++ b/modules/vibration/src/main/java/com/gluonhq/attach/vibration/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Vibration plugin, + * Primary API package for Attach - Vibration plugin, * contains the interface {@link com.gluonhq.attach.vibration.VibrationService} and related classes. */ package com.gluonhq.attach.vibration; \ No newline at end of file diff --git a/modules/vibration/src/main/native/ios/Vibration.m b/modules/vibration/src/main/native/ios/Vibration.m new file mode 100644 index 00000000..2df27dc3 --- /dev/null +++ b/modules/vibration/src/main/native/ios/Vibration.m @@ -0,0 +1,54 @@ +/* + * 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" +#import + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Vibration(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 +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_vibration_impl_IOSVibrationService_doVibrate +(JNIEnv *env, jclass jClass) +{ + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); +} + diff --git a/modules/video/src/main/java/com/gluonhq/attach/video/impl/DefaultVideoService.java b/modules/video/src/main/java/com/gluonhq/attach/video/impl/DefaultVideoService.java index b7984671..babf547b 100644 --- a/modules/video/src/main/java/com/gluonhq/attach/video/impl/DefaultVideoService.java +++ b/modules/video/src/main/java/com/gluonhq/attach/video/impl/DefaultVideoService.java @@ -72,7 +72,7 @@ public DefaultVideoService() { assetsFolder = new File(Services.get(StorageService.class) .flatMap(service -> service.getPrivateStorage()) - .orElseThrow(() -> new RuntimeException("Error accesing Private Storage folder")), "assets"); + .orElseThrow(() -> new RuntimeException("Error accessing Private Storage folder")), "assets"); if (! assetsFolder.exists()) { assetsFolder.mkdir(); diff --git a/modules/video/src/main/java/com/gluonhq/attach/video/impl/IOSVideoService.java b/modules/video/src/main/java/com/gluonhq/attach/video/impl/IOSVideoService.java new file mode 100644 index 00000000..c62ed130 --- /dev/null +++ b/modules/video/src/main/java/com/gluonhq/attach/video/impl/IOSVideoService.java @@ -0,0 +1,186 @@ +/* + * 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.video.impl; + +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.geometry.Pos; +import javafx.scene.media.MediaPlayer.Status; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class IOSVideoService extends DefaultVideoService { + + static { + System.loadLibrary("Video"); + initVideo(); + } + + private static final ReadOnlyObjectWrapper STATUS = new ReadOnlyObjectWrapper<>(); + private static final BooleanProperty FULL_SCREEN = new SimpleBooleanProperty(); + private static final IntegerProperty CURRENT_INDEX = new SimpleIntegerProperty(); + + public IOSVideoService() { + super(); + + if (debug) { + enableDebug(); + } + + playlist.addListener((Observable o) -> { + List list = new ArrayList<>(); + for (String s : playlist) { + if (checkFileInResources(s)) { + File videoFile = getFileFromAssets(s); + list.add(videoFile.getAbsolutePath()); + } else { + list.add(s); + } + } + setVideoPlaylist(list.toArray(new String[0])); + }); + + FULL_SCREEN.addListener((obs, ov, nv) -> setFullScreenMode(nv)); + CURRENT_INDEX.addListener((obs, ov, nv) -> currentIndex(nv.intValue())); + } + + @Override + public void show() { + showVideo(); + } + + @Override + public void play() { + playVideo(); + } + + @Override + public void stop() { + stopVideo(); + } + + @Override + public void pause() { + pauseVideo(); + } + + @Override + public void hide() { + hideVideo(); + } + + @Override + public void setPosition(Pos alignment, double topPadding, double rightPadding, double bottomPadding, double leftPadding) { + setPosition(alignment.getHpos().name(), alignment.getVpos().name(), topPadding, rightPadding, bottomPadding, leftPadding); + } + + @Override + public void setLooping(boolean looping) { + looping(looping); + } + + @Override + public void setControlsVisible(boolean controlsVisible) { + controlsVisible(controlsVisible); + } + + @Override + public void setFullScreen(boolean fullScreen) { + FULL_SCREEN.set(fullScreen); + } + + @Override + public BooleanProperty fullScreenProperty() { + return FULL_SCREEN; + } + + @Override + public ReadOnlyObjectProperty statusProperty() { + return STATUS.getReadOnlyProperty(); + } + + @Override + public void setCurrentIndex(int index) { + CURRENT_INDEX.set(index); + } + + @Override + public IntegerProperty currentIndexProperty() { + return CURRENT_INDEX; + } + + // native + private static native void initVideo(); // init IDs for java callbacks from native + private native void setVideoPlaylist(String[] playlist); + private native void showVideo(); + private native void playVideo(); + private native void stopVideo(); + private native void pauseVideo(); + private native void hideVideo(); + private native void looping(boolean looping); + private native void controlsVisible(boolean controlsVisible); + private native void currentIndex(int currentIndex); + private native void setFullScreenMode(boolean fullScreen); + private native void setPosition(String alignmentH, String alignmentV, double topPadding, double rightPadding, double bottomPadding, double leftPadding); + private static native void enableDebug(); + + // callbacks + private static void updateStatus(int value) { + Status s; + switch (value) { + case 0: s = Status.UNKNOWN; break; + case 1: s = Status.READY; break; + case 2: s = Status.PAUSED; break; + case 3: s = Status.PLAYING; break; + case 4: s = Status.STOPPED; break; + case 5: s = Status.DISPOSED; break; + default: s = Status.UNKNOWN; + } + Platform.runLater(() -> STATUS.set(s)); + } + + private static void updateFullScreen(boolean value) { + if (FULL_SCREEN.get() != value) { + Platform.runLater(() -> FULL_SCREEN.set(value)); + } + } + + private static void updateCurrentIndex(int index) { + if (CURRENT_INDEX.get() != index) { + Platform.runLater(() -> CURRENT_INDEX.set(index)); + } + } +} diff --git a/modules/video/src/main/java/com/gluonhq/attach/video/package-info.java b/modules/video/src/main/java/com/gluonhq/attach/video/package-info.java index 30b448ca..322cbd6f 100644 --- a/modules/video/src/main/java/com/gluonhq/attach/video/package-info.java +++ b/modules/video/src/main/java/com/gluonhq/attach/video/package-info.java @@ -27,7 +27,7 @@ */ /** - * Primary API package for Down - Video plugin, + * Primary API package for Attach - Video plugin, * contains the interface {@link com.gluonhq.attach.video.VideoService} and related classes. */ package com.gluonhq.attach.video; \ No newline at end of file diff --git a/modules/video/src/main/native/ios/Video.h b/modules/video/src/main/native/ios/Video.h new file mode 100644 index 00000000..24e195df --- /dev/null +++ b/modules/video/src/main/native/ios/Video.h @@ -0,0 +1,64 @@ +/* + * 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" +#import +#import + +typedef NS_ENUM(NSInteger, MediaPlayerStatus) { + MediaPlayerStatusUnknown, + MediaPlayerStatusReady, + MediaPlayerStatusPaused, + MediaPlayerStatusPlaying, + MediaPlayerStatusStopped, + MediaPlayerStatusDisposed +}; + +@interface Video :UIViewController +{ +} + @property (nonatomic, strong) NSArray *arrayOfPlaylist; + @property (nonatomic, strong) AVPlayerViewController *avPlayerViewcontroller; + @property (nonatomic, strong) dispatch_semaphore_t semaphore; + + - (void) initPlaylist:(NSArray *)playlist; + - (void) showVideo; + - (void) playVideo; + - (void) pauseVideo; + - (void) stopVideo; + - (void) hideVideo; + - (void) internalHide; + - (void) fullScreenVideo: (BOOL) value; + - (void) currentIndex: (int) index; + - (void) resizeRelocateVideo; +@end + +void status(MediaPlayerStatus status); +void updateFullScreen(BOOL value); +void updateCurrentIndex(int index); diff --git a/modules/video/src/main/native/ios/Video.m b/modules/video/src/main/native/ios/Video.m new file mode 100644 index 00000000..bb5795a3 --- /dev/null +++ b/modules/video/src/main/native/ios/Video.m @@ -0,0 +1,778 @@ +/* + * 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 "Video.h" + +JNIEnv *env; + +JNIEXPORT jint JNICALL +JNI_OnLoad_Video(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 VideoInited = 0; + +// Video +jclass mat_jVideoServiceClass; +jmethodID mat_jVideoService_updateStatus = 0; +jmethodID mat_jVideoService_updateFullScreen = 0; +jmethodID mat_jVideoService_updateCurrentIndex = 0; + +Video *_video; +UIView *_currentView; +UIViewController *rootViewController; + +BOOL init; +int currentMediaIndex = 0; +NSString *videoName; +NSURL *urlVideoFile; +BOOL showing; +MediaPlayerStatus videoStatus = (MediaPlayerStatus) MediaPlayerStatusUnknown; + +BOOL isVideo; +BOOL loop; +BOOL useControls; +BOOL fullScreenMode; +int alignH; +int alignV; +double topPadding, rightPadding, bottomPadding, leftPadding; +BOOL debugVideo; + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_initVideo +(JNIEnv *env, jclass jClass) +{ + if (VideoInited) + { + return; + } + VideoInited = 1; + + mat_jVideoServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/video/impl/IOSVideoService")); + mat_jVideoService_updateStatus = (*env)->GetStaticMethodID(env, mat_jVideoServiceClass, "updateStatus", "(I)V"); + mat_jVideoService_updateFullScreen = (*env)->GetStaticMethodID(env, mat_jVideoServiceClass, "updateFullScreen", "(Z)V"); + mat_jVideoService_updateCurrentIndex = (*env)->GetStaticMethodID(env, mat_jVideoServiceClass, "updateCurrentIndex", "(I)V"); + + AttachLog(@"Init Video"); + _video = [[Video alloc] init]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_setVideoPlaylist +(JNIEnv *env, jclass jClass, jobjectArray jPlaylistArray) +{ + int playlistCount = (*env)->GetArrayLength(env, jPlaylistArray); + NSMutableArray *playItems = [[NSMutableArray alloc] init]; + + for (int i = 0; i < playlistCount; i++) { + jstring jplayItem = (jstring) ((*env)->GetObjectArrayElement(env, jPlaylistArray, i)); + const jchar *playItemString = (*env)->GetStringChars(env, jplayItem, NULL); + NSString *playItem = [NSString stringWithCharacters:(UniChar *)playItemString length:(*env)->GetStringLength(env, jplayItem)]; + (*env)->ReleaseStringChars(env, jplayItem, playItemString); + [playItems addObject:playItem]; + } + if (debugVideo) { + AttachLog(@"Added video playlist with %lu items", (unsigned long)[playItems count]); + } + [_video initPlaylist:playItems]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_showVideo +(JNIEnv *env, jclass jClass, jstring jTitle) +{ + if (_video) + { + [_video showVideo]; + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_playVideo +(JNIEnv *env, jclass jClass, jstring jTitle) +{ + if (_video) + { + [_video playVideo]; + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_pauseVideo +(JNIEnv *env, jclass jClass) +{ + if (_video) + { + [_video pauseVideo]; + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_stopVideo +(JNIEnv *env, jclass jClass) +{ + if (_video) + { + [_video stopVideo]; + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_hideVideo +(JNIEnv *env, jclass jClass) +{ + if (_video) + { + [_video hideVideo]; + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_looping +(JNIEnv *env, jclass jClass, jboolean jLooping) +{ + loop = jLooping; + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_controlsVisible +(JNIEnv *env, jclass jClass, jboolean jControls) +{ + useControls = jControls; + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_setFullScreenMode +(JNIEnv *env, jclass jClass, jboolean jfullscreen) +{ + if (_video) + { + [_video fullScreenVideo:jfullscreen]; + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_currentIndex +(JNIEnv *env, jclass jClass, jint jindex) +{ + if (_video) + { + [_video currentIndex:jindex]; + } + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_setPosition +(JNIEnv *env, jclass jClass, jstring jalignmentH, jstring jalignmentV, jdouble jtopPadding, + jdouble jrightPadding, jdouble jbottomPadding, jdouble jleftPadding) +{ + const jchar *charsAlignH = (*env)->GetStringChars(env, jalignmentH, NULL); + NSString *sAlignH = [NSString stringWithCharacters:(UniChar *)charsAlignH length:(*env)->GetStringLength(env, jalignmentH)]; + (*env)->ReleaseStringChars(env, jalignmentH, charsAlignH); + + const jchar *charsAlignV = (*env)->GetStringChars(env, jalignmentV, NULL); + NSString *sAlignV = [NSString stringWithCharacters:(UniChar *)charsAlignV length:(*env)->GetStringLength(env, jalignmentV)]; + (*env)->ReleaseStringChars(env, jalignmentV, charsAlignV); + if (debugVideo) { + AttachLog(@"Video Alignment H: %@, V: %@", sAlignH, sAlignV); + } + + if ([sAlignH isEqualToString:@"LEFT"]) { + alignH = -1; + } else if ([sAlignH isEqualToString:@"RIGHT"]) { + alignH = 1; + } else { + alignH = 0; + } + if ([sAlignV isEqualToString:@"TOP"]) { + alignV = -1; + } else if ([sAlignV isEqualToString:@"BOTTOM"]) { + alignV = 1; + } else { + alignV = 0; + } + topPadding = jtopPadding; + rightPadding = jrightPadding; + bottomPadding = jbottomPadding; + leftPadding = jleftPadding; + + [_video resizeRelocateVideo]; + return; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_video_impl_IOSVideoService_enableDebug +(JNIEnv *env, jclass jClass) +{ + debugVideo = YES; +} + +void status(MediaPlayerStatus status) { + videoStatus = status; + if (debugVideo) { + AttachLog(@"Media Player Status: %ld", (long) status); + } + (*env)->CallStaticVoidMethod(env, mat_jVideoServiceClass, mat_jVideoService_updateStatus, status); +} + +void updateFullScreen(BOOL value) { + fullScreenMode = value; + (*env)->CallStaticVoidMethod(env, mat_jVideoServiceClass, mat_jVideoService_updateFullScreen, (value) ? JNI_TRUE : JNI_FALSE); +} + +void updateCurrentIndex(int index) { + currentMediaIndex = index; + (*env)->CallStaticVoidMethod(env, mat_jVideoServiceClass, mat_jVideoService_updateCurrentIndex, index); +} + +@implementation Video + +- (void) initPlaylist:(NSArray *)playlist +{ + if (_arrayOfPlaylist) { + [self logMessage:@"Update playlist"]; + if ([playlist count] == 0) { + [self hideVideo]; + } else if ([videoName length] > 0) { + if (! [playlist containsObject: videoName]) { + if (currentMediaIndex == 0) { + currentMediaIndex = -1; + } + [self logMessage:@"Update playlist to index 0"]; + [self currentIndex:0]; + } else { + NSUInteger index = [playlist indexOfObject:videoName]; + if (index != currentMediaIndex) { + [self logMessage:@"Update playlist from index %d to new index %d", currentMediaIndex, index]; + updateCurrentIndex(index); + } + } + } + } + _arrayOfPlaylist = [[NSArray alloc] initWithArray:playlist copyItems:YES]; + [self logMessage:@"Init array %@", _arrayOfPlaylist]; +} + +- (void)initVideo +{ + [self logMessage:@"Init window"]; + 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; + } + + _currentView = views[0]; + + rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; + if(!rootViewController) + { + AttachLog(@"rootViewController was nil"); + return; + } + + init = YES; +} + +- (void)showVideo +{ + if ([_arrayOfPlaylist count] == 0) { + AttachLog(@"There is no playlist available"); + return; + } + + if (! init) { + [_video initVideo]; + if (! init) { + return; + } + } + + if (showing) { + [self logMessage:@"Video layer was already added"]; + return; + } + + videoName = [_arrayOfPlaylist objectAtIndex:currentMediaIndex]; + showing = YES; + + if ([self prepareMedia]) { + [self logMessage:@"Video URL: %@", urlVideoFile.absoluteString]; + [self setupVideo]; + } + else { + [self logMessage:@"Invalid media file found, trying the next one"]; + dispatch_semaphore_signal(_semaphore); + showing = NO; + status(MediaPlayerStatusUnknown); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self currentIndex:currentMediaIndex + 1]; + }); + } +} + +- (void)playVideo +{ + if ([_arrayOfPlaylist count] == 0) { + AttachLog(@"There is no playlist available"); + return; + } + + if (videoStatus == MediaPlayerStatusStopped || videoStatus == MediaPlayerStatusDisposed) { + // rewind + status(MediaPlayerStatusUnknown); + [self internalHide]; + updateCurrentIndex(0); + } + + if (! showing) { + _semaphore = dispatch_semaphore_create(0); + runOnMainQueueWithoutDeadlocking(^{ + [self showVideo]; + }); + while (dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_NOW)) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; + } + dispatch_release(_semaphore); + + if (videoStatus == MediaPlayerStatusReady) { + [self logMessage:@"Video start playing [%d/%d]: %@", (currentMediaIndex + 1), [_arrayOfPlaylist count], videoName]; + [_avPlayerViewcontroller.player play]; + } + } else if (_avPlayerViewcontroller) { + [self logMessage:@"Video play"]; + [_avPlayerViewcontroller.player play]; + } +} + +- (BOOL)prepareMedia +{ + + if([[NSFileManager defaultManager] fileExistsAtPath:videoName]) { + [self logMessage:@"Video from resources"]; + urlVideoFile = [[NSURL alloc] initFileURLWithPath:videoName]; + return YES; + } + else if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:videoName]]) + { + [self logMessage:@"Video from URL"]; + urlVideoFile = [NSURL URLWithString:videoName]; + return YES; + } + else + { + AttachLog(@"Error: %@ is not a valid name", videoName); + return NO; + } +} + +- (void) setupVideo +{ + NSError* error = nil; + + if(_avPlayerViewcontroller) + { + runOnMainQueueWithoutDeadlocking(^{ + [self logMessage:@"Adding new item %@", urlVideoFile]; + [_avPlayerViewcontroller.player replaceCurrentItemWithPlayerItem:[AVPlayerItem playerItemWithURL:urlVideoFile]]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoDidFinish:) name:AVPlayerItemDidPlayToEndTimeNotification + object:[_avPlayerViewcontroller.player currentItem]]; + + [self resizeRelocateVideo]; + status(MediaPlayerStatusReady); + [self logMessage:@"Video ready"]; + if (_semaphore) { + dispatch_semaphore_signal(_semaphore); + } + }); + } + else { + _avPlayerViewcontroller = [[AVPlayerViewController alloc] init]; + _avPlayerViewcontroller.player = [AVPlayer playerWithURL:urlVideoFile]; + + if (! useControls) { + // a pinch gesture allows exiting full screen mode if embedded controls are not available + // When using embedded controls, a button is provided so the gesture is not required + _avPlayerViewcontroller.view.userInteractionEnabled = YES; + UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchAvPlayer:)]; + pinch.delegate = self; + NSString *ver = [[UIDevice currentDevice] systemVersion]; + float ver_float = [ver floatValue]; + if (ver_float < 8.0) { + [_avPlayerViewcontroller.view.subviews[0] addGestureRecognizer:pinch]; + } else { + _avPlayerViewcontroller.contentOverlayView.gestureRecognizers = @[pinch]; + } + } + + [self resizeRelocateVideo]; + + [_currentView addSubview:_avPlayerViewcontroller.view]; + + if(!_avPlayerViewcontroller) + { + AttachLog(@"Error creating player: %@", error); + return; + } + _avPlayerViewcontroller.showsPlaybackControls = useControls; + + [self logMessage:@"Adding listeners"]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoDidFinish:) name:AVPlayerItemDidPlayToEndTimeNotification + object:[_avPlayerViewcontroller.player currentItem]]; + + [_avPlayerViewcontroller.player addObserver:self forKeyPath:@"status" options:0 context:nil]; + [_avPlayerViewcontroller.player addObserver:self forKeyPath:@"rate" options:0 context:nil]; + [_avPlayerViewcontroller.contentOverlayView addObserver:self forKeyPath:@"bounds" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; + } + [self logMessage:@"Finished setupVideo"]; +} + +- (void)pauseVideo +{ + if(!_avPlayerViewcontroller) + { + return; + } + [self logMessage:@"Video pause"]; + [_avPlayerViewcontroller.player pause]; +} + +- (void)stopVideo +{ + if(!_avPlayerViewcontroller) + { + return; + } + [self logMessage:@"Video stop"]; + [_avPlayerViewcontroller.player pause]; + status(MediaPlayerStatusStopped); +} + +- (void)hideVideo +{ + [self internalHide]; + [self dispose]; +} + +- (void)internalHide +{ + if (showing) { + @try { + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; + } @catch (NSException *exception) { + AttachLog(@"Error removing NSNotificationCenter observer: %@", exception); + } + } + if (fullScreenMode) { + [self fullScreenVideo:NO]; + } + AttachLog(@"AVPlayer hidden"); + showing = NO; +} + +- (void) fullScreenVideo: (BOOL) value +{ + if (value == fullScreenMode || ! isVideo) { + return; + } + + if (useControls) { + AttachLog(@"Please, use the fullscreen button from the embedded controls"); + updateFullScreen(false); + return; + } + fullScreenMode = value; + [UIView animateKeyframesWithDuration:0.3f + delay:0.0f + options:UIViewKeyframeAnimationOptionLayoutSubviews + animations:^{ + [self resizeRelocateVideo]; + } + completion:^(BOOL finished){ + updateFullScreen(value); + } + ]; +} + +- (void) currentIndex: (int) index +{ + if (index == currentMediaIndex) { + return; + } + [self logMessage:@"Skipping current video from %d to %d", currentMediaIndex, index]; + + [self pauseVideo]; + [self logMessage:@"Hiding current video file"]; + [self internalHide]; + + if (0 <= index && index < [_arrayOfPlaylist count]) { + updateCurrentIndex(index); + [self logMessage:@"Showing new video file: %d", index]; + [self playVideo]; + } else if (loop) { + updateCurrentIndex(0); + [self logMessage:@"Showing first video file"]; + [self playVideo]; + } else { + [self logMessage:@"Disposing media player"]; + [self dispose]; + } +} + +- (void) resizeRelocateVideo +{ + [self logMessage:@"Video resize and relocate"]; + CGRect theLayerRect = [[UIScreen mainScreen] bounds]; + if (fullScreenMode) { + [_avPlayerViewcontroller.view setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:1]]; + _avPlayerViewcontroller.view.frame = theLayerRect; + } + else + { + double maxW = theLayerRect.size.width - (leftPadding + rightPadding); + double maxH = theLayerRect.size.height - (topPadding + bottomPadding); + [self logMessage:@"Video max size: %f x %f", maxW, maxH]; + + if ([[_avPlayerViewcontroller.player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo] count] != 0) { + isVideo = true; + [_currentView bringSubviewToFront:_avPlayerViewcontroller.view]; + AVAssetTrack *track = [_avPlayerViewcontroller.player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo][0]; + [self logMessage:@"Video track %@", track]; + + CGSize theNaturalSize = [track naturalSize]; + theNaturalSize = CGSizeApplyAffineTransform(theNaturalSize, track.preferredTransform); + if (theNaturalSize.width == 0.0f || theNaturalSize.width == 0.0f) { + return; + } + theNaturalSize.width = fabs(theNaturalSize.width); + theNaturalSize.height = fabs(theNaturalSize.height); + [self logMessage:@"Video track natural size %@", NSStringFromCGSize(theNaturalSize)]; + + CGFloat movieAspectRatio = theNaturalSize.width / theNaturalSize.height; + CGFloat viewAspectRatio = maxW / maxH; + [self logMessage:@"Video movie ratio: %f, view ratio: %f", movieAspectRatio, viewAspectRatio]; + + CGRect theVideoRect = CGRectZero; + [self logMessage:@"Video set video rect: %@", NSStringFromCGRect(theVideoRect)]; + + if (viewAspectRatio < movieAspectRatio) { + theVideoRect.size.width = maxW; + theVideoRect.size.height = maxW / movieAspectRatio; + [self logMessage:@"Video video size %@", NSStringFromCGSize(theVideoRect.size)]; + theVideoRect.origin.x = leftPadding; + if (alignV == -1) { + theVideoRect.origin.y = topPadding; + } else if (alignV == 0) { + theVideoRect.origin.y = topPadding + (maxH - theVideoRect.size.height) / 2; + } else { + theVideoRect.origin.y = topPadding + (maxH - theVideoRect.size.height); + } + } else { + theVideoRect.size.width = movieAspectRatio * maxH; + theVideoRect.size.height = maxH; + [self logMessage:@"Video video size %@", NSStringFromCGSize(theVideoRect.size)]; + if (alignH == -1) { + theVideoRect.origin.x = leftPadding; + } else if (alignH == 0) { + theVideoRect.origin.x = leftPadding + (maxW - theVideoRect.size.width) / 2; + } else { + theVideoRect.origin.x = leftPadding + (maxW - theVideoRect.size.width); + } + theVideoRect.origin.y = topPadding; + } + [self logMessage:@"Video video origin %f x %f", theVideoRect.origin.x, theVideoRect.origin.y]; + + [self logMessage:@"Video frame: %@", NSStringFromCGRect(theVideoRect)]; + [_avPlayerViewcontroller.view setBackgroundColor:[UIColor colorWithRed:1 green:1 blue:1 alpha:0]]; + _avPlayerViewcontroller.view.frame = theVideoRect; + } else { + isVideo = false; + [_currentView sendSubviewToBack:_avPlayerViewcontroller.view]; + if ([[_avPlayerViewcontroller.player.currentItem.asset tracksWithMediaType:AVMediaTypeAudio] count] != 0) { + AVAssetTrack *track = [_avPlayerViewcontroller.player.currentItem.asset tracksWithMediaType:AVMediaTypeAudio][0]; + [self logMessage:@"Audio track %@", track]; + } + } + } +} + +- (void)videoDidFinish:(NSNotification *)notification { + [self currentIndex:currentMediaIndex + 1]; +} + +- (void) dispose +{ + @try { + [_avPlayerViewcontroller.player removeObserver:self forKeyPath:@"status" context:nil]; + } @catch (NSException *exception) { + AttachLog(@"Error removing player status observer: %@", exception); + } + @try { + [_avPlayerViewcontroller.player removeObserver:self forKeyPath:@"rate" context:nil]; + } @catch (NSException *exception) { + AttachLog(@"Error removing player rate observer: %@", exception); + } + @try { + [_avPlayerViewcontroller.contentOverlayView removeObserver:self forKeyPath:@"bounds" context:nil]; + } @catch (NSException *exception) { + AttachLog(@"Error removing contentOverlayView observer: %@", exception); + } + @try { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; + } @catch (NSException *exception) { + AttachLog(@"Error removing orientation observer: %@", exception); + } + [_avPlayerViewcontroller.player replaceCurrentItemWithPlayerItem:nil]; + [_avPlayerViewcontroller dismissViewControllerAnimated:YES completion:nil]; + [_avPlayerViewcontroller.view removeFromSuperview]; + [_avPlayerViewcontroller release]; + _avPlayerViewcontroller = nil; + status(MediaPlayerStatusDisposed); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == _avPlayerViewcontroller.player && [keyPath isEqualToString:@"status"]) + { + if (_avPlayerViewcontroller.player.status == AVPlayerStatusFailed) { + AttachLog(@"AVPlayer Failed"); + status(MediaPlayerStatusUnknown); + } else if (_avPlayerViewcontroller.player.status == AVPlayerStatusReadyToPlay) { + AttachLog(@"AVPlayerStatusReadyToPlay"); + status(MediaPlayerStatusReady); + [self logMessage:@"Video ready"]; + } else if (_avPlayerViewcontroller.player.status == AVPlayerItemStatusUnknown) { + AttachLog(@"AVPlayer Unknown"); + status(MediaPlayerStatusUnknown); + } + if (_semaphore) { + dispatch_semaphore_signal(_semaphore); + } + } + else if (object == _avPlayerViewcontroller.player && [keyPath isEqualToString:@"rate"]) + { + if ([_avPlayerViewcontroller.player rate]) { + status(MediaPlayerStatusPlaying); // This changes the button to Pause + } + else { + status(MediaPlayerStatusPaused); // This changes the button to Play + } + } + else if (object == _avPlayerViewcontroller.contentOverlayView && [keyPath isEqualToString:@"bounds"]) + { + CGRect oldBounds = [change[NSKeyValueChangeOldKey] CGRectValue]; + CGRect newBounds = [change[NSKeyValueChangeNewKey] CGRectValue]; + BOOL wasFullscreen = CGRectEqualToRect(oldBounds, [UIScreen mainScreen].bounds); + BOOL isFullscreen = CGRectEqualToRect(newBounds, [UIScreen mainScreen].bounds); + if (isFullscreen && !wasFullscreen) + { + if (CGRectEqualToRect(oldBounds, CGRectMake(0, 0, newBounds.size.height, newBounds.size.width))) + { + [self logMessage:@"Video rotated fullscreen"]; + return; + } + else + { + [self logMessage:@"Video entered fullscreen"]; + } + fullScreenMode = YES; + } + else if (!isFullscreen && wasFullscreen) + { + [self logMessage:@"Video exited fullscreen"]; + fullScreenMode = NO; + } + + if (useControls && ((isFullscreen && !wasFullscreen) || (!isFullscreen && wasFullscreen))) { + // workaround to avoid a bug in one of the subviews constraints. + + CMTime currentTime = _avPlayerViewcontroller.player.currentTime; + [_avPlayerViewcontroller.player seekToTime:CMTimeMake(0, 1)]; + + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + updateFullScreen(isFullscreen); + [_avPlayerViewcontroller.player seekToTime:currentTime]; + [_avPlayerViewcontroller.player play]; + }); + } + } +} + +- (void)pinchAvPlayer:(UIPinchGestureRecognizer *)pinchGestureRecognizer { + UIGestureRecognizerState state = [pinchGestureRecognizer state]; + + if (state == UIGestureRecognizerStateEnded) + { + CGFloat scale = [pinchGestureRecognizer scale]; + [pinchGestureRecognizer setScale:1.0]; + if ((fullScreenMode && scale < 1.0) || (!fullScreenMode && scale > 1)) { + [self fullScreenVideo:! fullScreenMode]; + } + } +} + +void runOnMainQueueWithoutDeadlocking(void (^block)(void)) +{ + if ([NSThread isMainThread]) + { + block(); + } + else + { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +-(void)OrientationDidChange:(NSNotification*)notification +{ + [self logMessage:@"OrientationDidChange, resizing"]; + [self resizeRelocateVideo]; +} + +- (void) logMessage:(NSString *)format, ...; +{ + if (debugVideo) + { + va_list args; + va_start(args, format); + NSLogv([@"[Debug] " stringByAppendingString:format], args); + va_end(args); + } +} +@end \ No newline at end of file