From 76ccdda1a2e1b66b3d58344c4f6ed65692030a42 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 17 Sep 2025 20:00:35 +0530 Subject: [PATCH 01/17] feate: new arch --- CODE_OF_CONDUCT.md | 133 + CONTRIBUTING.md | 14 +- LICENSE | 221 +- MendixNative-Bridging-Header.h | 4 + MendixNative.podspec | 23 + README.md | 23 +- android/.gitignore | 15 - android/build.gradle | 154 +- android/fastlane/Fastfile | 29 - android/fastlane/README.md | 32 - android/gradle.properties | 33 +- android/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes android/gradlew | 185 - android/mendixnative/.gitignore | 1 - android/mendixnative/build.gradle | 85 - android/mendixnative/consumer-rules.pro | 0 android/mendixnative/proguard-rules.pro | 21 - .../mendixnative/ExampleInstrumentedTest.kt | 24 - .../mendixnative/src/main/AndroidManifest.xml | 4 - .../react/devsupport/DevInternalSettings.kt | 9 - .../devsupport/DevSupportManagerHelpers.kt | 17 - .../react/devsupport/MendixShakeDetector.kt | 23 - .../mendix/mendixnative/MendixInitializer.kt | 143 - .../mendixnative/MendixReactApplication.kt | 99 - .../activity/MendixReactActivity.kt | 112 - .../mendix/mendixnative/api/RuntimeInfo.kt | 52 - .../encryption/MendixEncryptedStorage.kt | 46 - .../MendixEncryptedStorageModule.kt | 54 - .../encryption/MendixEncryptionToolkit.kt | 115 - .../error/ErrorHandlerToRedBoxMapper.kt | 18 - .../mendix/mendixnative/error/ErrorType.kt | 17 - .../fragment/MendixReactFragment.kt | 125 - .../mendixnative/fragment/ReactFragment.kt | 178 - .../glide/MendixGlideEncryptedFileLoader.kt | 92 - .../handler/DevMenuTouchEventHandler.kt | 68 - .../mendix/mendixnative/react/ClearData.kt | 135 - .../com/mendix/mendixnative/react/CloseApp.kt | 8 - .../mendix/mendixnative/react/CopiedFrom.java | 13 - .../mendix/mendixnative/react/MendixApp.kt | 7 - .../mendixnative/react/MendixPackage.kt | 35 - .../mendixnative/react/MxConfiguration.java | 91 - .../react/NativeErrorHandler.java | 36 - .../mendixnative/react/NativeReloadHandler.kt | 102 - .../react/download/DownloadHelper.kt | 132 - .../react/download/NativeDownloadModule.kt | 93 - .../mendixnative/react/fs/FileBackend.kt | 271 - .../mendixnative/react/fs/NativeFsModule.java | 299 - .../mendixnative/react/menu/AppMenu.java | 5 - .../mendixnative/react/menu/DevAppMenu.kt | 100 - .../mendixnative/react/ota/NativeOtaModule.kt | 245 - .../react/ota/OTAJSBundleUrlProvider.kt | 34 - .../mendixnative/react/ota/OtaHelpers.kt | 42 - .../react/splash/MendixSplashScreenModule.kt | 19 - .../request/MendixNetworkInterceptor.kt | 121 - .../src/main/res/layout/app_menu_layout.xml | 94 - .../src/main/res/values/strings.xml | 15 - .../mendix/mendixnative/ExampleUnitTest.kt | 17 - android/settings.gradle | 19 - android/src/main/AndroidManifest.xml | 2 + .../react/devsupport/DevInternalSettings.kt | 10 + .../devsupport/DevSupportManagerHelpers.kt | 23 + .../react/devsupport/MendixShakeDetector.kt | 26 + .../mendixnative/JSBundleFileProvider.java | 2 +- .../mendixnative/MendixApplication.java | 15 +- .../mendix/mendixnative/MendixInitializer.kt | 145 + .../mendixnative/MendixReactApplication.kt | 106 + .../activity/MendixReactActivity.kt | 100 + .../mendix/mendixnative/api/RuntimeInfo.kt | 66 + .../mendixnative/config/AppPreferences.java | 12 +- .../mendix/mendixnative/config/AppUrl.java | 0 .../encryption/MendixEncryptedStorage.kt | 50 + .../MendixEncryptedStorageModule.kt | 39 + .../encryption/MendixEncryptionToolkit.kt | 114 + .../mendix/mendixnative/error/ErrorHandler.kt | 2 +- .../mendixnative/error/ErrorHandlerFactory.kt | 2 +- .../error/ErrorHandlerToRedBoxMapper.kt | 24 + .../mendix/mendixnative/error/ErrorType.kt | 17 + .../fragment/MendixReactFragment.kt | 122 + .../mendixnative/fragment/ReactFragment.kt | 184 + .../glide/MendixGlideEncryptedFileLoader.kt | 92 + .../mendixnative/glide/MendixGlideModule.kt | 16 +- .../handler/DevMenuTouchEventHandler.kt | 68 + .../handler/DummyErrorHandler.java | 8 +- .../mendix/mendixnative/react/ClearData.kt | 130 + .../com/mendix/mendixnative/react/CloseApp.kt | 8 + .../mendix/mendixnative/react/CopiedFrom.kt | 13 + .../mendix/mendixnative/react/MendixApp.kt | 10 + .../mendix/mendixnative/react/ModuleHelper.kt | 33 + .../mendixnative/react/MxConfiguration.kt | 95 + .../mendixnative/react/NativeErrorHandler.kt | 18 + .../mendixnative/react/NativeReloadHandler.kt | 78 + .../mendixnative/react/RunOnUiThread.kt | 0 .../react/ToggleElementInspector.kt | 3 +- .../react/cookie/NativeCookieModule.kt | 15 + .../react/download/DownloadHelper.kt | 132 + .../react/download/NativeDownloadModule.kt | 101 + .../mendixnative/react/fs/FileBackend.kt | 271 + .../mendixnative/react/fs/NativeFsModule.kt | 267 + .../react/fs/PathNotAccessibleException.kt | 7 + .../mendix/mendixnative/react/menu/AppMenu.kt | 5 + .../mendixnative/react/menu/DevAppMenu.kt | 113 + .../mendixnative/react/ota/NativeOtaModule.kt | 239 + .../react/ota/OTAJSBundleUrlProvider.kt | 34 + .../mendixnative/react/ota/OtaHelpers.kt | 42 + .../react/splash/MendixSplashScreenModule.kt | 20 + .../splash/MendixSplashScreenPresenter.kt | 4 +- .../request/MendixNetworkInterceptor.kt | 124 + .../mendixnative/util/CookieEncryption.kt | 26 + .../util/MendixBackwardsCompatUtility.kt | 0 .../util/MendixDoubleTapRecognizer.kt | 0 .../mendixnative/util/ReflectionUtils.java | 0 .../mendixnative/util/ResourceReader.java | 0 .../com/mendixnative/MendixNativeModule.kt | 174 + .../com/mendixnative/MendixNativePackage.kt | 39 + .../src/main/res/layout/app_menu_layout.xml | 94 + android/src/main/res/values/strings.xml | 17 + babel.config.js | 12 + eslint.config.mjs | 29 + example/.bundle/config | 2 + example/.watchmanconfig | 1 + example/Gemfile | 11 + example/Gemfile.lock | 124 + example/README.md | 99 + example/android/app/build.gradle | 119 + example/android/app/debug.keystore | Bin 0 -> 2257 bytes example/android/app/proguard-rules.pro | 10 + .../android/app/src/debug/AndroidManifest.xml | 9 + .../android/app/src/main/AndroidManifest.xml | 26 + .../java/mendixnative/example/MainActivity.kt | 22 + .../mendixnative/example/MainApplication.kt | 44 + .../res/drawable/rn_edit_text_material.xml | 37 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + example/android/build.gradle | 21 + example/android/gradle.properties | 44 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 8 +- example/android/gradlew | 252 + {android => example/android}/gradlew.bat | 37 +- example/android/settings.gradle | 6 + example/app.json | 4 + example/babel.config.js | 12 + example/index.js | 5 + example/ios/.xcode.env | 11 + .../ios/MendixNativeExample-Bridging-Header.h | 4 + .../project.pbxproj | 486 + .../xcschemes/MendixNativeExample.xcscheme | 52 +- .../contents.xcworkspacedata | 2 +- .../ios/MendixNativeExample/AppDelegate.swift | 29 + .../AppIcon.appiconset/Contents.json | 53 + .../Images.xcassets/Contents.json | 6 + example/ios/MendixNativeExample/Info.plist | 53 + .../LaunchScreen.storyboard | 47 + .../MendixNativeExample/PrivacyInfo.xcprivacy | 37 + example/ios/Podfile | 36 + example/ios/Podfile.lock | 1814 +++ example/jest.config.js | 3 + example/metro.config.js | 16 + example/package.json | 36 + example/react-native.config.js | 21 + example/src/App.tsx | 50 + ios/.xcode.env | 1 - ios/MendixNative.h | 5 + ios/MendixNative.mm | 90 + ios/MendixNative.xcodeproj/project.pbxproj | 811 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../AppPreferences/AppPreferences.h | 18 - .../AppPreferences/AppPreferences.m | 54 - ios/MendixNative/AppUrl/AppUrl.h | 16 - ios/MendixNative/AppUrl/AppUrl.m | 82 - ios/MendixNative/DevAppMenu/AppMenuProtocol.h | 10 - ios/MendixNative/DevAppMenu/DevAppMenu.h | 9 - ios/MendixNative/DevAppMenu/DevAppMenu.m | 113 - .../DevAppMenu/DevAppMenuUIAlertController.h | 7 - .../DevAppMenu/DevAppMenuUIAlertController.m | 4 - .../UIAlertActionExt/UIAlertActionExt.h | 11 - .../UIAlertActionExt/UIAlertActionExt.m | 14 - .../UIAlertControllerExt.h | 10 - .../UIAlertControllerExt.m | 14 - .../Encryption/MendixEncryptedStorageModule.h | 14 - .../Encryption/MendixEncryptedStorageModule.m | 143 - .../ErrorHandler/NativeErrorHandler.swift | 26 - .../ErrorHandler/NativeErrorHandlerBridge.m | 7 - .../JSBundleFileProviderProtocol.h | 9 - .../OtaJSBundleFileProvider.h | 10 - .../OtaJSBundleFileProvider.m | 56 - ios/MendixNative/MendixApp/MendixApp.h | 33 - ios/MendixNative/MendixApp/MendixApp.m | 52 - .../MendixBackwardsCompatUtility.h | 14 - .../MendixBackwardsCompatUtility.m | 36 - .../UnsupportedFeatures.h | 12 - .../UnsupportedFeatures.m | 18 - ios/MendixNative/MendixNative.h | 20 - .../MendixReactWindow/MendixReactWindow.h | 10 - .../MendixReactWindow/MendixReactWindow.m | 5 - .../MendixSplashScreen/MendixSplashScreen.m | 22 - .../SplashScreenPresenterProtocol.h | 10 - .../MxConfiguration/MxConfiguration.h | 26 - .../MxConfiguration/MxConfiguration.m | 79 - ios/MendixNative/MxConfigurationBridge.m | 4 - .../NativeDownloadHandler.h | 26 - .../NativeDownloadHandler.m | 121 - .../NativeErrorHandler/NativeErrorHandler.h | 9 - .../NativeErrorHandler/NativeErrorHandler.m | 29 - .../NativeFsModule/NativeFsModule.h | 19 - .../NativeFsModule/NativeFsModule.m | 415 - .../NativeOtaModule/NativeOtaModule.h | 14 - .../NativeOtaModule/NativeOtaModule.m | 244 - .../NativeOtaModule/OtaConstants.h | 35 - .../NativeOtaModule/OtaConstants.m | 32 - ios/MendixNative/NativeOtaModule/OtaHelpers.h | 13 - ios/MendixNative/NativeOtaModule/OtaHelpers.m | 52 - .../NativeReloadHandler/NativeReloadHandler.h | 12 - .../NativeReloadHandler/NativeReloadHandler.m | 36 - ios/MendixNative/ReactNative.h | 54 - ios/MendixNative/ReactNative.m | 338 - .../RuntimeInfoProvider/RuntimeInfo.h | 14 - .../RuntimeInfoProvider/RuntimeInfo.m | 16 - .../RuntimeInfoProvider/RuntimeInfoProvider.h | 10 - .../RuntimeInfoProvider/RuntimeInfoProvider.m | 60 - .../RuntimeInfoProvider/RuntimeInfoResponse.h | 13 - .../RuntimeInfoProvider/RuntimeInfoResponse.m | 14 - .../WarningFilter/WarningsFilter.h | 12 - .../WarningFilter/WarningsFilter.m | 7 - .../AppPreferences/AppPreferences.swift | 65 + ios/Modules/AppUrl/AppUrl.swift | 103 + ios/Modules/DevAppMenu/AppMenuProtocol.swift | 13 + ios/Modules/DevAppMenu/DevAppMenu.swift | 122 + .../DevAppMenuUIAlertController.swift | 14 + .../UIAlertActionExt/UIAlertActionExt.swift | 22 + .../UIAlertControllerExt.swift | 20 + ios/Modules/Encryption/EncryptedStorage.swift | 100 + .../ErrorHandler/NativeErrorHandler.swift | 26 + .../Extensions/RNCAsyncStorageExt.h | 0 .../JSBundleFileProviderProtocol.swift | 12 + .../OtaJSBundleFileProvider.swift | 68 + ios/Modules/MendixApp/MendixApp.swift | 119 + .../MendixBackwardsCompatUtility.swift | 65 + .../UnsupportedFeatures.swift | 25 + .../MendixReactWindow/MendixReactWindow.swift | 12 + .../MendixSplashScreen.swift | 20 + .../SplashScreenPresenterProtocol.swift | 14 + .../MxConfiguration/MxConfiguration.swift | 99 + .../NativeCookieModule.swift | 16 + .../NativeDownloadHandler.swift | 164 + .../NativeFsModule/NativeFsModule.swift | 378 + .../NativeOtaModule/NativeOtaModule.swift | 223 + .../NativeOtaModule/OtaConstants.swift | 34 + ios/Modules/NativeOtaModule/OtaHelpers.swift | 55 + .../NativeReloadHandler/ReloadHandler.swift | 24 + .../RCTRedBoxHelper/RCTRedBoxHelper.swift | 35 + ios/Modules/ReactNative.swift | 405 + .../RuntimeInfoProvider/RuntimeInfo.swift | 27 + .../RuntimeInfoProvider.swift | 89 + .../RuntimeInfoResponse.swift | 23 + .../WarningFilter/WarningsFilter.swift | 79 + ios/Podfile | 103 - ios/Podfile.lock | 726 - ios/fastlane/Fastfile | 60 - ios/fastlane/README.md | 32 - ios/fastlane/report.xml | 48 - lefthook.yml | 14 + mendix-native-0.0.1.tgz | Bin 0 -> 59071 bytes package-lock.json | 8130 ---------- package.json | 169 +- ...ix+react-native-sqlite-storage+5.1.3.patch | 21 - ...ative-community+async-storage+1.12.0.patch | 25 - patches/react-native-code-push+7.0.5.patch | 35 - .../react-native-gesture-handler+1.10.3.patch | 21 - src/cookie.ts | 5 + src/download-handler.ts | 5 + src/encrypted-storage.ts | 9 + src/error-handler.ts | 5 + src/events.ts | 3 + src/file-system.ts | 45 + src/index.ts | 10 + src/mx-configuration.ts | 3 + src/ota.ts | 6 + src/reload-handler.ts | 6 + src/specs/NativeMendixNative.ts | 128 + src/splash-screen.ts | 6 + tsconfig.build.json | 4 + tsconfig.json | 30 + turbo.json | 36 + yarn.lock | 12645 ++++++++++++++++ 295 files changed, 22972 insertions(+), 16468 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 MendixNative-Bridging-Header.h create mode 100644 MendixNative.podspec delete mode 100644 android/.gitignore delete mode 100644 android/fastlane/Fastfile delete mode 100644 android/fastlane/README.md delete mode 100644 android/gradle/wrapper/gradle-wrapper.jar delete mode 100755 android/gradlew delete mode 100644 android/mendixnative/.gitignore delete mode 100644 android/mendixnative/build.gradle delete mode 100644 android/mendixnative/consumer-rules.pro delete mode 100644 android/mendixnative/proguard-rules.pro delete mode 100644 android/mendixnative/src/androidTest/java/com/mendix/mendixnative/ExampleInstrumentedTest.kt delete mode 100644 android/mendixnative/src/main/AndroidManifest.xml delete mode 100644 android/mendixnative/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt delete mode 100644 android/mendixnative/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt delete mode 100644 android/mendixnative/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/MendixInitializer.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorage.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorageModule.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptionToolkit.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorHandlerToRedBoxMapper.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorType.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/glide/MendixGlideEncryptedFileLoader.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/handler/DevMenuTouchEventHandler.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/ClearData.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/CloseApp.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/CopiedFrom.java delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixApp.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixPackage.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/MxConfiguration.java delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.java delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/DownloadHelper.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/FileBackend.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.java delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.java delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OTAJSBundleUrlProvider.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/react/splash/MendixSplashScreenModule.kt delete mode 100644 android/mendixnative/src/main/java/com/mendix/mendixnative/request/MendixNetworkInterceptor.kt delete mode 100644 android/mendixnative/src/main/res/layout/app_menu_layout.xml delete mode 100644 android/mendixnative/src/main/res/values/strings.xml delete mode 100644 android/mendixnative/src/test/java/com/mendix/mendixnative/ExampleUnitTest.kt delete mode 100644 android/settings.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt create mode 100644 android/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt create mode 100644 android/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/JSBundleFileProvider.java (70%) rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/MendixApplication.java (56%) create mode 100644 android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/config/AppPreferences.java (93%) rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/config/AppUrl.java (100%) create mode 100644 android/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorage.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorageModule.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptionToolkit.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/error/ErrorHandler.kt (60%) rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/error/ErrorHandlerFactory.kt (62%) create mode 100644 android/src/main/java/com/mendix/mendixnative/error/ErrorHandlerToRedBoxMapper.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/error/ErrorType.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/glide/MendixGlideEncryptedFileLoader.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/glide/MendixGlideModule.kt (56%) create mode 100644 android/src/main/java/com/mendix/mendixnative/handler/DevMenuTouchEventHandler.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/handler/DummyErrorHandler.java (69%) create mode 100644 android/src/main/java/com/mendix/mendixnative/react/ClearData.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/CopiedFrom.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/MendixApp.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/ModuleHelper.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/react/RunOnUiThread.kt (100%) rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/react/ToggleElementInspector.kt (69%) create mode 100644 android/src/main/java/com/mendix/mendixnative/react/cookie/NativeCookieModule.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/download/DownloadHelper.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/fs/FileBackend.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/fs/PathNotAccessibleException.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/ota/OTAJSBundleUrlProvider.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/react/splash/MendixSplashScreenModule.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/react/splash/MendixSplashScreenPresenter.kt (63%) create mode 100644 android/src/main/java/com/mendix/mendixnative/request/MendixNetworkInterceptor.kt create mode 100644 android/src/main/java/com/mendix/mendixnative/util/CookieEncryption.kt rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/util/MendixBackwardsCompatUtility.kt (100%) rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/util/MendixDoubleTapRecognizer.kt (100%) rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java (100%) rename android/{mendixnative => }/src/main/java/com/mendix/mendixnative/util/ResourceReader.java (100%) create mode 100644 android/src/main/java/com/mendixnative/MendixNativeModule.kt create mode 100644 android/src/main/java/com/mendixnative/MendixNativePackage.kt create mode 100644 android/src/main/res/layout/app_menu_layout.xml create mode 100644 android/src/main/res/values/strings.xml create mode 100644 babel.config.js create mode 100644 eslint.config.mjs create mode 100644 example/.bundle/config create mode 100644 example/.watchmanconfig create mode 100644 example/Gemfile create mode 100644 example/Gemfile.lock create mode 100644 example/README.md create mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/debug.keystore create mode 100644 example/android/app/proguard-rules.pro create mode 100644 example/android/app/src/debug/AndroidManifest.xml create mode 100644 example/android/app/src/main/AndroidManifest.xml create mode 100644 example/android/app/src/main/java/mendixnative/example/MainActivity.kt create mode 100644 example/android/app/src/main/java/mendixnative/example/MainApplication.kt create mode 100644 example/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 example/android/app/src/main/res/values/strings.xml create mode 100644 example/android/app/src/main/res/values/styles.xml create mode 100644 example/android/build.gradle create mode 100644 example/android/gradle.properties create mode 100644 example/android/gradle/wrapper/gradle-wrapper.jar rename {android => example/android}/gradle/wrapper/gradle-wrapper.properties (81%) create mode 100755 example/android/gradlew rename {android => example/android}/gradlew.bat (81%) create mode 100644 example/android/settings.gradle create mode 100644 example/app.json create mode 100644 example/babel.config.js create mode 100644 example/index.js create mode 100644 example/ios/.xcode.env create mode 100644 example/ios/MendixNativeExample-Bridging-Header.h create mode 100644 example/ios/MendixNativeExample.xcodeproj/project.pbxproj rename ios/MendixNative.xcodeproj/xcshareddata/xcschemes/MendixNative.xcscheme => example/ios/MendixNativeExample.xcodeproj/xcshareddata/xcschemes/MendixNativeExample.xcscheme (50%) rename {ios/MendixNative.xcworkspace => example/ios/MendixNativeExample.xcworkspace}/contents.xcworkspacedata (76%) create mode 100644 example/ios/MendixNativeExample/AppDelegate.swift create mode 100644 example/ios/MendixNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/ios/MendixNativeExample/Images.xcassets/Contents.json create mode 100644 example/ios/MendixNativeExample/Info.plist create mode 100644 example/ios/MendixNativeExample/LaunchScreen.storyboard create mode 100644 example/ios/MendixNativeExample/PrivacyInfo.xcprivacy create mode 100644 example/ios/Podfile create mode 100644 example/ios/Podfile.lock create mode 100644 example/jest.config.js create mode 100644 example/metro.config.js create mode 100644 example/package.json create mode 100644 example/react-native.config.js create mode 100644 example/src/App.tsx delete mode 100644 ios/.xcode.env create mode 100644 ios/MendixNative.h create mode 100644 ios/MendixNative.mm delete mode 100644 ios/MendixNative.xcodeproj/project.pbxproj delete mode 100644 ios/MendixNative.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 ios/MendixNative/AppPreferences/AppPreferences.h delete mode 100644 ios/MendixNative/AppPreferences/AppPreferences.m delete mode 100644 ios/MendixNative/AppUrl/AppUrl.h delete mode 100644 ios/MendixNative/AppUrl/AppUrl.m delete mode 100644 ios/MendixNative/DevAppMenu/AppMenuProtocol.h delete mode 100644 ios/MendixNative/DevAppMenu/DevAppMenu.h delete mode 100644 ios/MendixNative/DevAppMenu/DevAppMenu.m delete mode 100644 ios/MendixNative/DevAppMenu/DevAppMenuUIAlertController.h delete mode 100644 ios/MendixNative/DevAppMenu/DevAppMenuUIAlertController.m delete mode 100644 ios/MendixNative/DevAppMenu/UIAlertActionExt/UIAlertActionExt.h delete mode 100644 ios/MendixNative/DevAppMenu/UIAlertActionExt/UIAlertActionExt.m delete mode 100644 ios/MendixNative/DevAppMenu/UIAlertControllerExt/UIAlertControllerExt.h delete mode 100644 ios/MendixNative/DevAppMenu/UIAlertControllerExt/UIAlertControllerExt.m delete mode 100644 ios/MendixNative/Encryption/MendixEncryptedStorageModule.h delete mode 100644 ios/MendixNative/Encryption/MendixEncryptedStorageModule.m delete mode 100644 ios/MendixNative/ErrorHandler/NativeErrorHandler.swift delete mode 100644 ios/MendixNative/ErrorHandler/NativeErrorHandlerBridge.m delete mode 100644 ios/MendixNative/JSBundleFileProvider/JSBundleFileProviderProtocol.h delete mode 100644 ios/MendixNative/JSBundleFileProvider/OtaJSBundleFileProvider.h delete mode 100644 ios/MendixNative/JSBundleFileProvider/OtaJSBundleFileProvider.m delete mode 100644 ios/MendixNative/MendixApp/MendixApp.h delete mode 100644 ios/MendixNative/MendixApp/MendixApp.m delete mode 100644 ios/MendixNative/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.h delete mode 100644 ios/MendixNative/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.m delete mode 100644 ios/MendixNative/MendixBackwardsCompatUtility/UnsupportedFeatures.h delete mode 100644 ios/MendixNative/MendixBackwardsCompatUtility/UnsupportedFeatures.m delete mode 100644 ios/MendixNative/MendixNative.h delete mode 100644 ios/MendixNative/MendixReactWindow/MendixReactWindow.h delete mode 100644 ios/MendixNative/MendixReactWindow/MendixReactWindow.m delete mode 100644 ios/MendixNative/MendixSplashScreen/MendixSplashScreen.m delete mode 100644 ios/MendixNative/MendixSplashScreen/SplashScreenPresenterProtocol.h delete mode 100644 ios/MendixNative/MxConfiguration/MxConfiguration.h delete mode 100644 ios/MendixNative/MxConfiguration/MxConfiguration.m delete mode 100644 ios/MendixNative/MxConfigurationBridge.m delete mode 100644 ios/MendixNative/NativeDownloadHandler/NativeDownloadHandler.h delete mode 100644 ios/MendixNative/NativeDownloadHandler/NativeDownloadHandler.m delete mode 100644 ios/MendixNative/NativeErrorHandler/NativeErrorHandler.h delete mode 100644 ios/MendixNative/NativeErrorHandler/NativeErrorHandler.m delete mode 100644 ios/MendixNative/NativeFsModule/NativeFsModule.h delete mode 100644 ios/MendixNative/NativeFsModule/NativeFsModule.m delete mode 100644 ios/MendixNative/NativeOtaModule/NativeOtaModule.h delete mode 100644 ios/MendixNative/NativeOtaModule/NativeOtaModule.m delete mode 100644 ios/MendixNative/NativeOtaModule/OtaConstants.h delete mode 100644 ios/MendixNative/NativeOtaModule/OtaConstants.m delete mode 100644 ios/MendixNative/NativeOtaModule/OtaHelpers.h delete mode 100644 ios/MendixNative/NativeOtaModule/OtaHelpers.m delete mode 100644 ios/MendixNative/NativeReloadHandler/NativeReloadHandler.h delete mode 100644 ios/MendixNative/NativeReloadHandler/NativeReloadHandler.m delete mode 100644 ios/MendixNative/ReactNative.h delete mode 100644 ios/MendixNative/ReactNative.m delete mode 100644 ios/MendixNative/RuntimeInfoProvider/RuntimeInfo.h delete mode 100644 ios/MendixNative/RuntimeInfoProvider/RuntimeInfo.m delete mode 100644 ios/MendixNative/RuntimeInfoProvider/RuntimeInfoProvider.h delete mode 100644 ios/MendixNative/RuntimeInfoProvider/RuntimeInfoProvider.m delete mode 100644 ios/MendixNative/RuntimeInfoProvider/RuntimeInfoResponse.h delete mode 100644 ios/MendixNative/RuntimeInfoProvider/RuntimeInfoResponse.m delete mode 100644 ios/MendixNative/WarningFilter/WarningsFilter.h delete mode 100644 ios/MendixNative/WarningFilter/WarningsFilter.m create mode 100644 ios/Modules/AppPreferences/AppPreferences.swift create mode 100644 ios/Modules/AppUrl/AppUrl.swift create mode 100644 ios/Modules/DevAppMenu/AppMenuProtocol.swift create mode 100644 ios/Modules/DevAppMenu/DevAppMenu.swift create mode 100644 ios/Modules/DevAppMenu/DevAppMenuUIAlertController.swift create mode 100644 ios/Modules/DevAppMenu/UIAlertActionExt/UIAlertActionExt.swift create mode 100644 ios/Modules/DevAppMenu/UIAlertControllerExt/UIAlertControllerExt.swift create mode 100644 ios/Modules/Encryption/EncryptedStorage.swift create mode 100644 ios/Modules/ErrorHandler/NativeErrorHandler.swift rename ios/{MendixNative => Modules}/Extensions/RNCAsyncStorageExt.h (100%) create mode 100644 ios/Modules/JSBundleFileProvider/JSBundleFileProviderProtocol.swift create mode 100644 ios/Modules/JSBundleFileProvider/OtaJSBundleFileProvider.swift create mode 100644 ios/Modules/MendixApp/MendixApp.swift create mode 100644 ios/Modules/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.swift create mode 100644 ios/Modules/MendixBackwardsCompatUtility/UnsupportedFeatures.swift create mode 100644 ios/Modules/MendixReactWindow/MendixReactWindow.swift create mode 100644 ios/Modules/MendixSplashScreen/MendixSplashScreen.swift create mode 100644 ios/Modules/MendixSplashScreen/SplashScreenPresenterProtocol.swift create mode 100644 ios/Modules/MxConfiguration/MxConfiguration.swift create mode 100644 ios/Modules/NativeCookieModule/NativeCookieModule.swift create mode 100644 ios/Modules/NativeDownloadHandler/NativeDownloadHandler.swift create mode 100644 ios/Modules/NativeFsModule/NativeFsModule.swift create mode 100644 ios/Modules/NativeOtaModule/NativeOtaModule.swift create mode 100644 ios/Modules/NativeOtaModule/OtaConstants.swift create mode 100644 ios/Modules/NativeOtaModule/OtaHelpers.swift create mode 100644 ios/Modules/NativeReloadHandler/ReloadHandler.swift create mode 100644 ios/Modules/RCTRedBoxHelper/RCTRedBoxHelper.swift create mode 100644 ios/Modules/ReactNative.swift create mode 100644 ios/Modules/RuntimeInfoProvider/RuntimeInfo.swift create mode 100644 ios/Modules/RuntimeInfoProvider/RuntimeInfoProvider.swift create mode 100644 ios/Modules/RuntimeInfoProvider/RuntimeInfoResponse.swift create mode 100644 ios/Modules/WarningFilter/WarningsFilter.swift delete mode 100644 ios/Podfile delete mode 100644 ios/Podfile.lock delete mode 100644 ios/fastlane/Fastfile delete mode 100644 ios/fastlane/README.md delete mode 100644 ios/fastlane/report.xml create mode 100644 lefthook.yml create mode 100644 mendix-native-0.0.1.tgz delete mode 100644 package-lock.json delete mode 100644 patches/@mendix+react-native-sqlite-storage+5.1.3.patch delete mode 100644 patches/@react-native-community+async-storage+1.12.0.patch delete mode 100644 patches/react-native-code-push+7.0.5.patch delete mode 100644 patches/react-native-gesture-handler+1.10.3.patch create mode 100644 src/cookie.ts create mode 100644 src/download-handler.ts create mode 100644 src/encrypted-storage.ts create mode 100644 src/error-handler.ts create mode 100644 src/events.ts create mode 100644 src/file-system.ts create mode 100644 src/index.ts create mode 100644 src/mx-configuration.ts create mode 100644 src/ota.ts create mode 100644 src/reload-handler.ts create mode 100644 src/specs/NativeMendixNative.ts create mode 100644 src/splash-screen.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 turbo.json create mode 100644 yarn.lock diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..45d257b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe93099..62fbd6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,13 +11,15 @@ This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/f - The library package in the root directory. - An example app in the `example/` directory. -To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: +To get started with the project, make sure you have the correct version of [Node.js](https://nodejs.org/) installed. See the [`.nvmrc`](./.nvmrc) file for the version used in this project. + +Run `yarn` in the root directory to install the required dependencies for each package: ```sh yarn ``` -> Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development. +> Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development without manually migrating. The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. @@ -47,6 +49,14 @@ To run the example app on iOS: yarn example ios ``` +To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this: + +```sh +Running "MendixNativeExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1} +``` + +Note the `"fabric":true` and `"concurrentRoot":true` properties. + Make sure your code passes TypeScript and ESLint. Run the following to verify: ```sh diff --git a/LICENSE b/LICENSE index dd0d3b1..a498d08 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,20 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 Mendix Technology BV - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2025 Yogendra Shelke +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MendixNative-Bridging-Header.h b/MendixNative-Bridging-Header.h new file mode 100644 index 0000000..1b2cb5d --- /dev/null +++ b/MendixNative-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/MendixNative.podspec b/MendixNative.podspec new file mode 100644 index 0000000..c4c4d92 --- /dev/null +++ b/MendixNative.podspec @@ -0,0 +1,23 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "MendixNative" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/mendix/mendix-native.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" + s.private_header_files = "ios/**/*.h" + s.public_header_files = "ios/**/*.h" + + s.dependency "SSZipArchive" + + install_modules_dependencies(s) +end diff --git a/README.md b/README.md index 245b57a..add783c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,37 @@ # mendix-native -mendix native library +Mendix native mobile package ## Installation + ```sh npm install mendix-native ``` + +## Usage + + +```js +import { multiply } from 'mendix-native'; + +// ... + +const result = multiply(3, 7); +``` + + ## Contributing -See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. +- [Development workflow](CONTRIBUTING.md#development-workflow) +- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) +- [Code of conduct](CODE_OF_CONDUCT.md) ## License MIT +--- + +Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index aa724b7..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/android/build.gradle b/android/build.gradle index b05850a..97a9c52 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,87 +1,95 @@ buildscript { - ext { - buildToolsVersion = "33.0.0" - minSdkVersion = 21 - compileSdkVersion = 33 - targetSdkVersion = 33 - ndkVersion = "23.1.7779620" - kotlin_version = "1.8.21" - - // needed by camera module - googlePlayServicesVersion = "17+" - supportLibVersion = "28.0.0" - lifecycleVersion = "2.0.0" - androidx_core_version = "1.6.0" - androidXBrowser = "1.2.0" - excludeAppGlideModule = true - androidx_lifecycle_version = "2.6.1" - constraint_layout_version = "2.0.4" - appcompat_version = "1.3.1" - excludeAppGlideModule = true - compose_ui_version = '1.2.0' - camerax_version = "1.3.0-alpha04" - - // Proxy repositories - bintray = "${System.getenv('GRADLE_BINTRAY_REPO') ?: project.findProperty('mendix.bintray')}" - jitpack = "${System.getenv('GRADLE_JITPACK_REPO') ?: project.findProperty('mendix.jitpack')}" - } + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['MendixNative_' + name] + } - repositories { - google() - mavenCentral() - maven { - url bintray - } - } + repositories { + google() + mavenCentral() + } - dependencies { - classpath "com.android.tools.build:gradle:7.2.2" - classpath "com.facebook.react:react-native-gradle-plugin" - classpath "com.google.gms:google-services:4.3.14" - classpath "de.undercouch:gradle-download-task:5.0.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } } -plugins { - id 'com.android.library' version '8.1.2' apply false - id 'org.jetbrains.kotlin.android' version '1.9.0' apply false + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply plugin: 'kotlin-kapt' +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["MendixNative_" + name]).toInteger() } -allprojects { - tasks.withType(JavaCompile) { - options.forkOptions.memoryMaximumSize = '512m' - } +android { + namespace "com.mendixnative" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + } - tasks.withType(GroovyCompile) { - groovyOptions.forkOptions.memoryMaximumSize = '512m' + buildFeatures { + dataBinding true + viewBinding true + buildConfig true + } + + buildTypes { + release { + minifyEnabled false } + } + + lintOptions { + disable "GradleCompatible" + } - repositories { - all { ArtifactRepository repo -> - if (repo.url.toString().contains("jcenter.bintray.com") || repo.url.toString().contains("jitpack.io")) { - remove repo - mavenCentral() - } - } - google() - maven { - url bintray - } - maven { - url jitpack - } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni" + ] } + } } -subprojects { - afterEvaluate { project -> - if(project.hasProperty('android')) { - project.android { - if (namespace == null) { - namespace project.group - } - } - } - } +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + + implementation 'com.fasterxml.jackson.core:jackson-core:2.11.3' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3' + + implementation 'androidx.security:security-crypto:1.1.0-alpha03' + + implementation "com.github.bumptech.glide:glide:4.12.0" + kapt "android.arch.lifecycle:compiler:1.1.1" + kapt 'com.github.bumptech.glide:compiler:4.12.0' + + + api "com.facebook.react:react-android:0.77.3" + api project(':op-engineering_op-sqlite') + api project(':react-native-async-storage_async-storage') + api project(':react-native-gesture-handler') } diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile deleted file mode 100644 index 72834bd..0000000 --- a/android/fastlane/Fastfile +++ /dev/null @@ -1,29 +0,0 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane - -default_platform(:android) - -platform :android do - before_all do - Dir.chdir("../..") do - sh("npm", "ci", "--legacy-peer-deps") - # Special hack to work-around alpine linux problem - File.getCanonicalPath is failing without a reason: - sh("find node_modules -name '*.gradle' -type f -exec sed -i.bak '/canonicalPath/d' {} +") - end - end - - desc "Build Mendix Native library" - lane :build_mendix_native do - gradle( - task: ":mendixnative:assembleRelease", - flags: "-x test", - ) - copy_artifacts( - target_path: "../artifacts", - artifacts: ["./mendixnative/build/outputs/aar/"], - ) - end -end diff --git a/android/fastlane/README.md b/android/fastlane/README.md deleted file mode 100644 index 3cb8143..0000000 --- a/android/fastlane/README.md +++ /dev/null @@ -1,32 +0,0 @@ -fastlane documentation ----- - -# Installation - -Make sure you have the latest version of the Xcode command line tools installed: - -```sh -xcode-select --install -``` - -For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) - -# Available Actions - -## Android - -### android build_mendix_native - -```sh -[bundle exec] fastlane android build_mendix_native -``` - -Build Mendix Native library - ----- - -This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. - -More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). - -The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/android/gradle.properties b/android/gradle.properties index 43653d3..50547f8 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,28 +1,5 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -android.useAndroidX=true -android.enableJetifier=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true -android.defaults.buildfeatures.buildconfig=true - -mendix.bintray=https://nexus.rnd.mendix.com/repository/bintray-proxy -mendix.jitpack=https://nexus.rnd.mendix.com/repository/jitpack-proxy +MendixNative_kotlinVersion=2.0.21 +MendixNative_minSdkVersion=24 +MendixNative_targetSdkVersion=34 +MendixNative_compileSdkVersion=35 +MendixNative_ndkVersion=27.1.12297006 diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index 4f906e0..0000000 --- a/android/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/android/mendixnative/.gitignore b/android/mendixnative/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/android/mendixnative/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/android/mendixnative/build.gradle b/android/mendixnative/build.gradle deleted file mode 100644 index 17393e4..0000000 --- a/android/mendixnative/build.gradle +++ /dev/null @@ -1,85 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -android { - namespace 'com.mendix.mendixnative' - compileSdk rootProject.compileSdkVersion - - ndkVersion rootProject.ndkVersion - - defaultConfig { - minSdk rootProject.minSdkVersion - targetSdkVersion rootProject.targetSdkVersion - - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - buildFeatures { - dataBinding true - viewBinding true - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } -} - -def jscFlavor = "org.webkit:android-jsc:+" - -dependencies { - implementation "androidx.core:core-ktx:$androidx_core_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_lifecycle_version" - implementation 'androidx.activity:activity-ktx:1.3.1' - implementation 'androidx.fragment:fragment-ktx:1.3.6' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation "com.fasterxml.jackson.core:jackson-core:2.11.3" - implementation "com.fasterxml.jackson.core:jackson-annotations:2.11.3" - implementation "com.fasterxml.jackson.core:jackson-databind:2.11.3" - - implementation 'androidx.security:security-crypto:1.1.0-alpha03' - - implementation "com.github.bumptech.glide:glide:4.12.0" - kapt "android.arch.lifecycle:compiler:1.1.1" - kapt 'com.github.bumptech.glide:compiler:4.12.0' - - api "com.android.support:appcompat-v7:$supportLibVersion" - api "com.google.android.gms:play-services-base:$googlePlayServicesVersion" - - //noinspection GradleDynamicVersion - api "com.facebook.react:react-android:0.72.7" - api "com.android.support.constraint:constraint-layout:$constraint_layout_version" - api project(':react-native-code-push') - api project(':mendix_react-native-sqlite-storage') - api project(':react-native-community_async-storage') - api project(':react-native-gesture-handler') - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.4' - testImplementation 'com.facebook.soloader:soloader:0.10.3' - testImplementation 'org.mockito:mockito-core:3.11.2' - testImplementation 'androidx.test:core:1.4.0' - testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - - implementation "com.facebook.react:react-android" - implementation jscFlavor - api project(':react-native-code-push') -} diff --git a/android/mendixnative/consumer-rules.pro b/android/mendixnative/consumer-rules.pro deleted file mode 100644 index e69de29..0000000 diff --git a/android/mendixnative/proguard-rules.pro b/android/mendixnative/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/android/mendixnative/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/mendixnative/src/androidTest/java/com/mendix/mendixnative/ExampleInstrumentedTest.kt b/android/mendixnative/src/androidTest/java/com/mendix/mendixnative/ExampleInstrumentedTest.kt deleted file mode 100644 index 317a760..0000000 --- a/android/mendixnative/src/androidTest/java/com/mendix/mendixnative/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.mendix.mendixnative - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.mendix.mendixnative.test", appContext.packageName) - } -} diff --git a/android/mendixnative/src/main/AndroidManifest.xml b/android/mendixnative/src/main/AndroidManifest.xml deleted file mode 100644 index 768d2af..0000000 --- a/android/mendixnative/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/android/mendixnative/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt b/android/mendixnative/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt deleted file mode 100644 index 4b3b8a6..0000000 --- a/android/mendixnative/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.facebook.react.devsupport - -import com.mendix.mendixnative.activity.MendixReactActivity -import com.mendix.mendixnative.util.ReflectionUtils - -fun getDevInternalSettings(activity: MendixReactActivity): DevInternalSettings? = - (activity.currentDevSupportManager as? DevSupportManagerBase)?.let { - return ReflectionUtils.getField(it, "mDevSettings") - } diff --git a/android/mendixnative/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt b/android/mendixnative/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt deleted file mode 100644 index 17cb345..0000000 --- a/android/mendixnative/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.facebook.react.devsupport - -import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener -import com.facebook.react.devsupport.interfaces.DevSupportManager -import com.mendix.mendixnative.util.ReflectionUtils - -fun setBundleDownloadListener(devSupportManager: DevSupportManager?, listener: DevBundleDownloadListener) { - devSupportManager?.apply { - ReflectionUtils.setFieldOfSuperclass(this, "mBundleDownloadListener", listener) - } -} - -fun overrideDevLoadingViewController(devSupportManager: DevSupportManager, devLoadingViewController: DefaultDevLoadingViewImplementation) { - devSupportManager.apply { - ReflectionUtils.setFieldOfSuperclass(this, "mDevLoadingViewManager", devLoadingViewController) - } -} diff --git a/android/mendixnative/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt b/android/mendixnative/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt deleted file mode 100644 index 4ddf405..0000000 --- a/android/mendixnative/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.facebook.react.devsupport - -import android.app.Activity -import android.content.Context -import android.hardware.SensorManager -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.common.ShakeDetector -import com.facebook.react.devsupport.interfaces.DevSupportManager -import com.mendix.mendixnative.util.ReflectionUtils - -const val SHAKE_DETECTECTOR_VAR = "mShakeDetector" - -fun makeShakeDetector(applicationContext: Context, onShake: () -> Unit): ShakeDetector { - val shakeDetector = ShakeDetector { onShake() } - shakeDetector.start(applicationContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager) - return shakeDetector -} - -fun attachMendixSupportManagerShakeDetector(shakeDetector: ShakeDetector, devSupportManager: DevSupportManager?): Unit = devSupportManager.let { supportManager -> - val devShakeDetector = ReflectionUtils.getFieldOfSuperclass(supportManager, SHAKE_DETECTECTOR_VAR) - (devShakeDetector != shakeDetector).let { devShakeDetector.stop() } - ReflectionUtils.setFieldOfSuperclass(supportManager, SHAKE_DETECTECTOR_VAR, shakeDetector) -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/MendixInitializer.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/MendixInitializer.kt deleted file mode 100644 index d6aca1e..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/MendixInitializer.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.mendix.mendixnative - -import android.app.Activity -import android.view.MotionEvent -import com.facebook.react.ReactInstanceEventListener -import com.facebook.react.ReactNativeHost -import com.facebook.react.bridge.ReactContext -import com.facebook.react.common.ShakeDetector -import com.facebook.react.config.ReactFeatureFlags -import com.facebook.react.devsupport.DevSupportManagerBase -import com.facebook.react.devsupport.attachMendixSupportManagerShakeDetector -import com.facebook.react.devsupport.makeShakeDetector -import com.facebook.react.modules.network.OkHttpClientProvider -import com.mendix.mendixnative.config.AppPreferences -import com.mendix.mendixnative.handler.DevMenuTouchEventHandler -import com.mendix.mendixnative.react.* -import com.mendix.mendixnative.request.MendixNetworkInterceptor - -class MendixInitializer( - private val context: Activity, - private val reactNativeHost: ReactNativeHost, - private val hasRNDeveloperSupport: Boolean = false, -) : ReactInstanceEventListener { - private var shakeDetector: ShakeDetector? = null - private var devMenuTouchEventHandler: DevMenuTouchEventHandler? = null - - fun onCreate( - mendixApp: MendixApp, - devAppMenuHandler: DevAppMenuHandler = object : DevAppMenuHandler { - override fun showDevAppMenu() {} - }, - clearData: Boolean, - ) { - // Assign mendix xas id interceptor to okhttp - OkHttpClientProvider.setOkHttpClientFactory { - OkHttpClientProvider.createClientBuilder() - .addNetworkInterceptor(MendixNetworkInterceptor()) - .build() - } - - val runtimeUrl = mendixApp.runtimeUrl - MxConfiguration.runtimeUrl = runtimeUrl - MxConfiguration.warningsFilter = mendixApp.warningsFilter - - // We disable in purpose the new turbo modules - ReactFeatureFlags.useTurboModules = false; - - // This is here to make sure that a clean host instance is initialised. - restartReactInstanceManager() - if (clearData) clearData(context.application) - if (hasRNDeveloperSupport) setupDeveloperApp(runtimeUrl, mendixApp) - if (mendixApp.attachCustomDeveloperMenu) attachCustomDeveloperMenu(devAppMenuHandler) - } - - private fun restartReactInstanceManager() { - if (reactNativeHost.hasInstance()) reactNativeHost.clear() - // Pre-initialize reactInstanceManager to be available for other methods - if(reactNativeHost.hasInstance()) reactNativeHost.reactInstanceManager - } - - private fun attachCustomDeveloperMenu(devAppMenuHandler: DevAppMenuHandler) { - devMenuTouchEventHandler = - DevMenuTouchEventHandler(object : DevMenuTouchEventHandler.DevMenuTouchListener { - override fun onTap() { - reactNativeHost.reactInstanceManager.currentReactContext?.getNativeModule( - NativeReloadHandler::class.java - )?.reloadClientWithState() - } - - override fun onLongPress() { - devAppMenuHandler.showDevAppMenu() - } - }) - - attachShakeDetector(devAppMenuHandler) - } - - fun onDestroy() { - // Stop shaking as early as possible to avoid orphaned dialogs - stopShakeDetector() - - if (hasRNDeveloperSupport) { - AppPreferences(context.applicationContext).setElementInspector(false) - reactNativeHost.reactInstanceManager.removeReactInstanceEventListener(this) - } - - // We need to clear the host to allow for reinitialization of the Native Modules - // Especially for when switching between apps - reactNativeHost.clear() - - // We need to close all databases separately to avoid hitting a read only state exception - // Databases need to close after we are done closing the react native host to avoid db locks - closeSqlDatabaseConnection(reactNativeHost.reactInstanceManager.currentReactContext) - } - - fun stopShakeDetector() { - shakeDetector?.stop() - } - - override fun onReactContextInitialized(context: ReactContext?) { - val preferences = AppPreferences(context) - if (preferences.isElementInspectorEnabled) { - toggleElementInspector(context) - } - } - - fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - return devMenuTouchEventHandler?.handle(ev) ?: false - } - - private fun attachShakeDetector(devAppMenuHandler: DevAppMenuHandler) { - if (shakeDetector == null) { - shakeDetector = makeShakeDetector(context.applicationContext) { - devAppMenuHandler.showDevAppMenu() - } - } - - (reactNativeHost.reactInstanceManager.devSupportManager as? DevSupportManagerBase)?.run { - attachMendixSupportManagerShakeDetector(shakeDetector!!, this) - } - } - - private fun setupDeveloperApp( - runtimeUrl: String, - mendixApp: MendixApp - ) { - val preferences = AppPreferences(context.applicationContext) - preferences.updatePackagerHost(runtimeUrl) - preferences.setRemoteDebugging(false) - preferences.setDeltas(false) - preferences.setDevMode((mendixApp.showExtendedDevMenu)) - - clearCachedReactNativeDevBundle(context.application) - val reactInstanceManager = reactNativeHost.reactInstanceManager - reactInstanceManager.addReactInstanceEventListener(this) - } - - -} - -interface DevAppMenuHandler { - fun showDevAppMenu() -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt deleted file mode 100644 index 2efee46..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.mendix.mendixnative - -import android.app.Application -import com.facebook.react.ReactNativeHost -import com.facebook.react.ReactPackage -import com.facebook.react.devsupport.interfaces.RedBoxHandler -import com.facebook.soloader.SoLoader -import com.mendix.mendixnative.error.ErrorHandler -import com.mendix.mendixnative.error.ErrorHandlerFactory -import com.mendix.mendixnative.error.mapErrorHandlerToRedBox -import com.mendix.mendixnative.handler.DummyErrorHandler -import com.mendix.mendixnative.react.MendixPackage -import com.mendix.mendixnative.react.ota.OtaJSBundleUrlProvider -import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter -import com.mendix.mendixnative.util.ResourceReader -import com.microsoft.codepush.react.CodePush -import java.util.* - -abstract class MendixReactApplication : Application(), MendixApplication, ErrorHandlerFactory { - private val appSessionId = "" + Math.random() * 1000 + Date().time - override fun getAppSessionId(): String = appSessionId - - private var codePushKey: String? = null - private var redBoxHandler = mapErrorHandlerToRedBox(createErrorHandler()) - private var splashScreenPresenter = createSplashScreenPresenter() - private var jsBundleFileProvider: JSBundleFileProvider? = jsBundleProvider - private var reactNativeHost: ReactNativeHost = object : ReactNativeHost(this) { - override fun getUseDeveloperSupport(): Boolean { - return this@MendixReactApplication.useDeveloperSupport - } - - override fun getPackages(): List { - val packages: MutableList = ArrayList() - packages.add(MendixPackage(splashScreenPresenter)) - packages.addAll(this@MendixReactApplication.packages) - return packages - } - - override fun getJSBundleFile(): String? { - return this@MendixReactApplication.jsBundleFile - } - - override fun getJSMainModuleName(): String { - return "index" - } - - override fun getBundleAssetName(): String? { - return super.getBundleAssetName() - } - - override fun getRedBoxHandler(): RedBoxHandler? { - return this@MendixReactApplication.redBoxHandler - } - } - - override fun onCreate() { - super.onCreate() - SoLoader.init(this, /* native exopackage */false) - codePushKey = ResourceReader.readString(this, "code_push_key") - } - - override fun getCodePushKey(): String { - return codePushKey!! - } - - override fun getJSBundleFile(): String? { - // Check for Native OTA - OtaJSBundleUrlProvider().getJSBundleFile(this)?.let { - return it - } - - // Check for CodePush - if (useCodePush()) return CodePush.getJSBundleFile() - - // Fallback to bundled bundle - return if (jsBundleFileProvider != null) jsBundleFileProvider!!.getJSBundleFile(this) else null - } - - private fun useCodePush(): Boolean { - return codePushKey!!.isNotEmpty() - } - - abstract override fun getUseDeveloperSupport(): Boolean - abstract override fun getPackages(): List - override fun createSplashScreenPresenter(): MendixSplashScreenPresenter? { - return null - } - - override fun createErrorHandler(): ErrorHandler { - return DummyErrorHandler() - } - - override fun getReactNativeHost(): ReactNativeHost { - return reactNativeHost - } - - open val jsBundleProvider: JSBundleFileProvider? - get() = null -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt deleted file mode 100644 index ad6a105..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.mendix.mendixnative.activity - -import android.os.Bundle -import android.view.KeyEvent -import android.view.MotionEvent -import com.facebook.react.ReactActivity -import com.facebook.react.ReactActivityDelegate -import com.facebook.react.ReactRootView -import com.facebook.react.bridge.ReactContext -import com.facebook.react.devsupport.interfaces.DevSupportManager -import com.mendix.mendixnative.DevAppMenuHandler -import com.mendix.mendixnative.MendixApplication -import com.mendix.mendixnative.MendixInitializer -import com.mendix.mendixnative.react.MendixApp -import com.mendix.mendixnative.react.NativeReloadHandler -import com.mendix.mendixnative.react.menu.DevAppMenu -import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter -import com.mendix.mendixnative.util.MendixBackwardsCompatUtility -import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView - -open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScreenHandler { - - @JvmField - protected var mendixApp: MendixApp? = null - - private lateinit var mendixInitializer: MendixInitializer - private var splashScreenPresenter: MendixSplashScreenPresenter? = - (application as? MendixApplication)?.createSplashScreenPresenter() - - override fun onCreate(savedInstanceState: Bundle?) { - mendixApp = mendixApp - ?: intent.getSerializableExtra(MENDIX_APP_INTENT_KEY) as? MendixApp - ?: throw IllegalStateException("MendixApp configuration can't be null") - val mendixApplication = application as? MendixApplication - ?: throw ClassCastException("Application needs to implement MendixApplication") - - mendixInitializer = - MendixInitializer(this, reactNativeHost, mendixApplication.useDeveloperSupport) - mendixInitializer.onCreate(mendixApp!!, this, intent.getBooleanExtra(CLEAR_DATA, false)) - - super.onCreate(savedInstanceState) - } - - override fun onDestroy() { - mendixInitializer.onDestroy() - super.onDestroy() - } - - override fun dispatchTouchEvent(ev: MotionEvent): Boolean { - return if (mendixInitializer.dispatchTouchEvent(ev)) { - true - } else super.dispatchTouchEvent(ev) - } - - override fun getMainComponentName(): String? { - return MAIN_COMPONENT_NAME - } - - override fun showDevAppMenu() { - DevAppMenu(this, mendixApp?.showExtendedDevMenu ?: false, { - currentReactContext?.getNativeModule(NativeReloadHandler::class.java)?.reload() - }, { - this.finish() - }).show() - } - - private val currentReactContext: ReactContext? - get() = if (reactNativeHost.hasInstance()) reactInstanceManager.currentReactContext else null - - val currentDevSupportManager: DevSupportManager? - get() = if (reactNativeHost.hasInstance()) reactNativeHost.reactInstanceManager.devSupportManager else null - - override fun createReactActivityDelegate(): ReactActivityDelegate { - return object : ReactActivityDelegate(this, mainComponentName) { - override fun createRootView(): ReactRootView { - return RNGestureHandlerEnabledRootView(this@MendixReactActivity) - } - - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_MENU) { - showDevAppMenu() - return true - } - return super.onKeyUp(keyCode, event) - } - } - } - - override fun showLaunchScreen() { - if (!MendixBackwardsCompatUtility.getInstance().unsupportedFeatures.hideSplashScreenInClient && splashScreenPresenter != null) { - splashScreenPresenter?.show(this) - } - } - - override fun hideLaunchScreen() { - if (splashScreenPresenter != null) { - splashScreenPresenter?.hide(this) - } - } - - companion object { - const val MAIN_COMPONENT_NAME = "App" - const val MENDIX_APP_INTENT_KEY = "mendixAppIntentKey" - const val CLEAR_DATA = "clearData" - } -} - -interface LaunchScreenHandler { - fun showLaunchScreen() - fun hideLaunchScreen() -} - diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt deleted file mode 100644 index ec6e8de..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.mendix.mendixnative.api - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.databind.ObjectMapper -import com.mendix.mendixnative.config.AppUrl -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import java.io.IOException -import java.util.concurrent.TimeUnit - -val client: OkHttpClient = OkHttpClient.Builder().connectTimeout(3, TimeUnit.SECONDS).callTimeout(10, TimeUnit.SECONDS).build() - -enum class ResponseStatus { - INACCESSIBLE, - SUCCEEDED, - FAILED -} - -fun getRuntimeInfo(runtimeUrl: String, cb: (info: RuntimeInfoResponse) -> Unit) { - client.newCall(Request.Builder() - .post(RequestBody.create("application/json; charset=utf-8".toMediaTypeOrNull(), "{\"action\":\"info\"}")) - .url(AppUrl.removeTrailingSlash(AppUrl.ensureProtocol(runtimeUrl)) + "/xas/") - .build()) - .enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - cb(RuntimeInfoResponse(null, ResponseStatus.INACCESSIBLE)) - } - - override fun onResponse(call: Call, response: Response) { - val body = response.body?.string() - if (!response.isSuccessful || body == null) { - cb(RuntimeInfoResponse(null, ResponseStatus.FAILED)) - return - } - try { - cb(RuntimeInfoResponse(ObjectMapper().readValue(body, RuntimeInfo::class.java), ResponseStatus.SUCCEEDED)) - } catch (e: Exception) { - cb(RuntimeInfoResponse(null, ResponseStatus.FAILED)) - } - } - }) -} - -class RuntimeInfoResponse(val data: RuntimeInfo?, val responseStatus: ResponseStatus) - -@JsonIgnoreProperties(ignoreUnknown = true) -class RuntimeInfo { - var cachebust: String = "" - var version: String = "" - var packagerPort: Int? = null - var nativeBinaryVersion: Int? = -1 -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorage.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorage.kt deleted file mode 100644 index b644318..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorage.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.mendix.mendixnative.encryption - -import android.content.Context -import android.content.SharedPreferences -import android.util.Log - -class MendixEncryptedStorage private constructor(context: Context) { - var isEncrypted: Boolean private set - private var store: SharedPreferences - - init { - try { - store = getEncryptedSharedPreferences(context, - getMasterKey(context), - STORE_NAME) - isEncrypted = true - } catch (e: Exception) { - // On Android 5.0 (API level 21) and Android 5.1 (API level 22), you cannot use the Android keystore to store keysets. - Log.e(MendixEncryptedStorage::class.simpleName, - "Using unencrypted storage due to exception", - e) - store = context.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE) - isEncrypted = false - } - } - - fun setItem( - key: String, - value: - String, - ): Boolean = store.edit().putString(key, value).commit() - - fun getItem(key: String): String? = store.getString(key, null) - - fun removeItem(key: String): Boolean = store.edit().remove(key).commit() - - fun clear(): Boolean = store.edit().clear().commit() - - companion object { - private var instance: MendixEncryptedStorage? = null - fun getMendixEncryptedStorage(context: Context): MendixEncryptedStorage { - if (instance == null) instance = MendixEncryptedStorage(context) - return instance!! - } - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorageModule.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorageModule.kt deleted file mode 100644 index a176e92..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptedStorageModule.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.mendix.mendixnative.encryption - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.module.annotations.ReactModule -import com.mendix.mendixnative.encryption.MendixEncryptedStorage.Companion.getMendixEncryptedStorage - -const val MODULE_NAME = "RNMendixEncryptedStorage" -const val STORE_NAME = "MENDIX_ENCRYPTED_STORAGE" - -@ReactModule(name = MODULE_NAME) -class MendixEncryptedStorageModule(reactApplicationContext: ReactApplicationContext) : - ReactContextBaseJavaModule(reactApplicationContext) { - override fun getName(): String = MODULE_NAME - private val storage = getMendixEncryptedStorage(reactApplicationContext) - - @ReactMethod - fun setItem(key: String, value: String, promise: Promise): Unit = - storage.setItem(key, value).let { - when (it) { - true -> promise.resolve(null) - false -> promise.reject(Exception("Failed to set item in encrypted store.")) - } - } - - @ReactMethod - fun getItem(key: String, promise: Promise): Unit = - storage.getItem(key).let { promise.resolve(it) } - - @ReactMethod - fun removeItem(key: String, promise: Promise): Unit = - storage.removeItem(key).let { - when (it) { - true -> promise.resolve(null) - false -> promise.reject(Exception("Failed to remove item $key from encrypted store.")) - } - } - - @ReactMethod - fun clear(promise: Promise): Unit = storage.clear().let { - when (it) { - true -> promise.resolve(null) - false -> promise.reject(Exception("Failed to clear encrypted store.")) - } - } - - override fun getConstants(): MutableMap { - return mutableMapOf( - "IS_ENCRYPTED" to storage.isEncrypted - ) - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptionToolkit.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptionToolkit.kt deleted file mode 100644 index 67105de..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptionToolkit.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.mendix.mendixnative.encryption - -import android.annotation.SuppressLint -import android.content.Context -import android.content.SharedPreferences -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import android.util.Base64 -import android.util.Base64.DEFAULT -import androidx.annotation.RequiresApi -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import java.io.IOException -import java.security.GeneralSecurityException -import java.security.Key -import java.security.KeyStore -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.spec.IvParameterSpec - -private const val STORE_AES_KEY = "AES_KEY" -private const val encryptionTransformationName = "AES/CBC/PKCS7Padding" - -private var masterKey: MasterKey? = null -fun getMasterKey(context: Context): MasterKey { - if (masterKey == null) { - masterKey = MasterKey.Builder(context) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() - } - return masterKey!! -} - -@Throws(GeneralSecurityException::class, IOException::class) -fun getEncryptedSharedPreferences( - context: Context, - key: MasterKey, - prefName: String, -): SharedPreferences { - return EncryptedSharedPreferences.create( - context, - prefName, - key, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) -} - -/** - * generates or returns an application wide AES key. - * - * @return Key - */ -@RequiresApi(Build.VERSION_CODES.M) -private fun getAESKey(): Key? { - val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } - if (!keyStore.containsAlias(STORE_AES_KEY)) { - val keyGenerator = - KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") - keyGenerator.init(KeyGenParameterSpec.Builder(STORE_AES_KEY, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_CBC) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7).build()) - keyGenerator.generateKey() - } - return keyStore.getKey(STORE_AES_KEY, null) -} - -/** - * Following best practices from https://developer.android.com/guide/topics/security/cryptography#encrypt-message to encrypt a value. - * >= API.M and higher AES encryption is used with Base64 encoding to preserve bytes - * < API.M values are Base64 encoded - * - * @param value, the value to encrypt - * @return Triple of Base64 encoded value, Based64 encoded iv, boolean value reflecting if value was encrypted - */ -fun encryptValue( - value: String, - @SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKey() }, -): Triple { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val cipher = Cipher.getInstance(encryptionTransformationName) - cipher.init(Cipher.ENCRYPT_MODE, getPassword()) - val encryptedValue = cipher.doFinal(value.encodeToByteArray()) - return Triple(Base64.encode(encryptedValue, DEFAULT), - Base64.encode(cipher.iv, DEFAULT), - true) - } - return Triple(Base64.encode(value.encodeToByteArray(), DEFAULT), null, false) -} - -/** - * Decrypts a base64 encoded and possibly AES encrypted value using the provided initialization value - * Encryption is only available for >= API M. - * - * @param value, Base64 encoded string - * @param iv, Base64 encoded value of the IV used when encrypting the value - * @return unencrypted value - */ -fun decryptValue( - value: String, - iv: String?, - @SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKey() }, -): String { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val cipher = Cipher.getInstance(encryptionTransformationName) - cipher.init(Cipher.DECRYPT_MODE, - getPassword(), - IvParameterSpec(Base64.decode(iv, DEFAULT))) - val unencryptedValue = cipher.doFinal(Base64.decode(value, DEFAULT)) - return String(unencryptedValue, Charsets.UTF_8) - } - return Base64.decode(value, DEFAULT).decodeToString() -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorHandlerToRedBoxMapper.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorHandlerToRedBoxMapper.kt deleted file mode 100644 index ceee4a2..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorHandlerToRedBoxMapper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mendix.mendixnative.error - -import android.content.Context -import com.facebook.react.devsupport.interfaces.StackFrame -import com.facebook.react.devsupport.interfaces.ErrorType -import com.facebook.react.devsupport.interfaces.RedBoxHandler -import com.mendix.mendixnative.error.ErrorType.Companion.fromReactErrorType - - -fun mapErrorHandlerToRedBox(errorHandler: ErrorHandler) = object : RedBoxHandler { - override fun handleRedbox(title: String?, stack: Array?, errorType: ErrorType?) = errorHandler.handleError(title, stack, fromReactErrorType(errorType)) - - override fun isReportEnabled(): Boolean = false - - override fun reportRedbox(context: Context?, title: String?, stack: Array?, sourceUrl: String?, reportCompletedListener: RedBoxHandler.ReportCompletedListener?) { - // Not supported - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorType.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorType.kt deleted file mode 100644 index 4ba72c5..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/error/ErrorType.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.mendix.mendixnative.error - -import com.facebook.react.devsupport.interfaces.ErrorType - -enum class ErrorType { - JS, - NATIVE, - UNDEFINED; - - companion object { - fun fromReactErrorType(errorType: ErrorType?) = when (errorType) { - ErrorType.JS -> JS - ErrorType.NATIVE -> NATIVE - else -> UNDEFINED - } - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt deleted file mode 100644 index 210c81b..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.mendix.mendixnative.fragment - -import android.content.Intent -import android.os.Bundle -import android.view.KeyEvent -import android.view.MotionEvent -import com.mendix.mendixnative.DevAppMenuHandler -import com.mendix.mendixnative.MendixApplication -import com.mendix.mendixnative.MendixInitializer -import com.mendix.mendixnative.activity.LaunchScreenHandler -import com.mendix.mendixnative.react.MendixApp -import com.mendix.mendixnative.react.NativeReloadHandler -import com.mendix.mendixnative.react.menu.DevAppMenu -import com.mendix.mendixnative.util.MendixDoubleTapRecognizer - -/** - * Class used for Sample apps - */ -open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { - - protected var mendixApp: MendixApp? = null - private lateinit var mendixInitializer: MendixInitializer - private var doubleTapReloadRecognizer = MendixDoubleTapRecognizer() - - companion object { - const val ARG_MENDIX_APP = "arg_mendix_app" - const val ARG_CLEAR_DATA = "arg_clear_data" - const val ARG_USE_DEVELOPER_SUPPORT = "arg_use_developer_support" - const val ARG_COMPONENT_NAME = "arg_component_name" - const val ARG_LAUNCH_OPTIONS = "arg_launch_options" - - fun newInstance( - componentName: String, - launchOptions: Bundle?, - mendixApp: MendixApp, - clearData: Boolean, - useDeveloperSupport: Boolean - ): MendixReactFragment { - val mendixReactFragment = MendixReactFragment() - val args = Bundle() - args.putString(ARG_COMPONENT_NAME, componentName) - args.putBundle(ARG_LAUNCH_OPTIONS, launchOptions) - args.putBoolean(ARG_CLEAR_DATA, clearData) - args.putBoolean(ARG_USE_DEVELOPER_SUPPORT, useDeveloperSupport) - args.putSerializable(ARG_MENDIX_APP, mendixApp) - mendixReactFragment.arguments = args - return mendixReactFragment - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - (activity !is LaunchScreenHandler).let { - if (it) throw java.lang.IllegalArgumentException("The Activity needs to implement LaunchScreenHandler") - } - - if (mendixApp == null) { - mendixApp = requireArguments().getSerializable(ARG_MENDIX_APP) as MendixApp? - ?: throw IllegalArgumentException("Mendix app is required") - } - - val clearData = requireArguments().getBoolean(ARG_CLEAR_DATA, false) - val hasRNDeveloperSupport = requireArguments().getBoolean(ARG_USE_DEVELOPER_SUPPORT, false) - - mendixInitializer = - MendixInitializer(requireActivity(), reactNativeHost, hasRNDeveloperSupport).also { - it.onCreate(mendixApp!!, this, clearData) - } - - super.onCreate(savedInstanceState) - } - - fun onNewIntent(intent: Intent) { - if (reactNativeHost.hasInstance()) { - reactNativeHost.reactInstanceManager.onNewIntent(intent); - } - } - - override fun onDestroy() { - mendixInitializer.onDestroy() - super.onDestroy() - } - - override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - if (keyCode == KeyEvent.KEYCODE_MENU || doubleTapReloadRecognizer.didDoubleTapBacktick( - keyCode, - view - ) - ) { - showDevAppMenu() - return true - } - return super.onKeyUp(keyCode, event) - } - - override fun showDevAppMenu() { - activity?.let { - DevAppMenu(it, mendixApp!!.showExtendedDevMenu, { - (it.application as MendixApplication).reactNativeHost.reactInstanceManager.currentReactContext?.getNativeModule( - NativeReloadHandler::class.java - )?.reload() - }, { if (!this.isDetached) this.onCloseProjectSelected() }).show() - } - } - - open fun onCloseProjectSelected() { - // Closing shake detection to avoid dialog from triggering while closing - mendixInitializer.stopShakeDetector(); - } - - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - return mendixInitializer.dispatchTouchEvent(ev) - } -} - -interface MendixReactFragmentView : DevAppMenuHandler, TouchEventDispatcher, BackButtonHandler { - fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean -} - -interface TouchEventDispatcher { - fun dispatchTouchEvent(ev: MotionEvent?): Boolean -} - -interface BackButtonHandler { - fun onBackPressed(): Boolean -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt deleted file mode 100644 index fa622b1..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.mendix.mendixnative.fragment - -import android.annotation.TargetApi -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.facebook.react.ReactApplication -import com.facebook.react.ReactDelegate -import com.facebook.react.ReactNativeHost -import com.facebook.react.modules.core.PermissionAwareActivity -import com.facebook.react.modules.core.PermissionListener -import com.mendix.mendixnative.react.CopiedFrom - - -/** - * Fragment for creating a React View. This allows the developer to "embed" a React Application - * inside native components such as a Drawer, ViewPager, etc. - */ -@CopiedFrom(com.facebook.react.ReactFragment::class) -open class ReactFragment : Fragment(), PermissionAwareActivity { - private var mReactDelegate: ReactDelegate? = null - private var mPermissionListener: PermissionListener? = null - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - var mainComponentName: String? = null - var launchOptions: Bundle? = null - if (arguments != null) { - mainComponentName = requireArguments().getString(ARG_COMPONENT_NAME) - launchOptions = requireArguments().getBundle(ARG_LAUNCH_OPTIONS) - } - checkNotNull(mainComponentName) { "Cannot loadApp if component name is null" } - mReactDelegate = ReactDelegate(activity, reactNativeHost, mainComponentName, launchOptions) - } - - /** - * Get the [ReactNativeHost] used by this app. By default, assumes [ ][Activity.getApplication] is an instance of [ReactApplication] and calls [ ][ReactApplication.getReactNativeHost]. Override this method if your application class does not - * implement `ReactApplication` or you simply have a different mechanism for storing a - * `ReactNativeHost`, e.g. as a static field somewhere. - */ - protected val reactNativeHost: ReactNativeHost - protected get() = (requireActivity().application as ReactApplication).reactNativeHost - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - mReactDelegate!!.loadApp() - // Adds tapjacking protection to the rootview - mReactDelegate!!.reactRootView.filterTouchesWhenObscured = true - return mReactDelegate!!.reactRootView - } - - override fun onResume() { - super.onResume() - mReactDelegate!!.onHostResume() - } - - override fun onPause() { - super.onPause() - mReactDelegate!!.onHostPause() - } - - override fun onDestroy() { - super.onDestroy() - mReactDelegate!!.onHostDestroy() - } - - // endregion - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - mReactDelegate!!.onActivityResult(requestCode, resultCode, data, true) - } - - /** - * Helper to forward hardware back presses to our React Native Host - * - * - * This must be called via a forward from your host Activity - */ - fun onBackPressed(): Boolean { - return mReactDelegate!!.onBackPressed() - } - - /** - * Helper to forward onKeyUp commands from our host Activity. This allows ReactFragment to handle - * double tap reloads and dev menus - * - * - * This must be called via a forward from your host Activity - * - * @param keyCode keyCode - * @param event event - * @return true if we handled onKeyUp - */ - open fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - return mReactDelegate!!.shouldShowDevMenuOrReload(keyCode, event) - } - - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (mPermissionListener != null - && mPermissionListener!!.onRequestPermissionsResult(requestCode, permissions, grantResults)) { - mPermissionListener = null - } - } - - override fun checkPermission(permission: String, pid: Int, uid: Int): Int { - return requireActivity().checkPermission(permission, pid, uid) - } - - @TargetApi(Build.VERSION_CODES.M) - override fun checkSelfPermission(permission: String): Int { - return requireActivity().checkSelfPermission(permission) - } - - @TargetApi(Build.VERSION_CODES.M) - override fun requestPermissions( - permissions: Array, requestCode: Int, listener: PermissionListener?) { - mPermissionListener = listener - requestPermissions(permissions, requestCode) - } - - /** Builder class to help instantiate a ReactFragment */ - class Builder { - var mComponentName: String? = null - var mLaunchOptions: Bundle? = null - - /** - * Set the Component name for our React Native instance. - * - * @param componentName The name of the component - * @return Builder - */ - fun setComponentName(componentName: String?): Builder { - mComponentName = componentName - return this - } - - /** - * Set the Launch Options for our React Native instance. - * - * @param launchOptions launchOptions - * @return Builder - */ - fun setLaunchOptions(launchOptions: Bundle?): Builder { - mLaunchOptions = launchOptions - return this - } - - fun build(): ReactFragment { - return newInstance(mComponentName, mLaunchOptions) - } - } - - companion object { - private const val ARG_COMPONENT_NAME = "arg_component_name" - private const val ARG_LAUNCH_OPTIONS = "arg_launch_options" - - /** - * @param componentName The name of the react native component - * @return A new instance of fragment ReactFragment. - */ - private fun newInstance(componentName: String?, launchOptions: Bundle?): ReactFragment { - val fragment = ReactFragment() - val args = Bundle() - args.putString(ARG_COMPONENT_NAME, componentName) - args.putBundle(ARG_LAUNCH_OPTIONS, launchOptions) - fragment.arguments = args - return fragment - } - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/glide/MendixGlideEncryptedFileLoader.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/glide/MendixGlideEncryptedFileLoader.kt deleted file mode 100644 index 72ee13a..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/glide/MendixGlideEncryptedFileLoader.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.mendix.mendixnative.glide - -import android.content.ContentResolver -import android.content.Context -import android.net.Uri -import com.bumptech.glide.Priority -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.Options -import com.bumptech.glide.load.data.DataFetcher -import com.bumptech.glide.load.model.ModelLoader -import com.bumptech.glide.load.model.ModelLoaderFactory -import com.bumptech.glide.load.model.MultiModelLoaderFactory -import com.bumptech.glide.signature.ObjectKey -import com.mendix.mendixnative.react.fs.FileBackend -import java.io.IOException -import java.io.InputStream -import java.security.GeneralSecurityException -import java.util.* - -class MendixGlideEncryptedFileLoader(private val factory: LocalUriFetcherFactory) : - ModelLoader { - override fun buildLoadData( - uri: Uri, width: Int, height: Int, options: Options - ): ModelLoader.LoadData { - return ModelLoader.LoadData(ObjectKey(uri), factory.build(uri)) - } - - override fun handles(uri: Uri): Boolean { - return SCHEMES.contains(uri.scheme) - } - - interface LocalUriFetcherFactory { - fun build(uri: Uri): DataFetcher - } - - class StreamFactory(private val context: Context) : ModelLoaderFactory, - LocalUriFetcherFactory { - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - return MendixGlideEncryptedFileLoader(this) - } - - override fun teardown() { - // Nothing - } - - override fun build(uri: Uri): DataFetcher { - return EncryptedLocalUriFetcher(context, uri) - } - } - - companion object { - private val SCHEMES = Collections.unmodifiableSet( - HashSet(listOf(ContentResolver.SCHEME_FILE)) - ) - } -} - -class EncryptedLocalUriFetcher(context: Context, private val uri: Uri) : - DataFetcher { - private val fileBackend: FileBackend = FileBackend(context) - - override fun loadData( - priority: Priority, callback: DataFetcher.DataCallback - ) { - try { - callback.onDataReady( - fileBackend.getFileInputStream(uri.toString().replace(uri.scheme + "://", "/")) - ) - } catch (e: GeneralSecurityException) { - callback.onLoadFailed(e) - } catch (e: IOException) { - callback.onLoadFailed(e) - } - } - - override fun cleanup() { - // nothing I guess. - } - - override fun cancel() { - // nothing I guess. - } - - override fun getDataClass(): Class { - return InputStream::class.java - } - - override fun getDataSource(): DataSource { - return DataSource.LOCAL - } - -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/handler/DevMenuTouchEventHandler.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/handler/DevMenuTouchEventHandler.kt deleted file mode 100644 index a2d75dc..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/handler/DevMenuTouchEventHandler.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.mendix.mendixnative.handler - -import android.view.MotionEvent -import kotlin.math.abs - -class DevMenuTouchEventHandler(private var listener: DevMenuTouchListener?) { - private val targetPointerCount = 3 - private val tapTimeout = 500 - private val moveThreshold = 100f - private var captureNextUpAction = false - private var pointerDownX = 0f - private var pointerDownY = 0f - - fun handle(event: MotionEvent?): Boolean { - when (event?.actionMasked) { - MotionEvent.ACTION_POINTER_DOWN -> onPointerDownAction(event) - MotionEvent.ACTION_POINTER_UP -> onPointerUpAction(event) - MotionEvent.ACTION_UP -> return onUpAction(event) - } - return false - } - - private fun onPointerDownAction(event: MotionEvent) { - captureNextUpAction = event.pointerCount == targetPointerCount - if (captureNextUpAction) { - pointerDownX = event.x - pointerDownY = event.y - } - } - - private fun onPointerUpAction(event: MotionEvent) { - if (event.pointerCount == targetPointerCount) { - val deltaX = abs(pointerDownX - event.x) - val deltaY = abs(pointerDownY - event.y) - if (deltaX > moveThreshold || deltaY > moveThreshold) { - captureNextUpAction = false - } - } - } - - private fun onUpAction(event: MotionEvent): Boolean { - if (!captureNextUpAction) { - return false - } - val timeSinceDownAction = event.eventTime - event.downTime - if (timeSinceDownAction < tapTimeout) { - onTap() - } else { - onLongPress() - } - captureNextUpAction = false - return true - } - - private fun onTap() { - listener?.onTap() - } - - private fun onLongPress() { - listener?.onLongPress() - } - - interface DevMenuTouchListener { - fun onTap() - fun onLongPress() - } - -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ClearData.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ClearData.kt deleted file mode 100644 index 7820fbb..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ClearData.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.mendix.mendixnative.react - -import android.app.Application -import android.content.Context -import android.util.Log -import android.webkit.CookieManager -import android.widget.Toast -import com.facebook.react.ReactNativeHost -import com.facebook.react.bridge.JavaOnlyMap -import com.facebook.react.bridge.PromiseImpl -import com.facebook.react.bridge.ReactContext -import com.facebook.react.modules.network.NetworkingModule -import com.mendix.mendixnative.encryption.MendixEncryptedStorage -import com.mendix.mendixnative.react.fs.FileBackend -import com.reactnativecommunity.asyncstorage.AsyncStorageModule -import org.pgsqlite.SQLitePlugin -import java.io.File - -fun clearData(applicationContext: Application) = clearCookies().also { - clearCachedReactNativeDevBundle(applicationContext) - val fileBackend = FileBackend(applicationContext) - for (ending in listOf("", "-shm", "-wal")) { - fileBackend.deleteFile( - File( - applicationContext.filesDir.parentFile, - "databases/" + MxConfiguration.defaultDatabaseName + ending - ).path - ) - fileBackend.deleteFile( - File( - applicationContext.filesDir.parentFile, - "databases/RKStorage$ending" - ).path - ) - } - fileBackend.deleteDirectory(applicationContext.filesDir) -} - -fun clearDataWithReactContext( - applicationContext: Application, - reactNativeHost: ReactNativeHost, - cb: (success: Boolean) -> Unit -) { - clearCachedReactNativeDevBundle(applicationContext) - val reactContext = reactNativeHost.reactInstanceManager.currentReactContext - val fileBackend = FileBackend(applicationContext) - fileBackend.deleteDirectory(applicationContext.filesDir) - val errorString = "Clearing %s failed. Please clear your data from the launch screen." - - - // TODO: Investigate why delete appDatabaseAsync fires twice [NALM-248] - // deleteAppDatabaseAsync is fired twice which results in the callback being called twice. - // Therefore we created a fire once callback that should be fired only once on success or failure. - deleteAppDatabaseAsync(reactContext, object : BooleanCallback { - var fired = false - - override fun invoke(success: Boolean) { - if (fired) return - - fired = true - - if (!success) { - reportError("database") - } - - if (!clearAsyncStorage(reactNativeHost)) { - reportError("async storage") - } - - clearSecureStorage(reactContext?.applicationContext) - if (!success) { - reportError("encrypted storage") - } - - runOnUiThread { - clearCookiesAsync(reactContext) { clearCookiesSuccessful -> - if (!clearCookiesSuccessful) { - reportError("cookies") - return@clearCookiesAsync - } - cb(true) - } - } - } - - private fun reportError(operation: String) { - Toast.makeText( - applicationContext, - String.format(errorString, operation), - Toast.LENGTH_LONG - ).show() - } - }) -} - -fun deleteAppDatabaseAsync(reactContext: ReactContext?, cb: BooleanCallback) = reactContext?.let { - val map = JavaOnlyMap() - map.putString("path", MxConfiguration.defaultDatabaseName) - (reactContext.catalystInstance.getNativeModule("SQLite") as SQLitePlugin).delete( - map, - { cb(true) }, - { cb(false) }) -} ?: cb(false) - -fun clearAsyncStorage(reactNativeHost: ReactNativeHost): Boolean = - reactNativeHost.reactInstanceManager.currentReactContext?.let { - it.getNativeModule(AsyncStorageModule::class.java)?.clearSensitiveData() - return true - } ?: false - - -fun clearSecureStorage(context: Context?): Boolean = - context?.let { MendixEncryptedStorage.getMendixEncryptedStorage(it).clear() } ?: false - -fun clearCookiesAsync(reactContext: ReactContext?, cb: (success: Boolean) -> Unit) = - reactContext?.let { - reactContext.getNativeModule(NetworkingModule::class.java)?.clearCookies { - cb(it[0] as Boolean) - } - } ?: cb(false) - -fun clearCachedReactNativeDevBundle(applicationContext: Application) { - try { - val fileBackend = FileBackend(applicationContext) - fileBackend.deleteFile(File(applicationContext.filesDir, "ReactNativeDevBundle.js").path) - } catch (e: Exception) { - Log.d("ClearData", "Clearing ReactNativeDevBundle skipped: $e") - } -} - -fun clearCookies() = CookieManager.getInstance()?.removeAllCookies(null) - -interface BooleanCallback { - operator fun invoke(res: Boolean) -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/CloseApp.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/CloseApp.kt deleted file mode 100644 index 350966b..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/CloseApp.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mendix.mendixnative.react - -import com.facebook.react.bridge.ReactContext -import org.pgsqlite.SQLitePlugin - -fun closeSqlDatabaseConnection(reactContext: ReactContext?) = reactContext?.let { - (it.catalystInstance.getNativeModule("SQLite") as SQLitePlugin).closeAllOpenDatabases() -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/CopiedFrom.java b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/CopiedFrom.java deleted file mode 100644 index bd9f14a..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/CopiedFrom.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.mendix.mendixnative.react; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.TYPE }) -@Retention(RetentionPolicy.SOURCE) -public @interface CopiedFrom { - Class value(); - String method() default ""; -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixApp.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixApp.kt deleted file mode 100644 index 0868e0d..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixApp.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mendix.mendixnative.react - -import java.io.Serializable - -data class MendixApp(val runtimeUrl: String, val warningsFilter: MxConfiguration.WarningsFilter, val showExtendedDevMenu: Boolean = false, val attachCustomDeveloperMenu: Boolean = false) : Serializable { - constructor(runtimeUrl: String, warningsFilter: MxConfiguration.WarningsFilter) : this(runtimeUrl, warningsFilter, false) -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixPackage.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixPackage.kt deleted file mode 100644 index 4c63df9..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MendixPackage.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mendix.mendixnative.react - -import com.facebook.react.ReactPackage -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ViewManager -import com.mendix.mendixnative.encryption.MendixEncryptedStorageModule -import com.mendix.mendixnative.react.download.NativeDownloadModule -import com.mendix.mendixnative.react.fs.NativeFsModule -import com.mendix.mendixnative.react.ota.NativeOtaModule -import com.mendix.mendixnative.react.splash.MendixSplashScreenModule -import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter - -class MendixPackage(private val splashScreenPresenter: MendixSplashScreenPresenter?) : - ReactPackage { - override fun createNativeModules(reactContext: ReactApplicationContext): List { - val modules = mutableListOf( - MxConfiguration(reactContext), - NativeErrorHandler(reactContext), - NativeReloadHandler(reactContext), - NativeFsModule(reactContext), - NativeDownloadModule(reactContext), - NativeOtaModule(reactContext), - MendixEncryptedStorageModule(reactContext) - ) - if (splashScreenPresenter != null) { - modules.add(MendixSplashScreenModule(splashScreenPresenter, reactContext)) - } - return modules - } - - override fun createViewManagers(reactContext: ReactApplicationContext): List> { - return emptyList() - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MxConfiguration.java b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MxConfiguration.java deleted file mode 100644 index 850bd60..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/MxConfiguration.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.mendix.mendixnative.react; - -import static com.mendix.mendixnative.react.ota.OtaHelpersKt.getNativeDependencies; -import static com.mendix.mendixnative.react.ota.OtaHelpersKt.getOtaManifestFilepath; - -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.mendix.mendixnative.MendixApplication; -import com.mendix.mendixnative.config.AppUrl; - -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.Map; - -public class MxConfiguration extends ReactContextBaseJavaModule { - MxConfiguration(ReactApplicationContext reactContext) { - super(reactContext); - } - /** - * Increment NATIVE_BINARY_VERSION to 4 for React native upgrade to version 0.72.7 - */ - public static final int NATIVE_BINARY_VERSION = 4; - static final String NAME = "MxConfiguration"; - static String defaultDatabaseName = "default"; - @Deprecated - static String defaultFilesDirectoryName = "files/default"; - - public static String defaultAppName = null; - public static String runtimeUrl; - public static MxConfiguration.WarningsFilter warningsFilter; - - /** - * Setter for the application name constant - * - * @param name the unique name or identifier that represents the application. This value should always be set to null for non-sample apps - */ - public static void setDefaultAppNameOrDefault(String name) { - defaultAppName = name; - } - - public static void setDefaultDatabaseNameOrDefault(String name) { - defaultDatabaseName = name != null ? name : "default"; - } - - public static void setDefaultFilesDirectoryOrDefault(String path) { - defaultFilesDirectoryName = path != null ? path : "files/default"; - } - - @Override - public Map getConstants() { - final MendixApplication application = ((MendixApplication) this.getReactApplicationContext().getApplicationContext()); - - if (runtimeUrl == null) { - if (warningsFilter != WarningsFilter.none) { - application.getReactNativeHost() - .getReactInstanceManager() - .getDevSupportManager() - .showNewJavaError("Runtime URL not specified.", new Throwable("Without the runtime URL, the app cannot retrieve any data.\n\nPlease redeploy the app.")); - - return new HashMap<>(); - } - - throw new IllegalStateException("Runtime URL not set in the MxConfiguration"); - } - - final Map constants = new HashMap<>(); - constants.put("RUNTIME_URL", AppUrl.forRuntime(runtimeUrl)); - constants.put("APP_NAME", defaultAppName); - constants.put("DATABASE_NAME", defaultDatabaseName); - constants.put("FILES_DIRECTORY_NAME", defaultFilesDirectoryName); // Not to be removed as it is required for backwards compatibility. - constants.put("WARNINGS_FILTER_LEVEL", warningsFilter.toString()); - constants.put("CODE_PUSH_KEY", application.getCodePushKey()); - constants.put("OTA_MANIFEST_PATH", getOtaManifestFilepath(getReactApplicationContext())); - constants.put("NATIVE_DEPENDENCIES", getNativeDependencies(getReactApplicationContext())); - constants.put("IS_DEVELOPER_APP", application.getUseDeveloperSupport()); - constants.put("NATIVE_BINARY_VERSION", NATIVE_BINARY_VERSION); - constants.put("APP_SESSION_ID", application.getAppSessionId()); - return constants; - } - - @NotNull - @Override - public String getName() { - return NAME; - } - - public enum WarningsFilter { - all, partial, none - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.java b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.java deleted file mode 100644 index 735b4d9..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.mendix.mendixnative.react; - -import com.facebook.common.logging.FLog; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.module.annotations.ReactModule; -import com.facebook.react.modules.core.ExceptionsManagerModule; - -import org.jetbrains.annotations.NotNull; - -// Used by previous versions of the client (<= 9.15) -@ReactModule(name = NativeErrorHandler.NAME) -public class NativeErrorHandler extends ReactContextBaseJavaModule { - static final String NAME = "NativeErrorHandler"; - - NativeErrorHandler(ReactApplicationContext reactContext) { - super(reactContext); - } - - @NotNull - @Override - public String getName() { - return NAME; - } - - @ReactMethod - public void handle(String message, ReadableArray stackTrace) { - ExceptionsManagerModule exceptionsManagerModule = getReactApplicationContext().getNativeModule(ExceptionsManagerModule.class); - exceptionsManagerModule.reportSoftException(message, stackTrace, 0); - exceptionsManagerModule.updateExceptionMessage(message, stackTrace, 0); - - FLog.e(getClass(), "Received JS exception: " + message); - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt deleted file mode 100644 index 7d281b4..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/NativeReloadHandler.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.mendix.mendixnative.react - -import android.os.Handler -import android.os.Looper -import com.facebook.common.logging.FLog -import com.facebook.react.ReactApplication -import com.facebook.react.bridge.JSBundleLoader -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.modules.core.DeviceEventManagerModule -import com.mendix.mendixnative.MendixApplication -import com.mendix.mendixnative.activity.LaunchScreenHandler -import com.mendix.mendixnative.util.ReflectionUtils - -@ReactModule(name = NativeReloadHandler.NAME) -class NativeReloadHandler internal constructor(reactContext: ReactApplicationContext) : - ReactContextBaseJavaModule(reactContext) { - override fun getName(): String { - return NAME - } - - @ReactMethod - fun reload() { - FLog.i(javaClass, "Reload bundle triggered from JS") - (reactApplicationContext.currentActivity as? LaunchScreenHandler)?.let { - postOnMainThread { - it.showLaunchScreen() - } - } - ?: FLog.e( - javaClass, - "Activity does not implement LaunchScreenHandler, skipping showing launch screen" - ) - handleJSBundleLoading() - reloadWithoutState() - } - - @ReactMethod - fun exitApp() { - reactApplicationContext?.currentActivity?.finishAffinity() - } - - private fun reloadWithoutState() { - postOnMainThread { - (reactApplicationContext.applicationContext as ReactApplication) - .reactNativeHost - .reactInstanceManager - .recreateReactContextInBackground() - } - - } - - fun reloadClientWithState() = - reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit("reloadWithState", null) - - companion object { - const val NAME = "NativeReloadHandler" - } - - private fun postOnMainThread(cb: () -> Unit) { - Handler(Looper.getMainLooper()).post { - cb.invoke() - } - } - - private fun handleJSBundleLoading() { - val bundle = (reactApplicationContext.applicationContext as MendixApplication).jsBundleFile - val instanceManager = - (reactApplicationContext.applicationContext as ReactApplication).reactNativeHost.reactInstanceManager - - val latestJSBundleLoader = if (bundle != null) { - getAssetLoader(bundle) - } else { - getAssetLoader("assets://index.android.bundle") - } - - ReflectionUtils.setField(instanceManager, "mBundleLoader", latestJSBundleLoader) - ReflectionUtils.setField( - instanceManager, - "mUseDeveloperSupport", - (reactApplicationContext.applicationContext as MendixApplication).useDeveloperSupport - ) - } - - override fun getConstants(): MutableMap { - return mutableMapOf(Pair("EVENT_RELOAD_WITH_STATE", "reloadWithState")) - } - - private fun getAssetLoader(bundle: String): JSBundleLoader? { - return when { - bundle.startsWith("assets://") -> JSBundleLoader.createAssetLoader( - reactApplicationContext, - bundle, - false - ) - else -> JSBundleLoader.createFileLoader(bundle) - } - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/DownloadHelper.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/DownloadHelper.kt deleted file mode 100644 index 2eb0e1a..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/DownloadHelper.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.mendix.mendixnative.react.download - -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import java.io.* -import java.net.ConnectException -import kotlin.math.abs - -@Throws( - IllegalArgumentException::class, - ConnectException::class, - FileAlreadyExistsException::class, - NoDataException::class, - FileCorruptionException::class, - IOException::class, - SecurityException::class, - ConnectException::class, - DownloadMimeTypeException::class -) - -fun downloadFile( - client: OkHttpClient, - url: String, - downloadPath: String, - onSuccess: () -> Unit, - onFailure: (e: Exception) -> Unit, - progressCallback: (receivedBytes: Double, totalBytes: Double) -> Unit = { _, _ -> }, -) { - downloadFile(client, url, downloadPath, null, onSuccess, onFailure, progressCallback) -} - -fun downloadFile( - client: OkHttpClient, - url: String, - downloadPath: String, - expectedMimeType: String?, - onSuccess: () -> Unit, - onFailure: (e: Exception) -> Unit, - progressCallback: (receivedBytes: Double, totalBytes: Double) -> Unit = { _, _ -> }, -) { - val outputFile = File(downloadPath) - if (outputFile.exists()) throw FileAlreadyExistsException(outputFile) - outputFile.parentFile?.mkdirs() - outputFile.createNewFile() - - client.newCall(Request.Builder().url(url).get().build()).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) = onFailure(e) - - override fun onResponse(call: Call, response: Response) { - try { - DownloadResponseHandler( - response, - expectedMimeType, - outputFile, - progressCallback, - ).handle() - onSuccess() - } catch (e: Exception) { - onFailure(e) - } - } - }) -} - - -fun makeProgressCallbackInvoker( - bytesInterval: Double, - cb: (receivedBytes: Double, totalBytes: Double) -> Unit -): (Double, Double) -> Unit { - var invokeNext = bytesInterval - return fun(receivedBytes: Double, totalBytes: Double) { - if (receivedBytes >= invokeNext) { - invokeNext = receivedBytes + bytesInterval - cb.invoke(receivedBytes, totalBytes) - } - } -} - -class DownloadResponseHandler( - private val response: Response, - private val expectedMimeType: String?, - private val outputFile: File, - private val progressCallback: (receivedBytes: Double, totalBytes: Double) -> Unit = { _, _ -> }, -) { - @Throws(ConnectException::class, NoDataException::class, DownloadMimeTypeException::class) - fun handle() { - var inputStream: BufferedInputStream? = null - var outputStream: BufferedOutputStream? = null - try { - if (!response.isSuccessful) throw ConnectException() - if (response.body == null) throw NoDataException() - val body = response.body - val mediaType = body?.contentType() - if (expectedMimeType != null && mediaType != expectedMimeType - .toMediaTypeOrNull() - ) throw DownloadMimeTypeException() - - - inputStream = BufferedInputStream(body!!.byteStream()) - - outputStream = - BufferedOutputStream(FileOutputStream(outputFile)) - - val totalBytes = response.body!!.contentLength().toDouble() - val progressCallbackInvoker = makeProgressCallbackInvoker( - totalBytes / 100, - progressCallback - ) - - var receivedBytes: Double - var data = inputStream.read() - while (data != -1) { - outputStream.write(data) - data = inputStream.read() - - receivedBytes = abs(inputStream.available().toDouble() - totalBytes) - progressCallbackInvoker(receivedBytes, totalBytes) - } - outputStream.flush() - } catch (e: Exception) { - outputFile.delete() - throw e - } finally { - outputStream?.close() - inputStream?.close() - } - } -} - -class NoDataException : IllegalStateException() -class FileCorruptionException : IllegalStateException() -class DownloadMimeTypeException : RuntimeException() diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt deleted file mode 100644 index 5b03a9e..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.mendix.mendixnative.react.download - -import com.facebook.react.bridge.* -import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter -import okhttp3.OkHttpClient -import java.io.IOException -import java.net.ConnectException -import java.util.concurrent.TimeUnit - -@ReactModule(name = NativeDownloadModule.NAME) -class NativeDownloadModule(context: ReactApplicationContext) : - ReactContextBaseJavaModule(context) { - val client = OkHttpClient() - - override fun getName(): String { - return NAME - } - - @ReactMethod - fun download(url: String, downloadPath: String, config: ReadableMap, promise: Promise) { - val connectionTimeout = - if (config.hasKey("connectionTimeout")) config.getInt("connectionTimeout") else 10000 - val mimeType = if (config.hasKey("mimeType")) config.getString("mimeType") else null - - downloadFile( - client.newBuilder() - .connectTimeout(connectionTimeout.toLong(), TimeUnit.MILLISECONDS).build(), - url, - downloadPath, - mimeType, - { promise.resolve(null) }, - { e -> - when (e) { - is DownloadMimeTypeException -> promise.reject( - ERROR_DOWNLOAD_FAILED, - "Mime type check failed", - e - ) - is FileAlreadyExistsException -> promise.reject( - FILE_ALREADY_EXISTS, - "File already exists", - e - ) - is NoDataException -> promise.reject( - ERROR_CONNECTION_FAILED, - "No data found", - e - ) - is FileCorruptionException -> promise.reject(IO_EXCEPTION, "File corrupted", e) - is IOException -> promise.reject(IO_EXCEPTION, "IO exception", e) - is SecurityException -> promise.reject( - FS_ACCESS_EXCEPTION, - "Access to filesystem denied", - e - ) - is ConnectException -> promise.reject( - ERROR_DOWNLOAD_FAILED, - "Failed to connect to endpoint", - e - ) - else -> promise.reject(ERROR_DOWNLOAD_FAILED, "Failed to download file", e) - } - } - ) { receivedBytes, totalBytes -> - postProgressEvent( - receivedBytes, - totalBytes - ) - } - } - - private fun postProgressEvent(receivedBytes: Double, totalBytes: Double) { - val params = Arguments.createMap() - params.putDouble("receivedBytes", receivedBytes) - params.putDouble("totalBytes", totalBytes) - this.reactApplicationContext - .getJSModule(RCTDeviceEventEmitter::class.java) - .emit(DOWNLOAD_PROGRESS_EVENT, params) - } - - companion object { - const val NAME = "NativeDownloadModule" - } -} - -private const val DOWNLOAD_PROGRESS_EVENT = "NDM_DOWNLOAD_PROGRESS_EVENT" -private const val ERROR_DOWNLOAD_FAILED = "ERROR_DOWNLOAD_FAILED" -private const val FILE_ALREADY_EXISTS = "FILE_ALREADY_EXISTS" -private const val ERROR_CONNECTION_FAILED = "ERROR_CONNECTION_FAILED" -private const val FS_ACCESS_EXCEPTION = "FS_ACCESS_EXCEPTION" -private const val IO_EXCEPTION = "IO_EXCEPTION" - diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/FileBackend.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/FileBackend.kt deleted file mode 100644 index 6af48db..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/FileBackend.kt +++ /dev/null @@ -1,271 +0,0 @@ -package com.mendix.mendixnative.react.fs - -import android.content.Context -import android.os.Build -import androidx.security.crypto.EncryptedFile -import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.ObjectMapper -import com.mendix.mendixnative.encryption.getMasterKey -import java.io.* -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import java.security.GeneralSecurityException -import java.util.* -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - -val FILE_ENCRYPTION_SCHEME = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB - -class FileBackend(val context: Context) { - private var encryptionEnabled = false - - fun setEncryptionEnabled(encryptionEnabled: Boolean){ - this.encryptionEnabled = encryptionEnabled - } - - @Throws(IOException::class) - fun save(data: ByteArray, filePath: String) { - - if (this.encryptionEnabled && !isOfflineFile(filePath)) { - val isOverride = exists(filePath) - val outputFilePath = if (isOverride) getTempFilePath(filePath) else filePath - - getEncryptedFileOutputStream(outputFilePath).apply { - write(data) - flush() - close() - } - - if (isOverride) { - moveFile(outputFilePath, filePath) - } - } else{ - getUnencryptedFileOutputStream(data, filePath); - } - } - - @Throws(IOException::class) - fun read(filePath: String): ByteArray { - return try { - if (this.encryptionEnabled) { - readAsEncryptedFile(filePath) - }else{ - readAsUnencryptedFile(filePath); - } - } catch (e: IOException) { - readAsUnencryptedFile(filePath) - } - } - - @Throws(IOException::class, GeneralSecurityException::class) - private fun getEncryptedFileOutputStream(filePath: String): FileOutputStream { - val file = File(filePath) - val encryptedFile = EncryptedFile.Builder( - this.context, - file, - getMasterKey(this.context), - FILE_ENCRYPTION_SCHEME - ).build() - - if (file.exists()) { - file.delete() - } - - file.parentFile?.mkdirs() - - return encryptedFile.openFileOutput() - } - - private fun getUnencryptedFileOutputStream(data: ByteArray, filePath: String) { - File(filePath).parentFile?.mkdirs() - FileOutputStream(filePath).use { outputStream -> outputStream.write(data) } - } - - - @Throws(IOException::class, GeneralSecurityException::class) - fun getFileInputStream(filePath: String): InputStream { - val file = File(filePath) - val encryptedFile = EncryptedFile.Builder( - context, - file, - getMasterKey(context), - FILE_ENCRYPTION_SCHEME - ).build() - return encryptedFile.openFileInput() - } - - @Throws(IOException::class) - fun moveFile(filePath: String, newPath: String) { - val src = File(filePath) - val dest = File(newPath) - if (!moveFileByRename(src, dest)) { - val data = read(filePath) - dest.parentFile?.mkdirs() - FileOutputStream(newPath).use { outputStream -> - outputStream.write(data) - File(filePath).delete() - } - } - } - - fun deleteFile(filePath: String) { - delete(File(filePath)) - } - - fun deleteDirectory(directoryPath: String) { - deleteDirectory(File(directoryPath)) - } - - fun list(dirPath: String): Array { - val directory = File(dirPath) - return directory.list() ?: emptyArray() - } - - fun exists(filePath: String): Boolean { - return File(filePath).exists() - } - - fun isDirectory(filePath: String): Boolean { - return File(filePath).isDirectory - } - - fun copyAssetToPath(context: Context, assetName: String, toFilePath: String) { - context.assets.open(assetName).let { inputStream -> - val outFile = File(toFilePath) - outFile.parentFile.let { parent -> - parent?.mkdirs() - } - val out = FileOutputStream(outFile) - val buffer = ByteArray(1024) - var read: Int - while (inputStream.read(buffer).also { read = it } != -1) { - out.write(buffer, 0, read) - } - inputStream.close() - out.flush() - out.close() - } - } - - fun unzip(zipPath: File, directory: File) { - unzip(zipPath.absolutePath, directory.absolutePath) - } - - fun unzip(zipPath: String, directory: String) { - ZipFile(zipPath).use { zip -> - zip.entries().asSequence().map { zipEntry -> - val file = File(directory, zipEntry.name) - file.parentFile?.run { mkdirs() } - listOf(zipEntry, file) - }.filter { - !(it[0] as ZipEntry).isDirectory - }.forEach { - zip.getInputStream(it[0] as ZipEntry).use { input -> - (it[1] as File).outputStream().use { output -> - input.copyTo(output) - } - } - } - } - } - - @Throws(JsonMappingException::class, IOException::class) - fun writeJson(map: HashMap, filepath: String) { - val bytes = - ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsBytes(map) - save(bytes, filepath) - } - - fun writeUnencryptedJson(map: HashMap, filepath: String) { - val bytes = - ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsBytes(map) - getUnencryptedFileOutputStream(bytes, filepath) - } - - - fun moveDirectory( - src: String, - dst: String, - ): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val dstFile = File(dst) - dstFile.parentFile?.mkdirs() - try { - Files.move( - File(src).toPath(), - dstFile.toPath(), - StandardCopyOption.REPLACE_EXISTING - ) - true - } catch (e: Exception) { - e.printStackTrace() - false - } - } else { - val directory = File(src) - require(directory.isDirectory) { return false } - val files = collectFilesInDirectory(directory).reversed() - files.forEach { - if (!it.isDirectory) { - val filePath = it.absolutePath - val toFilePath = filePath.replace(src, dst) - moveFile(filePath, toFilePath) - } - } - deleteDirectory(src) - true - } - } - - fun deleteDirectory(directory: File) { - if (!directory.isDirectory) return - val files = collectFilesInDirectory(directory).toList().reversed() - files.forEach { it.delete() } - } - - private fun collectFilesInDirectory(directory: File): Array = - require(directory.isDirectory).let { - arrayOf(directory).apply { - for (file in directory.listFiles() ?: emptyArray()) { - if (file.isDirectory) this.plus(collectFilesInDirectory(file)) - else this.plus(file) - } - } - } - - private fun moveFileByRename(src: File, dest: File): Boolean { - return try { - require(src.exists() && src.isFile) - dest.parentFile?.mkdirs() - src.renameTo(dest) - } catch (e: java.lang.Exception) { - e.printStackTrace() - false - } - } - - private fun delete(file: File) { - file.delete() - } - - @Throws(IOException::class) - private fun readAsEncryptedFile(filePath: String): ByteArray { - val inputStream = getFileInputStream(filePath) - return inputStream.readBytes() - } - - @Throws(IOException::class) - fun readAsUnencryptedFile(filePath: String): ByteArray { - val inputStream = File(filePath).inputStream() - return inputStream.readBytes() - } - - private fun getTempFilePath(filePath: String): String { - return filePath + "temp" - } - - private fun isOfflineFile(filePath: String):Boolean{ - return filePath.contains("GUID") - } - -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.java b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.java deleted file mode 100644 index cb525bf..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.mendix.mendixnative.react.fs; - -import static java.util.Objects.requireNonNull; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeArray; -import com.facebook.react.bridge.WritableNativeMap; -import com.facebook.react.module.annotations.ReactModule; -import com.facebook.react.modules.blob.BlobModule; -import com.facebook.react.modules.blob.FileReaderModule; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -@ReactModule(name = NativeFsModule.NAME) -public class NativeFsModule extends ReactContextBaseJavaModule { - static final String NAME = "NativeFsModule"; - - private static final String ERROR_INVALID_BLOB = "ERROR_INVALID_BLOB"; - private static final String ERROR_READ_FAILED = "ERROR_READ_FAILED"; - private static final String ERROR_CACHE_FAILED = "ERROR_CACHE_FAILED"; - private static final String ERROR_MOVE_FAILED = "ERROR_MOVE_FAILED"; - private static final String ERROR_SERIALIZATION_FAILED = "ERROR_SERIALIZATION_FAILED"; - private static final String INVALID_PATH = "INVALID_PATH"; - - private final ReactApplicationContext reactContext; - private final FileBackend fileBackend; - private final String filesDir; - private final String cacheDir; - - public NativeFsModule(ReactApplicationContext reactContext) { - super(reactContext); - this.reactContext = reactContext; - filesDir = reactContext.getFilesDir().getAbsolutePath(); - cacheDir = reactContext.getCacheDir().getAbsolutePath(); - fileBackend = new FileBackend(reactContext); - } - - @NotNull - @Override - public String getName() { - return NAME; - } - - @ReactMethod - public void setEncryptionEnabled(Boolean encryptionEnabled) { - this.fileBackend.setEncryptionEnabled(encryptionEnabled); - } - - @ReactMethod - public void save(ReadableMap blob, String filePath, Promise promise) { - BlobModule blobModule = reactContext.getNativeModule(BlobModule.class); - String blobId = blob.getString("blobId"); - - byte[] bytes = blobModule.resolve(blobId, blob.getInt("offset"), blob.getInt("size")); - if (bytes == null) { - promise.reject(ERROR_INVALID_BLOB, "The specified blob is invalid"); - return; - } - - try { - fileBackend.save(bytes, ensureWhiteListedPath(filePath)); - } catch (IOException e) { - e.printStackTrace(); - promise.reject(ERROR_CACHE_FAILED, "Failed writing file to disk"); - return; - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - return; - } - - blobModule.release(blobId); - promise.resolve(null); - } - - @ReactMethod - public void read(String filePath, Promise promise) { - try { - promise.resolve(read(ensureWhiteListedPath(filePath))); - } catch (FileNotFoundException e) { - promise.resolve(null); - } catch (IOException e) { - e.printStackTrace(); - promise.reject(ERROR_READ_FAILED, "Failed reading file from disk"); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - } - } - - @ReactMethod - public void move(String filePath, String newPath, Promise promise) { - String fromPath; - String toPath; - - try { - fromPath = ensureWhiteListedPath(filePath); - toPath = ensureWhiteListedPath(newPath); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - return; - } - - if (!fileBackend.exists(fromPath)) { - promise.reject(ERROR_READ_FAILED, "File does not exist"); - } - - try { - if (fileBackend.isDirectory(fromPath)) { - fileBackend.moveDirectory(fromPath, toPath); - } else { - fileBackend.moveFile(fromPath, toPath); - } - promise.resolve(null); - } catch (IOException e) { - e.printStackTrace(); - promise.reject(ERROR_MOVE_FAILED, e); - } - } - - @ReactMethod - public void remove(String filePath, Promise promise) { - try { - if (fileBackend.isDirectory(filePath)) { - fileBackend.deleteDirectory(filePath); - } else { - fileBackend.deleteFile(ensureWhiteListedPath(filePath)); - } - promise.resolve(null); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - } - } - - @ReactMethod - public void list(String dirPath, Promise promise) { - WritableNativeArray result = new WritableNativeArray(); - - /* - This is for backwards compatibility for a assumption/bug(?) in the client. The client assumes - it can list any path without verifying its validity and expects to get an empty array back as it chains unconditionally. - */ - File directory = new File(dirPath); - if (!directory.exists() || !directory.isDirectory()) { - promise.resolve(result); - return; - } - - try { - for (String file : requireNonNull(fileBackend.list(ensureWhiteListedPath(dirPath)))) { - result.pushString(file); - } - promise.resolve(result); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - } catch (Exception e) { - e.printStackTrace(); - promise.reject(e); - } - } - - @ReactMethod - public void readAsDataURL(String filePath, Promise promise) { - try { - FileReaderModule fileReaderModule = - reactContext.getNativeModule(FileReaderModule.class); - fileReaderModule.readAsDataURL(read(ensureWhiteListedPath(filePath)), promise); - } catch (FileNotFoundException e) { - promise.resolve(null); - } catch (IOException e) { - e.printStackTrace(); - promise.reject(ERROR_READ_FAILED, "Failed reading file from disk"); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - } - } - - @ReactMethod - public void readAsText(String filePath, Promise promise) { - try { - promise.resolve(new String(fileBackend.read(filePath), StandardCharsets.UTF_8)); - } catch (IOException e) { - promise.reject("no text", e); - } - } - - @ReactMethod - public void fileExists(String filePath, Promise promise) { - try { - promise.resolve(fileBackend.exists(ensureWhiteListedPath(filePath))); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - } - } - - @ReactMethod - public void writeJson(ReadableMap data, String filepath, Promise promise) { - try { - fileBackend.writeJson(data.toHashMap(), ensureWhiteListedPath(filepath)); - promise.resolve(null); - } catch (JsonMappingException e) { - e.printStackTrace(); - promise.reject(ERROR_SERIALIZATION_FAILED, "Failed to serialize JSON", e); - } catch (IOException e) { - e.printStackTrace(); - promise.reject(ERROR_CACHE_FAILED, "Failed to write to disk", e); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - } - } - - @ReactMethod - public void readJson(String filepath, Promise promise) { - try { - byte[] bytes = - fileBackend.read(ensureWhiteListedPath(filepath)); - TypeReference> typeRef = - new TypeReference>() { - }; - promise.resolve(Arguments.makeNativeMap(new ObjectMapper().readValue(bytes, typeRef))); - } catch (FileNotFoundException e) { - e.printStackTrace(); - promise.resolve("null"); - } catch (JsonParseException | JsonMappingException e) { - e.printStackTrace(); - promise.reject(ERROR_SERIALIZATION_FAILED, "Failed to deserialize JSON", e); - } catch (IOException e) { - e.printStackTrace(); - promise.reject(ERROR_READ_FAILED, "Failed reading file from disk"); - } catch (PathNotAccessibleException e) { - e.printStackTrace(); - promise.reject(INVALID_PATH, e); - } - } - - @Override - public Map getConstants() { - HashMap constants = new HashMap<>(); - constants.put("DocumentDirectoryPath", filesDir); - constants.put( - "SUPPORTS_DIRECTORY_MOVE", - true); // Client uses this const to identify if functionality is supported - constants.put( - "SUPPORTS_ENCRYPTION", - true); - return constants; - } - - private ReadableMap read(String filePath) throws IOException { - byte[] data; - data = fileBackend.read(filePath); - - BlobModule blobModule = reactContext.getNativeModule(BlobModule.class); - WritableMap blob = new WritableNativeMap(); - blob.putString("blobId", blobModule.store(data)); - blob.putInt("offset", 0); - blob.putInt("size", data.length); - return blob; - } - - private String ensureWhiteListedPath(String path) throws PathNotAccessibleException { - if (!(path.startsWith(filesDir) || path.startsWith(cacheDir))) { - throw new PathNotAccessibleException(path); - } - return path; - } -} - -class PathNotAccessibleException extends Exception { - PathNotAccessibleException(String path) { - super( - "Cannot write to " - + path - + ". Path needs to be an absolute path to the apps accessible space."); - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.java b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.java deleted file mode 100644 index e8ff430..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.mendix.mendixnative.react.menu; - -public interface AppMenu { - void show(); -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt deleted file mode 100644 index 72f4a5b..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.mendix.mendixnative.react.menu - -import android.app.Activity -import android.app.AlertDialog -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.widget.Toast -import com.mendix.mendixnative.MendixApplication -import com.mendix.mendixnative.R -import com.mendix.mendixnative.config.AppPreferences -import com.mendix.mendixnative.databinding.AppMenuLayoutBinding -import com.mendix.mendixnative.react.clearDataWithReactContext -import com.mendix.mendixnative.react.toggleElementInspector - -class DevAppMenu(val activity: Activity, isDevModeEnabled: Boolean = false, handleReload: () -> Unit, onCloseProjectSelected: (() -> Unit)? = null) : AppMenu { - private val dialog: AlertDialog - - init { - val preferences = AppPreferences(activity.applicationContext) - val binding = AppMenuLayoutBinding.inflate(LayoutInflater.from(activity)) - val view = binding.root - - binding.advancedSettingsButton - - dialog = AlertDialog.Builder(activity) - .setView(view) - .create() - - binding.advancedSettingsContainer.visibility = View.GONE - binding.advancedSettingsButton.visibility = visibleWhenDevModeEnabled(isDevModeEnabled) - binding.advancedSettingsButton.setOnClickListener { - binding.advancedSettingsContainer.visibility = when (binding.advancedSettingsContainer.visibility) { - (View.GONE) -> View.VISIBLE - else -> View.GONE - } - } - - binding.remoteDebuggingButton.visibility = visibleWhenDevModeEnabled(isDevModeEnabled) - binding.remoteDebuggingButton.text = activity.resources.getText(remoteDebugginButtonTextResource(preferences.isRemoteJSDebugEnabled)) - binding.remoteDebuggingButton.setOnClickListener { - preferences.setRemoteDebugging(!preferences.isRemoteJSDebugEnabled) - handleReload() - binding.remoteDebuggingButton.text = activity.resources.getText(remoteDebugginButtonTextResource(preferences.isRemoteJSDebugEnabled)) - dialog.dismiss() - } - - binding.advancedClearData.setOnClickListener { - activity.runOnUiThread { - clearDataWithReactContext(activity.application, (activity.application as MendixApplication).reactNativeHost) { success: Boolean -> - if (success) { - activity.runOnUiThread { - handleReload() - } - } else { - Toast.makeText(activity, "Clearing data failed.", Toast.LENGTH_LONG) - } - } - } - dialog.dismiss() - } - - binding.elementInspectorButton.visibility = visibleWhenDevModeEnabled(isDevModeEnabled) - binding.elementInspectorButton.setOnClickListener { - preferences.setElementInspector(!preferences.isElementInspectorEnabled) - toggleElementInspector((activity.application as MendixApplication).reactNativeHost.reactInstanceManager.currentReactContext) - dialog.dismiss() - } - - binding.reloadButton.setOnClickListener { - handleReload() - dialog.dismiss() - } - - binding.closeButton.setOnClickListener { - dialog.dismiss() - onCloseProjectSelected?.invoke() - } - } - - override fun show() { - if (!activity.isDestroyed) { - dialog.show() - } else { - Log.d("DevAppMenu", "Attempted to show dialog in a destroyed activity") - } - } - - private fun visibleWhenDevModeEnabled(devModeEnabled: Boolean): Int = if (devModeEnabled) { - View.VISIBLE - } else { - View.GONE - } - - private fun remoteDebugginButtonTextResource(isRemoteJsDebugEnabled: Boolean): Int = if (isRemoteJsDebugEnabled) { - R.string.dev_menu_disable_remote_debugging - } else { - R.string.dev_menu_enable_remote_debugging - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt deleted file mode 100644 index 862b569..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt +++ /dev/null @@ -1,245 +0,0 @@ -package com.mendix.mendixnative.react.ota - -import android.content.Context -import android.util.Log -import com.facebook.react.bridge.* -import com.mendix.mendixnative.react.MxConfiguration -import com.mendix.mendixnative.react.download.downloadFile -import com.mendix.mendixnative.react.fs.FileBackend -import okhttp3.OkHttpClient -import org.json.JSONObject -import java.io.File -import java.util.* - - -const val INVALID_RUNTIME_URL = "INVALID_RUNTIME_URL" -const val INVALID_DEPLOY_CONFIG = "INVALID_DEPLOY_CONFIG" -const val INVALID_DOWNLOAD_CONFIG = "INVALID_DOWNLOAD_CONFIG" -const val OTA_ZIP_FILE_MISSING = "OTA_ZIP_FILE_MISSING" -const val OTA_UNZIP_DIR_EXISTS = "OTA_UNZIP_DIR_EXISTS" -const val OTA_DEPLOYMENT_FAILED = "OTA_DEPLOYMENT_FAILED" -const val OTA_DOWNLOAD_FAILED = "OTA_DOWNLOAD_FAILED" - -const val MANIFEST_OTA_DEPLOYMENT_ID_KEY = "otaDeploymentID" -const val MANIFEST_RELATIVE_BUNDLE_PATH_KEY = "relativeBundlePath" -const val MANIFEST_APP_VERSION_KEY = "appVersion" -const val DOWNLOAD_RESULT_OTA_PACKAGE_KEY = "otaPackage" -const val DEPLOY_CONFIG_DEPLOYMENT_ID_KEY = "otaDeploymentID" -const val DEPLOY_CONFIG_OTA_PACKAGE_KEY = DOWNLOAD_RESULT_OTA_PACKAGE_KEY -const val DEPLOY_CONFIG_EXTRACTION_DIR_KEY = "extractionDir" - -val TAG = "OTA" - -class NativeOtaModule( - context: ReactApplicationContext, - getOtaDir: (context: Context) -> String = { c -> - com.mendix.mendixnative.react.ota.getOtaDir( - c - ) - }, - val getAppVersion: (context: Context) -> String = { c: Context -> - resolveAppVersion(c) - }, - val getOtaManifestFilepath: (context: Context) -> String = { c -> - com.mendix.mendixnative.react.ota.getOtaManifestFilepath(c) - }, - val resolveAbsolutePathRelativeToOtaDir: (context: Context, relativePath: String) -> String = { c, relativePath -> - com.mendix.mendixnative.react.ota.resolveAbsolutePathRelativeToOtaDir(c, relativePath) - }, -) : - ReactContextBaseJavaModule(context) { - private val fileBackend = FileBackend(context) - private val otaDir: String = getOtaDir(context) - - init { - // Ensure the dir exist - File(otaDir).mkdirs() - } - - override fun getName(): String = "NativeOtaModule" - - /** - * Accepts a structure of: - * { - * url: string, // url to download from - * } - * - * Returns a structure of: - * { - * otaPackage: string // zip file name - * } - */ - @ReactMethod - fun download(config: ReadableMap, promise: Promise) { - Log.i(TAG, "Downloading...") - val url = config.getString("url") ?: return promise.reject( - INVALID_DOWNLOAD_CONFIG, - "Key url is invalid." - ) - if (!url.startsWith(MxConfiguration.runtimeUrl)) { - return promise.reject(INVALID_RUNTIME_URL, "Invalid OTA URL.") - } - - val zipFileName = generateZipFilename() - downloadFile( - client = OkHttpClient(), - url = url, - downloadPath = getOtaZipFilePath( - reactApplicationContext, - zipFileName - ), - onSuccess = { - Log.i(TAG, "OTA downloaded.") - promise.resolve(WritableNativeMap().also { - it.putString(DOWNLOAD_RESULT_OTA_PACKAGE_KEY, zipFileName) - }) - }, - onFailure = { - Log.e(TAG, "OTA download failed.") - promise.reject(OTA_DOWNLOAD_FAILED, it) - } - ) - } - - /** - * Accepts a structure: - * { - * otaDeploymentID: string, // current ota deployment id - * otaPackage: string, // the zip filename to unzip - * extractionDir: string, // the relative path to extract the bundle to - * } - * - * Generates a manifest.json: - * { - * otaDeploymentID: string, // current ota deployment id - * relativeBundlePath: string, // relative path to the index.*.bundle - * appVersion: string // Version number + version at the installation time - * } - */ - @ReactMethod - fun deploy(deployConfig: ReadableMap, promise: Promise) { - - val otaDeploymentID = deployConfig.getStringOrNull(DEPLOY_CONFIG_DEPLOYMENT_ID_KEY) - ?: return promise.reject( - INVALID_DEPLOY_CONFIG, - "Key $DEPLOY_CONFIG_DEPLOYMENT_ID_KEY is invalid." - ) - val zipFile = File( - getOtaZipFilePath( - reactApplicationContext, - deployConfig.getStringOrNull(DEPLOY_CONFIG_OTA_PACKAGE_KEY) - ?: return promise.reject( - INVALID_DEPLOY_CONFIG, - "Key $DEPLOY_CONFIG_OTA_PACKAGE_KEY is invalid." - ) - ) - ) - val extractionDir = File( - otaDir, - deployConfig.getStringOrNull(DEPLOY_CONFIG_EXTRACTION_DIR_KEY) - ?: return promise.reject( - INVALID_DEPLOY_CONFIG, - "Key $DEPLOY_CONFIG_EXTRACTION_DIR_KEY is invalid." - ) - ) - val oldManifest = readManifestJson(reactApplicationContext, fileBackend) - - Log.i(TAG, "Deploying ota with id: $otaDeploymentID") - - if (!zipFile.exists()) { - return reject(promise, OTA_ZIP_FILE_MISSING, "OTA package does not exist") - } - - if (extractionDir.exists()) { - Log.w(TAG, "Unzip directory exists. Removing it...") - fileBackend.deleteDirectory(extractionDir.absolutePath) - } - - try { - Log.i(TAG, "Unzipping bundle...") - fileBackend.unzip(zipFile, extractionDir) - - fileBackend.writeUnencryptedJson( - OtaManifest( - otaDeploymentID = otaDeploymentID, - relativeBundlePath = File( - extractionDir.relativeTo(File(otaDir)), - "index.android.bundle" - ).path, - appVersion = getAppVersion(reactApplicationContext) - ).toHasMap(), getOtaManifestFilepath(reactApplicationContext) - ) - - // Old bundle cleanup - val shouldRemoveOldBundle = - oldManifest != null && oldManifest.otaDeploymentID != otaDeploymentID - if (shouldRemoveOldBundle) { - File( - resolveAbsolutePathRelativeToOtaDir( - reactApplicationContext, - oldManifest!!.relativeBundlePath - ) - ).parentFile?.deleteRecursively() - } - zipFile.delete() - } catch (e: Exception) { - extractionDir.deleteRecursively() - return reject(promise, OTA_DEPLOYMENT_FAILED, "OTA deployment failed", e) - } - Log.i(TAG, "OTA deployed.") - promise.resolve(null) - } - - private fun generateZipFilename(): String { - return "${UUID.randomUUID()}.zip" - } - - private fun getOtaZipFilePath(context: Context, fileName: String): String = - resolveAbsolutePathRelativeToOtaDir(context, fileName) - - private fun reject( - promise: Promise, - code: String, - message: String, - throwable: Throwable? = null - ) { - Log.e(TAG, message) - promise.reject(code, message, throwable) - } -} - -private fun ReadableMap.getStringOrNull(key: String): String? { - return try { - this.getString(key) - } catch (e: Exception) { - null - } -} - -fun readManifestJson(context: Context, fileBackend: FileBackend): OtaManifest? { - return try { - val data = fileBackend.readAsUnencryptedFile(getOtaManifestFilepath(context)) - val json = JSONObject(String(data)) - return OtaManifest( - otaDeploymentID = json.getString(MANIFEST_OTA_DEPLOYMENT_ID_KEY), - relativeBundlePath = json.getString(MANIFEST_RELATIVE_BUNDLE_PATH_KEY), - appVersion = json.getString(MANIFEST_APP_VERSION_KEY) - ) - } catch (error: Exception) { - null - } -} - -data class OtaManifest( - val otaDeploymentID: String, - val relativeBundlePath: String, - val appVersion: String -) - -fun OtaManifest.toHasMap(): HashMap { - return hashMapOf( - MANIFEST_OTA_DEPLOYMENT_ID_KEY to otaDeploymentID, - MANIFEST_RELATIVE_BUNDLE_PATH_KEY to relativeBundlePath, - MANIFEST_APP_VERSION_KEY to appVersion - ) -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OTAJSBundleUrlProvider.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OTAJSBundleUrlProvider.kt deleted file mode 100644 index aab1964..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OTAJSBundleUrlProvider.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.mendix.mendixnative.react.ota - -import android.content.Context -import com.mendix.mendixnative.JSBundleFileProvider -import com.mendix.mendixnative.react.fs.FileBackend -import java.io.File - -/* -* Returns the OTA bundle's location URL if an OTA bundle has bee downloaded and deployed. -* It: -* - Reads the OTA manifest.json -* - Verifies current app version matches the OTA's deployed app version -* - Verifies a bundle exists in the location expected -* - Returns the absolute path to the OTA bundle if it succeeds -*/ -class OtaJSBundleUrlProvider : JSBundleFileProvider { - override fun getJSBundleFile(context: Context): String? { - val manifestFilePath = getOtaManifestFilepath(context) - if (!File(manifestFilePath).exists()) return null - - val manifest = readManifestJson(context, FileBackend(context)) - ?: return null - - // If the app version does not match the manifest version we assume the app has been updated/downgraded - // In this case do not use the OTA bundle. - if (manifest.appVersion != resolveAppVersion(context)) { - return null - } - val bundlePath = manifest.relativeBundlePath - val relativeBundlePath = resolveAbsolutePathRelativeToOtaDir(context, bundlePath) - if (!File(relativeBundlePath).exists()) return null - return relativeBundlePath - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt deleted file mode 100644 index ae376eb..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.mendix.mendixnative.react.ota - -import android.content.Context -import android.os.Build -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.ObjectMapper -import com.mendix.mendixnative.util.ResourceReader -import java.io.File -import java.util.* - -const val OTA_DIR_NAME = "Ota" -const val MANIFEST_FILE_NAME = "manifest.json" - -fun resolveAbsolutePathRelativeToOtaDir(context: Context, path: String): String = - File(getOtaDir(context), path).absolutePath - -fun getOtaDir(context: Context): String = File(context.filesDir.parent, OTA_DIR_NAME).absolutePath -fun getOtaManifestFilepath(context: Context): String = - resolveAbsolutePathRelativeToOtaDir(context, MANIFEST_FILE_NAME) - -fun resolveAppVersion(context: Context): String { - return context.packageManager.getPackageInfo( - context.packageName, - 0 - ).let { info -> - info.versionName.let { versionName -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - "$versionName-${info.longVersionCode}" - else - "$versionName-${info.versionCode}" - } - } -} - -fun getNativeDependencies(context: Context): Map { - var nativeDependencies = ResourceReader.readString(context, "native_dependencies") - if (nativeDependencies.isEmpty()) { - return emptyMap() - } - val typeRef = object : TypeReference>() {} - return ObjectMapper().readValue(nativeDependencies, typeRef).toMap() -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/splash/MendixSplashScreenModule.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/react/splash/MendixSplashScreenModule.kt deleted file mode 100644 index 6d8a1fb..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/react/splash/MendixSplashScreenModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.mendix.mendixnative.react.splash - -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod - -class MendixSplashScreenModule(private val presenter: MendixSplashScreenPresenter, reactContext: ReactApplicationContext?) : ReactContextBaseJavaModule(reactContext!!) { - override fun getName() = "MendixSplashScreen" - - @ReactMethod - fun show() = currentActivity?.let { - presenter.show(it) - } - - @ReactMethod - fun hide() = currentActivity?.let { - presenter.hide(it) - } -} diff --git a/android/mendixnative/src/main/java/com/mendix/mendixnative/request/MendixNetworkInterceptor.kt b/android/mendixnative/src/main/java/com/mendix/mendixnative/request/MendixNetworkInterceptor.kt deleted file mode 100644 index 7ec609c..0000000 --- a/android/mendixnative/src/main/java/com/mendix/mendixnative/request/MendixNetworkInterceptor.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.mendix.mendixnative.request - -import com.mendix.mendixnative.config.AppUrl -import com.mendix.mendixnative.encryption.decryptValue -import com.mendix.mendixnative.encryption.encryptValue -import com.mendix.mendixnative.react.MxConfiguration -import okhttp3.* -import okhttp3.HttpUrl.Companion.toHttpUrl -import java.util.* - -/** - * OkHttp interceptor handling cookie encryption for all app related cookies that use the React Native - * OkHttp factory to get a client. - */ -class MendixNetworkInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val requestUrl = request.url - val runtimeUrl = AppUrl.forRuntime(MxConfiguration.runtimeUrl).toHttpUrl() - - return if (runtimeUrl.host != requestUrl.host) - chain.proceed(request) - else - chain.proceed(request.withDecryptedCookies()).withEncryptedCookies() - } -} - -const val ivDelimiter = - "___enc___" // Delimits encoded value and Initialization Vector used by encryption -const val encryptedCookieKeyPrefix = "MxEnc" // Prefix for encrypted cookie keys - -/** - * Request extension to decrypt possibly encrypted cookies - */ -fun Request.withDecryptedCookies(): Request { - val cookiePairs = this.header("Cookie")?.split("; ") - val encryptedCookieExists = cookiePairs?.any { cookie -> cookie.startsWith(encryptedCookieKeyPrefix) } - val decryptedCookies = cookiePairs?.map { - val (key, value) = it.split("=", limit = 2) - - if(encryptedCookieExists!! && key.startsWith(encryptedCookieKeyPrefix)){ - val params = cookieValueToDecryptionParams(value) - val decryptedValue = decryptValue(params.first, params.second) - - return@map "${key.removePrefix(encryptedCookieKeyPrefix)}=$decryptedValue" - } else if (!encryptedCookieExists) { - return@map it; - } - - return@map null - }?.filterNotNull()?.joinToString(separator = "; ") - - return when { - decryptedCookies != null && decryptedCookies.isNotBlank() -> this.newBuilder() - .removeHeader("Cookie") - .addHeader("Cookie", decryptedCookies).build() - else -> this - } -} - -/** - * Response extension to encrypt cookies - * It maps the cookies to pairs that represent the encrypted cookie to be set and a version of its unencrypted - * equivalent to be removed. - * Finally it iterates over the pairs and creates Set-Cookie headers both for setting the encrypted cookie - * and removing the unencrypted cookie. - */ -fun Response.withEncryptedCookies(): Response { - val cookies = Cookie.parseAll(this.request.url, this.headers) - val encryptedCookiesPairs = cookies.map { - val newCookie = makeCookie( - name = getEncryptedCookieName(it.name), - value = encryptionResultToCookieValue(encryptValue(it.value)), - hostOnlyDomain = it.domain, - path = it.path, - httpOnly = it.httpOnly, - secure = it.secure, - expiresAt = it.expiresAt) - val unencryptedExpiredCookie = - makeCookie(it.name, "", it.domain, it.path, it.httpOnly, it.secure, -1) - return@map Pair(newCookie, unencryptedExpiredCookie) - } - val headerBuilder = this.headers.newBuilder() - headerBuilder.removeAll("Set-Cookie") - encryptedCookiesPairs.forEach { - headerBuilder.add("Set-Cookie", it.first.toString()) - headerBuilder.add("Set-Cookie", it.second.toString()) - } - return this.newBuilder().headers(headerBuilder.build()).build() -} - -fun makeCookie( - name: String, - value: String, - hostOnlyDomain: String, - path: String, - httpOnly: Boolean, - secure: Boolean, - expiresAt: Long, -): Cookie { - return Cookie.Builder().let { - it.name(name).value(value).hostOnlyDomain(hostOnlyDomain).path(path).expiresAt(expiresAt) - if (httpOnly) it.httpOnly() - if (secure) it.secure() - it.build() - } -} - -fun getEncryptedCookieName(name: String) = "$encryptedCookieKeyPrefix${name}" - -fun cookieValueToDecryptionParams(value: String): Pair { - val parts = value.split(ivDelimiter) - return Pair(parts[0], if (parts.size > 1) parts[1] else null) -} - - -fun encryptionResultToCookieValue(triple: Triple): String { - return "\"${triple.first.decodeToString()}${if (triple.third) "${ivDelimiter}${triple.second!!.decodeToString()}" else ""}\"".replace( - "\n".toRegex(), - "") -} diff --git a/android/mendixnative/src/main/res/layout/app_menu_layout.xml b/android/mendixnative/src/main/res/layout/app_menu_layout.xml deleted file mode 100644 index 3b67188..0000000 --- a/android/mendixnative/src/main/res/layout/app_menu_layout.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - -