diff --git a/.github/workflows/android-appcenter.yml b/.github/workflows/android-browserstack.yml
similarity index 61%
rename from .github/workflows/android-appcenter.yml
rename to .github/workflows/android-browserstack.yml
index ad4e75404..604ceeafb 100644
--- a/.github/workflows/android-appcenter.yml
+++ b/.github/workflows/android-browserstack.yml
@@ -1,18 +1,18 @@
-name: Android AppCenter Tests
+name: Android BrowserStack Tests
 
 on:
   workflow_dispatch:
   push:
     branches: [ master ]
     paths:
-      - '.github/workflows/android-appcenter.yml'
+      - '.github/workflows/android-browserstack.yml'
       - 'sdk/android/PicovoiceTestApp/**'
       - 'resources/.test/**'
       - 'resources/audio_samples/**'
   pull_request:
     branches: [ master, 'v[0-9]+.[0-9]+' ]
     paths:
-      - '.github/workflows/android-appcenter.yml'
+      - '.github/workflows/android-browserstack.yml'
       - 'sdk/android/PicovoiceTestApp/**'
       - 'resources/.test/**'
       - 'resources/audio_samples/**'
@@ -23,7 +23,7 @@ defaults:
 
 jobs:
   build:
-    name: Run Android Tests on AppCenter
+    name: Run Android Tests on BrowserStack
     runs-on: ubuntu-latest
 
     steps:
@@ -32,18 +32,17 @@ jobs:
       with:
         submodules: recursive
 
-    - name: Set up Node.js LTS
-      uses: actions/setup-node@v3
+    - name: Installing Python
+      uses: actions/setup-python@v5
       with:
-        node-version: lts/*
+        python-version: '3.10'
+    - run:
+        pip3 install requests
 
-    - name: Install AppCenter CLI
-      run: npm install -g appcenter-cli
-
-    - name: set up JDK 11
+    - name: set up JDK 17
       uses: actions/setup-java@v3
       with:
-        java-version: '11'
+        java-version: '17'
         distribution: 'temurin'
 
     - name: Copy test_resources
@@ -71,18 +70,18 @@ jobs:
     - name: Build androidTest
       run: ./gradlew assembleEnDebugAndroidTest
 
-    - name: Run tests on AppCenter
-      run: appcenter test run espresso
-        --token ${{secrets.APPCENTERAPITOKEN}}
-        --app "Picovoice/Picovoice-Android-Activity"
-        --devices "Picovoice/android-min-max"
-        --app-path picovoice-test-app/build/outputs/apk/en/debug/picovoice-test-app-en-debug.apk
-        --test-series "porcupine-android"
-        --locale "en_US"
-        --build-dir picovoice-test-app/build/outputs/apk/androidTest/en/debug
+    - name: Run tests on BrowserStack
+      run: python3 ../../../script/automation/browserstack.py
+        --type espresso
+        --username "${{secrets.BROWSERSTACK_USERNAME}}"
+        --access_key "${{secrets.BROWSERSTACK_ACCESS_KEY}}"
+        --project_name "Picovoice-Android"
+        --devices "android-min-max"
+        --app_path "picovoice-test-app/build/outputs/apk/en/debug/picovoice-test-app-en-debug.apk"
+        --test_path "picovoice-test-app/build/outputs/apk/androidTest/en/debug/picovoice-test-app-en-debug-androidTest.apk"
 
   build-integ:
-    name: Run Android Integration Tests on AppCenter
+    name: Run Android Integration Tests on BrowserStack
     runs-on: ubuntu-latest
 
     steps:
@@ -91,18 +90,17 @@ jobs:
       with:
         submodules: recursive
 
-    - name: Set up Node.js LTS
-      uses: actions/setup-node@v3
+    - name: Installing Python
+      uses: actions/setup-python@v5
       with:
-        node-version: lts/*
-
-    - name: Install AppCenter CLI
-      run: npm install -g appcenter-cli
+        python-version: '3.10'
+    - run:
+        pip3 install requests
 
-    - name: set up JDK 11
+    - name: set up JDK 17
       uses: actions/setup-java@v3
       with:
-        java-version: '11'
+        java-version: '17'
         distribution: 'temurin'
 
     - name: Copy test_resources
@@ -130,12 +128,12 @@ jobs:
     - name: Build androidTest
       run: ./gradlew assembleEnReleaseAndroidTest -DtestBuildType=integ
 
-    - name: Run tests on AppCenter
-      run: appcenter test run espresso
-        --token ${{secrets.APPCENTERAPITOKEN}}
-        --app "Picovoice/Picovoice-Android-Activity"
-        --devices "Picovoice/android-min-max"
-        --app-path picovoice-test-app/build/outputs/apk/en/release/picovoice-test-app-en-release.apk
-        --test-series "picovoice-android"
-        --locale "en_US"
-        --build-dir picovoice-test-app/build/outputs/apk/androidTest/en/release
+    - name: Run tests on BrowserStack
+      run: python3 ../../../script/automation/browserstack.py
+        --type espresso
+        --username "${{secrets.BROWSERSTACK_USERNAME}}"
+        --access_key "${{secrets.BROWSERSTACK_ACCESS_KEY}}"
+        --project_name "Picovoice-Android-Integration"
+        --devices "android-min-max"
+        --app_path "picovoice-test-app/build/outputs/apk/en/release/picovoice-test-app-en-release.apk"
+        --test_path "picovoice-test-app/build/outputs/apk/androidTest/en/release/picovoice-test-app-en-release-androidTest.apk"
diff --git a/.github/workflows/android-demos.yml b/.github/workflows/android-demos.yml
index 0479a91b3..612370b80 100644
--- a/.github/workflows/android-demos.yml
+++ b/.github/workflows/android-demos.yml
@@ -27,10 +27,10 @@ jobs:
     steps:
     - uses: actions/checkout@v3
 
-    - name: set up JDK 11
+    - name: set up JDK 17
       uses: actions/setup-java@v3
       with:
-        java-version: '11'
+        java-version: '17'
         distribution: 'temurin'
 
     - name: Build English
@@ -48,10 +48,10 @@ jobs:
     steps:
     - uses: actions/checkout@v3
 
-    - name: set up JDK 11
+    - name: set up JDK 17
       uses: actions/setup-java@v3
       with:
-        java-version: '11'
+        java-version: '17'
         distribution: 'temurin'
 
     - name: Build
diff --git a/.github/workflows/ios-appcenter.yml b/.github/workflows/ios-browserstack.yml
similarity index 52%
rename from .github/workflows/ios-appcenter.yml
rename to .github/workflows/ios-browserstack.yml
index fa99703c7..7d77b6341 100644
--- a/.github/workflows/ios-appcenter.yml
+++ b/.github/workflows/ios-browserstack.yml
@@ -1,11 +1,11 @@
-name: iOS AppCenter Tests
+name: iOS BrowserStack Tests
 
 on:
   workflow_dispatch:
   push:
     branches: [ master ]
     paths:
-      - '.github/workflows/ios-appcenter.yml'
+      - '.github/workflows/ios-browserstack.yml'
       - 'sdk/ios/PicovoiceAppTest/**'
       - 'resources/audio_samples/**'
       - 'resources/porcupine'
@@ -14,7 +14,7 @@ on:
   pull_request:
     branches: [ master, 'v[0-9]+.[0-9]+' ]
     paths:
-      - '.github/workflows/ios-appcenter.yml'
+      - '.github/workflows/ios-browserstack.yml'
       - 'sdk/ios/PicovoiceAppTest/**'
       - 'resources/audio_samples/**'
       - 'resources/porcupine'
@@ -27,7 +27,7 @@ defaults:
 
 jobs:
   build:
-    name: Run iOS Tests on AppCenter
+    name: Run iOS Tests on BrowserStack
     runs-on: macos-latest
 
     steps:
@@ -36,32 +36,26 @@ jobs:
         with:
           submodules: recursive
 
-      - name: Set up Node.js LTS
-        uses: actions/setup-node@v3
+      - name: Installing Python
+        uses: actions/setup-python@v5
         with:
-          node-version: lts/*
+          python-version: '3.10'
+      - run:
+          pip3 install requests
 
       - name: Install Cocoapods
         run: gem install cocoapods
 
-      - name: Install AppCenter CLI
-        run: npm install -g appcenter-cli
-
       - name: Make build dir
         run: mkdir ddp
 
-      - name: Install resource script dependency
-        run: |
-          brew update
-          brew install convmv
-
       - name: Copy test_resources
         run: ./copy_test_resources.sh
 
       - name: Run Cocoapods
         run: pod install
 
-      - name: Inject AppID
+      - name: Inject AccessKey
         run: sed -i '.bak' 's:{TESTING_ACCESS_KEY_HERE}:${{secrets.PV_VALID_ACCESS_KEY}}:'
           PicovoiceAppTestUITests/BaseTest.swift
 
@@ -74,11 +68,23 @@ jobs:
           -derivedDataPath ddp
           CODE_SIGNING_ALLOWED=NO
 
-      - name: Run Tests on AppCenter
-        run: appcenter test run xcuitest
-          --token ${{secrets.APPCENTERAPITOKEN}}
-          --app "Picovoice/Picovoice-iOS"
-          --devices "Picovoice/ios-min-max"
-          --test-series "picovoice-ios"
-          --locale "en_US"
-          --build-dir ddp/Build/Products/Debug-iphoneos
+      - name: Generating ipa
+        run: cd ddp/Build/Products/Debug-iphoneos/ &&
+          mkdir Payload &&
+          cp -r PicovoiceAppTest.app Payload &&
+          zip --symlinks -r PicovoiceAppTest.ipa Payload &&
+          rm -r Payload
+
+      - name: Zipping Tests
+        run: cd ddp/Build/Products/Debug-iphoneos/ &&
+          zip --symlinks -r PicovoiceAppTestUITests.zip PicovoiceAppTestUITests-Runner.app
+
+      - name: Run tests on BrowserStack
+        run: python3 ../../../script/automation/browserstack.py
+          --type xcuitest
+          --username "${{secrets.BROWSERSTACK_USERNAME}}"
+          --access_key "${{secrets.BROWSERSTACK_ACCESS_KEY}}"
+          --project_name "Picovoice-iOS"
+          --devices "ios-min-max"
+          --app_path "ddp/Build/Products/Debug-iphoneos/PicovoiceAppTest.ipa"
+          --test_path "ddp/Build/Products/Debug-iphoneos/PicovoiceAppTestUITests.zip"
diff --git a/.github/workflows/ios-demos.yml b/.github/workflows/ios-demos.yml
index 65133601b..35bc4e6fd 100644
--- a/.github/workflows/ios-demos.yml
+++ b/.github/workflows/ios-demos.yml
@@ -34,9 +34,6 @@ jobs:
       - name: Install Cocoapods
         run: gem install cocoapods
 
-      - name: Install AppCenter CLI
-        run: npm install -g appcenter-cli
-
       - name: Make build dir
         run: mkdir ddp
 
@@ -81,9 +78,6 @@ jobs:
       - name: Install Cocoapods
         run: gem install cocoapods
 
-      - name: Install AppCenter CLI
-        run: npm install -g appcenter-cli
-
       - name: Make build dir
         run: mkdir ddp
 
@@ -119,9 +113,6 @@ jobs:
       - name: Install Cocoapods
         run: gem install cocoapods
 
-      - name: Install AppCenter CLI
-        run: npm install -g appcenter-cli
-
       - name: Make build dir
         run: mkdir ddp
 
diff --git a/resources/.lint/spell-check/dict.txt b/resources/.lint/spell-check/dict.txt
index 428a05b26..f222a7689 100644
--- a/resources/.lint/spell-check/dict.txt
+++ b/resources/.lint/spell-check/dict.txt
@@ -304,6 +304,7 @@ webvp
 weiß
 Wohnzimmer
 xcframework
+xcuitest
 xcworkspace
 xcodeproj
 xcshareddata
diff --git a/script/automation/browserstack.py b/script/automation/browserstack.py
new file mode 100644
index 000000000..54c0e77a0
--- /dev/null
+++ b/script/automation/browserstack.py
@@ -0,0 +1,136 @@
+import argparse
+import requests
+import time
+
+APP_URI = 'https://api-cloud.browserstack.com/app-automate/{}/v2/app'
+TEST_URI = 'https://api-cloud.browserstack.com/app-automate/{}/v2/test-suite'
+BUILD_URI = 'https://api-cloud.browserstack.com/app-automate/{}/v2/build'
+STATUS_URI = 'https://api-cloud.browserstack.com/app-automate/{}/v2/builds/{}'
+
+devices_dict = {
+    'android-min-max': [
+        'Samsung Galaxy S8-7.0',
+        'Samsung Galaxy M52-11.0',
+        'Google Pixel 9-15.0'
+    ],
+    'android-perf': [
+        'Google Pixel 6 Pro-15.0'
+    ],
+    'ios-min-max': [
+        'iPhone SE 2022-15',
+        'iPhone 14 Plus-16',
+        'iPhone 14-18'
+    ],
+    'ios-perf': [
+        'iPhone 13-18',
+    ]
+}
+
+
+def main(args: argparse.Namespace) -> None:
+    app_files = {
+        'file': open(args.app_path, 'rb')
+    }
+
+    app_response = requests.post(
+        APP_URI.format(args.type),
+        files=app_files,
+        auth=(args.username, args.access_key)
+    )
+    app_response_json = app_response.json()
+
+    if not app_response.ok:
+        print('App Upload Failed', app_response_json)
+        exit(1)
+
+    test_files = {
+        'file': open(args.test_path, 'rb')
+    }
+    test_response = requests.post(
+        TEST_URI.format(args.type),
+        files=test_files,
+        auth=(args.username, args.access_key)
+    )
+    test_response_json = test_response.json()
+
+    if not test_response.ok:
+        print('Test Upload Failed', test_response_json)
+        exit(1)
+
+    build_headers = {
+        'Content-Type': 'application/json'
+    }
+    build_data = {
+        'app': app_response_json['app_url'],
+        'testSuite': test_response_json['test_suite_url'],
+        'project': args.project_name,
+        'devices': devices_dict[args.devices],
+        'deviceLogs': True
+    }
+
+    while True:
+        build_response = requests.post(
+            BUILD_URI.format(args.type),
+            headers=build_headers,
+            json=build_data,
+            auth=(args.username, args.access_key)
+        )
+        if (build_response is not None and 'message' in build_response.json() and '[BROWSERSTACK_ALL_PARALLELS_IN_USE]'
+                in build_response.json()['message']):
+            print('Parallel threads limit reached. Waiting...', flush=True)
+            time.sleep(60)
+        else:
+            break
+
+    if build_response is None:
+        print('Build Failed')
+        exit(1)
+
+    build_response_json = build_response.json()
+
+    if not build_response.ok:
+        print('Build Failed', build_response.json())
+        exit(1)
+
+    if build_response_json['message'] != 'Success':
+        print('Build Unsuccessful')
+        exit(1)
+
+    print(
+        'View build results at https://app-automate.browserstack.com/dashboard/v2/builds/{}'
+        .format(build_response_json['build_id']))
+
+    while True:
+        time.sleep(60)
+        status_response = requests.get(
+            STATUS_URI.format(args.type, build_response_json['build_id']),
+            auth=(args.username, args.access_key)
+        )
+        status_response_json = status_response.json()
+        status = status_response_json['status']
+
+        if not status_response.ok:
+            print('Status Request Failed', status_response_json)
+            exit(1)
+
+        if status != 'queued' and status != 'running':
+            break
+
+    print('Status:', status)
+    if status != 'passed':
+        exit(1)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--type', choices=['espresso', 'xcuitest'], required=True)
+    parser.add_argument('--username', required=True)
+    parser.add_argument('--access_key', required=True)
+
+    parser.add_argument('--project_name', required=True)
+    parser.add_argument('--devices', choices=devices_dict.keys(), required=True)
+    parser.add_argument('--app_path', required=True)
+    parser.add_argument('--test_path', required=True)
+    args = parser.parse_args()
+
+    main(args)
diff --git a/sdk/android/PicovoiceTestApp/build.gradle b/sdk/android/PicovoiceTestApp/build.gradle
index b82865ec6..9967371fc 100644
--- a/sdk/android/PicovoiceTestApp/build.gradle
+++ b/sdk/android/PicovoiceTestApp/build.gradle
@@ -1,6 +1,6 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 ext {
-    defaultTargetSdkVersion = 31
+    defaultTargetSdkVersion = 33
 }
 
 buildscript {
@@ -9,7 +9,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.2.2'
+        classpath 'com.android.tools.build:gradle:8.2.2'
     }
 }
 
@@ -20,6 +20,6 @@ allprojects {
     }
 }
 
-task clean(type: Delete) {
+tasks.register('clean', Delete) {
     delete rootProject.buildDir
 }
diff --git a/sdk/android/PicovoiceTestApp/gradle.properties b/sdk/android/PicovoiceTestApp/gradle.properties
index c52ac9b79..091e018b3 100644
--- a/sdk/android/PicovoiceTestApp/gradle.properties
+++ b/sdk/android/PicovoiceTestApp/gradle.properties
@@ -16,4 +16,7 @@ org.gradle.jvmargs=-Xmx2048m
 # https://developer.android.com/topic/libraries/support-library/androidx-rn
 android.useAndroidX=true
 # Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
\ No newline at end of file
+android.enableJetifier=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
\ No newline at end of file
diff --git a/sdk/android/PicovoiceTestApp/gradle/wrapper/gradle-wrapper.properties b/sdk/android/PicovoiceTestApp/gradle/wrapper/gradle-wrapper.properties
index f3b860b96..e43611f06 100644
--- a/sdk/android/PicovoiceTestApp/gradle/wrapper/gradle-wrapper.properties
+++ b/sdk/android/PicovoiceTestApp/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/build.gradle b/sdk/android/PicovoiceTestApp/picovoice-test-app/build.gradle
index 10b598dc3..2f1ca86a3 100644
--- a/sdk/android/PicovoiceTestApp/picovoice-test-app/build.gradle
+++ b/sdk/android/PicovoiceTestApp/picovoice-test-app/build.gradle
@@ -24,7 +24,7 @@ if (rootProject.file("local.properties").exists()) {
 }
 
 android {
-    compileSdkVersion defaultTargetSdkVersion
+    compileSdk defaultTargetSdkVersion
 
     defaultConfig {
         applicationId "ai.picovoice.picovoice.testapp"
@@ -130,12 +130,14 @@ android {
             }
         }
     }
+    namespace 'ai.picovoice.picovoice.testapp'
 }
 
 dependencies {
     implementation 'androidx.appcompat:appcompat:1.3.0'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
     implementation 'com.google.code.gson:gson:2.10'
+    implementation 'com.google.errorprone:error_prone_annotations:2.36.0'
     implementation 'ai.picovoice:picovoice-android:3.0.1'
 
     // Espresso UI Testing
@@ -143,7 +145,6 @@ dependencies {
     androidTestImplementation('androidx.test.espresso:espresso-core:3.2.0', {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
-    androidTestImplementation('com.microsoft.appcenter:espresso-test-extension:1.4')
     androidTestImplementation('androidx.test.espresso:espresso-intents:3.5.1')
 }
 
@@ -165,5 +166,17 @@ afterEvaluate {
 
             tasks."merge${flavor.name.capitalize()}DebugAssets".dependsOn "${flavor.name}CopyAudio"
             tasks."merge${flavor.name.capitalize()}ReleaseAssets".dependsOn "${flavor.name}CopyAudio"
+
+            tasks."generate${flavor.name.capitalize()}ReleaseLintVitalReportModel".dependsOn "${flavor.name}CopyAudio"
+            tasks."generate${flavor.name.capitalize()}ReleaseLintVitalReportModel".dependsOn "${flavor.name}CopyContext"
+            tasks."generate${flavor.name.capitalize()}ReleaseLintVitalReportModel".dependsOn "${flavor.name}CopyRhinoParams"
+            tasks."generate${flavor.name.capitalize()}ReleaseLintVitalReportModel".dependsOn "${flavor.name}CopyPorcupineParams"
+            tasks."generate${flavor.name.capitalize()}ReleaseLintVitalReportModel".dependsOn "${flavor.name}CopyWakeword"
+
+            tasks."lintVitalAnalyze${flavor.name.capitalize()}Release".dependsOn "${flavor.name}CopyAudio"
+            tasks."lintVitalAnalyze${flavor.name.capitalize()}Release".dependsOn "${flavor.name}CopyContext"
+            tasks."lintVitalAnalyze${flavor.name.capitalize()}Release".dependsOn "${flavor.name}CopyRhinoParams"
+            tasks."lintVitalAnalyze${flavor.name.capitalize()}Release".dependsOn "${flavor.name}CopyPorcupineParams"
+            tasks."lintVitalAnalyze${flavor.name.capitalize()}Release".dependsOn "${flavor.name}CopyWakeword"
     }
 }
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/proguard-rules.pro b/sdk/android/PicovoiceTestApp/picovoice-test-app/proguard-rules.pro
index 158caf35e..280693dab 100644
--- a/sdk/android/PicovoiceTestApp/picovoice-test-app/proguard-rules.pro
+++ b/sdk/android/PicovoiceTestApp/picovoice-test-app/proguard-rules.pro
@@ -20,4 +20,8 @@
 # hide the original source file name.
 #-renamesourcefileattribute SourceFile
 -keep class com.google.** { *; }
--keep class com.microsoft.** { *; }
\ No newline at end of file
+-keep class com.microsoft.** { *; }
+
+-dontwarn com.google.errorprone.annotations.CheckReturnValue
+-dontwarn com.google.errorprone.annotations.MustBeClosed
+-dontwarn javax.lang.model.element.Modifier
\ No newline at end of file
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/BaseTest.java b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/BaseTest.java
index 5ee88674d..323f9288f 100644
--- a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/BaseTest.java
+++ b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/BaseTest.java
@@ -7,12 +7,7 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.microsoft.appcenter.espresso.Factory;
-import com.microsoft.appcenter.espresso.ReportHelper;
-
-import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -33,9 +28,6 @@
 
 public class BaseTest {
 
-    @Rule
-    public ReportHelper reportHelper = Factory.getReportHelper();
-
     boolean isWakeWordDetected = false;
     PicovoiceWakeWordCallback wakeWordCallback = new PicovoiceWakeWordCallback() {
         @Override
@@ -59,16 +51,6 @@ public void invoke(RhinoInference inference) {
     String testResourcesPath;
     String accessKey;
 
-    @After
-    public void TearDown() {
-        isWakeWordDetected = false;
-        inferenceResult = null;
-        if (picovoice != null) {
-            picovoice.delete();
-        }
-        reportHelper.label("Stopping App");
-    }
-
     @Before
     public void Setup() throws IOException {
         testContext = InstrumentationRegistry.getInstrumentation().getContext();
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/IntegrationTest.java b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/IntegrationTest.java
index 261ea3d2b..d4317c9dd 100644
--- a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/IntegrationTest.java
+++ b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/IntegrationTest.java
@@ -16,9 +16,6 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
-import com.microsoft.appcenter.espresso.Factory;
-import com.microsoft.appcenter.espresso.ReportHelper;
-
 import org.hamcrest.Matcher;
 import org.junit.After;
 import org.junit.Before;
@@ -74,9 +71,6 @@ public void perform(UiController uiController, View view) {
 @RunWith(AndroidJUnit4.class)
 public class IntegrationTest {
 
-    @Rule
-    public ReportHelper reportHelper = Factory.getReportHelper();
-
     @Rule
     public ActivityScenarioRule<MainActivity> activityScenarioRule =
             new ActivityScenarioRule<>(MainActivity.class);
@@ -91,11 +85,6 @@ public void intentsTeardown() {
         Intents.release();
     }
 
-    @After
-    public void TearDown() {
-        reportHelper.label("Stopping App");
-    }
-
     @Test
     public void testPicovoice() {
         onView(withId(R.id.testButton)).perform(click());
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/LanguageTests.java b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/LanguageTests.java
new file mode 100644
index 000000000..9dd1290ce
--- /dev/null
+++ b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/LanguageTests.java
@@ -0,0 +1,140 @@
+package ai.picovoice.picovoice.testapp;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import ai.picovoice.picovoice.Picovoice;
+
+@RunWith(Parameterized.class)
+public class LanguageTests extends BaseTest {
+
+    @Parameterized.Parameter(value = 0)
+    public String porcupineModelFile;
+
+    @Parameterized.Parameter(value = 1)
+    public String rhinoModelFile;
+
+    @Parameterized.Parameter(value = 2)
+    public String keywordFile;
+
+    @Parameterized.Parameter(value = 3)
+    public String contextFile;
+
+    @Parameterized.Parameter(value = 4)
+    public String testAudioFile;
+
+    @Parameterized.Parameter(value = 5)
+    public String expectedIntent;
+
+    @Parameterized.Parameter(value = 6)
+    public Map<String, String> expectedSlots;
+
+    @Parameterized.Parameters(name = "{4}")
+    public static Collection<Object[]> initParameters() throws IOException {
+        String testDataJsonString = getTestDataString();
+
+        JsonObject testDataJson = JsonParser.parseString(testDataJsonString).getAsJsonObject();
+        JsonArray testParametersJson = testDataJson.getAsJsonObject("tests").getAsJsonArray("parameters");
+
+        List<Object[]> parameters = new ArrayList<>();
+        for (int i = 0; i < testParametersJson.size(); i++) {
+            JsonObject testData = testParametersJson.get(i).getAsJsonObject();
+            String language = testData.get("language").getAsString();
+            String wakeword = testData.get("wakeword").getAsString();
+            String contextName = testData.get("context_name").getAsString();
+            String audioFilename = testData.get("audio_file").getAsString();
+            JsonObject inferenceJson = testData.getAsJsonObject("inference");
+
+            String porcupineModelFile = String.format("porcupine_model_files/porcupine_params_%s.pv", language);
+            String rhinoModelFile = String.format("rhino_model_files/rhino_params_%s.pv", language);
+            String keywordFile = String.format("keyword_files/%s/%s_android.ppn", language, wakeword);
+            String contextFile = String.format("context_files/%s/%s_android.rhn", language, contextName);
+            String audioFile = String.format("audio_samples/%s", audioFilename);
+
+            String intent = inferenceJson.get("intent").getAsString();
+            HashMap<String, String> slots = new HashMap<String, String>();
+            for (Map.Entry<String, JsonElement> entry : inferenceJson.getAsJsonObject("slots").asMap().entrySet()) {
+                slots.put(entry.getKey(), entry.getValue().getAsString());
+            }
+
+            if (Objects.equals(language, "en")) {
+                porcupineModelFile = "porcupine_model_files/porcupine_params.pv";
+                rhinoModelFile = "rhino_model_files/rhino_params.pv";
+            }
+
+            parameters.add(new Object[] {
+                    porcupineModelFile,
+                    rhinoModelFile,
+                    keywordFile,
+                    contextFile,
+                    audioFile,
+                    intent,
+                    slots,
+            });
+        }
+
+        return parameters;
+    }
+
+    @Test
+    public void testProcess() throws Exception {
+
+        String porcupineModelPath = new File(testResourcesPath, porcupineModelFile).getAbsolutePath();
+        String rhinoModelPath = new File(testResourcesPath, rhinoModelFile).getAbsolutePath();
+        String keywordPath = new File(testResourcesPath, keywordFile).getAbsolutePath();
+        String contextPath = new File(testResourcesPath, contextFile).getAbsolutePath();
+
+        picovoice = new Picovoice.Builder()
+                .setAccessKey(accessKey)
+                .setPorcupineModelPath(porcupineModelPath)
+                .setRhinoModelPath(rhinoModelPath)
+                .setKeywordPath(keywordPath)
+                .setContextPath(contextPath)
+                .setWakeWordCallback(wakeWordCallback)
+                .setInferenceCallback(inferenceCallback)
+                .build(appContext);
+
+        File testAudio = new File(testResourcesPath, testAudioFile);
+
+        processTestAudio(picovoice, testAudio);
+        Thread.sleep(500);
+
+        assertTrue(isWakeWordDetected);
+        assertNotNull(inferenceResult);
+        assertTrue(inferenceResult.getIsUnderstood());
+        assertEquals(expectedIntent, inferenceResult.getIntent());
+        assertEquals(expectedSlots, inferenceResult.getSlots());
+
+        isWakeWordDetected = false;
+        inferenceResult = null;
+
+        // test again
+        processTestAudio(picovoice, testAudio);
+        Thread.sleep(500);
+
+        assertTrue(isWakeWordDetected);
+        assertNotNull(inferenceResult);
+        assertTrue(inferenceResult.getIsUnderstood());
+        assertEquals(expectedIntent, inferenceResult.getIntent());
+        assertEquals(expectedSlots, inferenceResult.getSlots());
+    }
+}
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/PicovoiceTest.java b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/PicovoiceTest.java
deleted file mode 100644
index 5e85c7614..000000000
--- a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/PicovoiceTest.java
+++ /dev/null
@@ -1,493 +0,0 @@
-package ai.picovoice.picovoice.testapp;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
-import org.junit.Test;
-import org.junit.experimental.runners.Enclosed;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import ai.picovoice.picovoice.Picovoice;
-import ai.picovoice.picovoice.PicovoiceException;
-
-@RunWith(Enclosed.class)
-public class PicovoiceTest {
-
-    public static class StandardTests extends BaseTest {
-
-        @Test
-        public void testInitSuccessSimple() throws PicovoiceException {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            picovoice = new Picovoice.Builder()
-                    .setAccessKey(accessKey)
-                    .setKeywordPath(keywordPath.getAbsolutePath())
-                    .setContextPath(contextPath.getAbsolutePath())
-                    .setWakeWordCallback(wakeWordCallback)
-                    .setInferenceCallback(inferenceCallback)
-                    .build(appContext);
-
-            assertTrue(picovoice.getVersion() != null && !picovoice.getVersion().equals(""));
-            assertTrue(picovoice.getFrameLength() > 0);
-            assertTrue(picovoice.getSampleRate() > 0);
-            assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
-        }
-
-        @Test
-        public void testInitSuccessCustomModelPaths() throws PicovoiceException {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            File porcupineModelPath = new File(testResourcesPath, "porcupine_model_files/porcupine_params.pv");
-            File rhinoModelPath = new File(testResourcesPath, "rhino_model_files/rhino_params.pv");
-            picovoice = new Picovoice.Builder()
-                    .setAccessKey(accessKey)
-                    .setKeywordPath(keywordPath.getAbsolutePath())
-                    .setContextPath(contextPath.getAbsolutePath())
-                    .setPorcupineModelPath(porcupineModelPath.getAbsolutePath())
-                    .setRhinoModelPath(rhinoModelPath.getAbsolutePath())
-                    .setWakeWordCallback(wakeWordCallback)
-                    .setInferenceCallback(inferenceCallback)
-                    .build(appContext);
-
-            assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
-        }
-
-        @Test
-        public void testInitSuccessCustomSensitivities() throws PicovoiceException {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            picovoice = new Picovoice.Builder()
-                    .setAccessKey(accessKey)
-                    .setKeywordPath(keywordPath.getAbsolutePath())
-                    .setContextPath(contextPath.getAbsolutePath())
-                    .setPorcupineSensitivity(0.7f)
-                    .setRhinoSensitivity(0.35f)
-                    .setWakeWordCallback(wakeWordCallback)
-                    .setInferenceCallback(inferenceCallback)
-                    .build(appContext);
-
-            assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
-        }
-
-        @Test
-        public void testInitSuccessCustomEndpointSettings() throws PicovoiceException {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            picovoice = new Picovoice.Builder()
-                    .setAccessKey(accessKey)
-                    .setKeywordPath(keywordPath.getAbsolutePath())
-                    .setContextPath(contextPath.getAbsolutePath())
-                    .setEndpointDurationSec(3.0f)
-                    .setRequireEndpoint(false)
-                    .setWakeWordCallback(wakeWordCallback)
-                    .setInferenceCallback(inferenceCallback)
-                    .build(appContext);
-
-            assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
-        }
-
-        @Test
-        public void testInitFailWithMismatchedPorcupineLanguage() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/fr/framboise_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithMismatchedRhinoLanguage() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/de/beleuchtung_android.rhn");
-
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithInvalidKeywordPath() {
-            File keywordPath = new File(testResourcesPath, "bad_path/bad_path.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithInvalidContextPath() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "bad_path/bad_path.rhn");
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithInvalidPorcupineModelPath() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            File porcupineModelPath = new File(testResourcesPath, "bad_path/bad_path.pv");
-
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setPorcupineModelPath(porcupineModelPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithInvalidRhinoModelPath() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-            File rhinoModelPath = new File(testResourcesPath, "bad_path/bad_path.pv");
-
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setRhinoModelPath(rhinoModelPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithInvalidPorcupineSensitivity() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setPorcupineSensitivity(10)
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithInvalidRhinoSensitivity() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setRhinoSensitivity(-1)
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithWrongPorcupinePlatform() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/alexa_linux.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitFailWithWrongRhinoPlatform() {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_linux.rhn");
-
-            boolean didFail = false;
-            try {
-                new Picovoice.Builder()
-                        .setAccessKey(accessKey)
-                        .setKeywordPath(keywordPath.getAbsolutePath())
-                        .setContextPath(contextPath.getAbsolutePath())
-                        .setWakeWordCallback(wakeWordCallback)
-                        .setInferenceCallback(inferenceCallback)
-                        .build(appContext);
-
-            } catch (PicovoiceException e) {
-                didFail = true;
-            }
-
-            assertTrue(didFail);
-        }
-
-        @Test
-        public void testInitWithNonAsciiModelName() throws PicovoiceException {
-            File keywordPath = new File(testResourcesPath, "keyword_files/es/murciélago_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/es/iluminación_inteligente_android.rhn");
-            File porcupineModelPath = new File(testResourcesPath, "porcupine_model_files/porcupine_params_es.pv");
-            File rhinoModelPath = new File(testResourcesPath, "rhino_model_files/rhino_params_es.pv");
-            picovoice = new Picovoice.Builder()
-                    .setAccessKey(accessKey)
-                    .setKeywordPath(keywordPath.getAbsolutePath())
-                    .setContextPath(contextPath.getAbsolutePath())
-                    .setPorcupineModelPath(porcupineModelPath.getAbsolutePath())
-                    .setRhinoModelPath(rhinoModelPath.getAbsolutePath())
-                    .setWakeWordCallback(wakeWordCallback)
-                    .setInferenceCallback(inferenceCallback)
-                    .build(appContext);
-
-            assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
-        }
-
-        @Test
-        public void testReset() throws Exception {
-            File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
-            File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
-
-            picovoice = new Picovoice.Builder()
-                    .setAccessKey(accessKey)
-                    .setKeywordPath(keywordPath.getAbsolutePath())
-                    .setContextPath(contextPath.getAbsolutePath())
-                    .setWakeWordCallback(() -> {
-                        try {
-                            isWakeWordDetected = true;
-                            picovoice.reset();
-                        } catch (PicovoiceException e) {
-                            assertNull(e);
-                        }
-                    })
-                    .setInferenceCallback(inferenceCallback)
-                    .build(appContext);
-
-            File testAudio = new File(testResourcesPath, "audio_samples/picovoice-coffee.wav");
-
-            inferenceResult = null;
-            processTestAudio(picovoice, testAudio);
-            Thread.sleep(500);
-
-            assertTrue(isWakeWordDetected);
-            assertNull(inferenceResult);
-        }
-    }
-
-    @RunWith(Parameterized.class)
-    public static class LanguageTests extends BaseTest {
-
-        @Parameterized.Parameter(value = 0)
-        public String porcupineModelFile;
-
-        @Parameterized.Parameter(value = 1)
-        public String rhinoModelFile;
-
-        @Parameterized.Parameter(value = 2)
-        public String keywordFile;
-
-        @Parameterized.Parameter(value = 3)
-        public String contextFile;
-
-        @Parameterized.Parameter(value = 4)
-        public String testAudioFile;
-
-        @Parameterized.Parameter(value = 5)
-        public String expectedIntent;
-
-        @Parameterized.Parameter(value = 6)
-        public Map<String, String> expectedSlots;
-
-        @Parameterized.Parameters(name = "{4}")
-        public static Collection<Object[]> initParameters() throws IOException {
-            String testDataJsonString = getTestDataString();
-
-            JsonObject testDataJson = JsonParser.parseString(testDataJsonString).getAsJsonObject();
-            JsonArray testParametersJson = testDataJson.getAsJsonObject("tests").getAsJsonArray("parameters");
-
-            List<Object[]> parameters = new ArrayList<>();
-            for (int i = 0; i < testParametersJson.size(); i++) {
-                JsonObject testData = testParametersJson.get(i).getAsJsonObject();
-                String language = testData.get("language").getAsString();
-                String wakeword = testData.get("wakeword").getAsString();
-                String contextName = testData.get("context_name").getAsString();
-                String audioFilename = testData.get("audio_file").getAsString();
-                JsonObject inferenceJson = testData.getAsJsonObject("inference");
-
-                String porcupineModelFile = String.format("porcupine_model_files/porcupine_params_%s.pv", language);
-                String rhinoModelFile = String.format("rhino_model_files/rhino_params_%s.pv", language);
-                String keywordFile = String.format("keyword_files/%s/%s_android.ppn", language, wakeword);
-                String contextFile = String.format("context_files/%s/%s_android.rhn", language, contextName);
-                String audioFile = String.format("audio_samples/%s", audioFilename);
-
-                String intent = inferenceJson.get("intent").getAsString();
-                HashMap<String, String> slots = new HashMap<String, String>();
-                for (Map.Entry<String, JsonElement> entry : inferenceJson.getAsJsonObject("slots").asMap().entrySet()) {
-                    slots.put(entry.getKey(), entry.getValue().getAsString());
-                }
-
-                if (Objects.equals(language, "en")) {
-                    porcupineModelFile = "porcupine_model_files/porcupine_params.pv";
-                    rhinoModelFile = "rhino_model_files/rhino_params.pv";
-                }
-
-                parameters.add(new Object[] {
-                        porcupineModelFile,
-                        rhinoModelFile,
-                        keywordFile,
-                        contextFile,
-                        audioFile,
-                        intent,
-                        slots,
-                });
-            }
-
-            return parameters;
-        }
-
-        @Test
-        public void testProcess() throws Exception {
-
-            String porcupineModelPath = new File(testResourcesPath, porcupineModelFile).getAbsolutePath();
-            String rhinoModelPath = new File(testResourcesPath, rhinoModelFile).getAbsolutePath();
-            String keywordPath = new File(testResourcesPath, keywordFile).getAbsolutePath();
-            String contextPath = new File(testResourcesPath, contextFile).getAbsolutePath();
-
-            picovoice = new Picovoice.Builder()
-                    .setAccessKey(accessKey)
-                    .setPorcupineModelPath(porcupineModelPath)
-                    .setRhinoModelPath(rhinoModelPath)
-                    .setKeywordPath(keywordPath)
-                    .setContextPath(contextPath)
-                    .setWakeWordCallback(wakeWordCallback)
-                    .setInferenceCallback(inferenceCallback)
-                    .build(appContext);
-
-            File testAudio = new File(testResourcesPath, testAudioFile);
-
-            processTestAudio(picovoice, testAudio);
-            Thread.sleep(500);
-
-            assertTrue(isWakeWordDetected);
-            assertNotNull(inferenceResult);
-            assertTrue(inferenceResult.getIsUnderstood());
-            assertEquals(expectedIntent, inferenceResult.getIntent());
-            assertEquals(expectedSlots, inferenceResult.getSlots());
-
-            isWakeWordDetected = false;
-            inferenceResult = null;
-
-            // test again
-            processTestAudio(picovoice, testAudio);
-            Thread.sleep(500);
-
-            assertTrue(isWakeWordDetected);
-            assertNotNull(inferenceResult);
-            assertTrue(inferenceResult.getIsUnderstood());
-            assertEquals(expectedIntent, inferenceResult.getIntent());
-            assertEquals(expectedSlots, inferenceResult.getSlots());
-        }
-    }
-}
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/StandardTests.java b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/StandardTests.java
new file mode 100644
index 000000000..2669cebd3
--- /dev/null
+++ b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/androidTest/java/ai/picovoice/picovoice/testapp/StandardTests.java
@@ -0,0 +1,359 @@
+package ai.picovoice.picovoice.testapp;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+import ai.picovoice.picovoice.Picovoice;
+import ai.picovoice.picovoice.PicovoiceException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+@RunWith(AndroidJUnit4.class)
+public class StandardTests extends BaseTest {
+    @Test
+    public void testInitSuccessSimple() throws PicovoiceException {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        picovoice = new Picovoice.Builder()
+                .setAccessKey(accessKey)
+                .setKeywordPath(keywordPath.getAbsolutePath())
+                .setContextPath(contextPath.getAbsolutePath())
+                .setWakeWordCallback(wakeWordCallback)
+                .setInferenceCallback(inferenceCallback)
+                .build(appContext);
+
+        assertTrue(picovoice.getVersion() != null && !picovoice.getVersion().equals(""));
+        assertTrue(picovoice.getFrameLength() > 0);
+        assertTrue(picovoice.getSampleRate() > 0);
+        assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
+    }
+
+    @Test
+    public void testInitSuccessCustomModelPaths() throws PicovoiceException {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        File porcupineModelPath = new File(testResourcesPath, "porcupine_model_files/porcupine_params.pv");
+        File rhinoModelPath = new File(testResourcesPath, "rhino_model_files/rhino_params.pv");
+        picovoice = new Picovoice.Builder()
+                .setAccessKey(accessKey)
+                .setKeywordPath(keywordPath.getAbsolutePath())
+                .setContextPath(contextPath.getAbsolutePath())
+                .setPorcupineModelPath(porcupineModelPath.getAbsolutePath())
+                .setRhinoModelPath(rhinoModelPath.getAbsolutePath())
+                .setWakeWordCallback(wakeWordCallback)
+                .setInferenceCallback(inferenceCallback)
+                .build(appContext);
+
+        assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
+    }
+
+    @Test
+    public void testInitSuccessCustomSensitivities() throws PicovoiceException {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        picovoice = new Picovoice.Builder()
+                .setAccessKey(accessKey)
+                .setKeywordPath(keywordPath.getAbsolutePath())
+                .setContextPath(contextPath.getAbsolutePath())
+                .setPorcupineSensitivity(0.7f)
+                .setRhinoSensitivity(0.35f)
+                .setWakeWordCallback(wakeWordCallback)
+                .setInferenceCallback(inferenceCallback)
+                .build(appContext);
+
+        assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
+    }
+
+    @Test
+    public void testInitSuccessCustomEndpointSettings() throws PicovoiceException {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        picovoice = new Picovoice.Builder()
+                .setAccessKey(accessKey)
+                .setKeywordPath(keywordPath.getAbsolutePath())
+                .setContextPath(contextPath.getAbsolutePath())
+                .setEndpointDurationSec(3.0f)
+                .setRequireEndpoint(false)
+                .setWakeWordCallback(wakeWordCallback)
+                .setInferenceCallback(inferenceCallback)
+                .build(appContext);
+
+        assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
+    }
+
+    @Test
+    public void testInitFailWithMismatchedPorcupineLanguage() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/fr/framboise_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithMismatchedRhinoLanguage() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/de/beleuchtung_android.rhn");
+
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithInvalidKeywordPath() {
+        File keywordPath = new File(testResourcesPath, "bad_path/bad_path.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithInvalidContextPath() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "bad_path/bad_path.rhn");
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithInvalidPorcupineModelPath() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        File porcupineModelPath = new File(testResourcesPath, "bad_path/bad_path.pv");
+
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setPorcupineModelPath(porcupineModelPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithInvalidRhinoModelPath() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+        File rhinoModelPath = new File(testResourcesPath, "bad_path/bad_path.pv");
+
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setRhinoModelPath(rhinoModelPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithInvalidPorcupineSensitivity() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setPorcupineSensitivity(10)
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithInvalidRhinoSensitivity() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setRhinoSensitivity(-1)
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithWrongPorcupinePlatform() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/alexa_linux.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitFailWithWrongRhinoPlatform() {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_linux.rhn");
+
+        boolean didFail = false;
+        try {
+            new Picovoice.Builder()
+                    .setAccessKey(accessKey)
+                    .setKeywordPath(keywordPath.getAbsolutePath())
+                    .setContextPath(contextPath.getAbsolutePath())
+                    .setWakeWordCallback(wakeWordCallback)
+                    .setInferenceCallback(inferenceCallback)
+                    .build(appContext);
+
+        } catch (PicovoiceException e) {
+            didFail = true;
+        }
+
+        assertTrue(didFail);
+    }
+
+    @Test
+    public void testInitWithNonAsciiModelName() throws PicovoiceException {
+        File keywordPath = new File(testResourcesPath, "keyword_files/es/murciélago_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/es/iluminación_inteligente_android.rhn");
+        File porcupineModelPath = new File(testResourcesPath, "porcupine_model_files/porcupine_params_es.pv");
+        File rhinoModelPath = new File(testResourcesPath, "rhino_model_files/rhino_params_es.pv");
+        picovoice = new Picovoice.Builder()
+                .setAccessKey(accessKey)
+                .setKeywordPath(keywordPath.getAbsolutePath())
+                .setContextPath(contextPath.getAbsolutePath())
+                .setPorcupineModelPath(porcupineModelPath.getAbsolutePath())
+                .setRhinoModelPath(rhinoModelPath.getAbsolutePath())
+                .setWakeWordCallback(wakeWordCallback)
+                .setInferenceCallback(inferenceCallback)
+                .build(appContext);
+
+        assertTrue(picovoice.getContextInformation() != null && !picovoice.getContextInformation().equals(""));
+    }
+
+    @Test
+    public void testReset() throws Exception {
+        File keywordPath = new File(testResourcesPath, "keyword_files/en/picovoice_android.ppn");
+        File contextPath = new File(testResourcesPath, "context_files/en/coffee_maker_android.rhn");
+
+        picovoice = new Picovoice.Builder()
+                .setAccessKey(accessKey)
+                .setKeywordPath(keywordPath.getAbsolutePath())
+                .setContextPath(contextPath.getAbsolutePath())
+                .setWakeWordCallback(() -> {
+                    try {
+                        isWakeWordDetected = true;
+                        picovoice.reset();
+                    } catch (PicovoiceException e) {
+                        assertNull(e);
+                    }
+                })
+                .setInferenceCallback(inferenceCallback)
+                .build(appContext);
+
+        File testAudio = new File(testResourcesPath, "audio_samples/picovoice-coffee.wav");
+
+        inferenceResult = null;
+        processTestAudio(picovoice, testAudio);
+        Thread.sleep(500);
+
+        assertTrue(isWakeWordDetected);
+        assertNull(inferenceResult);
+    }
+}
diff --git a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/main/AndroidManifest.xml b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/main/AndroidManifest.xml
index 45c0f2f96..41d2b1dad 100644
--- a/sdk/android/PicovoiceTestApp/picovoice-test-app/src/main/AndroidManifest.xml
+++ b/sdk/android/PicovoiceTestApp/picovoice-test-app/src/main/AndroidManifest.xml
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="ai.picovoice.picovoice.testapp">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
diff --git a/sdk/ios/PicovoiceAppTest/copy_test_resources.sh b/sdk/ios/PicovoiceAppTest/copy_test_resources.sh
index d1a2247fa..f495e3987 100755
--- a/sdk/ios/PicovoiceAppTest/copy_test_resources.sh
+++ b/sdk/ios/PicovoiceAppTest/copy_test_resources.sh
@@ -50,6 +50,3 @@ cp ${RHINO_LIB_DIR}/common/*.pv ${ASSETS_DIR}/model_files
 
 echo "Copying test data file..."
 cp ${PICOVOICE_RESOURCE_DIR}/.test/test_data.json ${ASSETS_DIR}
-
-echo "Fixing filename encodings for Appcenter compatibility"
-convmv --notest -f utf8 -t utf8 --nfd -r ${ASSETS_DIR}