diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 0000000..9509231 --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,34 @@ +name: Secret Scan + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + workflow_dispatch: + +permissions: + contents: read + security-events: write + +jobs: + gitleaks: + name: Gitleaks Scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run gitleaks + id: gitleaks + uses: gitleaks/gitleaks-action@v2 + with: + config-path: .gitleaks.toml + args: --report-format sarif --report-path gitleaks.sarif + + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: gitleaks.sarif diff --git a/.gitignore b/.gitignore index d741fcd..821dab1 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ DerivedData project.xcworkspace *.env.local ios/Pods +example/ios/Pods +ios/build +example/vendor # Android/IJ # diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..1faa223 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,71 @@ +title = "mendix-native gitleaks config" +# Base config uses gitleaks defaults; we extend with allowlists and a few custom regexes + +[allowlist] + description = "Global allowlist" + files = [ + "yarn.lock", + "package-lock.json", + "pnpm-lock.yaml", + "gradlew", + "gradlew.bat", + "example/ios/Pods/", + "example/android/" + ] + regexes = [ + # Common false positives + '''(?i)localhost(:[0-9]{2,5})?''', + '''(?i)internal-slot''', + '''(?i)eastasianwidth''' + ] + +[[rules]] + id = "generic-api-key" + description = "Generic API key format" + regex = '''(?i)(api|access|auth)[_-]?key["'\s:=]+[A-Za-z0-9_\-]{16,}''' + tags = ["api", "key", "generic"] + +[[rules]] + id = "bearer-token-inline" + description = "Potential hard-coded bearer token" + regex = '''Bearer\s+[A-Za-z0-9\-_.]{20,}''' + tags = ["auth", "token"] + +[[rules]] + id = "jwt" + description = "JSON Web Token" + regex = '''eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}''' + tags = ["jwt", "token"] + +[[rules]] + id = "aws-access-key" + description = "AWS Access Key ID" + regex = '''AKIA[0-9A-Z]{16}''' + tags = ["aws", "key"] + +[[rules]] + id = "github-token" + description = "GitHub Personal Access Token" + regex = '''ghp_[A-Za-z0-9]{36,}''' + tags = ["github", "token"] + +[[rules]] + id = "slack-token" + description = "Slack token" + regex = '''xox[baprs]-[A-Za-z0-9\-]{10,}''' + tags = ["slack", "token"] + +[[rules]] + id = "stripe-secret-key" + description = "Stripe live secret key" + regex = '''sk_live_[0-9a-zA-Z]{10,}''' + tags = ["stripe", "secret"] + +[[rules]] + id = "private-key-block" + description = "Private key block" + regex = '''-----BEGIN (EC|RSA|DSA|OPENSSH|PRIVATE) KEY-----''' + tags = ["crypto", "private-key"] + +[whitelist] # backward compatibility for older gitleaks versions + description = "Legacy whitelist alias" diff --git a/.yarn/patches/@op-engineering-op-sqlite-npm-15.0.7-39fbf4933a.patch b/.yarn/patches/@op-engineering-op-sqlite-npm-15.0.7-39fbf4933a.patch new file mode 100644 index 0000000..d8c4e7e --- /dev/null +++ b/.yarn/patches/@op-engineering-op-sqlite-npm-15.0.7-39fbf4933a.patch @@ -0,0 +1,256 @@ +diff --git a/android/build.gradle b/android/build.gradle +index d36fd855813e87b17da43156be64782b325b2733..751355645c0b6e28e2df01e9bdc32f545d8dc83c 100644 +--- a/android/build.gradle ++++ b/android/build.gradle +@@ -1,5 +1,4 @@ + import java.nio.file.Paths +-import groovy.json.JsonSlurper + + buildscript { + ext.getExtOrDefault = {name -> +@@ -27,57 +26,16 @@ def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["OPSQLite_" + name]).toInteger() + } + +-def useSQLCipher = false ++def useSQLCipher = true + def useLibsql = false + def useCRSQLite = false +-def performanceMode = false ++def performanceMode = true + def sqliteFlags = "" + def enableFTS5 = false + def useSqliteVec = false + def enableRtree = false + def tokenizers = [] + +-// On the example app, the package.json is located at the root of the project +-// On the user app, the package.json is located at the root of the node_modules directory +-def isUserApp = rootDir.absolutePath.contains("node_modules") +-def packageJsonFile +- +-if (isUserApp) { +- // Start from the root + 1 level up (to avoid detecting the op-sqlite/package.json) and traverse upwards to find the first package.json +- File currentDir = new File("$rootDir/../") +- packageJsonFile = null +- +- // Try to find package.json by traversing upwards +- while (currentDir != null) { +- File potential = new File(currentDir, "package.json") +- if (potential.exists()) { +- packageJsonFile = potential +- break +- } +- currentDir = currentDir.parentFile +- } +-} else { +- packageJsonFile = new File("$rootDir/../package.json") +-} +- +- +-def packageJson = new JsonSlurper().parseText(packageJsonFile.text) +- +-def opsqliteConfig = packageJson["op-sqlite"] +- +-if(opsqliteConfig) { +- println "[OP-SQLITE] Detected op-sqlite config from package.json at: " + packageJsonFile.absolutePath +- useSQLCipher = opsqliteConfig["sqlcipher"] +- useCRSQLite = opsqliteConfig["crsqlite"] +- useSqliteVec = opsqliteConfig["sqliteVec"] +- performanceMode = opsqliteConfig["performanceMode"] +- sqliteFlags = opsqliteConfig["sqliteFlags"] ? opsqliteConfig["sqliteFlags"] : "" +- enableFTS5 = opsqliteConfig["fts5"] +- useLibsql = opsqliteConfig["libsql"] +- enableRtree = opsqliteConfig["rtree"] +- tokenizers = opsqliteConfig["tokenizers"] ? opsqliteConfig["tokenizers"] : [] +-} +- + if(useSQLCipher) { + println "[OP-SQLITE] using sqlcipher." + } else if(useLibsql) { +diff --git a/android/cpp-adapter.cpp b/android/cpp-adapter.cpp +index 8feaf7719661ef248113f11b1643deedb4b510af..2393963bf982f80ccff2ed396f1538be7ee18fcb 100644 +--- a/android/cpp-adapter.cpp ++++ b/android/cpp-adapter.cpp +@@ -19,8 +19,8 @@ struct OPSQLiteBridge : jni::JavaClass { + static void registerNatives() { + javaClassStatic()->registerNatives( + {makeNativeMethod("installNativeJsi", OPSQLiteBridge::installNativeJsi), +- makeNativeMethod("clearStateNativeJsi", +- OPSQLiteBridge::clearStateNativeJsi)}); ++ makeNativeMethod("clearStateNativeJsi", OPSQLiteBridge::clearStateNativeJsi), ++ makeNativeMethod("deleteAllDBsJsi", OPSQLiteBridge::deleteAllDBsJsi)}); + } + + private: +@@ -39,6 +39,10 @@ private: + static void clearStateNativeJsi(jni::alias_ref thiz) { + opsqlite::invalidate(); + } ++ ++ static bool deleteAllDBsJsi(jni::alias_ref thiz) { ++ return opsqlite::deleteAllDbs(); ++ } + }; + + JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { +diff --git a/android/src/main/java/com/op/sqlite/OPSQLiteBridge.kt b/android/src/main/java/com/op/sqlite/OPSQLiteBridge.kt +index 44f86df6a21a6f7272b2c79b196586ef8fec886b..9d9f7100fd34361701b2addf09a4f36e33b35d56 100644 +--- a/android/src/main/java/com/op/sqlite/OPSQLiteBridge.kt ++++ b/android/src/main/java/com/op/sqlite/OPSQLiteBridge.kt +@@ -12,6 +12,7 @@ class OPSQLiteBridge { + docPath: String + ) + private external fun clearStateNativeJsi() ++ private external fun deleteAllDBsJsi(): Boolean + + fun install(context: ReactContext) { + val jsContextPointer = context.javaScriptContextHolder!!.get() +@@ -31,6 +32,10 @@ class OPSQLiteBridge { + clearStateNativeJsi() + } + ++ fun deleteAllDBs() { ++ deleteAllDBsJsi() ++ } ++ + companion object { + val instance = OPSQLiteBridge() + } +diff --git a/android/src/main/java/com/op/sqlite/OPSQLiteModule.kt b/android/src/main/java/com/op/sqlite/OPSQLiteModule.kt +index 688832fa2f9a7f91d16cd50495caa8c9f8873864..9ea814bfa63f27356e804b82e941b7121152db3a 100644 +--- a/android/src/main/java/com/op/sqlite/OPSQLiteModule.kt ++++ b/android/src/main/java/com/op/sqlite/OPSQLiteModule.kt +@@ -13,7 +13,7 @@ import java.io.OutputStream + import com.facebook.react.util.RNLog; + + //@ReactModule(name = OPSQLiteModule.NAME) +-internal class OPSQLiteModule(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { ++class OPSQLiteModule(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { + override fun getName(): String { + return NAME + } +@@ -56,6 +56,17 @@ internal class OPSQLiteModule(context: ReactApplicationContext?) : ReactContextB + return true + } + ++ @ReactMethod(isBlockingSynchronousMethod = true) ++ fun closeAllConnections() { ++ OPSQLiteBridge.instance.invalidate() ++ } ++ ++ @ReactMethod(isBlockingSynchronousMethod = true) ++ fun deleteAllDBs() { ++ OPSQLiteBridge.instance.deleteAllDBs(); ++ } ++ ++ + @ReactMethod + fun moveAssetsDatabase(args: ReadableMap, promise: Promise) { + val filename = args.getString("filename")!! +diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp +index 85710eea286d45685aa526ed3851e8f1e1411039..8cf10f21ba467dea430aab106d43dd4e2adeacd6 100644 +--- a/cpp/DBHostObject.cpp ++++ b/cpp/DBHostObject.cpp +@@ -889,6 +889,10 @@ void DBHostObject::invalidate() { + #endif + } + ++void DBHostObject::drop() { ++ opsqlite_remove(db, db_name, std::string(base_path)); ++} ++ + DBHostObject::~DBHostObject() { invalidate(); } + + } // namespace opsqlite +diff --git a/cpp/DBHostObject.h b/cpp/DBHostObject.h +index cc174b7c8c5ce500a6ffe5dc6fe092d282d2554c..ff36f742a22b8a84f37d6dd28441dbe9d0c6c873 100644 +--- a/cpp/DBHostObject.h ++++ b/cpp/DBHostObject.h +@@ -73,6 +73,7 @@ class JSI_EXPORT DBHostObject : public jsi::HostObject { + void on_commit(); + void on_rollback(); + void invalidate(); ++ void drop(); + ~DBHostObject() override; + + private: +diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp +index 5e1c1de234e7bdb131769728fc862d389f9995a5..dc21c6503ffe18f3ae1cf99f327e8aa1fc587b71 100644 +--- a/cpp/bindings.cpp ++++ b/cpp/bindings.cpp +@@ -36,6 +36,13 @@ void invalidate() { + dbs.clear(); + } + ++bool deleteAllDbs() { ++ for(const auto &db : dbs) { ++ db->drop(); ++ } ++ return true; ++} ++ + void install(jsi::Runtime &rt, + const std::shared_ptr &invoker, + const char *base_path, const char *crsqlite_path, +diff --git a/op-sqlite.podspec b/op-sqlite.podspec +index 375cc3ef0838a3cffb87ec970f636880a8676bb3..e6fce21630ed00aa863f2baae7b3d04de783dcb0 100644 +--- a/op-sqlite.podspec ++++ b/op-sqlite.podspec +@@ -1,4 +1,3 @@ +-require "json" + require_relative "./generate_tokenizers_header_file" + + log_message = lambda do |message| +@@ -39,11 +38,10 @@ else + app_package = JSON.parse(File.read(File.join(__dir__, "example", "package.json"))) + end + +-op_sqlite_config = app_package["op-sqlite"] +-use_sqlcipher = false ++use_sqlcipher = true + use_crsqlite = false + use_libsql = false +-performance_mode = false ++performance_mode = true + phone_version = false + sqlite_flags = "" + fts5 = false +@@ -51,37 +49,6 @@ rtree = false + use_sqlite_vec = false + tokenizers = [] + +-if(op_sqlite_config != nil) +- use_sqlcipher = op_sqlite_config["sqlcipher"] == true +- use_crsqlite = op_sqlite_config["crsqlite"] == true +- use_libsql = op_sqlite_config["libsql"] == true +- performance_mode = op_sqlite_config["performanceMode"] || false +- phone_version = op_sqlite_config["iosSqlite"] == true +- sqlite_flags = op_sqlite_config["sqliteFlags"] || "" +- fts5 = op_sqlite_config["fts5"] == true +- rtree = op_sqlite_config["rtree"] == true +- use_sqlite_vec = op_sqlite_config["sqliteVec"] == true +- tokenizers = op_sqlite_config["tokenizers"] || [] +-end +- +-if phone_version then +- if use_sqlcipher then +- raise "SQLCipher is not supported with phone version" +- end +- +- if use_crsqlite then +- raise "CRSQLite is not supported with phone version" +- end +- +- if rtree then +- raise "RTree is not supported with phone version" +- end +- +- if use_sqlite_vec then +- raise "SQLite Vec is not supported with phone version" +- end +-end +- + Pod::Spec.new do |s| + s.name = "op-sqlite" + s.version = package["version"] diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..11969f7 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,2 @@ +nodeLinker: node-modules +nmHoistingLimits: workspaces \ No newline at end of file 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..0a44d04 100644 --- a/LICENSE +++ b/LICENSE @@ -199,3 +199,4 @@ Apache License 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. + \ No newline at end of file diff --git a/MendixNative.podspec b/MendixNative.podspec new file mode 100644 index 0000000..178c100 --- /dev/null +++ b/MendixNative.podspec @@ -0,0 +1,28 @@ +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.public_header_files = "ios/**/*.h" + s.private_header_files = "ios/**/*.h" + + s.dependency "SSZipArchive" + s.dependency "RNCAsyncStorage" + s.dependency "ReactCommon" + s.dependency "ReactAppDependencyProvider" + s.dependency 'React-Core' + s.dependency 'React-RCTAppDelegate' + + 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 e708b1c..0000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ 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/mendix.gradle b/android/mendix.gradle new file mode 100644 index 0000000..88f9e4a --- /dev/null +++ b/android/mendix.gradle @@ -0,0 +1,438 @@ +import groovy.json.JsonSlurper + +def LOG_PREFIX = ":Mendix: " + +def rootDir = buildscript.sourceFile.toString().split("node_modules")[0] +def cliBinPath = "${rootDir}/node_modules/.bin/react-native${System.properties['os.name'].toLowerCase().contains('windows') ? ".cmd" : ""}" + +def generatedFilePackage = "com.mendix.nativetemplate" +def mainActivityObserverFileName = "MendixActivityObserver.java" +def mainActivityObserverTemplate = """package $generatedFilePackage; + +import android.content.Context; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +{{imports}} + +public class MendixActivityObserver implements LifecycleObserver { + private final Context context; + + public MendixActivityObserver(Context activity) { + this.context = activity; + } + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + void onCreate() { + {{onCreate}} + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + void onResume() { + {{onResume}} + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + void onStart() { + {{onStart}} + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + void onPause() { + {{onPause}} + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + void onStop() { + {{onStop}} + } + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + void onDestroy() { + {{onDestroy}} + } +} +""" +def mendixPackageListFileName = "MendixPackageList.java" +def mendixPackageListTemplate = """package $generatedFilePackage; + +import android.app.Application; +import android.content.Context; +import android.content.res.Resources; + +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainPackageConfig; +import com.facebook.react.shell.MainReactPackage; +import java.util.Arrays; +import java.util.ArrayList; + +{{imports}} + +public class MendixPackageList { + private Application application; + + public MendixPackageList(Application application) { + this.application = application; + } + + private Resources getResources() { + return this.getApplication().getResources(); + } + + private Application getApplication() { + return this.application; + } + + private Context getApplicationContext() { + return this.getApplication().getApplicationContext(); + } + + public ArrayList getPackages() { + return new ArrayList<>(Arrays.asList( + {{packageClassInstances}} + )); + } +} +""" + +class MendixModules { + private static String LINE_ENDING_CHAR = "\n" + + private String cliBinPath + private String rootDir + private String logPrefix + private Logger logger + private ArrayList> reactNativeModules + private Map dependenciesConfig = [:] + private File capabilitiesConfigFile + private File projectCapabilitiesFile + private File nodeModulesDependenciesConfigFile + + MendixModules(File capabilitiesConfigFile, File nodeModulesDependenciesConfigFile, File projectCapabilitiesFile, String cliBinPath, String rootDir, Logger logger, String logPrefix) { + this.logger = logger + this.rootDir = rootDir + this.cliBinPath = cliBinPath + this.logPrefix = logPrefix + this.capabilitiesConfigFile = capabilitiesConfigFile + this.nodeModulesDependenciesConfigFile = nodeModulesDependenciesConfigFile + this.projectCapabilitiesFile = projectCapabilitiesFile + + def (nativeModules) = this.getReactNativeConfig() + this.reactNativeModules = nativeModules + parseDependenciesConfig() + } + + void printDependencies() { + this.reactNativeModules.each { + logDebug(it["name"]) + } + } + + void parseDependenciesConfig() { + def dependenciesConfig = [:] + def capabilitiesConfig = [:] + + try { + capabilitiesConfig = new JsonSlurper().parse(this.capabilitiesConfigFile) + def projectCapabilities = new JsonSlurper().parse(this.projectCapabilitiesFile) + capabilitiesConfig.retainAll { capabilityConfig -> + projectCapabilities.find { enabledCapability -> + enabledCapability.key == capabilityConfig.key && enabledCapability.value == true + } && capabilityConfig.value["android"] != null + } + } catch (ignored) { + this.logLifecycle("Failed parsing the capabilities file. Error?") + } + + if (this.nodeModulesDependenciesConfigFile.exists()) { + try { + dependenciesConfig = new JsonSlurper().parse(this.nodeModulesDependenciesConfigFile) + (dependenciesConfig as Map).retainAll { dependencyConfig -> + this.reactNativeModules.find { nativeModule -> + nativeModule.get("name") == dependencyConfig.key + } && dependencyConfig.value["android"] != null + } + } catch (ignored) { + this.logLifecycle("Failed parsing the configuration for unlinked node_modules. Error?") + } + } + + this.dependenciesConfig = capabilitiesConfig + dependenciesConfig + printDependencies() + } + + void generateMainActivityObserver(File outDir, String fileName, String template) { + def activityImports = [] + def activityOnCreateEntries = [] + def activityOnStartEntries = [] + def activityOnResumeEntries = [] + def activityOnPauseEntries = [] + def activityOnStopEntries = [] + def activityOnDestroyEntries = [] + + dependenciesConfig.each { + def mainActivityDelegateEntry = it.value["android"]["MainActivity"] + if (!mainActivityDelegateEntry) + return + + def imports = mainActivityDelegateEntry.get("imports") + if (imports) + activityImports.addAll(imports) + + def onCreateEntries = mainActivityDelegateEntry.get("onCreate") + if (onCreateEntries) + activityOnCreateEntries.addAll(onCreateEntries) + + def onStartEntries = mainActivityDelegateEntry.get("onStart") + if (onStartEntries) + activityOnStartEntries.addAll(onStartEntries) + + def onResumeEntries = mainActivityDelegateEntry.get("onResume") + if (onResumeEntries) + activityOnResumeEntries.addAll(onResumeEntries) + + def onPauseEntries = mainActivityDelegateEntry.get("onPause") + if (onPauseEntries) + activityOnPauseEntries.addAll(onPauseEntries) + + def onStopEntries = mainActivityDelegateEntry.get("onStop") + if (onStopEntries) + activityOnStopEntries.addAll(onStopEntries) + + def onDestroyEntries = mainActivityDelegateEntry.get("onDestroy") + if (onDestroyEntries) + activityOnDestroyEntries.addAll(onDestroyEntries) + } + + String CODE_PADDING = "${LINE_ENDING_CHAR} " + String generatedFileContents = template + .replace("{{imports}}", activityImports.join(LINE_ENDING_CHAR)) + .replace("{{onCreate}}", activityOnCreateEntries.join(CODE_PADDING)) + .replace("{{onStart}}", activityOnStartEntries.join(CODE_PADDING)) + .replace("{{onResume}}", activityOnResumeEntries.join(CODE_PADDING)) + .replace("{{onPause}}", activityOnPauseEntries.join(CODE_PADDING)) + .replace("{{onStop}}", activityOnStopEntries.join(CODE_PADDING)) + .replace("{{onDestroy}}", activityOnDestroyEntries.join(CODE_PADDING)) + + outDir.mkdirs() + new FileTreeBuilder(outDir).file(fileName).newWriter().withWriter { + w -> + w << generatedFileContents + } + } + + void generateMendixPackageList(File outDir, String fileName, String template) { + String CODE_PADDING = "${LINE_ENDING_CHAR} " + def imports = [] + def entries = [] + def entrySeparator = "," + CODE_PADDING + dependenciesConfig.each { + def packageListEntry = it.value["android"]["packageListEntries"] + if (packageListEntry) { + def importsEntry = packageListEntry["imports"] + def packageClassInstances = packageListEntry["packageClassInstances"] + if (importsEntry) + imports.addAll(importsEntry) + if (packageClassInstances) + entries.addAll(packageClassInstances) + } + } + + String generatedFileContents = template.replace("{{imports}}", imports.join(CODE_PADDING)).replace("{{packageClassInstances}}", entries.join(entrySeparator)) + + outDir.mkdirs() + new FileTreeBuilder(outDir).file(fileName).newWriter().withWriter { + w -> + w << generatedFileContents + } + } + + void addClassPaths(Project project) { + project.buildscript { + dependencies { + dependenciesConfig.each { + def gradle = (it.value as Object)["android"]["gradle"] + if (!gradle) { + return + } + def customClassPaths = gradle.get("classpaths") as ArrayList + customClassPaths.each { customClassPath -> + this.logLifecycle("Adding classPath ${customClassPath}") + classpath(customClassPath) + } + } + } + } + } + + void addExtraDependencies(Project project) { + project.dependencies { + dependenciesConfig.each { + def dependencies = it.value["android"]["externalDependencies"] as ArrayList + dependencies.each { dependency -> + this.logLifecycle("Registering extra library ${dependency}") + implementation(dependency) + } + } + } + } + + void addAndroidPlugins(Project project) { + dependenciesConfig.each { + def gradleConfig = it.value["android"]["gradle"] + if (!gradleConfig) + return + + def dependencies = gradleConfig["plugins"] as ArrayList + if (!dependencies) + return + + dependencies.each { plugin -> + this.logLifecycle("Adding plugin ${plugin}") + project.getPluginManager().apply(plugin) + } + } + } + + void logDebug(String message) { + this.logger.debug("${this.logPrefix}${message}") + } + + void logLifecycle(String message) { + this.logger.lifecycle("${this.logPrefix}${message}") + } + + void logError(String message) { + this.logger.error("${this.logPrefix}${message}") + } + + /** + * Runs a specified command using Runtime exec() in a specified directory. + * Throws when the command result is empty. + */ + String getCommandOutput(String[] command, File directory) { + try { + def output = "" + def cmdProcess = Runtime.getRuntime().exec(command, null, directory) + def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream())) + def buff = "" + def readBuffer = new StringBuffer() + while ((buff = bufferedReader.readLine()) != null) { + readBuffer.append(buff) + } + output = readBuffer.toString() + if (!output) { + this.logger.error("${logPrefix}Unexpected empty result of running '${command}' command.") + def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream())) + def errBuff = "" + def readErrorBuffer = new StringBuffer() + while ((errBuff = bufferedErrorReader.readLine()) != null) { + readErrorBuffer.append(errBuff) + } + throw new Exception(readErrorBuffer.toString()) + } + return output + } catch (Exception exception) { + this.logError("Running '${command}' command failed.") + throw exception + } + } + + /** + * Runs a process to call the React Native CLI Config command and parses the output + */ + ArrayList> getReactNativeConfig() { + if (this.reactNativeModules != null) return this.reactNativeModules + + ArrayList> reactNativeModules = new ArrayList>() + + String[] reactNativeConfigCommand = [this.cliBinPath, "config"] + def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, new File(this.rootDir)) + + def json + try { + json = new JsonSlurper().parseText(reactNativeConfigOutput) + } catch (Exception exception) { + throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}"); + } + def dependencies = json["dependencies"] + def project = json["project"]["android"] + + if (project == null) { + throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}") + } + + dependencies.each { name, value -> + def platformsConfig = value["platforms"]; + def androidConfig = platformsConfig["android"] + + if (androidConfig != null && androidConfig["sourceDir"] != null) { + this.logger.info("${logPrefix}Automatically adding native module '${name}'") + + HashMap reactNativeModuleConfig = new HashMap() + reactNativeModuleConfig.put("name", name) + reactNativeModuleConfig.put("nameCleansed", name.replaceAll('^@([\\w-]+)/', '$1_')) + reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"]) + reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"]) + reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"]) + this.logger.trace("${logPrefix}'${name}': ${reactNativeModuleConfig.toMapString()}") + + reactNativeModules.add(reactNativeModuleConfig) + } else { + this.logger.info("${logPrefix}Skipping native module '${name}'") + } + } + + return [reactNativeModules, json["project"]["android"]["packageName"]]; + } +} + +def generatedSrcDir = new File(buildDir, "generated/mendix/src/main/java") +def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/')) + +def capabilitiesConfig = new File("${rootDir}capabilities-setup-config.json") +def unlinkedDependenciesConfigFile = new File("${rootDir}unlinked-dependency-config.json") +def capabilitiesFile = new File("${rootDir}capabilities.android.json") +def mendixModules = new MendixModules(capabilitiesConfig, unlinkedDependenciesConfigFile, capabilitiesFile, cliBinPath, rootDir, logger, LOG_PREFIX) + +def logLifecycle = { String message -> logger.lifecycle("${LOG_PREFIX}${message}") } + + +ext.applyMendixGradle = { Project project -> + logLifecycle("Registering extra dependencies") + mendixModules.addExtraDependencies(project) + + logLifecycle("Registering plugins") + mendixModules.addAndroidPlugins(project) + task generateMendixDependencies { + doLast { + logLifecycle("Executing Mendix Module Generator") + logLifecycle("App root: ${rootDir}") + logLifecycle("CLI path: ${cliBinPath}") + + logLifecycle("Generating ${mainActivityObserverFileName}") + mendixModules.generateMainActivityObserver(generatedCodeDir, mainActivityObserverFileName, mainActivityObserverTemplate) + + logLifecycle("Generating ${mendixPackageListFileName}") + mendixModules.generateMendixPackageList(generatedCodeDir, mendixPackageListFileName, mendixPackageListTemplate) + } + } + + preBuild.dependsOn generateMendixDependencies + + android { + sourceSets { + main { + java { + srcDirs += generatedSrcDir + } + } + } + } +} + +ext.applyMendixClassPaths = { Project project -> + logLifecycle("Registering class paths") + mendixModules.addClassPaths(project) +} 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/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 @@ - - - -